"""Unit tests for v3 correlation-aware clustering.

Tests for compute_n_eff, compute_cluster_llr, and cluster_evidence functions.

Requirements validated: 4.1–4.5
"""
from __future__ import annotations

from datetime import datetime, timezone

import pytest

from services.aggregation.scoring import EvidenceUnit
from services.aggregation.worker import (
    cluster_evidence,
    compute_cluster_llr,
    compute_n_eff,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_unit(cluster_id: str = "cluster_a", symbol: str = "AAPL") -> EvidenceUnit:
    """Create a minimal EvidenceUnit for testing."""
    return EvidenceUnit(
        symbol=symbol,
        layer="company",
        event_type="earnings",
        source_id="doc_1",
        source_group="reuters",
        timestamp=datetime(2024, 1, 15, 12, 0, tzinfo=timezone.utc),
        horizon="7d",
        direction=1,
        sentiment_strength=0.8,
        impact=0.7,
        extraction_conf=0.9,
        source_cred=0.85,
        novelty=0.6,
        event_base_rate=0.25,
        cluster_id=cluster_id,
    )


# ---------------------------------------------------------------------------
# Test: 3 identical articles from same source → n_eff < 3
# Requirement: 4.2, 4.3
# ---------------------------------------------------------------------------


class TestNEffIdenticalArticles:
    """3 identical articles from same wire (default rho=0.80) → n_eff < 3."""

    def test_n_eff_less_than_cluster_size(self):
        llrs = [1.0, 1.0, 1.0]
        # Default correlations: rho=0.80 for all pairs (same wire/source)
        n_eff = compute_n_eff(llrs)

        # Formula: (3)² / (3 + 2×3×0.80×1×1) = 9 / (3 + 4.8) = 9/7.8 ≈ 1.154
        expected = 9.0 / 7.8
        assert n_eff < 3.0
        assert n_eff == pytest.approx(expected, rel=1e-6)

    def test_n_eff_discounts_correlated_signals(self):
        """Higher correlation → lower n_eff."""
        llrs = [1.0, 1.0, 1.0]
        n_eff_correlated = compute_n_eff(llrs)  # default rho=0.80

        # Independent: rho=0.0
        identity = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        n_eff_independent = compute_n_eff(llrs, correlations=identity)

        assert n_eff_correlated < n_eff_independent


# ---------------------------------------------------------------------------
# Test: 3 independent articles → n_eff ≈ 3
# Requirement: 4.2, 4.3
# ---------------------------------------------------------------------------


class TestNEffIndependentArticles:
    """3 independent articles (rho=0.0) → n_eff = 3.0."""

    def test_n_eff_equals_cluster_size(self):
        llrs = [1.0, 1.0, 1.0]
        # Zero off-diagonal correlations
        correlations = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        n_eff = compute_n_eff(llrs, correlations=correlations)

        # n_eff = (3)² / (3 + 0) = 3.0
        assert n_eff == pytest.approx(3.0, rel=1e-6)

    def test_n_eff_with_varying_magnitudes(self):
        """Independent signals with different magnitudes still give n <= cluster size."""
        llrs = [0.5, 1.0, 2.0]
        correlations = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        n_eff = compute_n_eff(llrs, correlations=correlations)

        # With zero correlations, n_eff = (sum |w|)^2 / sum(w^2)
        # = (0.5+1.0+2.0)^2 / (0.25+1.0+4.0) = 12.25 / 5.25 ≈ 2.333
        expected = (3.5**2) / (0.25 + 1.0 + 4.0)
        assert n_eff == pytest.approx(expected, rel=1e-6)
        assert n_eff <= 3.0


# ---------------------------------------------------------------------------
# Test: Single signal cluster → n_eff = 1.0
# Requirement: 4.2
# ---------------------------------------------------------------------------


class TestNEffSingleSignal:
    """Single signal in a cluster → n_eff = 1.0."""

    def test_single_signal(self):
        llrs = [0.5]
        n_eff = compute_n_eff(llrs)
        assert n_eff == 1.0

    def test_empty_cluster(self):
        llrs: list[float] = []
        n_eff = compute_n_eff(llrs)
        assert n_eff == 1.0


# ---------------------------------------------------------------------------
# Test: Cluster LLR clamp at ±2.5
# Requirement: 4.4, 4.5
# ---------------------------------------------------------------------------


class TestClusterLLRClamp:
    """Cluster LLR is clamped to [-2.5, 2.5]."""

    def test_positive_clamp(self):
        """Very large positive LLRs with high n_eff → clamped to 2.5."""
        llrs = [2.0, 2.0, 2.0, 2.0, 2.0]
        # Use independent correlations for max n_eff
        correlations = [
            [1, 0, 0, 0, 0],
            [0, 1, 0, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 0, 1, 0],
            [0, 0, 0, 0, 1],
        ]
        n_eff = compute_n_eff(llrs, correlations=correlations)
        cluster_llr = compute_cluster_llr(llrs, n_eff)

        # weighted_mean = 2.0, sqrt(5) ≈ 2.236, raw = 4.47 → clamp to 2.5
        assert cluster_llr == pytest.approx(2.5, rel=1e-6)

    def test_negative_clamp(self):
        """Very negative LLRs with high n_eff → clamped to -2.5."""
        llrs = [-2.0, -2.0, -2.0, -2.0, -2.0]
        correlations = [
            [1, 0, 0, 0, 0],
            [0, 1, 0, 0, 0],
            [0, 0, 1, 0, 0],
            [0, 0, 0, 1, 0],
            [0, 0, 0, 0, 1],
        ]
        n_eff = compute_n_eff(llrs, correlations=correlations)
        cluster_llr = compute_cluster_llr(llrs, n_eff)

        assert cluster_llr == pytest.approx(-2.5, rel=1e-6)

    def test_within_bounds_no_clamp(self):
        """Small LLRs with low n_eff → no clamping needed."""
        llrs = [0.3, 0.4]
        n_eff = compute_n_eff(llrs)
        cluster_llr = compute_cluster_llr(llrs, n_eff)

        assert -2.5 <= cluster_llr <= 2.5
        # Should NOT be at the clamp boundary
        assert abs(cluster_llr) < 2.5

    def test_single_signal_clamp(self):
        """Single signal beyond clamp → clamped."""
        llrs = [3.0]
        cluster_llr = compute_cluster_llr(llrs, n_eff=1.0)
        assert cluster_llr == pytest.approx(2.5, rel=1e-6)

    def test_single_signal_negative_clamp(self):
        """Single negative signal beyond clamp → clamped to -2.5."""
        llrs = [-3.0]
        cluster_llr = compute_cluster_llr(llrs, n_eff=1.0)
        assert cluster_llr == pytest.approx(-2.5, rel=1e-6)


# ---------------------------------------------------------------------------
# Test: All-zero LLRs → cluster_llr = 0.0
# Requirement: 4.4
# ---------------------------------------------------------------------------


class TestClusterLLRZero:
    """All-zero LLRs produce zero cluster LLR."""

    def test_all_zeros(self):
        llrs = [0.0, 0.0, 0.0]
        n_eff = compute_n_eff(llrs)
        cluster_llr = compute_cluster_llr(llrs, n_eff)
        assert cluster_llr == 0.0

    def test_empty_llrs(self):
        """Empty LLR list → 0.0."""
        cluster_llr = compute_cluster_llr([], n_eff=1.0)
        assert cluster_llr == 0.0


# ---------------------------------------------------------------------------
# Test: Grouping by correct key dimensions (cluster_id)
# Requirement: 4.1
# ---------------------------------------------------------------------------


class TestClusterEvidence:
    """cluster_evidence groups EvidenceUnits by cluster_id."""

    def test_grouping_by_cluster_id(self):
        """Units with same cluster_id are grouped together."""
        unit_a1 = _make_unit(cluster_id="cluster_a")
        unit_a2 = _make_unit(cluster_id="cluster_a")
        unit_b1 = _make_unit(cluster_id="cluster_b")

        units = [unit_a1, unit_a2, unit_b1]
        llrs = [1.0, 0.5, -0.3]

        clusters = cluster_evidence(units, llrs)

        assert len(clusters) == 2

        # Find clusters by id
        cluster_map = {c.cluster_id: c for c in clusters}

        assert "cluster_a" in cluster_map
        assert "cluster_b" in cluster_map

        # Cluster A has 2 units
        assert len(cluster_map["cluster_a"].units) == 2
        assert cluster_map["cluster_a"].llrs == [1.0, 0.5]

        # Cluster B has 1 unit
        assert len(cluster_map["cluster_b"].units) == 1
        assert cluster_map["cluster_b"].llrs == [-0.3]

    def test_single_cluster(self):
        """All units with same cluster_id → one cluster."""
        units = [_make_unit(cluster_id="only") for _ in range(4)]
        llrs = [0.1, 0.2, 0.3, 0.4]

        clusters = cluster_evidence(units, llrs)

        assert len(clusters) == 1
        assert clusters[0].cluster_id == "only"
        assert len(clusters[0].units) == 4
        assert clusters[0].llrs == [0.1, 0.2, 0.3, 0.4]

    def test_each_unit_different_cluster(self):
        """Each unit in its own cluster → N clusters."""
        units = [_make_unit(cluster_id=f"c_{i}") for i in range(5)]
        llrs = [0.1 * i for i in range(5)]

        clusters = cluster_evidence(units, llrs)

        assert len(clusters) == 5
        for c in clusters:
            assert len(c.units) == 1

    def test_empty_input(self):
        """No units → no clusters."""
        clusters = cluster_evidence([], [])
        assert clusters == []

    def test_llrs_parallel_to_units(self):
        """LLRs are correctly associated with their units."""
        unit_x = _make_unit(cluster_id="x")
        unit_y = _make_unit(cluster_id="y")
        unit_x2 = _make_unit(cluster_id="x")

        units = [unit_x, unit_y, unit_x2]
        llrs = [1.5, -0.7, 2.3]

        clusters = cluster_evidence(units, llrs)
        cluster_map = {c.cluster_id: c for c in clusters}

        assert cluster_map["x"].llrs == [1.5, 2.3]
        assert cluster_map["y"].llrs == [-0.7]
