# tests/test_calibration_monitor.py
import pytest
import numpy as np
from llm_guard.calibration.calibration_monitor_legacy import CalibrationConvergenceMonitor


def test_not_converged_before_window():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.8, 0.81, 0.79, 0.80]:  # 4 rounds, window=5
        m.record_round(p)
    assert not m.is_converged()

def test_not_converged_at_window_minus_one():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.8, 0.81, 0.79, 0.80]:
        m.record_round(p)
    assert len(m.precision_history) == 4
    assert not m.is_converged()

def test_converges_at_exactly_window_rounds_when_stable():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.80, 0.800, 0.801, 0.799, 0.800]:  # std ≈ 0.0007 < 0.02
        m.record_round(p)
    assert m.is_converged()

def test_not_converged_when_volatile():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.9, 0.5, 0.8, 0.4, 0.7]:  # high variance
        m.record_round(p)
    assert not m.is_converged()

def test_convergence_round_none_before_convergence():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    m.record_round(0.8)
    assert m.convergence_round is None

def test_convergence_round_set_after_convergence():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.80, 0.800, 0.801, 0.799, 0.800]:
        m.record_round(p)
    assert isinstance(m.convergence_round, int)
    assert m.convergence_round == 5  # declared at round 5

def test_should_collect_more_labels_is_inverse():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.80, 0.800, 0.801, 0.799, 0.800]:
        m.record_round(p)
    assert m.is_converged() != m.should_collect_more_labels()

def test_precision_history_recorded():
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    precisions = [0.8, 0.82, 0.79]
    for p in precisions:
        m.record_round(p)
    assert m.precision_history == precisions

def test_convergence_persists_after_declared():
    """Once converged, remains converged even with new (stable) rounds added."""
    m = CalibrationConvergenceMonitor(window=5, stability_threshold=0.02)
    for p in [0.80, 0.800, 0.801, 0.799, 0.800]:
        m.record_round(p)
    m.record_round(0.801)  # add one more stable round
    assert m.is_converged()
    assert m.convergence_round == 5  # convergence_round doesn't change


def test_invalid_params_raise():
    """window < 2 and stability_threshold <= 0 should raise ValueError."""
    with pytest.raises(ValueError, match="window"):
        CalibrationConvergenceMonitor(window=1)
    with pytest.raises(ValueError, match="stability_threshold"):
        CalibrationConvergenceMonitor(stability_threshold=0.0)


from llm_guard.scoring.agent_guard import GuardPhaseController


def test_phase_controller_none_monitor_unchanged():
    """monitor=None behaves identically to current behavior."""
    ctrl = GuardPhaseController(min_wrong=3, min_total=6, convergence_monitor=None)
    for _ in range(3):
        ctrl.observe(1)  # wrong
    for _ in range(3):
        ctrl.observe(0)  # correct
    assert ctrl.status.phase in ("calibrating", "self_sustaining")


def test_phase_controller_blocks_on_unconverged_monitor():
    """Even if count threshold met, don't switch when monitor not converged."""
    from llm_guard.calibration.calibration_monitor_legacy import CalibrationConvergenceMonitor
    monitor = CalibrationConvergenceMonitor(window=10, stability_threshold=0.02)
    ctrl = GuardPhaseController(min_wrong=3, min_total=6, convergence_monitor=monitor)
    for _ in range(3):
        ctrl.observe(1)
    for _ in range(3):
        ctrl.observe(0)
    # monitor has no rounds recorded → not converged → stays in bootstrap/calibrating
    status = ctrl.status
    assert status.phase in ("bootstrap", "calibrating")


def test_phase_controller_allows_switch_when_converged():
    """Switches when both count threshold AND monitor converged."""
    from llm_guard.calibration.calibration_monitor_legacy import CalibrationConvergenceMonitor
    monitor = CalibrationConvergenceMonitor(window=3, stability_threshold=0.05)
    for p in [0.85, 0.85, 0.85]:
        monitor.record_round(p)
    assert monitor.is_converged()
    ctrl = GuardPhaseController(min_wrong=2, min_total=4, convergence_monitor=monitor)
    for _ in range(2):
        ctrl.observe(1)
    for _ in range(2):
        ctrl.observe(0)
    status = ctrl.status
    assert status.phase == "self_sustaining"


def test_phase_controller_floor_enforced_regardless_of_monitor():
    """n_total < 20 never switches to self_sustaining even if monitor is converged."""
    from llm_guard.calibration.calibration_monitor_legacy import CalibrationConvergenceMonitor
    monitor = CalibrationConvergenceMonitor(window=3, stability_threshold=0.05)
    for p in [0.85, 0.85, 0.85]:
        monitor.record_round(p)
    ctrl = GuardPhaseController(min_wrong=1, min_total=2, convergence_monitor=monitor)
    ctrl.observe(1)
    ctrl.observe(0)
    # n_total=2 < 20 floor → should NOT be self_sustaining
    status = ctrl.status
    assert status.n_total < 20
