"""D3 (Task 4) — the Overview's "real trades" sub-row signal: `DashboardService.fleet()` marks an executing
strategy row (its `*_exec` twin has real captured IBKR data) `exec_status="EXEC"` + attaches the shared
`recon` (Task 3's `exec_vs_sim`, no duplicate math); a pure-sim strategy (no real IBKR account behind it)
gets `exec_status == ""` and `recon is None`. `overview()` rolls the executing rows up into an
`IbkrAccountRollup` (account NAV + count executing + mean divergence) by REUSING each row's already-computed
`recon` — it is NOT rendered as its own card on the Overview (Task 5's consolidated Paper-books page is the
account home; see cockpit.html), only carried on the model for that page to reuse.

`/` renders a nested "real trades" sub-row (reusing the existing `.exec-tag` +
`tr.exrow` style) + the shared "behind/ahead of plan" pill on the executing row, and NOTHING on a sim-only
row or when no exec data exists anywhere (never a 500). The row's own PAPER exec badge marks the mode.

Plain-language HARD RULE (Task 3's precedent): no raw registry id / `bps` / "vs sim" / "divergence" may ever
appear — asserted against the VISIBLE text via `_visible_text` (mirrors Task 3's helper).
"""
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.ibkr_account_repo import IbkrAccountRepo
from fxhnt.adapters.web.app import create_app
from fxhnt.application.dashboard_service import DashboardService
from fxhnt.application.exec_capture import FillDTO
from fxhnt.application.forward_models import ForwardSummary


def _visible_text(html: str) -> str:
    """The user-FACING text of a rendered page: `<style>`/`<script>` blocks dropped and every HTML tag
    stripped, so CSS class names and href internals don't false-trip a plain-language copy assertion."""
    html = re.sub(r"<(style|script)\b[^>]*>.*?</\1>", " ", html, flags=re.DOTALL | re.IGNORECASE)
    return re.sub(r"<[^>]+>", " ", html).lower()


def _seeded_forward(with_multistrat_exec: bool = True) -> ForwardNavRepo:
    repo = ForwardNavRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 7, 14, 12, 0)
    repo.upsert_summary(ForwardSummary("multistrat", "2026-07-14", 20, 1.038, 0.038, 0.9, -0.02),
                        "WAIT", "20/20 forward days", at)
    if with_multistrat_exec:
        repo.upsert_summary(ForwardSummary("multistrat_exec", "2026-07-14", 20, 1.031, 0.031, 0.8, -0.02),
                            "WAIT", "20/20 forward days", at)
    # a pure-sim strategy with no real IBKR account behind it at all — the "sim only, no sub-row" control.
    repo.upsert_summary(ForwardSummary("unlock", "2026-06-06", 2, 1.0201, 0.0201, 1.2, -0.01),
                        "WAIT", "2/20 forward days", at)
    return repo


def _seeded_ibkr() -> IbkrAccountRepo:
    repo = IbkrAccountRepo("sqlite://")
    repo.migrate()
    at1 = dt.datetime(2026, 7, 10, 15, 0)
    at2 = dt.datetime(2026, 7, 14, 15, 0)
    repo.replace_snapshot("2026-07-10", nlv=1_000_000.0, cash=1000.0, gross=999_000.0,
                          positions={"SPY": 100.0, "IEF": 200.0}, at=at1)
    repo.replace_snapshot("2026-07-14", nlv=1_031_000.0, cash=500.0, gross=1_030_500.0,
                          positions={"SPY": 100.0, "IEF": 200.0}, at=at2)
    # 3 fills but only 2 holdings (SPY traded twice) — deliberately DISTINCT so the rendered
    # "N trades · M holdings" cannot pass if the two counts are swapped.
    repo.record_fills(strategy_id="multistrat", rebalance_id="r1",
                      fills=[FillDTO(symbol="SPY", side="BOT", qty=60.0, price=450.0, fee=0.6,
                                     status="Filled", shortfall_bps=0.0),
                             FillDTO(symbol="SPY", side="BOT", qty=40.0, price=451.0, fee=0.4,
                                     status="Filled", shortfall_bps=0.0),
                             FillDTO(symbol="IEF", side="BOT", qty=200.0, price=95.0, fee=0.5,
                                     status="Filled", shortfall_bps=0.0)],
                      at=at1)
    return repo


def test_fleet_marks_executing_strategy_exec_status() -> None:
    svc = DashboardService(_seeded_forward(), _seeded_ibkr())
    fleet = svc.fleet()
    multistrat = next(f for f in fleet if f.strategy_id == "multistrat")
    assert multistrat.exec_status == "EXEC"
    assert multistrat.recon is not None and multistrat.recon.has_exec is True
    assert round(multistrat.recon.divergence, 4) == round(0.031 - 0.038, 4)

    sim_only = next(f for f in fleet if f.strategy_id == "unlock")
    assert sim_only.exec_status == ""
    assert sim_only.recon is None


def test_fleet_no_captured_exec_data_yet_gives_empty_status() -> None:
    # multistrat is an IBKR real-account book, but its exec twin has no forward summary yet (not started /
    # suspended) — must degrade to the same "no sub-row" defaults as a plain sim strategy, never crash.
    svc = DashboardService(_seeded_forward(with_multistrat_exec=False), _seeded_ibkr())
    fleet = svc.fleet()
    multistrat = next(f for f in fleet if f.strategy_id == "multistrat")
    assert multistrat.exec_status == ""
    assert multistrat.recon is None


def test_overview_ibkr_rollup_reuses_fleet_recon() -> None:
    svc = DashboardService(_seeded_forward(), _seeded_ibkr())
    overview = svc.overview()
    assert overview.ibkr_rollup is not None
    assert overview.ibkr_rollup.total_nav == 1_031_000.0   # latest ibkr_account_nav
    assert overview.ibkr_rollup.n_executing == 1
    assert round(overview.ibkr_rollup.agg_divergence, 4) == round(0.031 - 0.038, 4)


def test_no_ibkr_repo_wired_gives_no_rollup_and_no_exec_status() -> None:
    svc = DashboardService(_seeded_forward())   # no ibkr_repo at all
    fleet = svc.fleet()
    multistrat = next(f for f in fleet if f.strategy_id == "multistrat")
    assert multistrat.exec_status == ""
    assert multistrat.recon is None
    assert svc.overview(fleet).ibkr_rollup is None


def _client(fwd: ForwardNavRepo, ibkr: IbkrAccountRepo) -> TestClient:
    return TestClient(create_app(fwd, ibkr_account_repo=ibkr))


def test_web_overview_shows_real_trades_subrow() -> None:
    c = _client(_seeded_forward(), _seeded_ibkr())
    html = c.get("/").text
    text = _visible_text(html)
    assert "real trades" in text
    assert "badge-exec paper" in html   # the executing row carries a plain PAPER badge (no separate REAL PAPER)
    # PINNED exact rendered forms (not loose substrings) so a stray sign, a wrong divisor, or a swapped
    # trade/holdings count fails the test. Fixture: exec cum +3.1%, sim +3.8% -> 0.7% behind plan; honest
    # 0.0-bps market orders -> 0.0% cost; 3 fills over 2 held symbols (SPY traded twice) -> "3 trades · 2 holdings".
    assert "real +3.10%" in text                     # executed return, signed + inline "real" label (not a naked % under the borrowed "allocated" header)
    assert "0.7% behind plan" in text                # abs(0.031 - 0.038) * 100 = 0.7, behind (exec < sim)
    assert "ahead of plan" not in text               # sign guard: exec trails sim, must NOT read "ahead"
    assert "cost to trade 0.0%" in text              # median 0.0 bps / 100 = 0.0% — honest, not fabricated
    assert "3 trades · 2 holdings" in text           # count guard: swapped ("2 trades · 3 holdings") would fail
    assert "2 trades · 3 holdings" not in text
    # the "N trades · M holdings →" click-through to the strategy's own holdings detail.
    assert 'href="/strategy/multistrat"' in html


def test_web_overview_sim_only_row_has_no_real_trades_subrow() -> None:
    # No exec data captured yet anywhere for the IBKR book -> no badge, no sub-row, on ANY row.
    c = _client(_seeded_forward(with_multistrat_exec=False), _seeded_ibkr())
    text = _visible_text(c.get("/").text)
    assert "real paper" not in text
    assert "real trades" not in text


def test_web_overview_no_exec_data_anywhere_no_subrow_no_500() -> None:
    fwd = ForwardNavRepo("sqlite://")
    fwd.migrate()
    ibkr = IbkrAccountRepo("sqlite://")
    ibkr.migrate()
    r = _client(fwd, ibkr).get("/")
    assert r.status_code == 200
    text = _visible_text(r.text)
    assert "real paper" not in text
    assert "real trades" not in text


def test_web_overview_plain_language_no_raw_ids_no_bps_no_divergence() -> None:
    c = _client(_seeded_forward(), _seeded_ibkr())
    html = c.get("/").text
    for raw in (">multistrat<", ">multistrat_exec<"):
        assert raw not in html, f"raw id leaked as text: {raw}"
    text = _visible_text(html)
    assert "bps" not in text
    assert "divergence" not in text
    assert "vs sim" not in text and "vs. sim" not in text


def test_new_real_trades_subrow_has_no_raw_venue_slug() -> None:
    # MINOR whole-branch-review fix: the D3 sub-row's `exec-tag` used to render the raw registry venue slug
    # ("ibkr-equity") — a raw-id leak on a NEW surface. Scoped to just the `tr.exrow` sub-row (NOT the
    # pre-existing exec-twin row a few lines down, which has its own venue rendering, out of scope here —
    # and NOT the "All forward tracks" table's own per-track venue label, which is pre-existing/out of scope).
    c = _client(_seeded_forward(), _seeded_ibkr())
    html = c.get("/").text
    exrows = re.findall(r'<tr class="exrow">.*?</tr>', html, re.DOTALL)
    assert exrows, "expected at least one 'real trades' sub-row to be rendered"
    subrow_text = _visible_text(" ".join(exrows))
    assert "ibkr-equity" not in subrow_text
    assert "ibkr-xsp" not in subrow_text
    assert "bybit-perp" not in subrow_text
    assert "ibkr paper account" in subrow_text
