"""Guard: a persisted, ONE-WAY promotion (`promoted_strategy_ids()`, never deleted) must NOT re-enter
`deploy_sids` once its registry entry has had `promote_on_pass` removed — e.g. xsfunding/unlock folded into
the `bybit_4edge` book as CONSTITUENTS (2026-07-16). Without the guard, a stale promotion row would double-
count that edge's risk (once via the book, once via the stale standalone promotion)."""
import datetime as dt

from fxhnt import registry
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.allocation_ingest import record_allocation
from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO


def _seed_nav(repo, sid, at, days=60):
    dates = [(dt.date(2026, 5, 1) + dt.timedelta(days=i)).isoformat() for i in range(days)]
    for i, dd in enumerate(dates):
        repo.upsert_rows([NavRowDTO(strategy_id=sid, date=dd,
                                    ret=(0.01 if (i % 2) == 0 else -0.008), nav=1.0)], at=at)


def test_stale_promotion_of_declared_constituent_excluded_from_deploy_set(tmp_path, monkeypatch):
    dsn = f"sqlite:///{tmp_path}/guard.db"
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    at = dt.datetime(2026, 7, 16, 3, 0)

    # "still_deployable": promoted AND its registry entry STILL declares promote_on_pass -> included.
    monkeypatch.setitem(registry.STRATEGY_REGISTRY, "still_deployable",
                        {"display_name": "Still deployable", "sleeve": "x", "tier": "research",
                         "gate_spec": {}, "promote_on_pass": True})
    # "now_constituent": promoted historically (stale row), but promote_on_pass was since REMOVED (folded
    # into a book as a constituent) -> must be excluded despite the persisted promotion.
    monkeypatch.setitem(registry.STRATEGY_REGISTRY, "now_constituent",
                        {"display_name": "Now a book constituent", "sleeve": "x", "tier": "research",
                         "gate_spec": {}})  # no promote_on_pass key at all

    repo.promote("still_deployable", at)
    repo.promote("now_constituent", at)
    _seed_nav(repo, "still_deployable", at)
    _seed_nav(repo, "now_constituent", at)

    out = record_allocation(dsn, equity=20_000.0)

    assert "now_constituent" not in out                      # excluded: no allocation dollars at all
    assert "now_constituent" not in repo.current_allocation()  # and no recorded row
    # the still-declared promotion is considered (present in the recorded allocation, weight may be 0/nonzero
    # depending on the sizing engine — the guard only cares that it was NOT silently dropped).
    assert "still_deployable" in repo.current_allocation()


def test_deploy_sids_guard_logic_directly(monkeypatch):
    """Narrower unit check on the exact guard expression, independent of the sizing engine: only a promoted
    sid whose registry entry still declares promote_on_pass should union into deploy_sids."""
    monkeypatch.setitem(registry.STRATEGY_REGISTRY, "still_deployable",
                        {"tier": "research", "promote_on_pass": True})
    monkeypatch.setitem(registry.STRATEGY_REGISTRY, "now_constituent",
                        {"tier": "research"})

    class _FakeRepo:
        def promoted_strategy_ids(self):
            return {"still_deployable", "now_constituent"}

    repo = _FakeRepo()
    deploy_sids = {sid for sid, m in registry.STRATEGY_REGISTRY.items() if m.get("tier") == "deploy"} \
        | {sid for sid in repo.promoted_strategy_ids()
           if registry.STRATEGY_REGISTRY.get(sid, {}).get("promote_on_pass")}

    assert "still_deployable" in deploy_sids
    assert "now_constituent" not in deploy_sids
