"""Unit tests for the pure cross-sectional crypto PRICE/VOLUME factor scores.
Synthetic panels only — no I/O, no live network."""
import math

from fxhnt.domain.crypto_factors import (
    xs_illiquidity_score,
    xs_lowvol_score,
    xs_reversal_score,
)

Panel = dict[str, dict[int, tuple[float, float, float]]]


def _flat_panel(closes_by_sym: dict[str, list[float]], qvol: float = 5e6,
                funding: float = 0.0, start: int = 1000) -> Panel:
    """Build a Panel {sym:{day:(close, qvol, funding)}} from per-symbol close lists."""
    panel: Panel = {}
    for sym, closes in closes_by_sym.items():
        panel[sym] = {start + i: (c, qvol, funding) for i, c in enumerate(closes)}
    return panel


def test_reversal_ranks_recent_losers_highest():
    # WIN rallied recently, LOSE dropped, FLAT unchanged. Reversal longs the loser.
    panel = _flat_panel({
        "WIN": [100.0] * 10 + [100.0, 110.0, 120.0, 130.0],
        "LOSE": [100.0] * 10 + [100.0, 90.0, 80.0, 70.0],
        "FLAT": [100.0] * 14,
        "MID": [100.0] * 10 + [100.0, 102.0, 104.0, 106.0],
    })
    asof = 1013
    elig = ["WIN", "LOSE", "FLAT", "MID"]
    scores = xs_reversal_score(panel, elig, asof, lookback=3)
    assert set(scores) == set(elig)
    # the recent loser scores highest, the recent winner lowest
    assert scores["LOSE"] == max(scores.values())
    assert scores["WIN"] == min(scores.values())


def test_lowvol_ranks_low_vol_coins_highest():
    # CALM has tiny daily moves, WILD oscillates hard. Low-vol longs CALM.
    calm = [100.0 + 0.05 * ((-1) ** i) for i in range(60)]
    wild = [100.0 + 10.0 * ((-1) ** i) for i in range(60)]
    medium = [100.0 + 2.0 * ((-1) ** i) for i in range(60)]
    other = [100.0 + 1.0 * ((-1) ** i) for i in range(60)]
    panel = _flat_panel({"CALM": calm, "WILD": wild, "MED": medium, "OTH": other})
    asof = 1059
    elig = ["CALM", "WILD", "MED", "OTH"]
    scores = xs_lowvol_score(panel, elig, asof, window=30)
    assert set(scores) == set(elig)
    assert scores["CALM"] == max(scores.values())
    assert scores["WILD"] == min(scores.values())


def test_illiquidity_ranks_low_qvol_coins_highest():
    # Same prices, different dollar-volume. Illiquidity longs the THIN coin.
    panel: Panel = {}
    for sym, qv in {"THIN": 1e5, "FAT": 1e9, "MID": 5e6, "OTH": 2e7}.items():
        panel[sym] = {1000 + i: (100.0, qv, 0.0) for i in range(40)}
    asof = 1039
    elig = ["THIN", "FAT", "MID", "OTH"]
    scores = xs_illiquidity_score(panel, elig, asof, window=30)
    assert set(scores) == set(elig)
    assert scores["THIN"] == max(scores.values())
    assert scores["FAT"] == min(scores.values())


def test_scores_aligned_and_robust_to_insufficient_history():
    # NEW has only one bar -> its factor value is None -> robust_z neutralizes to 0.0,
    # but it is still present in every returned dict (aligned to `eligible`).
    panel = _flat_panel({
        "A": [100.0] * 10 + [100.0, 90.0, 80.0, 70.0],
        "B": [100.0] * 10 + [100.0, 110.0, 120.0, 130.0],
        "C": [100.0] * 14,
    })
    panel["NEW"] = {1013: (100.0, 5e6, 0.0)}   # single bar at asof, no history
    asof = 1013
    elig = ["A", "B", "C", "NEW"]
    for fn in (xs_reversal_score, xs_lowvol_score, xs_illiquidity_score):
        scores = fn(panel, elig, asof)
        assert set(scores) == set(elig)
        assert all(math.isfinite(v) for v in scores.values())
        assert scores["NEW"] == 0.0   # insufficient data -> neutral


def test_reversal_is_point_in_time():
    # A coin's FUTURE prices must not change its score at an earlier asof.
    base = {
        "A": [100.0] * 10 + [100.0, 90.0, 80.0, 70.0],
        "B": [100.0] * 10 + [100.0, 110.0, 120.0, 130.0],
        "C": [100.0] * 14,
        "D": [100.0] * 10 + [100.0, 105.0, 95.0, 100.0],
    }
    panel = _flat_panel(base)
    asof = 1013
    elig = ["A", "B", "C", "D"]
    before = xs_reversal_score(panel, elig, asof, lookback=3)
    # Append a wild FUTURE move to A after the asof day — must not change the asof score.
    panel["A"][1014] = (1000.0, 5e6, 0.0)
    panel["A"][1015] = (5.0, 5e6, 0.0)
    after = xs_reversal_score(panel, elig, asof, lookback=3)
    assert before == after


def test_lowvol_is_point_in_time():
    calm = [100.0 + 0.05 * ((-1) ** i) for i in range(60)]
    wild = [100.0 + 10.0 * ((-1) ** i) for i in range(60)]
    med = [100.0 + 2.0 * ((-1) ** i) for i in range(60)]
    oth = [100.0 + 1.0 * ((-1) ** i) for i in range(60)]
    panel = _flat_panel({"CALM": calm, "WILD": wild, "MED": med, "OTH": oth})
    asof = 1040
    elig = ["CALM", "WILD", "MED", "OTH"]
    before = xs_lowvol_score(panel, elig, asof, window=30)
    # future explosion in CALM after asof must not affect its asof score
    panel["CALM"][1041] = (10000.0, 5e6, 0.0)
    panel["CALM"][1042] = (1.0, 5e6, 0.0)
    after = xs_lowvol_score(panel, elig, asof, window=30)
    assert before == after
