"""Cockpit surfacing of the Positioning forward-tracked edge + the Bybit book sleeve breakdown:
  * the edges table includes the Positioning row (DashboardService.fleet returns it);
  * the /strategy/bybit_4edge detail page renders the 4-sleeve backtest breakdown (with positioning) from
    the precomputed bybit_sleeve_ret table;
  * single-sleeve strategies (positioning) get NO breakdown;
  * the new pages stay leak-clean (relative URLs only) for the nginx tailnet proxy.
All seeded in-memory — NO network, NO live DB.

(xvenue_carry was retired from forward-tracking 2026-07-07 — see registry.py; its cockpit assertions
were removed with it.)
"""
from __future__ import annotations

import datetime as dt
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

_ABS_URL = re.compile(r"https?://(?!www\.w3\.org/)[^\s\"'<>]+")
# The 4 book constituents. Each is a (drift, daily-vol) pair giving a REALISTIC, well-defined Sharpe with
# finite vol — so vol-targeting produces a sane annual return (~Sharpe×0.20) and a large raw cumulative (the
# 42× artifact the fix replaces). positioning is the standout (high Sharpe).
_BYBIT_SLEEVES = {
    "crypto_tstrend": (0.0010, 0.020),
    "unlock": (0.0009, 0.018),
    "xsfunding": (0.0007, 0.016),
    "positioning": (0.0030, 0.014),   # the standout edge (Sharpe ~ drift/vol × √365 ≈ 4)
}


def _seeded_paper_repo() -> PaperRepo:
    import random
    import zlib
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
    base = dt.date(2026, 1, 1)
    series = {}
    for sleeve, (drift, vol) in _BYBIT_SLEEVES.items():
        # DETERMINISTIC per-sleeve seed: zlib.crc32 is stable across processes, unlike hash() which is
        # PYTHONHASHSEED-salted — the salted seed made the vol-targeted-vs-raw assertion below flaky (it
        # tripped for a small fraction of process salts under the full suite).
        rng = random.Random(zlib.crc32(sleeve.encode()))
        series[sleeve] = [drift + rng.gauss(0.0, vol) for _ in range(400)]
    for i in range(400):
        d = (base + dt.timedelta(days=i)).isoformat()
        for sleeve in _BYBIT_SLEEVES:
            repo.upsert_bybit_sleeve_ret(d, sleeve, series[sleeve][i], at=at)
    return repo


def _client() -> TestClient:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()                       # registry seeds positioning + bybit_4edge (no nav rows)
    return TestClient(create_app(fwd, paper_repo=_seeded_paper_repo()))


def test_edges_table_includes_positioning() -> None:
    r = _client().get("/")
    assert r.status_code == 200
    assert "Positioning (contrarian long/short ratio)" in r.text


def test_bybit_4edge_detail_renders_sleeve_breakdown_with_positioning() -> None:
    r = _client().get("/strategy/bybit_4edge")
    assert r.status_code == 200
    assert "Sleeves" in r.text
    for sleeve in ("crypto_tstrend", "unlock", "xsfunding", "positioning"):
        assert sleeve in r.text                       # all 4 constituents listed
    # comparable: the label says each edge is shown on its own, and the column is annual not raw cumulative.
    assert "each shown on its own" in r.text
    assert "ann ret%" in r.text
    assert "total ret%" not in r.text
    # the breakdown table lists exactly the 4 book sleeves. Redesigned: the section is now a collapsed
    # <details><summary>Sleeves inside this book ...</summary> block rather than a standalone <h3>.
    breakdown = r.text.split("<summary>Sleeves inside this book", 1)[1]
    for sleeve in ("crypto_tstrend", "unlock", "xsfunding", "positioning"):
        assert sleeve in breakdown


def test_sleeve_breakdown_shows_vol_targeted_not_raw_42x() -> None:
    # The vol-targeted return must be DRAMATICALLY smaller than the misleading raw cumulative the old
    # breakdown showed (positioning's real raw cumulative is ~+4228% = 42×), and must track Sharpe × 0.20.
    from fxhnt.application.bybit_book_eval import vol_targeted_metrics

    repo = _seeded_paper_repo()
    series = repo.bybit_sleeve_returns()["positioning"]
    m = vol_targeted_metrics(series)
    raw_cum = 1.0
    for d in sorted(series):
        raw_cum *= (1.0 + series[d])
    raw_cum -= 1.0
    assert m["ann_return"] > 0.0
    assert m["ann_return"] < raw_cum                  # vol-targeted is far below the raw cumulative
    # same order as Sharpe × vol_target (the CAGR of a high-Sharpe vol-scaled series runs a bit above the
    # arithmetic Sharpe×vol; the precise ≈ relation at realistic Sharpe is covered in the unit test).
    assert 0.5 * m["sharpe"] * 0.20 < m["ann_return"] < 2.0 * m["sharpe"] * 0.20


def test_single_sleeve_strategy_has_no_book_breakdown() -> None:
    r = _client().get("/strategy/positioning")
    assert r.status_code == 200
    # single edges get the Backtest-reference card, NOT the multi-row book breakdown table.
    assert "<h3>Sleeves" not in r.text


def test_positioning_detail_shows_backtest_reference() -> None:
    # Redesigned: the old standalone "Backtest reference" <h3> is gone; a single-edge strategy with no
    # BacktestSummary row (positioning) now falls back to `backtest_reference` inside the same "Backtested
    # edge" stat card the book-level verdict uses — Sharpe and maxDD (vol-targeted @ 20%, same basis as the
    # sleeve breakdown) still render there.
    r = _client().get("/strategy/positioning")
    assert r.status_code == 200
    assert "Backtested edge" in r.text
    assert "Sharpe 2.9" in r.text                      # backtest_reference.sharpe rendered
    assert "maxDD -11%" in r.text                      # backtest_reference.max_dd_pct rendered


def test_strategy_without_backtest_source_omits_section() -> None:
    # bybit_4edge is a book (no single-edge reference); sixtyforty has backtest kind "none" (a benchmark
    # reference line, not an edge) — both render fine with NO Backtest-reference card and no error.
    c = _client()
    for sid in ("bybit_4edge", "sixtyforty"):
        r = c.get(f"/strategy/{sid}")
        assert r.status_code == 200
        assert "Backtest reference" not in r.text


def test_new_pages_are_leak_clean() -> None:
    c = _client()
    for href in ("/", "/strategy/bybit_4edge", "/strategy/positioning"):
        hits = _ABS_URL.findall(c.get(href).text)
        assert not hits, f"leak-clean violation on {href}: {hits}"


def test_short_window_stats_are_dimmed() -> None:
    # No forward nav rows are seeded, so every research tracker has 0 forward days < its gate min_days. In the
    # SSOT console the research section keeps these short-window forward Sharpes but renders them DIMMED with a
    # "· pre-gate" marker + an explanatory title, so a tiny-sample number can never read as authoritative.
    r = _client().get("/")
    assert r.status_code == 200
    assert "statistically provisional" in r.text                  # the explanatory title on the dimmed span
    assert "· pre-gate" in r.text                                 # the pre-gate marker on the dimmed forward Sharpe
    assert "section research" in r.text                           # the dimmed, not-the-fund research section
