"""The nightly `_persist_track_backtest_ref` step (fxhnt-backtest-refs Job) persists a recompute-replay
strategy's (multistrat/multistrat_levered — the IBKR paper books) FULL inception return series into the
`sim_curve_ret` table, so /paper/sim reads a precomputed IBKR curve instead of ever re-running
`ForwardStrategy.advance` on a web request (Task 1/D1). Asserts:
  * the series persisted is the SAME rows the reconciliation-gate `backtest_summary` ref is derived from
    (one replay call, two writes);
  * `PaperRepo.sim_curve_returns(strategy_id)` reads it back as {epoch_day: ret};
  * idempotent — a second run on the same replay overwrites, no duplicate rows;
  * a `report`/`sleeve`-kind strategy (not recompute-replay) writes nothing to `sim_curve_ret`.
NO network — settings point at an in-memory/tmp sqlite DSN; the replay strategy is a fake.
"""
from __future__ import annotations

import datetime as dt

from dagster import build_op_context

from fxhnt.adapters.persistence.paper_repo import PaperRepo


class _Replay:
    """A fake recompute-replay strategy: `advance(None, {})` returns its full inception series. 10 varied
    daily returns (not the degenerate 3-row series) so the gauntlet's 60/40 split produces a finite DSR —
    `bybit_forward_track.backtest_summary_from_rows` NOT-NULLs `dsr`, and too few/uniform points make it NaN."""
    def advance(self, last, extra):  # noqa: ANN001
        rets = [0.01, -0.005, 0.02, 0.008, -0.012, 0.015, -0.003, 0.011, -0.007, 0.019]
        return [(f"2024-01-{i + 1:02d}", r) for i, r in enumerate(rets)], {}


def test_persist_track_backtest_ref_writes_sim_curve_ret_for_recompute_replay(tmp_path, monkeypatch):
    import fxhnt.adapters.orchestration.assets as assets
    from fxhnt.config import Settings

    dsn = f"sqlite:///{tmp_path / 'sim_curve.db'}"
    monkeypatch.setattr("fxhnt.config.get_settings", lambda: Settings(operational_dsn=dsn))

    assets._persist_track_backtest_ref(build_op_context(), "multistrat_nav", lambda: _Replay())

    repo = PaperRepo(dsn)
    repo.migrate()
    series = repo.sim_curve_returns("multistrat")
    base = dt.date(2024, 1, 1).toordinal()
    expected = {0.01, -0.005, 0.02, 0.008, -0.012, 0.015, -0.003, 0.011, -0.007, 0.019}
    assert len(series) == 10 and set(series) == {base + i for i in range(10)}
    assert set(series.values()) == expected


def test_persist_track_backtest_ref_also_writes_the_reconciliation_ref(tmp_path, monkeypatch):
    """The curve write and the reconciliation-gate ref come from the SAME replay call — both land."""
    import fxhnt.adapters.orchestration.assets as assets
    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    from fxhnt.config import Settings

    dsn = f"sqlite:///{tmp_path / 'sim_curve2.db'}"
    monkeypatch.setattr("fxhnt.config.get_settings", lambda: Settings(operational_dsn=dsn))

    assets._persist_track_backtest_ref(build_op_context(), "multistrat_levered_nav", lambda: _Replay())

    fwd = ForwardNavRepo(dsn)
    fwd.migrate()
    assert any(b.strategy_id == "multistrat_levered" for b in fwd.all_backtest_summaries())

    repo = PaperRepo(dsn)
    repo.migrate()
    assert repo.sim_curve_returns("multistrat_levered")   # non-empty


def test_persist_track_backtest_ref_idempotent_on_rerun(tmp_path, monkeypatch):
    import fxhnt.adapters.orchestration.assets as assets
    from fxhnt.config import Settings

    dsn = f"sqlite:///{tmp_path / 'sim_curve3.db'}"
    monkeypatch.setattr("fxhnt.config.get_settings", lambda: Settings(operational_dsn=dsn))

    assets._persist_track_backtest_ref(build_op_context(), "multistrat_nav", lambda: _Replay())
    assets._persist_track_backtest_ref(build_op_context(), "multistrat_nav", lambda: _Replay())

    repo = PaperRepo(dsn)
    repo.migrate()
    series = repo.sim_curve_returns("multistrat")
    assert len(series) == 10   # (strategy_id, run_date) PK overwrite — no duplicates


def test_persist_track_backtest_ref_report_kind_writes_no_sim_curve(tmp_path, monkeypatch):
    """A report-kind strategy (e.g. bybit_4edge) is NOT a recompute-replay book — this path must not write
    anything to `sim_curve_ret` (its curve is `bybit_sleeve_ret`, written by `_persist_bybit_book` instead)."""
    import fxhnt.adapters.orchestration.assets as assets
    from fxhnt.config import Settings

    dsn = f"sqlite:///{tmp_path / 'sim_curve4.db'}"
    monkeypatch.setattr("fxhnt.config.get_settings", lambda: Settings(operational_dsn=dsn))

    # bybit_4edge is report-kind: build_strategy is unused on that branch (backtest_ref dispatches on the
    # registry kind, not on what build_strategy returns), so a strategy that would raise if called proves it.
    def _boom():
        raise AssertionError("report-kind must not call build_strategy via the recompute-replay branch")

    assets._persist_track_backtest_ref(build_op_context(), "bybit_4edge_nav", _boom)

    repo = PaperRepo(dsn)
    repo.migrate()
    assert repo.sim_curve_returns("bybit_4edge") == {}


class _TooShortReplay:
    """A recompute-replay strategy whose inception series is too short (< 2 rows) for a backtest_summary —
    `backtest_summary_from_rows` returns None (sm is None)."""
    def advance(self, last, extra):  # noqa: ANN001
        return [("2024-01-01", 0.01)], {}


def test_insufficient_replay_writes_neither_ref_nor_sim_curve(tmp_path, monkeypatch):
    """Invariant lock (review Minor): the sim_curve_ret write and the backtest_summary ref are gated together
    on the SAME `sm is None` condition. A too-short replay (no gate ref) must ALSO write NO sim curve — never
    a degenerate curve with no matching gate ref (same-basis invariant)."""
    import fxhnt.adapters.orchestration.assets as assets
    from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
    from fxhnt.config import Settings

    dsn = f"sqlite:///{tmp_path / 'sim_curve5.db'}"
    monkeypatch.setattr("fxhnt.config.get_settings", lambda: Settings(operational_dsn=dsn))

    assets._persist_track_backtest_ref(build_op_context(), "multistrat_nav", lambda: _TooShortReplay())

    fwd = ForwardNavRepo(dsn)
    fwd.migrate()
    assert not any(b.strategy_id == "multistrat" for b in fwd.all_backtest_summaries())  # no gate ref

    repo = PaperRepo(dsn)
    repo.migrate()
    assert repo.sim_curve_returns("multistrat") == {}                                    # AND no sim curve
