"""`fxhnt bybit-persist-sleeve-ret` — the OWN-MEMORY writer of the Bybit 4-edge BACKTEST data.

This is the DECOUPLED replacement for the (removed) nightly-daemon persistence: the heavy 4-sleeve
compute used to run inside `bybit_4edge_nav` in the 4Gi dagster daemon and OOM-killed it (exit 137),
which — because OOM is uncatchable — also skipped the forward-nav step. The compute now runs only here
(as an own-memory Job), so:

  * the nightly `bybit_4edge_nav` asset must NEVER call `persist_bybit_sleeve_returns` (forward-nav only);
  * the CLI computes the 4-sleeve series on the liquid `bybit_features` universe + upserts `bybit_sleeve_ret`,
    is memory-bounded (liquid-universe-restricted), and is idempotent.

NO network: the store is in-memory SQLite (seeded), the operational DSN points at a temp sqlite db, and
the unlock-calendar loader is stubbed.
"""
from __future__ import annotations

import inspect
import math

from typer.testing import CliRunner

from fxhnt.cli import app

_DAY = 86_400


def _seed_momentum(store, *, 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, *, 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 _patch(monkeypatch, tmp_path):
    """Point the operational DSN at a temp sqlite db, seed a synthetic bybit_features store on it, and
    stub the unlock-calendar loader to empty (absent calendar → unlock sleeve simply omitted)."""
    dsn = f"sqlite:///{tmp_path / 'op.db'}"
    monkeypatch.setenv("FXHNT_OPERATIONAL_DSN", dsn)
    from fxhnt import cli as climod
    climod.get_settings.cache_clear()

    from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
    seed_store = TimescaleFeatureStore(dsn, table="bybit_features")
    _seed_momentum(seed_store)
    _seed_carry(seed_store)
    seed_store.close()

    monkeypatch.setattr("fxhnt.application.unlock_calendar_loader.load_unlock_events",
                        lambda *a, **k: [])
    return dsn


def test_nightly_asset_does_not_persist_sleeve_ret(monkeypatch):
    """TRIPWIRE: the nightly forward-nav asset must NOT invoke the heavy sleeve-ret persistence.

    OOM regression guard — heavy compute must not run in the 4Gi daemon. `@asset` wraps the function in
    an AssetsDefinition, so we read the asset's own decorated function source and assert the heavy persist
    call no longer appears anywhere in the forward-nav path."""
    from fxhnt.adapters.orchestration import assets as assets_mod

    asset_def = assets_mod.bybit_4edge_nav
    fn = asset_def.op.compute_fn.decorated_fn  # the underlying @asset-decorated function
    src = inspect.getsource(fn)
    assert "persist_bybit_sleeve_returns" not in src, (
        "bybit_4edge_nav must NOT call persist_bybit_sleeve_returns — heavy compute OOMs the daemon")


def test_cli_persists_sleeve_ret_into_bybit_table(monkeypatch, tmp_path):
    dsn = _patch(monkeypatch, tmp_path)
    res = CliRunner().invoke(app, ["bybit-persist-sleeve-ret", "--min-dollar-vol", "0"])
    assert res.exit_code == 0, res.output
    assert "rows" in res.output.lower()

    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    series = PaperRepo(dsn).bybit_sleeve_returns()
    assert len(series) >= 2, f"expected >=2 sleeves, got {list(series)}"
    for sleeve, by_day in series.items():
        assert by_day, f"{sleeve} has no days"


def test_backtest_refs_all_also_writes_bybit_sleeve_ret(monkeypatch, tmp_path):
    """REGRESSION LOCK (Task 6 Critical fix): `fxhnt backtest-refs --all` replaced the retired
    `fxhnt-bybit-sleeve-ret-job.yaml` Job, but the first cut only wrote the 3 report-kind backtest refs and
    silently dropped the `bybit_sleeve_ret` write (the cockpit sim's precomputed backtest data, read live in
    ~6 places in adapters/web/app.py) — freezing that table forever after merge. Both commands now share the
    compute-once `_persist_bybit_book` helper, so `backtest-refs --all` must ALSO populate `bybit_sleeve_ret`,
    not just `backtest_summary`."""
    dsn = _patch(monkeypatch, tmp_path)
    res = CliRunner().invoke(app, ["backtest-refs", "--all"])
    assert res.exit_code == 0, res.output
    assert "bybit_sleeve_ret" in res.output

    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    series = PaperRepo(dsn).bybit_sleeve_returns()
    assert len(series) >= 2, (
        f"backtest-refs --all must persist bybit_sleeve_ret rows (cockpit precompute); got sleeves {list(series)}")
    for sleeve, by_day in series.items():
        assert by_day, f"{sleeve} has no days"

    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    sids = {row.strategy_id for row in ForwardNavRepo(dsn).all_backtest_summaries()}
    assert {"bybit_4edge", "bybit_4edge_levered", "positioning", "xsfunding", "unlock"} <= sids, (
        f"backtest-refs --all must also persist the report-kind backtest_summary refs "
        f"(incl. the migrated single-sleeve xsfunding/unlock tracks — Phase 0b); got {sids}")


def test_cli_is_idempotent(monkeypatch, tmp_path):
    dsn = _patch(monkeypatch, tmp_path)
    runner = CliRunner()
    r1 = runner.invoke(app, ["bybit-persist-sleeve-ret", "--min-dollar-vol", "0"])
    assert r1.exit_code == 0, r1.output
    from fxhnt.adapters.persistence.paper_repo import PaperRepo
    first = PaperRepo(dsn).bybit_sleeve_returns()

    r2 = runner.invoke(app, ["bybit-persist-sleeve-ret", "--min-dollar-vol", "0"])
    assert r2.exit_code == 0, r2.output
    second = PaperRepo(dsn).bybit_sleeve_returns()

    assert first.keys() == second.keys()
    for sleeve in first:
        assert first[sleeve] == second[sleeve], f"{sleeve} changed on re-run (not idempotent)"
