"""Ingest reads each track's DB record (the engine's `forward_summary` + `forward_nav` rows), computes the
gate, and writes the verdict back. A track with no DB summary yet is skipped, never aborting the batch."""
from __future__ import annotations

import datetime as dt

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_ingest import ForwardIngestService
from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO
from fxhnt.application.forward_models import ForwardSummary
from fxhnt.application.reconciliation_gate import BacktestReference


def _setup() -> ForwardNavRepo:
    repo = ForwardNavRepo("sqlite://")
    repo.migrate()
    return repo


def _seed_days_list(repo: ForwardNavRepo, sid: str, rets: list[float], at: dt.datetime, *,
                    start: dt.date = dt.date(2026, 6, 1), gap_after: int | None = None) -> None:
    """Seed the DB as the engine (`forward_engine.run_track`) would for a days-list track: `forward_nav`
    rows + a `forward_summary` computed from them (nav compounds the daily returns; total_return is the
    cumulative fractional return). `gate_status`/`gate_reason` start at the engine's WAIT placeholder —
    `ForwardIngestService.ingest` is what computes + overwrites the real verdict."""
    rows: list[NavRowDTO] = []
    nav = 1.0
    d = start
    for i, r in enumerate(rets):
        nav *= 1.0 + r
        rows.append(NavRowDTO(strategy_id=sid, date=d.isoformat(), ret=r, nav=nav))
        d = d + dt.timedelta(days=1)
        if gap_after is not None and i == gap_after:
            d = d + dt.timedelta(days=1)
    repo.upsert_rows(rows, at)
    repo.upsert_summary(
        ForwardSummary(strategy_id=sid, as_of=rows[-1].date, days=len(rows), nav=nav,
                       total_return=nav - 1.0, sharpe=0.0, maxdd=0.0),
        gate_status="WAIT", gate_reason="building", at=at)


def test_ingest_populates_summaries_and_rows() -> None:
    repo = _setup()
    at = dt.datetime(2026, 6, 14, 23, 30)
    _seed_days_list(repo, "xsfunding", [0.01, 0.0099], at)                        # 2 forward days
    _seed_days_list(repo, "sixtyforty", [-0.0007, 0.0001, -0.0002, 0.0003, -0.0001], at)  # 5 forward days
    n = ForwardIngestService(repo).ingest(at=at)
    assert n == 2                                              # two DB-recorded trackers evaluated
    summaries = {r.strategy_id: r for r in repo.all_summaries()}
    assert "xsfunding" in summaries and "sixtyforty" in summaries
    assert summaries["xsfunding"].gate_status == "WAIT"  # 2 < 20 days
    assert len(repo.nav_history("xsfunding")) == 2


def test_ingest_skips_tracks_with_no_db_summary() -> None:
    repo = _setup()
    at = dt.datetime(2026, 6, 14, 23, 30)
    _seed_days_list(repo, "xsfunding", [0.01, 0.0099], at)   # only xsfunding has a DB record
    n = ForwardIngestService(repo).ingest(at=at)
    assert n == 1


class _StubBacktestRef:
    """Injected backtest reference for the reconciliation gate — keeps ingest pure + network-free in tests."""

    def __init__(self, refs: dict[str, BacktestReference]) -> None:
        self._refs = refs

    def reference(self, strategy_id, forward_days):
        return self._refs.get(strategy_id)


def test_ingest_reconciliation_gate_passes_when_forward_tracks_backtest() -> None:
    repo = _setup()
    at = dt.datetime(2026, 6, 20, 23, 30)
    # 22 clean days (past bybit_4edge's 21-day min window), ~+4.5% — tracks/outperforms the backtest.
    _seed_days_list(repo, "bybit_4edge", [0.002] * 22, at)
    bt = _StubBacktestRef({"bybit_4edge": BacktestReference(expected_window_return=0.032)})
    ForwardIngestService(repo, backtest_ref=bt).ingest(at=at)
    summaries = {r.strategy_id: r for r in repo.all_summaries()}
    assert summaries["bybit_4edge"].gate_status == "PASS"


def test_ingest_reconciliation_gate_catches_the_mirage() -> None:
    # The KEY end-to-end test: backtest "should" be strongly positive, but the forward track is strongly
    # negative (Binance's +7097% backtest is -36% live) → WAIT past the min window, never PASS.
    repo = _setup()
    at = dt.datetime(2026, 6, 24, 23, 30)
    # 22 clean days (past the 21-day min), ~-36% — a sustained collapse that contradicts the backtest.
    _seed_days_list(repo, "bybit_4edge", [-0.02] * 22, at)
    bt = _StubBacktestRef({"bybit_4edge": BacktestReference(expected_window_return=1.0)})
    ForwardIngestService(repo, backtest_ref=bt).ingest(at=at)
    summaries = {r.strategy_id: r for r in repo.all_summaries()}
    s = summaries["bybit_4edge"]
    assert s.gate_status == "WAIT" and "diverg" in s.gate_reason.lower()


def test_ingest_reconciliation_gate_flags_execution_gap() -> None:
    repo = _setup()
    at = dt.datetime(2026, 6, 20, 23, 30)
    _seed_days_list(repo, "bybit_4edge", [0.002] * 22, at, gap_after=7)  # >=21 days, but a hole in the record
    bt = _StubBacktestRef({"bybit_4edge": BacktestReference(expected_window_return=0.032)})
    ForwardIngestService(repo, backtest_ref=bt).ingest(at=at)
    summaries = {r.strategy_id: r for r in repo.all_summaries()}
    s = summaries["bybit_4edge"]
    assert s.gate_status == "WAIT" and ("gap" in s.gate_reason.lower() or "execution" in s.gate_reason.lower())


def test_ingest_isolates_a_failing_tracker(tmp_path, monkeypatch):
    """A raising tracker must not abort the batch — later tracks in registry-iteration order still get
    ingested. STRATEGY_REGISTRY iterates `xsfunding` before `unlock`, so `xsfunding` is the one made to
    raise here (the failing tracker must iterate FIRST for this test to prove anything)."""
    from fxhnt.application import forward_ingest as fi

    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/iso.db")
    repo.migrate()
    at = dt.datetime(2026, 7, 8, 23, 30)
    # seed two reconciliation tracks with DB summaries + nav rows
    for sid in ("xsfunding", "unlock"):
        repo.upsert_rows([NavRowDTO(strategy_id=sid, date="2026-06-01", ret=0.001, nav=1.001)], at=at)
        repo.upsert_summary(ForwardSummary(strategy_id=sid, as_of="2026-06-01", days=1, nav=1.001,
                                           total_return=0.001, sharpe=0.0, maxdd=0.0),
                            gate_status="WAIT", gate_reason="building", at=at)
    # make the gate verdict RAISE for the first-iterated of the two, leave the other working
    real = fi._gate_verdict

    def flaky(summary, rows, gate_spec, backtest_ref, sid):
        if sid == "xsfunding":
            raise RuntimeError("boom")
        return real(summary, rows, gate_spec, backtest_ref, sid)

    monkeypatch.setattr(fi, "_gate_verdict", flaky)
    n = fi.ForwardIngestService(repo).ingest(at=at)
    assert n == 1  # the failing tracker is skipped; the other is still ingested (batch not aborted)


def test_ingest_computes_gate_from_db_record(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/ing.db")
    repo.migrate()
    at = dt.datetime(2026, 7, 8, 23, 30)
    sid = "xsfunding"   # a reconciliation-gate track
    repo.upsert_rows([NavRowDTO(strategy_id=sid, date=f"2026-06-{d:02d}", ret=0.001, nav=1.0 + 0.001 * i)
                      for i, d in enumerate(range(1, 6))], at=at)
    repo.upsert_summary(ForwardSummary(strategy_id=sid, as_of="2026-06-05", days=5, nav=1.005,
                                       total_return=0.005, sharpe=0.5, maxdd=-0.001),
                        gate_status="WAIT", gate_reason="building", at=at)
    n = ForwardIngestService(repo).ingest(at=at)
    assert n >= 1
    got = {s.strategy_id: s for s in repo.all_summaries()}[sid]
    # 5 days < min_forward_days(14) → reconciliation gate stays WAIT (WAIT-safe), verdict written from DB
    assert got.gate_status == "WAIT"


def test_ingest_stamps_policy_on_verdict(tmp_path):
    from fxhnt.application.gate_policy import POLICY, POLICY_VERSION, policy_hash

    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/st.db")
    repo.migrate()
    at = dt.datetime(2026, 7, 9, 23, 30)
    sid = "xsfunding"
    repo.upsert_rows([NavRowDTO(strategy_id=sid, date="2026-06-01", ret=0.001, nav=1.001)], at=at)
    repo.upsert_summary(ForwardSummary(strategy_id=sid, as_of="2026-06-01", days=1, nav=1.001,
                                       total_return=0.001, sharpe=0.0, maxdd=0.0),
                        "WAIT", "building", at)
    ForwardIngestService(repo).ingest(at=at)
    row = {s.strategy_id: s for s in repo.all_summaries()}[sid]
    assert row.policy_version == POLICY_VERSION
    assert row.policy_hash == policy_hash(POLICY, POLICY_VERSION)
