"""Runtime staleness/health axis (forward_health) — the anti-silent-rot backbone that would have caught the
2-week vrp rot. Non-vacuous: STALE-by-source, STALE-by-freeze (forward 'updates' but the frozen input is
stale), NO_REF, the observed/dormant carve-out, end-to-end persistence, and the LOUD cockpit badge."""
from __future__ import annotations

import datetime as dt

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from fxhnt.adapters.persistence.cockpit_models import CockpitBase, XspOptionBarRow
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_health import (
    BROKEN,
    HEALTHY,
    NO_REF,
    STALE,
    compute_health,
    evaluate_health,
)
from fxhnt.application.forward_models import ForwardSummary

_RUN = dt.date(2026, 7, 14)


# ------------------------------------------------------------------ compute_health (pure state machine)
def test_recompute_track_fresh_is_healthy():
    v = compute_health("sixtyforty", record_mode="recompute", is_reconciliation=False,
                       as_of="2026-07-13", ref_available=False, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0="2026-01-01")
    assert v.health == HEALTHY and v.stale_days <= 1


def test_recompute_track_stale_source_data_flags_stale_with_age():
    v = compute_health("bybit_4edge", record_mode="recompute", is_reconciliation=True,
                       as_of="2026-06-20", ref_available=True, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0="2026-01-01")
    assert v.health == STALE and v.stale_days > 3 and "source data" in v.reason


def test_freeze_track_stale_even_when_forward_updated():
    """The exact vrp failure mode: as_of looks FRESH (forward 'updates') but the frozen xsp_option_bars max
    date is weeks behind → STALE via the freeze axis, not source-data."""
    v = compute_health("vrp", record_mode="recompute", is_reconciliation=True,
                       as_of="2026-07-13",                 # forward looks fresh (1 business day)
                       ref_available=True, freeze_max_date="2026-06-15",   # frozen input is weeks stale
                       run_date=_RUN, threshold=3, anchor_t0="2026-01-01")
    assert v.health == STALE and v.stale_days > 3 and "freeze table" in v.reason


def test_reconciliation_track_without_ref_is_no_ref():
    v = compute_health("unlock", record_mode="recompute", is_reconciliation=True,
                       as_of="2026-07-13", ref_available=False, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0="2026-01-01")
    assert v.health == NO_REF and "reconcile" in v.reason.lower()


def test_observed_track_is_dormant_tolerant():
    """Observed (externally-driven) twins are legitimately dormant when execution is suspended — never
    STALE/NO_REF, even with an ancient as_of and no ref."""
    v = compute_health("vrp_exec", record_mode="observed", is_reconciliation=True,
                       as_of="2025-01-01", ref_available=False, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0=None)
    assert v.health == HEALTHY


def test_track_with_no_data_is_not_stale():
    """A GENUINELY new track: anchor t0=today, no forward data yet → HEALTHY (the legitimate carve-out)."""
    v = compute_health("newtrack", record_mode="recompute", is_reconciliation=False,
                       as_of=None, ref_available=True, freeze_max_date=None, run_date=_RUN, threshold=3,
                       anchor_t0=_RUN.isoformat())
    assert v.health == HEALTHY


def test_gated_recompute_track_with_old_anchor_and_no_data_is_stale():
    """The hole this closes: a reconciliation-gated recompute track whose anchor t0 is OLDER than the stale
    threshold but which has produced ZERO forward data (no summary row) and an empty freeze table must NOT
    read HEALTHY off the "no data yet" brand-new-track carve-out — it is silently broken-from-inception."""
    v = compute_health("vrp", record_mode="recompute", is_reconciliation=True,
                       as_of=None, ref_available=True, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0="2026-06-01")
    assert v.health == STALE and v.stale_days > 3
    assert "t0=2026-06-01" in v.reason


def test_recompute_track_with_no_anchor_at_all_is_not_healthy():
    """A live/gated recompute track with NO anchor at all (should have one) is a real signal, never a
    silent HEALTHY."""
    v = compute_health("vrp", record_mode="recompute", is_reconciliation=True,
                       as_of=None, ref_available=True, freeze_max_date=None,
                       run_date=_RUN, threshold=3, anchor_t0=None)
    assert v.health == STALE
    assert "anchor" in v.reason.lower()


# ------------------------------------------------------------------ evaluate_health (end-to-end persistence)
def _seed_summary(repo: ForwardNavRepo, sid: str, as_of: str, at: dt.datetime) -> None:
    repo.upsert_summary(ForwardSummary(strategy_id=sid, as_of=as_of, days=25, nav=1.0,
                                       total_return=0.0, sharpe=0.0, maxdd=0.0),
                        gate_status="WAIT", gate_reason="building", at=at)


def _seed_freeze_bars(dsn: str, rows: list[dict], at: dt.datetime) -> None:
    """Insert rows directly into the kept `xsp_option_bars` table (no VRP repo class involved) — mirrors
    what the retired XspOptionBarsRepo.migrate()/upsert_bars() used to do, for the freeze-staleness axis."""
    kw = {} if dsn.startswith("postgresql") else {"connect_args": {"check_same_thread": False}}
    engine = create_engine(dsn, **kw)
    CockpitBase.metadata.create_all(engine)
    with Session(engine) as s:
        for r in rows:
            s.merge(XspOptionBarRow(
                osi_symbol=r["osi_symbol"], date=dt.date.fromisoformat(r["date"]),
                expiry=dt.date.fromisoformat(r["expiry"]), strike=float(r["strike"]),
                right=r["right"], close=float(r["close"]), src_updated_at=at))
        s.commit()


def _freeze_track_entry() -> dict:
    """A synthetic opra-pit-sourced registry entry — VRP was the only real track ever wired to the freeze
    axis (`_FREEZE_SOURCES = {"opra-pit"}`), and it is now fully deleted, but the axis itself is strategy-
    agnostic and must stay covered generically (per-track monkeypatch, mirrors test_forward_definition.py's
    `t_ov` pattern)."""
    return {"display_name": "Freeze-axis test track", "sleeve": "test",
            "gate_spec": {"gate_type": "reconciliation"},
            "definition": {"params": {"source": "opra-pit"}, "record_mode": "recompute"}}


def test_evaluate_health_persists_and_catches_stale_freeze(tmp_path, monkeypatch):
    from fxhnt.registry import STRATEGY_REGISTRY

    monkeypatch.setitem(STRATEGY_REGISTRY, "t_freeze", _freeze_track_entry())
    dsn = f"sqlite:///{tmp_path}/health.db"
    at = dt.datetime(2026, 7, 14, 23, 30)
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    # t_freeze: fresh forward summary but a STALE frozen table (the incident) → must read STALE.
    _seed_summary(repo, "t_freeze", "2026-07-13", at)
    # sixtyforty: recompute, fresh, absolute gate → HEALTHY.
    _seed_summary(repo, "sixtyforty", "2026-07-13", at)
    # unlock: fresh forward data (isolates the NO_REF path below from the separate no-anchor/no-data STALE
    # check — this test seeds no forward_anchor rows at all, so without real forward data unlock would hit
    # that check first).
    _seed_summary(repo, "unlock", "2026-07-13", at)
    # seed the freeze table with a max date weeks behind the run date.
    _seed_freeze_bars(dsn, [{"osi_symbol": "XSP   260620P00470000", "date": "2026-06-15",
                             "expiry": "2026-07-17", "strike": 470.0, "right": "P", "close": 1.0}], at)

    verdicts = {v.strategy_id: v for v in evaluate_health(dsn, at=at)}
    assert verdicts["t_freeze"].health == STALE and verdicts["t_freeze"].stale_days > 3
    assert "freeze table" in verdicts["t_freeze"].reason
    assert verdicts["sixtyforty"].health == HEALTHY
    # unlock is a reconciliation recompute track with no backtest ref seeded → NO_REF (M6 invariant).
    assert verdicts["unlock"].health == NO_REF
    # persisted to strategy_health, readable by the dashboard.
    persisted = {h.strategy_id: h for h in repo.all_health()}
    assert persisted["t_freeze"].health == STALE and persisted["t_freeze"].stale_days > 3


def test_evaluate_health_replaces_snapshot_on_recovery(tmp_path, monkeypatch):
    """A track that recovers must CLEAR its old STALE row (replace-all semantics)."""
    from fxhnt.application.forward_models import BacktestSummary
    from fxhnt.registry import STRATEGY_REGISTRY

    monkeypatch.setitem(STRATEGY_REGISTRY, "t_freeze", _freeze_track_entry())
    dsn = f"sqlite:///{tmp_path}/rec.db"
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    # run 1: freeze stale → t_freeze STALE
    at1 = dt.datetime(2026, 7, 14, 23, 30)
    # a backtest ref so a recovered t_freeze resolves to HEALTHY (not NO_REF) — isolates the STALE→clear
    # behavior.
    repo.upsert_backtest_summary(BacktestSummary(strategy_id="t_freeze", as_of="2026-06-01", cagr=0.2,
                                 ann_vol=0.1, sharpe=1.0, max_drawdown=-0.05, passed=True, dsr=0.9,
                                 is_sharpe=1.0, oos_sharpe=0.9, pvalue=0.01), at1)
    _seed_summary(repo, "t_freeze", "2026-07-13", at1)
    _seed_freeze_bars(dsn, [{"osi_symbol": "X", "date": "2026-06-15", "expiry": "2026-07-17",
                             "strike": 470.0, "right": "P", "close": 1.0}], at1)
    assert {v.strategy_id: v for v in evaluate_health(dsn, at=at1)}["t_freeze"].health == STALE
    # run 2: freeze advances to yesterday → t_freeze HEALTHY, the old STALE row cleared
    _seed_freeze_bars(dsn, [{"osi_symbol": "Y", "date": "2026-07-13", "expiry": "2026-07-17",
                             "strike": 470.0, "right": "P", "close": 1.0}], at1)
    _seed_summary(repo, "t_freeze", "2026-07-13", at1)
    assert {v.strategy_id: v for v in evaluate_health(dsn, at=at1)}["t_freeze"].health == HEALTHY
    assert {h.strategy_id: h for h in repo.all_health()}["t_freeze"].health == HEALTHY


# ------------------------------------------------------------------ LOUD cockpit badge (rendering)
def test_cockpit_renders_stale_badge(tmp_path):
    from fastapi.testclient import TestClient

    from fxhnt.adapters.persistence.cockpit_models import StrategyHealthRow
    from fxhnt.adapters.web.app import create_app

    dsn = f"sqlite:///{tmp_path}/render.db"
    repo = ForwardNavRepo(dsn)
    repo.migrate()
    at = dt.datetime(2026, 7, 14)
    # NOTE: uses `multistrat` (a visible recompute book) rather than `vrp` — vrp is ARCHIVED (hidden from the
    # cockpit as of 2026-07-15), so it never renders a badge on the Overview. The STALE-badge RENDERING is
    # strategy-agnostic; this asserts the LOUD red badge shows for any visible track with a STALE health row.
    repo.replace_health([StrategyHealthRow(strategy_id="multistrat", health="STALE", stale_days=14,
                                           reason="freeze table stale 14 business days")], at)
    r = TestClient(create_app(repo)).get("/")
    assert r.status_code == 200
    assert "STALE 14d" in r.text                 # the LOUD red badge, distinct from a healthy WAIT
    # the FleetRow carries the health field through to the detail page too.
    d = TestClient(create_app(repo)).get("/strategy/multistrat")
    assert d.status_code == 200 and "STALE 14d" in d.text


def test_fleet_row_defaults_healthy_without_health_table(tmp_path):
    """A pre-migration DB (no strategy_health rows) → every FleetRow defaults HEALTHY, never a false badge."""
    from fxhnt.application.dashboard_service import DashboardService

    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/def.db")
    repo.migrate()
    rows = DashboardService(repo).fleet()
    assert rows and all(f.health == HEALTHY and f.health_stale_days == 0 for f in rows)


def test_broken_state_constant_is_wired():
    """BROKEN is a real state in the enum (a track whose health can't be computed is not healthy)."""
    assert BROKEN == "BROKEN"
