"""The nightly Bybit asset persists the 4-edge BACKTEST data (per-sleeve daily returns) into the
`bybit_sleeve_ret` table the cockpit sim reads — so the cockpit configures a precomputed backtest and
NEVER recomputes the heavy 4-edge book on a web request.

Asserts:
  * `persist_bybit_sleeve_returns` writes >=2 sleeves' daily return series from a synthetic `bybit_features`
    store into `bybit_sleeve_ret`, and `PaperRepo.bybit_sleeve_returns()` reads them back;
  * it is idempotent (re-run on the same data → same rows, no dup — (run_date, sleeve) PK overwrite);
  * the persisted Bybit returns are SEPARATE from the Binance `paper_sleeve_ret` table (no clobber);
  * the rows compound to the SAME naive eq-wt curve `bybit_book_eval` reports (same series, same engine).
NO network — the store is in-memory SQLite; external inputs are injected.
"""
from __future__ import annotations

import datetime as dt
import math

from fxhnt.adapters.orchestration.assets import persist_bybit_sleeve_returns
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore

_DAY = 86_400


def _seed_momentum(store: TimescaleFeatureStore, *, n_symbols: int = 8, days: int = 200,
                   drift: float = 0.01) -> None:
    for idx in range(n_symbols):
        sign = 1.0 if idx % 2 == 0 else -1.0
        px = 100.0
        rows = []
        for d in range(days):
            wobble = 0.003 * math.cos(0.4 * d + idx)
            px *= (1.0 + sign * drift + wobble)
            rows.append((d * _DAY, {"close": px}))
        store.write_features(f"SYM{idx:02d}USDT", rows)


def _seed_carry(store: TimescaleFeatureStore, *, days: int = 200) -> None:
    funding = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
               "XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
    for idx, (sym, fund) in enumerate(funding.items()):
        rows = []
        for d in range(days):
            f = fund + 0.0002 * math.cos(0.5 * d + idx)
            rows.append((d * _DAY, {"funding": f, "close": 100.0, "spot_close": 100.0}))
        store.write_features(sym, rows)


def _store() -> TimescaleFeatureStore:
    store = TimescaleFeatureStore("sqlite://", table="bybit_features")
    _seed_momentum(store)
    _seed_carry(store)
    return store


def test_persist_writes_sleeve_series_readable_by_repo() -> None:
    store = _store()
    repo = PaperRepo("sqlite://")
    repo.migrate()
    n = persist_bybit_sleeve_returns(repo, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0)
    store.close()
    assert n > 0
    series = repo.bybit_sleeve_returns()
    assert len(series) >= 2                       # at least tstrend + xsfunding have data
    for sleeve, by_day in series.items():
        assert by_day, f"{sleeve} has no days"


def test_persist_reuses_precomputed_sleeve_returns_identically() -> None:
    """Passing precomputed `sleeve_returns` writes the IDENTICAL bybit_sleeve_ret data as recomputing from the
    store — the compute-once optimization (the persist Job computes the sleeves once and reuses them for the
    backtest references instead of recomputing the heavy backtest per consumer)."""
    from fxhnt.application.bybit_book_eval import _DEFAULT_BYBIT_SLEEVES, sleeve_returns_from_store

    store = _store()
    rbs = {s: sleeve_returns_from_store(store, s, cost_bps=0.0) for s in _DEFAULT_BYBIT_SLEEVES}
    r1 = PaperRepo("sqlite://")
    r1.migrate()
    persist_bybit_sleeve_returns(r1, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0)
    r2 = PaperRepo("sqlite://")
    r2.migrate()
    persist_bybit_sleeve_returns(r2, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0, sleeve_returns=rbs)
    store.close()
    assert r1.bybit_sleeve_returns() == r2.bybit_sleeve_returns()


def test_persist_is_idempotent() -> None:
    store = _store()
    repo = PaperRepo("sqlite://")
    repo.migrate()
    n1 = persist_bybit_sleeve_returns(repo, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0)
    first = repo.bybit_sleeve_returns()
    n2 = persist_bybit_sleeve_returns(repo, store, at=dt.datetime(2026, 6, 25), cost_bps=0.0)
    store.close()
    second = repo.bybit_sleeve_returns()
    assert n1 == n2                               # same data → same number of rows written
    assert first.keys() == second.keys()
    for sleeve in first:
        assert first[sleeve] == second[sleeve]    # values unchanged (idempotent overwrite)


def test_bybit_returns_do_not_clobber_binance_paper_sleeve_ret() -> None:
    store = _store()
    repo = PaperRepo("sqlite://")
    repo.migrate()
    # A Binance paper_sleeve_ret row must survive a Bybit persist (separate tables).
    repo.upsert_sleeve_ret("2026-01-01", "crypto_tstrend", 0.005, at=dt.datetime(2026, 1, 1))
    persist_bybit_sleeve_returns(repo, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0)
    store.close()
    binance = repo.sleeve_returns(before="2999-01-01")
    assert binance.get("crypto_tstrend", {}).get(dt.date(2026, 1, 1).toordinal()) == 0.005




def test_persisted_rows_match_bybit_book_eval_naive_curve() -> None:
    """The rows the sim reads must compound to the SAME naive eq-wt total_return bybit_book_eval reports."""
    from fxhnt.application.bybit_book_eval import risk_managed_book_from_store
    from fxhnt.application.bybit_forward_track import naive_eqwt_daily_returns

    store = _store()
    book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=0.0)
    repo = PaperRepo("sqlite://")
    repo.migrate()
    persist_bybit_sleeve_returns(repo, store, at=dt.datetime(2026, 6, 24), cost_bps=0.0)
    store.close()

    # Rebuild {sleeve: {epoch_day: ret}} from the persisted rows (toordinal keys → epoch-day keys).
    epoch_ord = dt.date(1970, 1, 1).toordinal()
    rbs = {s: {ordd - epoch_ord: r for ordd, r in by_day.items()}
           for s, by_day in repo.bybit_sleeve_returns().items()}
    daily = naive_eqwt_daily_returns(rbs, book["sleeves"], cost_bps=0.0)
    nav = 1.0
    for _d, r in daily:
        nav *= (1.0 + r)
    assert math.isclose(nav - 1.0, book["naive_equal_weight"]["total_return"], rel_tol=1e-9, abs_tol=1e-9)
