"""Web smoke for the paper-replay scrubber: /paper/replay replays the BYBIT 4-edge deploy book (the
cockpit's only paper-book venue since Task 7a removed the Binance venue toggle); /paper/replay/at?date=<d>
renders the book at that date; empty history shows the backfill prompt.

The legacy bug this guards against: replay used to read the binance (legacy 2-edge) book regardless of the
deploy venue, so it showed a different, older, smaller book — missing the bybit 4-edge sleeves (positioning)."""
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
from fxhnt.domain.paper import Position


def _seeded_bybit_repo() -> PaperRepo:
    """The BYBIT 4-edge DEPLOY book: the 3 POSITION-based sleeves (crypto_tstrend, unlock, positioning) are
    persisted per run_date; carry (xsfunding) is return-defined, so it has NO scrubbable positions. Nav ends
    at ~$865k."""
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
    repo.upsert_nav("2026-06-22", capital=100_000.0, realized=0.0, unrealized=0.0,
                    equity=800_000.0, at=at)
    repo.replace_positions(
        "2026-06-23",
        [Position("crypto_tstrend", "BTCUSDT", 1.0, 100.0, "2026-06-23"),
         Position("unlock", "APTUSDT", -5.0, 8.0, "2026-06-23"),
         Position("positioning", "ETHUSDT", 2.0, 3000.0, "2026-06-23")],
        {"crypto_tstrend": 0.4, "unlock": 0.3, "positioning": 0.3}, at)
    repo.upsert_nav("2026-06-23", capital=100_000.0, realized=1000.0, unrealized=2000.0,
                    equity=865_000.0, at=at)
    return repo


def _bybit_client(bybit_repo: PaperRepo) -> TestClient:
    """Client with the BYBIT deploy book seeded (paper_repo, the sim's DB handle, is a separate empty repo —
    unused by replay)."""
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    binance = PaperRepo("sqlite://")
    binance.migrate()
    return TestClient(create_app(fwd, paper_repo=binance, bybit_paper_repo=bybit_repo))


# ---- Bybit 4-edge book replay ---------------------------------------------------------------------------

def test_replay_defaults_to_bybit_4edge_book() -> None:
    c = _bybit_client(_seeded_bybit_repo())
    r = c.get("/paper/replay")
    assert r.status_code == 200
    for sleeve in ("crypto_tstrend", "unlock", "positioning"):
        assert sleeve in r.text, f"bybit deploy sleeve {sleeve!r} missing from replay"
    assert "865,000" in r.text                    # the bybit deploy nav
    assert "<svg" in r.text                         # the equity curve
    assert 'type="range"' in r.text                # the scrubber


def test_replay_at_renders_that_dates_book() -> None:
    c = _bybit_client(_seeded_bybit_repo())
    r = c.get("/paper/replay/at", params={"date": "2026-06-23"})
    assert r.status_code == 200
    assert "positioning" in r.text
    assert "ETHUSDT" in r.text
    assert "2026-06-23" in r.text


def test_replay_has_no_venue_toggle() -> None:
    # The Binance venue toggle was removed in Task 7a — there is only one book.
    c = _bybit_client(_seeded_bybit_repo())
    r = c.get("/paper/replay")
    assert r.status_code == 200
    assert "/paper/replay?venue=" not in r.text
    assert "Binance" not in r.text


def test_replay_at_ignores_a_stray_venue_param() -> None:
    # No `venue` route param any more — an old bookmarked `?venue=binance` link is simply ignored.
    c = _bybit_client(_seeded_bybit_repo())
    r = c.get("/paper/replay/at", params={"date": "2026-06-23", "venue": "binance"})
    assert r.status_code == 200
    assert "positioning" in r.text                  # still the bybit book


# ---- carry (xsfunding) is return-defined / live-only — not fabricated into the replay book ----------

def test_replay_carry_note_is_return_defined_and_accurate() -> None:
    c = _bybit_client(_seeded_bybit_repo())
    txt = c.get("/paper/replay").text.lower()
    assert "xsfunding" in txt or "carry" in txt
    assert "return-defined" in txt
    assert "live-only" in txt
    assert "no scrubbable positions" in txt          # carry is NOT fabricated into the replay book


def test_replay_levered_track_toggle_renders_position_less() -> None:
    """The Replay exposes a naive<->levered TRACK toggle. `track=levered` renders the observe-only levered
    shadow position-less (no scrubber — it is return-defined), 200, and links to the levered Backtest. With no
    levered forward_nav seeded it shows the 'just started' note. The default (no track) stays the naive book."""
    c = _bybit_client(_seeded_bybit_repo())
    page = c.get("/paper/replay", params={"track": "levered"})
    assert page.status_code == 200
    assert "track=levered" in page.text and "track=naive" in page.text        # the toggle pills
    assert "Levered" in page.text and "shadow" in page.text
    assert "book=bybit_4edge_levered" in page.text                            # links to the levered Backtest
    assert "replay-scrub" not in page.text                                    # no scrubber for the shadow
    assert c.get("/paper/replay").status_code == 200                          # naive default still renders


def test_replay_levered_curve_renders_once_the_track_has_days() -> None:
    """Once the levered shadow track has >=2 recorded forward_nav days, the Replay renders its NAV curve (an
    <svg> sparkline) + the return-defined note — NOT the 'just started' placeholder."""
    from fxhnt.application.forward_models import ForwardNavRow as Row
    from fxhnt.application.forward_models import ForwardSummary

    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    at = dt.datetime(2026, 7, 6, 12, 0)
    rows = [Row("bybit_4edge_levered", f"2026-07-{5 + i:02d}", 0.01, 1.0 + 0.01 * (i + 1)) for i in range(4)]
    fwd.upsert_rows(rows, at)
    fwd.upsert_summary(ForwardSummary("bybit_4edge_levered", "2026-07-08", 4, 1.04, 0.04, 2.0, -0.01),
                       "WAIT", "building (4/21 days)", at)
    bybit = PaperRepo("sqlite://")
    bybit.migrate()
    c = TestClient(create_app(fwd, paper_repo=PaperRepo("sqlite://"), bybit_paper_repo=bybit))

    page = c.get("/paper/replay", params={"track": "levered"})
    assert page.status_code == 200
    assert "<svg" in page.text or "<polyline" in page.text          # the levered NAV curve renders
    assert "return-defined" in page.text                            # position-less note
    assert "just started" not in page.text                          # NOT the placeholder anymore
    assert "replay-scrub" not in page.text                          # still no scrubber (return-defined)


# ---- empty history / SSOT consistency -----------------------------------------------------------------

def test_replay_empty_history_shows_backfill_prompt() -> None:
    repo = PaperRepo("sqlite://")
    repo.migrate()
    c = _bybit_client(repo)
    r = c.get("/paper/replay")
    assert r.status_code == 200
    assert "paper-backfill" in r.text


def test_replay_matches_the_consistent_look() -> None:
    """Replay rolls to the SSOT skin: a section header with a PAPER exec_badge (it's the paper record), the
    wall-of-text intro cut to a one-line caption, and the unique scrubber + as-of book retained."""
    c = _bybit_client(_seeded_bybit_repo())
    r = c.get("/paper/replay")
    assert r.status_code == 200
    # consistent header + PAPER badge (same component the Overview/Paper pages use)
    assert "<title>Replay · fxhnt</title>" in r.text
    assert "badge-exec paper" in r.text and "PAPER" in r.text
    assert 'class="section"' in r.text
    # the OLD wall-of-text intro is gone
    assert "played back day-by-day" not in r.text
    assert "real</b> live-paper history" not in r.text
    # the unique replay detail survives: scrubber + as-of positions/trades
    assert 'type="range"' in r.text                    # the timeline scrubber
    assert "▶ Play" in r.text
    assert "BTCUSDT" in r.text                          # the played-back positions
    assert "positions" in r.text and "trades" in r.text


# ---- the scrub server fetch is DEBOUNCED (mobile flakiness fix) --------------------------------------

def test_replay_scrub_fetch_is_debounced() -> None:
    """Dragging the slider must NOT fire a server round-trip per drag-step (the mobile-flaky behavior).
    The scrub's loadAt debounces the fetch (~180ms trailing), updating only the label instantly."""
    r = _bybit_client(_seeded_bybit_repo()).get("/paper/replay")
    assert r.status_code == 200
    txt = r.text
    # the scrub handler goes through the debounced loadAt (not a raw fetch on every input)
    assert "setTimeout" in txt and "180" in txt
    assert "clearTimeout(fetchTimer)" in txt
    # the date label still updates instantly (visual liveness preserved while dragging)
    assert "label.textContent = dates[i]" in txt


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


def test_replay_is_leak_clean() -> None:
    hits = _ABS_URL.findall(_bybit_client(_seeded_bybit_repo()).get("/paper/replay").text)
    assert not hits, f"leak-clean violation: {hits}"
