import numpy as np

from fxhnt.application.allocation_engine import (
    cvar_daily,
    equal_weight_marginal_prune,
    full_history_vol,
    killswitch_active,
)


def test_full_history_vol_is_annualised_std():
    r = {
        f"2026-06-{d:02d}": v
        for d, v in zip(range(1, 6), [0.01, -0.01, 0.02, -0.02, 0.0], strict=True)
    }
    assert (
        abs(
            full_history_vol(r)
            - float(np.std([0.01, -0.01, 0.02, -0.02, 0.0]) * np.sqrt(365.0))
        )
        < 1e-12
    )
    assert full_history_vol({"2026-06-01": 0.01}) == 0.0  # < 2 obs → 0


def test_marginal_prune_benches_non_additive_strategy():
    dates = [f"2026-06-{d:02d}" for d in range(1, 41)]
    good = {d: (0.01 if i % 2 else -0.008) for i, d in enumerate(dates)}  # positive-Sharpe
    noise = {d: (0.03 if i % 3 == 0 else -0.02) for i, d in enumerate(dates)}  # drags the book
    kept = equal_weight_marginal_prune({"good": good, "noise": noise})
    assert "good" in kept and "noise" not in kept


def test_killswitch_latches_on_deep_dd_and_reenters():
    # a −30% run then recovery; kill at 0.25, reenter at 0.10
    down = {f"2026-06-{d:02d}": -0.04 for d in range(1, 11)}  # ~ -33% cumulative → breach 0.25
    assert killswitch_active(down, kill_dd=0.25, reenter_dd=0.10) is True
    calm = {f"2026-06-{d:02d}": 0.001 for d in range(1, 11)}
    assert killswitch_active(calm, kill_dd=0.25, reenter_dd=0.10) is False


def test_marginal_prune_tie_break_is_deterministic():
    # two identical NON-additive noise strategies + one good one: whichever gets benched must be STABLE
    # (sorted order), not hash-order-dependent. Run it and assert the survivor set is the same each call.
    dates = [f"2026-06-{d:02d}" for d in range(1, 41)]
    good = {d: (0.01 if i % 2 else -0.008) for i, d in enumerate(dates)}
    noise = {d: (0.03 if i % 3 == 0 else -0.02) for i, d in enumerate(dates)}
    inp = {"good": good, "noiseA": dict(noise), "noiseB": dict(noise)}
    r1 = equal_weight_marginal_prune(inp)
    r2 = equal_weight_marginal_prune(inp)
    assert r1 == r2                     # deterministic
    assert "good" in r1                 # the good one survives


def test_cvar_daily_worst_tail_positive_loss():
    # 10 days; alpha=0.10 -> worst 1 day = -0.05 -> CVaR = +0.05 (positive loss)
    r = {f"d{i}": v for i, v in enumerate([0.01, 0.02, -0.05, 0.0, 0.01, 0.02, -0.01, 0.0, 0.01, 0.02])}
    assert abs(cvar_daily(r, 0.10) - 0.05) < 1e-9


def test_cvar_daily_all_gains_is_zero():
    r = {f"d{i}": 0.01 for i in range(10)}      # worst 10% still +0.01 -> loss magnitude floored at 0
    assert cvar_daily(r, 0.10) == 0.0


def test_cvar_daily_too_short_is_zero():
    assert cvar_daily({"d0": -0.5}, 0.10) == 0.0


def test_cvar_daily_full_history_not_trailing():
    base = {f"d{i}": 0.001 for i in range(20)}
    worse = dict(base)
    worse["old"] = -0.20      # an OLD bad day must change full-history CVaR
    assert cvar_daily(worse, 0.10) > cvar_daily(base, 0.10)
