import json

import numpy as np

from fxhnt.application.trend_runner import (
    TrendRunner,
    TrendRunResult,
    evaluate_trend,
)

_PPY = 252


def _uptrend_panel(n: int = 300) -> dict[str, dict[int, float]]:
    """Three assets: one sustained uptrend, two flat-ish (small noise)."""
    rng = np.random.default_rng(0)
    panel: dict[str, dict[int, float]] = {}
    # sustained uptrend
    panel["UP"] = {i: float(100.0 * (1.0 + 0.003) ** i) for i in range(n)}
    # flat with tiny deterministic wobble
    panel["FLAT1"] = {i: float(100.0 + 0.5 * np.sin(i / 7.0)) for i in range(n)}
    panel["FLAT2"] = {i: float(50.0 + 0.3 * np.cos(i / 5.0)) for i in range(n)}
    return panel


def _downtrend_panel(n: int = 300) -> dict[str, dict[int, float]]:
    """One sustained downtrend asset (for crisis-alpha short test)."""
    return {"DOWN": {i: float(100.0 * (1.0 - 0.003) ** i) for i in range(n)}}


def test_runner_returns_result_shape():
    res = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30).run()
    assert isinstance(res, TrendRunResult)
    assert isinstance(res.returns, np.ndarray)
    assert res.n_rebalances == len(res.returns)
    assert res.n_rebalances > 0
    assert np.isfinite(res.returns).all()
    assert res.n_assets_avg > 0.0


def test_runner_emits_dates_parallel_to_returns():
    res = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30).run()
    assert len(res.dates) == len(res.returns)
    assert res.dates == sorted(res.dates)
    assert len(set(res.dates)) == len(res.dates)
    assert all(isinstance(d, int) for d in res.dates)


def test_captures_uptrend_long():
    res = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30, target_vol=0.10).run()
    # going long the persistent uptrend earns positive cumulative return
    assert float(np.sum(res.returns)) > 0.0


def test_shorts_downtrend_when_long_short():
    ls = TrendRunner(_downtrend_panel(), min_history=130, vol_window=30,
                     long_short=True).run()
    lf = TrendRunner(_downtrend_panel(), min_history=130, vol_window=30,
                     long_short=False).run()
    # long_short shorts the downtrend -> net positive; long/flat earns ~0 (flat at 0)
    assert float(np.sum(ls.returns)) > 0.0
    assert float(np.sum(ls.returns)) > float(np.sum(lf.returns))
    assert abs(float(np.sum(lf.returns))) < 1e-6  # long-only on a downtrend = stays flat


def test_determinism():
    r1 = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30).run().returns
    r2 = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30).run().returns
    assert np.array_equal(r1, r2)


def test_gross_cap_limits_leverage():
    res = TrendRunner(_uptrend_panel(), min_history=130, vol_window=30,
                      target_vol=5.0, gross_cap=1.0).run()
    # even with an absurd target vol, gross is capped -> returns stay bounded per period
    assert np.isfinite(res.returns).all()
    assert np.max(np.abs(res.returns)) < 1.0


def test_funding_overlay_subtracts_position_times_funding():
    """With a funding panel, each booked day's return is the price-only return MINUS Σ position·funding
    charged on the realized day — the exact perp funding P&L a long book pays. Longs (w>0) at positive
    funding pay, so the net return is strictly lower."""
    panel = _uptrend_panel()
    r = TrendRunner(panel, min_history=130, vol_window=30, long_short=False)
    price = r.run()
    F = 0.001  # constant +10 bp/day funding on every symbol
    funding = {sym: {d: F for d in series} for sym, series in panel.items()}
    net = TrendRunner(panel, min_history=130, vol_window=30, long_short=False, funding=funding).run()
    wc = r.weights_and_contributions()   # {realized_day: {sym: (weight, ret)}}
    assert net.dates == price.dates
    for i, day in enumerate(net.dates):
        expected_fc = sum(w * F for (w, _r) in wc[day].values())
        assert abs((price.returns[i] - net.returns[i]) - expected_fc) < 1e-12
    # a long-biased book at positive funding is a NET DRAG
    assert float(np.sum(net.returns)) < float(np.sum(price.returns))


def test_weights_and_contributions_reconcile_with_funding():
    """The reconciliation invariant survives funding: Σ weight·contribution == run().returns even when a
    funding panel is set. The per-symbol contribution nets the funding each position pays, so the surfaced
    attribution still reproduces the booked (cost-free) return — no live-book/returns-book drift."""
    panel = _uptrend_panel()
    F = 0.001
    funding = {sym: {d: F for d in series} for sym, series in panel.items()}
    # cost_bps=0: the weights reproduce the COST-FREE return (turnover is a separate downstream haircut,
    # by design); with that isolated, Σ weight·contribution must equal run()'s funding-net return.
    r = TrendRunner(panel, min_history=130, vol_window=30, long_short=False, cost_bps=0.0, funding=funding)
    res = r.run()
    wc = r.weights_and_contributions()
    for i, day in enumerate(res.dates):
        implied = sum(w * ret for (w, ret) in wc[day].values())
        assert abs(implied - float(res.returns[i])) < 1e-12


def test_no_funding_panel_leaves_returns_unchanged():
    """funding=None (default) is exactly the price-only behavior — no silent change to existing tracks."""
    panel = _uptrend_panel()
    a = TrendRunner(panel, min_history=130, vol_window=30).run().returns
    b = TrendRunner(panel, min_history=130, vol_window=30, funding=None).run().returns
    assert np.array_equal(a, b)


def test_short_positions_receive_funding_credit():
    """Sign correctness: a SHORT (w<0) at positive funding RECEIVES funding, so long_short net return
    EXCEEDS the price-only return on a shorted downtrend."""
    panel = _downtrend_panel()
    F = 0.001
    funding = {sym: {d: F for d in series} for sym, series in panel.items()}
    price = TrendRunner(panel, min_history=130, vol_window=30, long_short=True).run()
    net = TrendRunner(panel, min_history=130, vol_window=30, long_short=True, funding=funding).run()
    assert float(np.sum(net.returns)) > float(np.sum(price.returns))


def test_evaluate_trend_matrix():
    panel = _uptrend_panel()
    configs = [
        {"label": "fast", "lookbacks": (10, 20, 40)},
        {"label": "slow", "lookbacks": (20, 60, 120)},
        {"label": "long_only", "lookbacks": (20, 60, 120), "long_short": False},
    ]
    out = evaluate_trend(panel, periods_per_year=_PPY, configs=configs)
    assert out["n_trials"] == len(configs)
    assert set(out["cells"]) == {"fast", "slow", "long_only"}
    for label, cell in out["cells"].items():
        assert cell["config_label"] == label
        assert "stats" in cell and "verdict" in cell
        assert cell["verdict"]["n_trials"] == len(configs)
    # JSON-serializable (NaN-safe)
    json.dumps(out)
