"""R7 (partial) — offline detector calibration checks (gate items 3 & 4).

Acceptance predicate (roadmap R7, the offline-verifiable half):
  - entropy metric returns log2(N) for N equiprobable senders   (gate item 4);
  - the bridge-correlation scorer gives AUC ≈ 1 on a known-linked control pair
    and ≈ 0.5 on a known-unlinked pair                          (gate item 3).

Calibrated on synthetic fixtures only (CLAUDE.md: detectors are never fit to
confirmatory-cell data). These detectors read no pcaps, spawn no engine, and
move no traffic — fully offline. The traffic-moving R7 pieces (churn VM
spin/kill, live selector rebuild, metrics.json emission) are HELD for R4/R6 + a
live grid and are intentionally absent.
"""

import math
import statistics

import pytest

from cmd_chat.sor.analysis.detectors import (
    auc,
    bridge_correlation_auc,
    pearson,
    shannon_entropy_bits,
    synthetic_bridge_fixture,
)


# --------------------------------------------------------------------------- #
# Gate item 4 — anonymity-set entropy.
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("n", [1, 2, 3, 4, 5, 8, 16, 17])
def test_entropy_equiprobable_is_log2n(n):
    # N equiprobable senders -> exactly log2(N) bits (the gate item 4 predicate).
    h = shannon_entropy_bits({f"sender{i}": 1 for i in range(n)})
    assert h == pytest.approx(math.log2(n), abs=1e-12)


def test_entropy_is_maximized_by_the_uniform_distribution():
    n = 8
    uniform = shannon_entropy_bits([1] * n)
    skewed = shannon_entropy_bits([10, 1, 1, 1, 1, 1, 1, 1])
    assert skewed < uniform == pytest.approx(3.0)


def test_entropy_degenerate_cases():
    assert shannon_entropy_bits({"only": 5}) == 0.0  # one sender -> 0 bits
    assert shannon_entropy_bits([]) == 0.0
    assert shannon_entropy_bits([0, 0]) == 0.0


def test_entropy_known_skewed_value():
    # 3:1 split -> H = 0.75*log2(4/3) + 0.25*log2(4) = 0.811278...
    assert shannon_entropy_bits([3, 1]) == pytest.approx(0.8112781, abs=1e-6)


# --------------------------------------------------------------------------- #
# Correlation + AUC primitives.
# --------------------------------------------------------------------------- #
def test_pearson_identities():
    assert pearson([1, 2, 3, 4], [1, 2, 3, 4]) == pytest.approx(1.0)
    assert pearson([1, 2, 3, 4], [4, 3, 2, 1]) == pytest.approx(-1.0)
    assert pearson([1, 2, 3], [5, 5, 5]) == 0.0  # zero variance -> no signal
    assert pearson([], []) == 0.0
    assert pearson([1, 2], [1, 2, 3]) == 0.0  # length mismatch


def test_auc_basics():
    assert auc([3, 4, 5], [0, 1, 2]) == 1.0  # perfectly separated
    assert auc([0, 1], [2, 3]) == 0.0  # perfectly inverted
    assert auc([1, 1], [1, 1]) == 0.5  # all ties -> chance
    assert auc([], [1]) == 0.5


# --------------------------------------------------------------------------- #
# Gate item 3 — bridge-correlation AUC calibration.
# --------------------------------------------------------------------------- #
def test_known_linked_pair_scores_auc_near_one():
    # A linked bridge (egress = ingress + small jitter): the diagonal dominates.
    ingress, egress = synthetic_bridge_fixture(42, linked=True)
    assert bridge_correlation_auc(ingress, egress) == pytest.approx(1.0)
    # Robust across seeds — never below near-perfect.
    for seed in (1, 123456789, 777, 2024):
        ig, eg = synthetic_bridge_fixture(seed, linked=True)
        assert bridge_correlation_auc(ig, eg) >= 0.99


def test_known_unlinked_pair_scores_auc_near_half():
    # A single unlinked pair is noisy (finite flows); the estimator is *unbiased*,
    # so the ensemble mean over many seeds sits at chance (0.5).
    aucs = [
        bridge_correlation_auc(*synthetic_bridge_fixture(seed, n_flows=24, linked=False))
        for seed in range(40)
    ]
    assert statistics.mean(aucs) == pytest.approx(0.5, abs=0.05)
    # A representative single pair is within a chance band (not linkable).
    ig, eg = synthetic_bridge_fixture(42, n_flows=24, linked=False)
    assert 0.35 <= bridge_correlation_auc(ig, eg) <= 0.65


def test_linked_separates_from_unlinked():
    linked = bridge_correlation_auc(*synthetic_bridge_fixture(7, n_flows=24, linked=True))
    unlinked = bridge_correlation_auc(*synthetic_bridge_fixture(7, n_flows=24, linked=False))
    assert linked > unlinked


def test_fixture_is_seed_deterministic():
    a = synthetic_bridge_fixture(99, linked=True)
    b = synthetic_bridge_fixture(99, linked=True)
    assert a == b
    # Different ground truth from the same seed is actually different data.
    assert synthetic_bridge_fixture(99, linked=True) != synthetic_bridge_fixture(99, linked=False)
