"""Tests for the Stage-1 EM-Asia (China/India) LOO marginal-Sharpe kill-switch."""
from __future__ import annotations

import datetime as dt

import numpy as np
import pytest

from fxhnt.application import em_asia_marginal_eval as ev
from fxhnt.domain.strategies import multistrat


def _dates(n: int, start: str = "2016-01-04") -> list[str]:
    d0 = dt.date.fromisoformat(start)
    out, d = [], d0
    while len(out) < n:
        if d.weekday() < 5:            # weekdays only (equity calendar)
            out.append(d.isoformat())
        d += dt.timedelta(days=1)
    return out


def _closes_from_rets(dates: list[str], rets: np.ndarray, p0: float = 100.0) -> dict[str, float]:
    px = p0 * np.cumprod(1.0 + rets)
    return dict(zip(dates, px, strict=True))


# ---------------- _turnover_cost ----------------

def test_turnover_cost_zero_scale_is_zero():
    cost = ev._turnover_cost(np.zeros(50), half_spread_bps=10.0)
    assert np.allclose(cost, 0.0)


def test_turnover_cost_charges_resizing():
    # A single re-size step of +1.0 in scale pays a full round-trip (2*hs) on that unit of notional.
    scale = np.array([0.0, 0.0, 1.0, 1.0])
    cost = ev._turnover_cost(scale, half_spread_bps=10.0)
    rt = 2.0 * 10.0 / 1e4
    # day 2: |1.0-0.0|*rt = rt (resize) vs floor 1.0*rt/21 -> resize dominates
    assert cost[2] == pytest.approx(rt)


def test_turnover_cost_floor_applies_on_quiet_hold():
    # A constant nonzero scale => zero re-size turnover, so the calendar floor must still charge cost.
    scale = np.full(30, 2.0)
    cost = ev._turnover_cost(scale, half_spread_bps=10.0)
    rt = 2.0 * 10.0 / 1e4
    expected_floor = 2.0 * rt / ev._REBALANCE_FLOOR_DAYS
    # day 0 has prepend==self so resize=0; every day the floor applies (scale constant => resize 0 after day 0)
    assert cost[5] == pytest.approx(expected_floor)
    assert cost[5] > 0.0            # the ghost-killer: a quiet hold is NOT free


def test_turnover_cost_takes_max_of_components():
    scale = np.array([1.0, 5.0])   # big re-size on day 1
    cost = ev._turnover_cost(scale, half_spread_bps=10.0)
    rt = 2.0 * 10.0 / 1e4
    resize = abs(5.0 - 1.0) * rt
    floor = 5.0 * rt / ev._REBALANCE_FLOOR_DAYS
    assert cost[1] == pytest.approx(max(resize, floor)) == pytest.approx(resize)


def test_turnover_cost_scales_with_half_spread():
    scale = np.full(30, 2.0)
    c10 = ev._turnover_cost(scale, half_spread_bps=10.0)
    c20 = ev._turnover_cost(scale, half_spread_bps=20.0)
    assert np.allclose(c20, 2.0 * c10)


# ---------------- CandidateVerdict.passes ----------------

def test_passes_requires_both_positive():
    base = dict(symbol="X.L", sleeve_id="x", window_start="a", window_end="b", n_days=100, corr_to_book=0.1)
    assert ev.CandidateVerdict(**base, marginal_sharpe_blend=0.1, delta_sharpe_inbook=0.1).passes
    assert not ev.CandidateVerdict(**base, marginal_sharpe_blend=0.1, delta_sharpe_inbook=-0.1).passes
    assert not ev.CandidateVerdict(**base, marginal_sharpe_blend=-0.1, delta_sharpe_inbook=0.1).passes
    assert not ev.CandidateVerdict(**base, marginal_sharpe_blend=0.0, delta_sharpe_inbook=0.1).passes


def test_passes_false_on_nan():
    base = dict(symbol="X.L", sleeve_id="x", window_start="a", window_end="b", n_days=100, corr_to_book=0.1)
    assert not ev.CandidateVerdict(**base, marginal_sharpe_blend=float("nan"), delta_sharpe_inbook=0.1).passes


# ---------------- evaluate_candidate end-to-end (synthetic) ----------------

def _fund_closes(dates: list[str], rng: np.random.Generator) -> dict[str, dict[str, float]]:
    """Independent low-vol streams for the fund instruments so the book is a real adaptive book."""
    closes: dict[str, dict[str, float]] = {}
    for sym in multistrat.FUND_INSTRUMENTS:
        rets = rng.normal(0.0004, 0.008, len(dates))
        closes[sym] = _closes_from_rets(dates, rets)
    return closes


def test_evaluate_candidate_kills_cocrashing_beta():
    # A candidate that is just amplified book beta (perfectly correlated, higher vol, zero own alpha) must
    # KILL: it adds risk without risk-adjusted return. This is the VRP/unlock failure mode.
    rng = np.random.default_rng(7)
    dates = _dates(1500)
    closes = _fund_closes(dates, rng)
    # book proxy = SPY stream; candidate = 1.5x that stream + noise (co-crashing beta, no diversification)
    spy = np.array([closes["SPY"][d] for d in dates])
    spy_ret = np.zeros(len(dates))
    spy_ret[1:] = spy[1:] / spy[:-1] - 1
    cand_ret = 1.5 * spy_ret + rng.normal(0, 0.001, len(dates))
    closes["BETA.L"] = _closes_from_rets(dates, cand_ret)

    v = ev.evaluate_candidate(closes, "BETA.L", "beta_test")
    assert v.n_days > 200
    # corr is measured against the ADAPTIVE book return (vol-normed, trust-weighted), so raw 1.5x-SPY beta
    # shows as moderate positive co-movement, not ~1.0 — that dilution is the point of the adaptive overlay.
    assert v.corr_to_book > 0.15                     # genuinely co-moving (diluted through the overlay)
    assert v.marginal_sharpe_blend < 0.0             # amplified beta adds vol without risk-adjusted return
    assert not v.passes                              # KILL — no marginal risk-adjusted return


def test_evaluate_candidate_passes_true_diversifier():
    # A candidate with independent positive-Sharpe returns (a genuine uncorrelated edge) should PASS both
    # measures — proves the gate isn't rigged to always kill.
    rng = np.random.default_rng(11)
    dates = _dates(1500)
    closes = _fund_closes(dates, rng)
    # strong independent Sharpe, uncorrelated to the book
    cand_ret = rng.normal(0.0009, 0.006, len(dates))
    closes["DIV.L"] = _closes_from_rets(dates, cand_ret)

    v = ev.evaluate_candidate(closes, "DIV.L", "div_test")
    assert v.marginal_sharpe_blend > 0.0
    assert v.delta_sharpe_inbook > 0.0
    assert v.passes


def test_evaluate_candidate_reports_context_columns():
    rng = np.random.default_rng(3)
    dates = _dates(1500, start="2019-01-02")          # covers covid_2020q1 + bear_2022 windows
    closes = _fund_closes(dates, rng)
    closes["CTX.L"] = _closes_from_rets(dates, rng.normal(0.0003, 0.007, len(dates)))
    v = ev.evaluate_candidate(closes, "CTX.L", "ctx")
    assert set(v.cost_sensitivity) == {"5bp", "10bp", "20bp"}
    # more cost never HELPS marginal Sharpe (monotone non-increasing in spread)
    assert v.cost_sensitivity["5bp"] >= v.cost_sensitivity["20bp"] - 1e-9
    assert "covid_2020q1" in v.crash_drawdowns


def test_run_stage1_orchestration_uses_injected_fetch():
    rng = np.random.default_rng(5)
    dates = _dates(1200)
    base = _fund_closes(dates, rng)
    base["CNYA.L"] = _closes_from_rets(dates, rng.normal(0.0003, 0.012, len(dates)))
    base["NDIA.L"] = _closes_from_rets(dates, rng.normal(0.0004, 0.011, len(dates)))

    verdicts = ev.run_stage1(lambda s: base[s])
    assert {v.sleeve_id for v in verdicts} == {"china_a", "india"}
    assert all(v.n_days > 100 for v in verdicts)
