"""Web smoke for the IBKR paper book (multistrat) in the cockpit Backtest (Task 1/D1): /paper/sim
generalizes beyond the Bybit-only venue to also render this recompute-replay book's honest-cost curve, read
from the PRECOMPUTED `sim_curve_ret` table (never recomputed on the request path) — mirrors the Bybit
measured-cost path's shape (curve + metrics + Live forward), with plain-language pill labels (never the raw
registry id "multistrat").

NOTE: vrp has been FULLY REMOVED from the codebase (shelved 2026-07-15, code+data deleted in the
`chore/remove-vrp-code` cleanup) — there is no longer a `STRATEGY_REGISTRY` entry, `_SIM_BOOKS` pill, or
recompute-replay support for it at all. This test file uses a synthetic non-existent book id
(`no-such-book`) as an honest stand-in to probe the SAME fallback behavior an archived/unknown book must
still get: never selectable, never rendered, always a graceful fallback to the default book (see the
archived-book tests below).

Asserts:
  * /paper/sim shows the multistrat pill labelled in plain language, not the raw id, and NO stray pill for an
    unknown book;
  * /paper/sim/run?book=multistrat renders a real curve + metrics from the precomputed sim_curve_ret series;
  * capital scales the curve linearly (same contract as the Bybit measured path);
  * the request path never recomputes the replay (a tripwire on ForwardStrategy.advance would fire if it did —
    proven implicitly: the seeded repo carries no build_strategy at all, so a recompute is structurally
    impossible; the curve still renders from sim_curve_ret alone);
  * the Live forward section renders for multistrat (its own forward track), not just bybit_4edge;
  * an unknown/archived book id is not selectable — the request falls back to the default book, never
    rendering that id;
  * absent precompute -> the graceful 'precomputing' note (never a 500, never the Bybit-specific caption).
NO network — in-memory SQLite repos, seeded directly.
"""
from __future__ import annotations

import datetime as dt
import json
import re

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 ForwardNavRow as NavRowDTO
from fxhnt.application.forward_models import ForwardSummary


def _seed_ibkr_curve(repo: PaperRepo, book: str, *, start: dt.date = dt.date(2023, 1, 1),
                     days: int = 250) -> None:
    """Persist a synthetic precomputed recompute-replay curve into sim_curve_ret — the nightly
    `_persist_track_backtest_ref` write /paper/sim reads for its IBKR books."""
    at = dt.datetime(2026, 7, 1, tzinfo=dt.UTC)
    for i in range(days):
        d = (start + dt.timedelta(days=i)).isoformat()
        ret = 0.002 * (1.0 if (i + hash(book)) % 3 else -0.7)
        repo.upsert_sim_curve_ret(book, d, ret, at=at)


def _seeded_paper_repo(*, books: tuple[str, ...] = ("multistrat", "no-such-book")) -> PaperRepo:
    repo = PaperRepo("sqlite://")
    repo.migrate()
    for b in books:
        _seed_ibkr_curve(repo, b)
    return repo


def _seeded_forward_repo(strategy_id: str = "multistrat") -> ForwardNavRepo:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    at = dt.datetime(2026, 6, 24)
    base = dt.date(2026, 4, 1)
    nav = 1.0
    rows = []
    for i in range(22):
        nav *= 1.0008
        rows.append(NavRowDTO(strategy_id=strategy_id,
                              date=(base + dt.timedelta(days=i)).isoformat(), ret=0.0008, nav=nav))
    fwd.upsert_rows(rows, at=at)
    sm = ForwardSummary(strategy_id=strategy_id, as_of="2026-04-22", days=22, nav=nav,
                        total_return=nav - 1.0, sharpe=0.9, maxdd=-0.015)
    fwd.upsert_summary(sm, gate_status="WAIT", gate_reason="22/20 forward days", at=at)
    return fwd


def _empty_forward_repo() -> ForwardNavRepo:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    return fwd


def _client(paper: PaperRepo, fwd: ForwardNavRepo | None = None) -> TestClient:
    return TestClient(create_app(fwd or _empty_forward_repo(), paper_repo=paper))


def _sim_data(html: str) -> dict:
    m = re.search(r'<script type="application/json" id="sim-data">(.*?)</script>', html, re.DOTALL)
    assert m, "embedded #sim-data JSON not found"
    return json.loads(m.group(1))


def test_sim_page_shows_multistrat_pill_and_hides_unknown_book() -> None:
    c = _client(_seeded_paper_repo())
    html = c.get("/paper/sim").text
    assert "ETF Portfolio" in html
    # a book id absent from `_SIM_BOOKS` (unknown or archived) must never get a pill on the Backtest page.
    assert 'data-book="no-such-book"' not in html and "book=no-such-book" not in html
    # the raw registry id must never leak as pill TEXT (it is fine as a query-string value).
    assert ">multistrat<" not in html and ">no-such-book<" not in html


def test_multistrat_book_is_selectable_and_lazy_loads() -> None:
    c = _client(_seeded_paper_repo())
    r = c.get("/paper/sim", params={"book": "multistrat"})
    assert r.status_code == 200
    assert 'data-book="multistrat"' in r.text
    assert 'hx-get="/paper/sim/run' in r.text and "book=multistrat" in r.text
    assert "<svg" not in r.text                            # curve is lazy-loaded, not inline on the page


def test_multistrat_run_renders_curve_and_metrics_from_precomputed_sim_curve_ret() -> None:
    c = _client(_seeded_paper_repo())
    r = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 100000})
    assert r.status_code == 200
    assert "<svg" in r.text and "CAGR" in r.text and "Sharpe" in r.text
    assert "Backtest (honest replay cost)" in r.text
    data = _sim_data(r.text)
    assert data["dates"] and data["equity"]
    assert 80000 < data["equity"][0] < 120000              # curve starts near the configured capital


def test_unknown_book_run_falls_back_and_never_renders_it() -> None:
    """A book id absent from `_SIM_BOOKS` (unknown or archived) makes `/paper/sim/run?book=no-such-book`
    fall back to the default book (bybit_4edge) — it renders that book's view (or the graceful pending
    note), never the unknown id's curve/label."""
    c = _client(_seeded_paper_repo())
    r = c.get("/paper/sim/run", params={"book": "no-such-book", "capital": 100000})
    assert r.status_code == 200                       # graceful fallback, never a 500
    assert 'data-book="no-such-book"' not in r.text   # the effective book is the default, not the unknown id


def test_ibkr_capital_scales_the_curve_linearly() -> None:
    c = _client(_seeded_paper_repo())
    small = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 50000})
    big = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 200000})
    e_small = _sim_data(small.text)["equity"][-1]
    e_big = _sim_data(big.text)["equity"][-1]
    assert abs(e_big / e_small - 4.0) < 0.01


def test_ibkr_run_never_recomputes_the_replay(monkeypatch) -> None:
    """Tripwire: the sim's IBKR path must read the precomputed sim_curve_ret series and never build/advance a
    ForwardStrategy on the request path."""
    import fxhnt.application.paper_sim as psim

    real_naive = psim.simulate_naive_eqwt
    calls = {"n": 0}

    def _spy(*a, **k):
        calls["n"] += 1
        return real_naive(*a, **k)
    monkeypatch.setattr(psim, "simulate_naive_eqwt", _spy)

    c = _client(_seeded_paper_repo())
    r = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 100000})
    assert r.status_code == 200
    assert _sim_data(r.text)["dates"]
    assert calls["n"] == 1                                 # the cheap compound-only pass, no strategy replay


def test_ibkr_view_shows_live_forward_track_with_gate() -> None:
    c = _client(_seeded_paper_repo(), _seeded_forward_repo("multistrat"))
    r = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 100000})
    assert r.status_code == 200
    assert "Live forward" in r.text
    assert "22/20 forward days" in r.text
    assert "WAIT" in r.text


def test_ibkr_missing_precompute_shows_pending_note_not_bybit_caption() -> None:
    repo = PaperRepo("sqlite://")
    repo.migrate()                                          # no sim_curve_ret rows for multistrat
    c = _client(repo)
    r = c.get("/paper/sim/run", params={"book": "multistrat", "capital": 100000})
    assert r.status_code == 200
    assert "precomputing" in r.text.lower() and "nightly" in r.text.lower()
    assert "Corwin" not in r.text and "L1 quoted spread" not in r.text   # never the Bybit-specific caption
    assert 'data-cost-mode="flat"' not in r.text


def test_sim_book_switch_covers_visible_books_and_excludes_unknown_book() -> None:
    c = _client(_seeded_paper_repo())
    # the three VISIBLE books each render their own page...
    for book in ("bybit_4edge", "bybit_4edge_levered", "multistrat"):
        r = c.get("/paper/sim", params={"book": book})
        assert r.status_code == 200 and f'data-book="{book}"' in r.text
    # ...but an unknown/archived book id is not selectable: it falls back to the default book (200, never it).
    r = c.get("/paper/sim", params={"book": "no-such-book"})
    assert r.status_code == 200 and 'data-book="no-such-book"' not in r.text
