"""HARD RECONCILIATION CONSTRAINT (the whole point): the per-symbol weights extracted for the live Bybit
paper book reproduce each sleeve's stored daily return EXACTLY — so the live book and the returns book are
the SAME book, no edge drift.

For every sleeve and day:  Σ_symbol weight·symbol_return  ==  sleeve_returns_from_store(.., cost_bps=0.0)

(The stored `bybit_sleeve_ret` is computed with cost; the extraction reproduces the GROSS cost-free return
and the per-turnover cost is applied identically downstream by the live book's trading_cost — so cost_bps=0
is the clean invariant to assert. We assert the engines agree at cost_bps=0.)

NO network: an in-memory sqlite TimescaleFeatureStore seeded with synthetic close/funding/spot/long_ratio.
"""
from __future__ import annotations

import math

from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.bybit_book_eval import sleeve_returns_from_store
from fxhnt.application.bybit_paper_weights import (
    reconciliation_series,
    sleeve_weights_and_contributions,
)

_DAY = 86_400


def _seed(store: TimescaleFeatureStore, *, days: int = 200) -> list[dict]:
    """Seed BTC + a few alts with close/funding/spot/long_ratio over `days`. Enough history for tstrend
    (min_history 130), funding/spot for xsfunding, oscillating long_ratio for positioning, and one unlock
    cliff for the unlock sleeve. Deterministic, mild trends so every sleeve produces non-trivial weights."""
    import math as _m
    syms = ["BTCUSDT", "AAAUSDT", "BBBUSDT", "CCCUSDT"]
    px = {s: 100.0 for s in syms}
    px["BTCUSDT"] = 50_000.0
    for d in range(days):
        crowd = (d % 3 == 0)
        for s in syms:
            # gentle deterministic drift per symbol so trend/vol are well-defined
            drift = {"BTCUSDT": 0.001, "AAAUSDT": 0.002, "BBBUSDT": -0.001, "CCCUSDT": 0.0015}[s]
            wobble = 0.003 * _m.sin(d / 5.0 + hash(s) % 7)
            px[s] *= (1.0 + drift + wobble)
            funding = 0.0005 if s != "BTCUSDT" else 0.0001
            spot = px[s] * (1.0 + 0.0002)   # small basis
            ratio = (0.70 if crowd else 0.30) if s == "AAAUSDT" else \
                    (0.30 if crowd else 0.70) if s == "BBBUSDT" else 0.50
            store.write_features(s, [(d * _DAY, {
                "close": px[s], "funding": funding, "spot_close": spot, "long_ratio": ratio})])
    # One unlock cliff for AAA at day ~ days-5 (so the pre-window 10 days are inside the sample). A big
    # `n` so the supply-shock clears min_shock against the warehouse qvol fallback.
    cliff = days - 5
    return [{"sym": "AAA", "cliff_day": cliff, "n": 5_000_000.0, "cat": "insiders"}]


def _gross_series(store, sleeve, *, unlock_events=None) -> dict[int, float]:
    """The engine's GROSS (cost-free) daily return for `sleeve` — the reconciliation target.

    Three sleeves forward `cost_bps` to their turnover haircut, so `sleeve_returns_from_store(cost_bps=0)`
    IS the gross series. The unlock runner does NOT forward `cost_bps` to its constructor (a pre-existing
    quirk: it always charges its DEFAULT 10bp turnover), so its stored series is net-of-10bp even at
    cost_bps=0; the gross is recovered by running the SAME UnlockShortRunner with cost_bps=0.0 (the engine's
    pre-cost book return). The extracted weights reproduce the GROSS book return in every case; the cost is a
    separate, identically-applied downstream haircut (the live book's trading_cost)."""
    if sleeve in ("unlock",) and unlock_events:
        from fxhnt.application.bybit_book_eval import _series_from_run
        from fxhnt.application.unlock_runner import UnlockShortRunner, warehouse_unlock_panel
        panel = store.crypto_close_panel()
        res = UnlockShortRunner(warehouse_unlock_panel(panel), unlock_events, cost_bps=0.0).run()
        return _series_from_run(res.dates, res.returns)
    return sleeve_returns_from_store(store, sleeve, cost_bps=0.0, unlock_events=unlock_events)


def _assert_reconciles(store, sleeve, *, unlock_events=None) -> int:
    extracted = sleeve_weights_and_contributions(store, sleeve, unlock_events=unlock_events)
    implied = reconciliation_series(extracted)
    stored = _gross_series(store, sleeve, unlock_events=unlock_events)
    # Every day the engine booked must be reproduced by Σ w·r to float tolerance.
    assert stored, f"{sleeve}: engine produced no stored return series (seed too short?)"
    common = set(implied) & set(stored)
    assert common, f"{sleeve}: no common days between extracted weights and stored returns"
    for day in sorted(stored):
        assert day in implied, f"{sleeve}: day {day} missing from extracted weights"
        assert math.isclose(implied[day], stored[day], rel_tol=1e-9, abs_tol=1e-12), (
            f"{sleeve} day {day}: Σ w·r = {implied[day]!r} != stored {stored[day]!r}")
    return len(stored)


def test_tstrend_weights_reproduce_stored_return() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    n = _assert_reconciles(store, "crypto_tstrend")
    store.close()
    assert n > 0


def test_positioning_weights_reproduce_stored_return() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    n = _assert_reconciles(store, "positioning")
    store.close()
    assert n > 0


def test_xsfunding_weights_reproduce_stored_return() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    n = _assert_reconciles(store, "xsfunding")
    store.close()
    assert n > 0


def test_unlock_weights_reproduce_stored_return() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    events = _seed(store)
    n = _assert_reconciles(store, "unlock", unlock_events=events)
    store.close()
    assert n > 0


def test_unlock_without_calendar_is_empty() -> None:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed(store)
    # No calendar → the edge is uncomputable → empty (never fabricated).
    assert sleeve_weights_and_contributions(store, "unlock", unlock_events=None) == {}
    store.close()


def _seed_positioning_shock(store: TimescaleFeatureStore, *, days: int = 4) -> None:
    """One coin (LAB) is deep-over-short (ratio 0.05 -> huge demeaned weight, uncapped >> 0.07) so the
    per-coin gross cap actually clips it; three coins sit near the mean (small weights, unaffected)."""
    px = {"LABUSDT": 100.0, "AAAUSDT": 100.0, "BBBUSDT": 100.0, "CCCUSDT": 100.0}
    for d in range(days):
        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


def test_positioning_weights_reconcile_with_coin_gross_cap() -> None:
    """SSOT invariant, CAPPED basis: when `positioning_coin_gross_cap` is set, the extracted per-symbol
    weights must reconcile against the CAPPED stored return (not the uncapped one) — the paper-book
    attribution must match whatever cap the live book actually used."""
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_positioning_shock(store)
    extracted = sleeve_weights_and_contributions(store, "positioning", positioning_coin_gross_cap=0.07)
    implied = reconciliation_series(extracted)
    stored = sleeve_returns_from_store(store, "positioning", cost_bps=0.0, coin_gross_cap=0.07)
    # and it must NOT trivially match the uncapped series (i.e. the cap is actually doing something —
    # otherwise this test would pass vacuously even if the kwarg were silently dropped).
    uncapped_stored = sleeve_returns_from_store(store, "positioning", cost_bps=0.0)
    store.close()
    assert stored, "positioning: engine produced no capped stored return series (seed too short?)"
    common = set(implied) & set(stored)
    assert common, "positioning: no common days between capped extracted weights and capped stored returns"
    for day in sorted(stored):
        assert day in implied, f"positioning day {day}: missing from capped extracted weights"
        assert math.isclose(implied[day], stored[day], rel_tol=1e-9, abs_tol=1e-12), (
            f"positioning day {day}: Σ w·r = {implied[day]!r} != capped stored {stored[day]!r}")
    assert any(not math.isclose(stored[d], uncapped_stored[d], rel_tol=1e-9, abs_tol=1e-12)
              for d in common), "cap had no effect on the fixture — strengthen the seed to exercise it"


def test_paper_book_positions_capped_by_coin_gross_cap() -> None:
    """The live paper-book per-symbol weights must honor the per-coin cap so executed positions match the
    capped reconciled return (SSOT). On a store with one dominant positioning coin, the capped weight for that
    coin is strictly smaller than uncapped."""
    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    from fxhnt.application.bybit_paper_book import combined_symbol_weights_and_returns
    _DAY = 86_400
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    for d in range(4):
        store.write_features("LABUSDT", [(d * _DAY, {"long_ratio": 0.02, "close": 100.0})])
        for i, sym in enumerate(("AAAUSDT", "BBBUSDT", "CCCUSDT", "DDDUSDT")):
            store.write_features(sym, [(d * _DAY, {"long_ratio": 0.45 + 0.02 * i, "close": 100.0})])
    w_unc, _r, _d = combined_symbol_weights_and_returns(store, sleeves=("positioning",))
    w_cap, _r2, _d2 = combined_symbol_weights_and_returns(
        store, sleeves=("positioning",), positioning_coin_gross_cap=0.07)
    store.close()
    assert w_unc.get("positioning") and w_cap.get("positioning"), "no positioning weights — fixture broken"
    lab_unc = abs(w_unc["positioning"].get("LABUSDT", 0.0))
    lab_cap = abs(w_cap["positioning"].get("LABUSDT", 0.0))
    assert lab_unc > 0.0
    assert lab_cap < lab_unc, f"cap must shrink the dominant coin's position: unc={lab_unc}, cap={lab_cap}"
