import json
import math

import numpy as np

from fxhnt.application.book_allocator import (
    align_edges,
    apply_drawdown_killswitch,
    combine_book,
    evaluate_book,
)


def test_drawdown_killswitch_flattens_and_reenters():
    # a sharp drawdown then recovery: killswitch flattens during the crash, re-enters after recovery
    book = {}
    d = 0
    for r in [0.01] * 20:           # ramp up (peak)
        book[d] = r; d += 1
    for r in [-0.05] * 5:           # -25% crash -> breach kill_dd=0.15
        book[d] = r; d += 1
    for r in [0.02] * 30:           # recovery
        book[d] = r; d += 1
    out = apply_drawdown_killswitch(book, kill_dd=0.15, reenter_dd=0.07)
    days = sorted(book)
    # no lookahead: identical until the kill triggers
    assert out[days[0]] == book[days[0]]
    # at least one day is flattened to 0 during/after the crash
    assert any(out[k] == 0.0 and book[k] != 0.0 for k in days)
    # after full recovery the switch re-enters (last day passes through)
    assert out[days[-1]] == book[days[-1]]


def _synth_edge(seed: int, n: int = 2000, drift: float = 0.0008, vol: float = 0.008,
                start: int = 1000) -> dict[int, float]:
    """A synthetic daily net-return series (positive drift + iid noise) keyed by epoch-day.

    Long sample + a drift comfortably above the noise floor so each edge has a similar,
    clearly-positive standalone Sharpe (the precondition for a diversification uplift — three
    independent same-Sharpe sleeves combine to a higher Sharpe by ~sqrt(n_edges))."""
    rng = np.random.default_rng(seed)
    rets = drift + vol * rng.standard_normal(n)
    return {start + i: float(rets[i]) for i in range(n)}


def test_align_edges_union_and_zero_fill():
    a = {1: 0.01, 2: 0.02, 3: 0.03}        # days 1-3
    b = {3: 0.10, 4: 0.20, 5: 0.30}        # days 3-5 (overlap on day 3, disjoint otherwise)
    dates, arrs = align_edges({"a": a, "b": b})
    assert dates == [1, 2, 3, 4, 5]        # sorted union
    assert np.allclose(arrs["a"], [0.01, 0.02, 0.03, 0.0, 0.0])   # 0.0-fill where absent
    assert np.allclose(arrs["b"], [0.0, 0.0, 0.10, 0.20, 0.30])
    assert set(arrs) == {"a", "b"}


def test_combine_book_hits_vol_target_and_diversification_uplift():
    edges = {
        "e1": _synth_edge(1),
        "e2": _synth_edge(2),
        "e3": _synth_edge(3),
    }
    target_vol = 0.12
    kelly = 0.5
    ppy = 365
    book = combine_book(edges, target_vol=target_vol, periods_per_year=ppy,
                        kelly_fraction=kelly, vol_lookback=60, rebalance_every=21)
    assert isinstance(book, dict)
    rets = np.array([book[d] for d in sorted(book)])
    # (2) book scaled toward the vol target — fractional-Kelly scales the vol-targeted exposure,
    # so the EFFECTIVE target the book is driven toward is kelly_fraction * target_vol.
    effective_target = kelly * target_vol
    realized_ann_vol = float(rets.std() * math.sqrt(ppy))
    assert abs(realized_ann_vol - effective_target) / effective_target < 0.40

    # diversification uplift: combined Sharpe >= max standalone Sharpe.
    def ann_sharpe(x: np.ndarray) -> float:
        x = np.asarray(x, float)
        return float(x.mean() / x.std() * math.sqrt(ppy)) if x.std() > 0 else 0.0

    combined_sr = ann_sharpe(rets)
    standalone = [ann_sharpe(np.array(list(e.values()))) for e in edges.values()]
    assert combined_sr >= max(standalone) - 1e-9


def test_combine_book_deterministic():
    edges = {"e1": _synth_edge(1), "e2": _synth_edge(2), "e3": _synth_edge(3)}
    b1 = combine_book(edges)
    b2 = combine_book(edges)
    assert b1.keys() == b2.keys()
    assert all(b1[d] == b2[d] for d in b1)


def test_anticorrelated_edges_reduce_vol():
    rng = np.random.default_rng(7)
    n = 800
    base = 0.0003 + 0.012 * rng.standard_normal(n)
    a = {1000 + i: float(0.0003 + base[i]) for i in range(n)}
    b = {1000 + i: float(0.0003 - base[i]) for i in range(n)}   # anti-correlated to a
    book = combine_book({"a": a, "b": b}, target_vol=10.0, vol_lookback=60, rebalance_every=21)
    # With a hard leverage cap (no vol-target inflation possible at this absurd target), the
    # combined book of two anti-correlated sleeves has far lower vol than either standalone.
    rets = np.array([book[d] for d in sorted(book)])
    sa = np.array(list(a.values()))
    sb = np.array(list(b.values()))
    assert rets.std() < 0.5 * min(sa.std(), sb.std())


def test_evaluate_book_report_is_json_safe():
    edges = {"e1": _synth_edge(1), "e2": _synth_edge(2), "e3": _synth_edge(3)}
    report = evaluate_book(edges, periods_per_year=365, oos_fraction=0.40)
    assert {"stats", "verdict", "correlation_matrix", "standalone_sharpe"} <= set(report)
    assert {"cagr", "ann_vol", "sharpe", "max_drawdown", "n_obs"} <= set(report["stats"])
    assert {"passed", "dsr", "is_sharpe", "oos_sharpe", "n_trials"} <= set(report["verdict"])
    # correlation matrix is pairwise over the edges
    cm = report["correlation_matrix"]
    assert set(cm) == {"e1", "e2", "e3"}
    for k in cm:
        assert set(cm[k]) == {"e1", "e2", "e3"}
        assert abs(cm[k][k] - 1.0) < 1e-9     # self-correlation = 1
    assert set(report["standalone_sharpe"]) == {"e1", "e2", "e3"}
    json.dumps(report)                          # must be JSON-serializable (no NaN/np types)
