# agent-office/tests/test_lotto_signals.py
import pytest

from app.curator import signals


def test_sim_consensus_top10_geomean():
    """top-10 consensus 평균이 기하평균 기반인지."""
    best_picks = [
        {"scores": [10, 10, 10, 10, 10]},  # high & uniform
        {"scores": [9,  9,  9,  9,  9]},
        {"scores": [8,  8,  8,  8,  8]},
        {"scores": [7,  7,  7,  7,  7]},
        {"scores": [6,  6,  6,  6,  6]},
        {"scores": [5,  5,  5,  5,  5]},
        {"scores": [4,  4,  4,  4,  4]},
        {"scores": [3,  3,  3,  3,  3]},
        {"scores": [2,  2,  2,  2,  2]},
        {"scores": [1,  1,  1,  1,  1]},  # top 10
        {"scores": [0,  0,  0,  0,  0]},  # bottom 10
    ] * 1 + [{"scores": [0, 0, 0, 0, 0]}] * 10
    result = signals.sim_consensus_score(best_picks)
    assert 0.0 <= result <= 1.0
    assert result > 0.4


def test_sim_consensus_geomean_penalizes_imbalance():
    """5종 중 한 종만 폭주하는 outlier 후보는 균형 후보보다 작아야 한다."""
    balanced = [{"scores": [5, 5, 5, 5, 5]}] * 20
    imbalanced = [{"scores": [25, 0, 0, 0, 0]}] * 20
    s_balanced = signals.sim_consensus_score(balanced)
    s_imbalanced = signals.sim_consensus_score(imbalanced)
    assert s_imbalanced < s_balanced


def test_strategy_drift_score():
    """drift = 전략별 가중치 변화 절댓값 합."""
    w_prev = {"gap_focus": 0.30, "hot_focus": 0.25, "pair_bias": 0.45}
    w_curr = {"gap_focus": 0.40, "hot_focus": 0.20, "pair_bias": 0.40}
    result = signals.strategy_drift_score(w_prev, w_curr)
    assert abs(result - 0.20) < 1e-9


def test_strategy_drift_new_strategy_appears():
    """이전에 없던 전략이 등장하면 그 가중치 전체가 drift에 가산."""
    w_prev = {"gap_focus": 0.5, "hot_focus": 0.5}
    w_curr = {"gap_focus": 0.4, "hot_focus": 0.4, "newbie": 0.2}
    result = signals.strategy_drift_score(w_prev, w_curr)
    assert abs(result - 0.4) < 1e-9


def test_confidence_score_passthrough():
    """confidence는 큐레이션 결과의 값 그대로 (0~1 clamp 확인)."""
    assert signals.confidence_score({"confidence": 0.85}) == 0.85
    assert signals.confidence_score({"confidence": 1.2}) == 1.0
    assert signals.confidence_score({"confidence": -0.1}) == 0.0
    assert signals.confidence_score({}) is None


def test_adaptive_baseline_cold_start():
    """window 크기 < 4 → warmup, z=None."""
    bl = signals.AdaptiveBaseline(window=[1.0, 1.1, 0.9], window_max=8)
    z, fire = bl.evaluate(value=1.5, z_normal=1.5, z_urgent=2.5)
    assert fire == "warmup"
    assert z is None


def test_adaptive_baseline_preparing():
    """window 4~7 → 보수적 임계치 z=2.0."""
    bl = signals.AdaptiveBaseline(window=[1.0, 1.0, 1.0, 1.0], window_max=8)
    z, fire = bl.evaluate(value=3.0, z_normal=1.5, z_urgent=2.5)
    assert fire in ("normal", "urgent")


def test_adaptive_baseline_normal_window_full():
    """window 8 풀, value가 평균보다 1.5σ 이상이면 normal."""
    bl = signals.AdaptiveBaseline(
        window=[1.0, 1.1, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0],
        window_max=8,
    )
    z, fire = bl.evaluate(value=1.12, z_normal=1.5, z_urgent=2.5)
    assert fire == "normal"
    assert z is not None and z >= 1.5


def test_adaptive_baseline_urgent():
    """z >= 2.5 → urgent."""
    bl = signals.AdaptiveBaseline(
        window=[1.0, 1.1, 0.9, 1.0, 1.0, 1.1, 0.9, 1.0],
        window_max=8,
    )
    z, fire = bl.evaluate(value=2.0, z_normal=1.5, z_urgent=2.5)
    assert fire == "urgent"


def test_adaptive_baseline_push_updates_window():
    """push 시 FIFO 동작."""
    bl = signals.AdaptiveBaseline(window=[1, 2, 3, 4, 5, 6, 7, 8], window_max=8)
    bl.push(9.0)
    assert bl.window == [2, 3, 4, 5, 6, 7, 8, 9.0]


def test_decide_fire_level_two_normals_escalate():
    sigs = [
        {"metric": "sim", "z": 1.6, "fire": "normal"},
        {"metric": "drift", "z": 1.7, "fire": "normal"},
        {"metric": "conf", "z": 0.5, "fire": "noop"},
    ]
    assert signals.decide_overall_fire(sigs) == "urgent"


def test_decide_fire_level_single_normal():
    sigs = [
        {"metric": "sim", "z": 1.6, "fire": "normal"},
        {"metric": "drift", "z": 0.3, "fire": "noop"},
    ]
    assert signals.decide_overall_fire(sigs) == "normal"


def test_decide_fire_level_single_urgent():
    sigs = [
        {"metric": "sim", "z": 3.0, "fire": "urgent"},
        {"metric": "drift", "z": 0.2, "fire": "noop"},
    ]
    assert signals.decide_overall_fire(sigs) == "urgent"


def test_decide_fire_level_all_noop():
    sigs = [{"metric": "sim", "z": 0.5, "fire": "noop"}]
    assert signals.decide_overall_fire(sigs) == "noop"
