"""The Overview/`/paper` headline is MATERIALIZED: a fresh `paper_nav_summary` row is served from a 1-row
read (NOT the full ~3650-row paper_nav), the read path self-heals when the summary is missing/stale, and ANY
failure in the materialized path falls back to the live compute (never a 500, numbers/curve never wrong). The
rendered headline numbers + curve are identical to the live compute on the same data. All in-memory — no DB,
no network."""
from __future__ import annotations

import datetime as dt

from fastapi.testclient import TestClient

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.web.app import create_app
from fxhnt.application.forward_models import BacktestSummary
from fxhnt.application.forward_models import ForwardNavRow as Row
from fxhnt.application.forward_models import ForwardSummary
from fxhnt.application.nav_summary import refresh_nav_summary

_AT = dt.datetime(2026, 6, 24, 12, 0)
_PTS = [100_000.0, 140_000.0, 120_000.0, 210_000.0, 300_000.0, 360_000.0, 393_955.0]


class _SpyRepo(PaperRepo):
    """A bybit PaperRepo that counts full-nav reads, so a test can assert the fast path never reads the
    ~3650-row series. Optionally forces `read_nav_summary` to raise (the fallback test)."""

    def __init__(self, dsn: str, *, raise_on_read: bool = False) -> None:
        super().__init__(dsn)
        self.nav_history_calls = 0
        self._raise_on_read = raise_on_read

    def nav_history(self):  # type: ignore[override]
        self.nav_history_calls += 1
        return super().nav_history()

    def read_nav_summary(self):  # type: ignore[override]
        if self._raise_on_read:
            raise RuntimeError("boom")
        return super().read_nav_summary()


def _seed_bybit(repo: PaperRepo) -> None:
    for i, eq in enumerate(_PTS):
        rd = dt.date(2021, 1, 1) + dt.timedelta(days=120 * i)
        repo.upsert_nav(rd.isoformat(), capital=100_000.0, realized=0.0, unrealized=0.0, equity=eq, at=_AT)


def _forward_repo() -> ForwardNavRepo:
    repo = ForwardNavRepo("sqlite://")
    repo.migrate()
    rows = [Row("bybit_4edge", f"2026-06-{5 + i:02d}", 0.004, 1.0 + 0.004 * (i + 1)) for i in range(8)]
    repo.upsert_rows(rows, _AT)
    repo.upsert_summary(
        ForwardSummary("bybit_4edge", "2026-06-12", 8, 1.032, 0.032, 1.4, -0.02),
        "WAIT", "building (8/14 days)", _AT)
    repo.upsert_backtest_summary(BacktestSummary(
        "bybit_4edge", "2026-05-30", cagr=0.30, ann_vol=0.20, sharpe=1.5, max_drawdown=-0.16,
        passed=True, dsr=0.7, is_sharpe=1.6, oos_sharpe=1.3, pvalue=0.01), _AT)
    return repo


def test_fresh_summary_served_without_full_nav_read() -> None:
    bybit = _SpyRepo("sqlite://")
    bybit.migrate()
    _seed_bybit(bybit)
    refresh_nav_summary(bybit)            # pre-materialize (the proactive nightly hook)
    bybit.nav_history_calls = 0                     # reset; the fast path must not touch the full nav
    client = TestClient(create_app(_forward_repo(), bybit_paper_repo=bybit))

    r = client.get("/")
    assert r.status_code == 200
    assert "393,955" in r.text                      # the real record, from the stored summary
    assert "<polyline" in r.text                    # the pre-rendered curve, from the stored summary
    assert bybit.nav_history_calls == 0, "fast path read the full nav instead of the 1-row summary"


def test_missing_summary_self_heals() -> None:
    bybit = _SpyRepo("sqlite://")
    bybit.migrate()
    _seed_bybit(bybit)                              # nav present, NO summary stored yet
    client = TestClient(create_app(_forward_repo(), bybit_paper_repo=bybit))

    r = client.get("/")
    assert r.status_code == 200
    assert "393,955" in r.text                      # self-heal refreshed + rendered the real record
    # The summary is now stored + versioned to the live latest run_date (so the next request is fast).
    stored = bybit.read_nav_summary()
    assert stored is not None and stored["nav_run_date"] == bybit.latest_nav_run_date()


def test_stale_summary_is_refreshed_not_served() -> None:
    bybit = _SpyRepo("sqlite://")
    bybit.migrate()
    _seed_bybit(bybit)
    # A STALE summary: an old version + deliberately WRONG numbers. The read path must detect the version
    # mismatch and refresh, so the page shows the REAL current record — never the stale stored numbers.
    bybit.upsert_nav_summary({
        "nav_run_date": "1999-01-01", "final_equity": 7.0, "total_return_pct": 0.0, "sharpe": 0.0,
        "max_dd_pct": 0.0, "since": "1999", "has_record": True, "curve_svg": "<svg></svg>"}, at=_AT)
    client = TestClient(create_app(_forward_repo(), bybit_paper_repo=bybit))

    r = client.get("/")
    assert r.status_code == 200
    assert "393,955" in r.text
    assert "$7" not in r.text                        # the stale wrong number is NOT served
    assert bybit.read_nav_summary()["nav_run_date"] == bybit.latest_nav_run_date()


def test_exception_falls_back_to_live_compute() -> None:
    bybit = _SpyRepo("sqlite://", raise_on_read=True)   # the materialized path always throws
    bybit.migrate()
    _seed_bybit(bybit)
    client = TestClient(create_app(_forward_repo(), bybit_paper_repo=bybit))

    r = client.get("/")
    assert r.status_code == 200                      # never a 500
    assert "393,955" in r.text                       # live compute still shows the real record
    assert "<polyline" in r.text
    assert bybit.nav_history_calls >= 1              # the fallback read the nav live


def test_paper_and_overview_show_the_same_headline() -> None:
    bybit = _SpyRepo("sqlite://")
    bybit.migrate()
    _seed_bybit(bybit)
    refresh_nav_summary(bybit)
    client = TestClient(create_app(_forward_repo(), bybit_paper_repo=bybit))

    home = client.get("/")
    paper = client.get("/paper/crypto?venue=bybit")
    assert home.status_code == 200 and paper.status_code == 200
    for needle in ("393,955", "<polyline"):
        assert needle in home.text and needle in paper.text
