"""Live FORWARD PAPER TRACK for the POSITIONING sleeve (contrarian long/short account-ratio) as its OWN
edge. Asserts (mirroring the bybit_4edge forward track):
  * the snapshot builds forward_nav from the positioning sleeve's daily return on a synthetic store, run
    through the deterministic engine (DB anchor + recompute);
  * the snapshot advances (recomputes more forward days) and is idempotent;
  * the gate is WAIT while building (reconciliation gate, ~2wk window), with T0 recorded;
  * the driver is READ-ONLY against the warehouse (a WriteTripwire never trips);
  * the daily return matches the single-sleeve `naive_eqwt_daily_returns(..., ['positioning'])` path.
NO network — in-memory SQLite store seeded directly.
"""
from __future__ import annotations

import datetime as dt
import math

from fxhnt.adapters.orchestration.assets import build_positioning_forward_nav
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_book_eval import sleeve_returns_from_store
from fxhnt.application.bybit_forward_track import BybitFourEdgeStrategy, naive_eqwt_daily_returns
from fxhnt.registry import STRATEGY_REGISTRY

_DAY = 86_400
_AT = dt.datetime(2026, 1, 1)


def _repo(tmp_path) -> ForwardNavRepo:  # noqa: ANN001
    repo = ForwardNavRepo(f"sqlite:///{tmp_path}/x.db")
    repo.migrate()
    return repo


def _inception_t0(store, **kwargs) -> str:  # noqa: ANN001, ANN003
    """The strategy's OWN latest known date at inception time — passed as `today` on the first `run_track`
    call so the anchor's T0 matches what the old JSON `ForwardTracker` would have frozen as inception."""
    rows, _ = BybitFourEdgeStrategy(store, sleeves=["positioning"], **kwargs).advance(None, {})
    return rows[-1][0]


class _WriteTripwireStore:
    """Read-only proxy: forwards reads, but ANY write/persist call trips an assertion."""

    _FORBIDDEN = frozenset({
        "write_features", "write_features_bulk", "upsert_feature_rows", "upsert_membership",
        "replace_positions", "upsert_nav", "upsert_sleeve_ret", "replace_shadow_positions",
        "replace_trades", "_create_schema", "_bulk_upsert",
    })

    def __init__(self, inner: TimescaleFeatureStore) -> None:
        object.__setattr__(self, "_inner", inner)

    def __getattr__(self, name: str):
        if name in _WriteTripwireStore._FORBIDDEN:
            raise AssertionError(f"READ-ONLY violation: tracker called write method {name!r}")
        return getattr(self._inner, name)


def _seed_positioning(store: TimescaleFeatureStore, *, days: int = 130) -> None:
    """A long/short-account-ratio fixture where fading retail is profitable: AAA is over-LONG (contrarian
    SHORT it) and falls; CCC is over-SHORT (contrarian LONG it) and rises; BBB is at the mean."""
    pa, pb, pc = 100.0, 100.0, 100.0
    for d in range(days):
        pa *= 0.999          # over-long crowd loses → short wins
        pc *= 1.001          # over-short crowd gains → long wins
        store.write_features("AAAUSDT", [(d * _DAY, {"long_ratio": 0.70, "close": pa})])
        store.write_features("BBBUSDT", [(d * _DAY, {"long_ratio": 0.50, "close": pb})])
        store.write_features("CCCUSDT", [(d * _DAY, {"long_ratio": 0.30, "close": pc})])


def _store(days: int) -> TimescaleFeatureStore:
    s = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_positioning(s, days=days)
    return s


def test_daily_return_matches_single_sleeve_naive_eqwt() -> None:
    store = _store(130)
    series = sleeve_returns_from_store(store, "positioning", cost_bps=0.0)
    store.close()
    assert series                                   # the positioning sleeve produced a return series
    daily = naive_eqwt_daily_returns({"positioning": series}, ["positioning"], cost_bps=0.0)
    # single sleeve at weight 1/1 → the book return IS the sleeve return per day.
    for d, r in daily:
        assert math.isclose(r, series[d], rel_tol=1e-12, abs_tol=1e-15)


def test_snapshot_freezes_t0_then_books_forward(tmp_path) -> None:
    """First run freezes T0 at the strategy's latest known date; recompute mode books `[T0, today]`
    INCLUSIVE of T0 itself (day 1 on first run) — unlike the retired JSON `ForwardTracker`, which froze T0
    but booked strictly-AFTER days only. The real invariant preserved is T0-freeze + a deterministic day
    count that GROWS as more data arrives, and re-running unchanged data is idempotent."""
    repo = _repo(tmp_path)
    early = _store(120)
    t0 = _inception_t0(early, cost_bps=0.0)
    out = build_positioning_forward_nav(repo, early, today=t0, at=_AT, cost_bps=0.0)
    early.close()
    assert out["t0"] == t0 == out["last_date"] and out["days"] == 1   # T0 frozen, inclusive day 1

    later = _store(130)
    out2 = build_positioning_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)
    assert out2["t0"] == t0                                          # T0 never moves
    assert out2["days"] > out["days"]                                # the new days got recomputed forward
    out3 = build_positioning_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)   # re-run same data
    later.close()
    assert out3["days"] == out2["days"]                              # idempotent
    assert out3["last_date"] == out2["last_date"]


def test_gate_is_wait_while_building(tmp_path) -> None:
    repo = _repo(tmp_path)
    early = _store(120)
    t0 = _inception_t0(early, cost_bps=0.0)
    build_positioning_forward_nav(repo, early, today=t0, at=_AT, cost_bps=0.0)
    early.close()
    later = _store(130)
    out = build_positioning_forward_nav(repo, later, today=t0, at=_AT, cost_bps=0.0)
    later.close()

    from fxhnt.application.forward_models import ForwardSummary
    from fxhnt.application.gate_policy import POLICY
    from fxhnt.application.gate_policy_resolve import resolve_policy
    from fxhnt.application.reconciliation_gate import evaluate_reconciliation_gate
    summary = ForwardSummary(strategy_id="positioning", as_of=out["last_date"], days=out["days"],
                             nav=out["nav"], total_return=out["total_return"], sharpe=out["sharpe"],
                             maxdd=out["maxdd"])
    rows = repo.nav_history("positioning")
    spec = STRATEGY_REGISTRY["positioning"]["gate_spec"]
    # reconciliation gate (~2wk window) replaces the blind 60-day calendar wait.
    assert spec["gate_type"] == "reconciliation" and spec["min_forward_days"] == 14
    v = evaluate_reconciliation_gate(summary, rows, None, resolve_policy(POLICY, spec))
    assert v.status == "WAIT" and "building" in v.reason.lower()   # only ~10 forward days booked
    assert rows and all(r.strategy_id == "positioning" for r in rows)


def test_driver_is_read_only(tmp_path) -> None:
    repo = _repo(tmp_path)
    store = _store(130)
    t0 = _inception_t0(store, cost_bps=0.0)
    guarded = _WriteTripwireStore(store)
    out = build_positioning_forward_nav(repo, guarded, today=t0, at=_AT, cost_bps=0.0)   # tripwire must not fire
    store.close()
    assert out["t0"]                                                   # produced a real result
    assert repo.nav_history("positioning")                             # persisted to the DB (allowed)


def test_forward_nav_applies_coin_gross_cap() -> None:
    """The forward NAV (BybitFourEdgeStrategy store-read path) must apply the per-coin cap when given, so it
    reconciles against the capped ref instead of diverging. Shock store: one over-short coin craters ~50%."""
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_forward_track import BybitFourEdgeStrategy

    _DAY = 86_400
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    px = {"LABUSDT": 100.0, "AAAUSDT": 100.0, "BBBUSDT": 100.0, "CCCUSDT": 100.0}
    for d in range(4):
        store.write_features("LABUSDT", [(d * _DAY, {"long_ratio": 0.05, "close": px["LABUSDT"]})])
        store.write_features("AAAUSDT", [(d * _DAY, {"long_ratio": 0.50, "close": px["AAAUSDT"]})])
        store.write_features("BBBUSDT", [(d * _DAY, {"long_ratio": 0.48, "close": px["BBBUSDT"]})])
        store.write_features("CCCUSDT", [(d * _DAY, {"long_ratio": 0.52, "close": px["CCCUSDT"]})])
        if d == 2:
            px["LABUSDT"] *= 0.50
    uncapped_rows, _ = BybitFourEdgeStrategy(store, sleeves=["positioning"]).advance(None, {})
    capped_rows, _ = BybitFourEdgeStrategy(
        store, sleeves=["positioning"], positioning_coin_gross_cap=0.07).advance(None, {})
    store.close()
    assert uncapped_rows and capped_rows, "forward strategy produced no rows — fixture broken"
    u = dict(uncapped_rows)
    c = dict(capped_rows)
    worst = min(u, key=lambda k: u[k])
    assert c[worst] > u[worst], f"cap must reduce the forward worst day: uncapped={u[worst]}, capped={c[worst]}"
