"""Web smoke for the simplified cockpit Backtest: /paper/sim renders a tiny book/capital/period form and
lazy-loads the precomputed real-cost (MEASURED) curve from /paper/sim/run. The configurable-cost SANDBOX is
GONE — no cost (bps) slider, no measured/flat cost-mode select, no sleeve-composition presets/checkboxes, no
Kelly slider, no controller select. The cost is a measured FACT, not a knob (drag it to 0 and you'd resurrect
the +7097% mirage). When the measured artifact has not been precomputed the page degrades to a graceful
'precomputing' note (never the flat slider, never a 500). Bybit is the cockpit's only venue (Task 7a removed
the Binance venue toggle + the binance-vs-bybit "compare" view — see test_cockpit_bybit_only.py).

The measured-curve behaviour (capital-scale, period-window, the measured marker, cache-absent note) is
covered in test_backtest_measured_single_book.py; this file is the page-skin / form / routing smoke.
NO network — in-memory SQLite repos, seeded directly.
"""
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


def _seeded_repo() -> PaperRepo:
    """A repo with ~120 days of precomputed BYBIT per-sleeve returns. No measured cache, so the single-book
    Backtest shows the graceful 'precomputing' note — the honest curve is read, never recomputed on the
    request path."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 22, tzinfo=dt.UTC)
    base = dt.date(2026, 1, 1)
    sleeves = {"crypto_tstrend": 0.004, "unlock": 0.0025, "positioning": 0.0015,
               "xsfunding": 0.0008}
    for i in range(120):
        d = (base + dt.timedelta(days=i)).isoformat()
        for sleeve, amp in sleeves.items():
            ret = amp * (1.0 if (i + hash(sleeve)) % 3 else -0.8)
            repo.upsert_bybit_sleeve_ret(d, sleeve, ret, at=at)
    return repo


def _client(repo: PaperRepo) -> TestClient:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    return TestClient(create_app(fwd, paper_repo=repo))


def test_sim_page_renders_book_capital_period_form_and_lazy_loads_result() -> None:
    """The page paints the tiny book/capital/period form WITHOUT running anything — the curve is fetched
    lazily by HTMX from /paper/sim/run on load. The configurable-cost sandbox controls are absent."""
    c = _client(_seeded_repo())
    r = c.get("/paper/sim")
    assert r.status_code == 200
    assert 'id="sim-form"' in r.text                       # the (tiny) config panel
    # the honest knobs: book/capital/period only.
    assert 'name="capital"' in r.text and 'name="start"' in r.text and 'name="end"' in r.text
    # the sandbox knobs are GONE.
    assert 'type="range"' not in r.text                    # no Kelly / cost sliders
    assert 'name="cost_bps"' not in r.text and 'name="cost_mode"' not in r.text
    assert 'name="sleeve"' not in r.text and 'name="kelly"' not in r.text
    assert 'name="controller"' not in r.text and "sim-preset" not in r.text
    # the result is lazy-loaded, not computed inline.
    assert 'id="sim-result-wrap"' in r.text
    assert 'hx-get="/paper/sim/run' in r.text and 'hx-trigger="load"' in r.text
    # the form refreshes via HTMX-NATIVE wiring on change (NOT a fragile inline <script> — the live bug was the
    # IIFE never executing in this template's content path, so adjusting capital/period did nothing).
    assert 'hx-trigger="submit, change' in r.text           # Enter/submit AND field change re-fetch (htmx catches Enter → no native page reload)
    assert 'keyup changed delay' in r.text                  # capital updates live as you TYPE (debounced), not just on blur
    assert 'type="hidden" name="book"' in r.text            # book travels with the htmx GET
    assert "<svg" not in r.text and "Sharpe" not in r.text


def test_sim_run_cache_absent_shows_pending_note_not_flat() -> None:
    """The default lazy-load fragment, with no measured artifact seeded, is the graceful 'precomputing' note —
    NOT a flat what-if slider, NOT a 500."""
    c = _client(_seeded_repo())
    r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
    assert r.status_code == 200
    assert "precomputing" in r.text.lower() and "nightly" in r.text.lower()
    assert 'data-cost-mode="flat"' not in r.text


def test_sim_page_works_with_empty_repo() -> None:
    repo = PaperRepo("sqlite://")
    repo.migrate()
    c = _client(repo)
    r = c.get("/paper/sim")
    assert r.status_code == 200
    assert 'id="sim-form"' in r.text


def test_sim_run_capital_field_is_lenient_never_422() -> None:
    """Regression: a cleared / comma-formatted / odd capital must NOT 422 the backtest — the run endpoint
    parses capital leniently (empty/garbage -> default)."""
    c = _client(_seeded_repo())
    for cap in ["", "100,000", "250000", "abc", "  50000  "]:
        r = c.get("/paper/sim/run", params={"capital": cap, "book": "bybit_4edge"})
        assert r.status_code == 200, f"capital={cap!r} should render, not 422"


def test_sim_page_result_url_capital_is_plain_decimal_not_scientific() -> None:
    """The lazy-load hx-get must format capital as a plain decimal (NOT :g scientific like 1e+06, whose '+'
    decodes to a space in the query string and 422s)."""
    c = _client(_seeded_repo())
    html = c.get("/paper/sim").text
    m = re.search(r'/paper/sim/run\?capital=([^&"]+)', html)
    assert m, "result_url with capital not found"
    cap = m.group(1)
    assert "e" not in cap.lower() and "+" not in cap, f"capital must be plain decimal, got {cap!r}"


def test_sim_defaults_to_bybit_deploy_book() -> None:
    """The Backtest must DEFAULT to the Bybit 4-edge deploy book. So a bare /paper/sim leads with the honest
    deployable curve: the form data-book + the lazy-load result_url are bybit."""
    c = _client(_seeded_repo())
    html = c.get("/paper/sim").text
    assert 'data-book="bybit_4edge"' in html               # the form runs the bybit book by default
    m = re.search(r'hx-get="(/paper/sim/run\?[^"]+)"', html)
    assert m, "lazy-load result URL not found"
    result_url = m.group(1)
    assert "book=bybit_4edge" in result_url


def test_sim_page_title_is_backtest_not_paper_sim() -> None:
    """The page title is 'Backtest' (consistent with the nav), not the old 'Paper sim'."""
    c = _client(_seeded_repo())
    html = c.get("/paper/sim").text
    assert "<title>Backtest · fxhnt</title>" in html
    assert "Paper sim" not in html


def test_sim_page_caption_is_honest_real_cost_not_configurable_cost() -> None:
    """The subtitle reflects the honest real-cost framing in PLAIN language (no jargon block): a what-if on
    capital & period at real trading cost. The old 'choose ... cost' / sleeves sandbox prose stays gone."""
    c = _client(_seeded_repo())
    html = c.get("/paper/sim").text
    assert "a what-if on capital" in html                  # the what-if framing (plain subtitle)
    assert "real trading cost" in html                     # honest-cost framing, plain (no "taker fee"/"L1")
    assert "choose book / capital / sleeves / period / cost" not in html   # the old sandbox prose is gone
    assert "configurable what-if" not in html


def test_caption_band_aid_is_gone() -> None:
    """The old special-case "≈ -36% / see Compare A/B" caption band-aid is REMOVED (both the Binance venue
    and the Compare A/B view are gone as of Task 7a). With no measured artifact the single-book view shows
    the graceful pending note (not the old caption, not the flat slider)."""
    c = _client(_seeded_repo())
    r = c.get("/paper/sim/run", params={"book": "bybit_4edge", "capital": 100000})
    assert r.status_code == 200
    assert "real measured costs" not in r.text and "-36%" not in r.text
    assert "Compare A/B" not in r.text
    assert "precomputing" in r.text.lower()
    assert 'data-cost-mode="flat"' not in r.text


def test_capital_input_does_not_reject_round_numbers() -> None:
    """Regression: min=1 step=1000 made the browser reject 100000 (not 1+n·1000) with 'enter a valid number'
    and block submit. The capital input must use step='any' so any amount is accepted client-side."""
    c = _client(_seeded_repo())
    html = c.get("/paper/sim").text
    m = re.search(r'<input[^>]*name="capital"[^>]*>', html)
    assert m, "capital input not found"
    tag = m.group(0)
    assert 'step="any"' in tag and 'step="1000"' not in tag, tag


def test_sim_book_switch_routes_all_200() -> None:
    """The 2-way book switch (Bybit deploy / Bybit levered shadow) renders; a stale binance_combined or
    compare book param (the removed Task-7a surfaces) degrades gracefully to the default rather than 500ing."""
    c = _client(_seeded_repo())
    for book in ("bybit_4edge", "bybit_4edge_levered"):
        assert c.get("/paper/sim", params={"book": book}).status_code == 200
    for stale_book in ("binance_combined", "compare"):
        r = c.get("/paper/sim", params={"book": stale_book})
        assert r.status_code == 200
        assert 'data-book="bybit_4edge"' in r.text          # degraded to the default deploy book


def test_sim_shows_the_levered_shadow_pill_and_runs_gracefully() -> None:
    """The Backtest exposes the levered shadow as a 4th book pill, and its lazy /run renders gracefully (the
    measured pending note when the levered cache is absent — never a 500, never a flat slider)."""
    c = _client(_seeded_repo())
    page = c.get("/paper/sim", params={"book": "bybit_4edge_levered"})
    assert page.status_code == 200
    assert "Bybit 4-edge levered" in page.text and "shadow" in page.text
    assert 'data-book="bybit_4edge_levered"' in page.text or "book=bybit_4edge_levered" in page.text
    run = c.get("/paper/sim/run", params={"book": "bybit_4edge_levered", "capital": 100000})
    assert run.status_code == 200
    assert "precomputing" in run.text.lower() and 'data-cost-mode="flat"' not in run.text


_SIM_ABS_URL = re.compile(r"https?://(?!www\.w3\.org/)[^\s\"'<>]+")


def test_sim_page_is_leak_clean() -> None:
    hits = _SIM_ABS_URL.findall(_client(_seeded_repo()).get("/paper/sim").text)
    assert not hits, f"leak-clean violation: {hits}"
