"""Unit tests for v3 LLR entropy contradiction score.

Tests compute_v3_contradiction from services.aggregation.contradiction.

Requirements validated: 7.1–7.7
"""
from __future__ import annotations

import math

import pytest

from services.aggregation.contradiction import compute_v3_contradiction
from services.aggregation.worker import EvidenceCluster


def _make_cluster(cluster_llr: float) -> EvidenceCluster:
    """Helper to create a minimal EvidenceCluster with a given cluster_llr."""
    return EvidenceCluster(
        cluster_id="test", units=[], llrs=[], n_eff=1.0, cluster_llr=cluster_llr
    )


# ---------------------------------------------------------------------------
# 1. All bullish (unidirectional positive) → contradiction = 0.0
# ---------------------------------------------------------------------------


class TestUnidirectionalPositive:
    """When all cluster LLRs are positive, there is no contradiction."""

    def test_all_bullish(self):
        clusters = [_make_cluster(1.0), _make_cluster(0.5), _make_cluster(0.8)]
        assert compute_v3_contradiction(clusters) == 0.0

    def test_single_bullish(self):
        clusters = [_make_cluster(2.0)]
        assert compute_v3_contradiction(clusters) == 0.0


# ---------------------------------------------------------------------------
# 2. All bearish (unidirectional negative) → contradiction = 0.0
# ---------------------------------------------------------------------------


class TestUnidirectionalNegative:
    """When all cluster LLRs are negative, there is no contradiction."""

    def test_all_bearish(self):
        clusters = [_make_cluster(-1.0), _make_cluster(-0.5)]
        assert compute_v3_contradiction(clusters) == 0.0

    def test_single_bearish(self):
        clusters = [_make_cluster(-2.5)]
        assert compute_v3_contradiction(clusters) == 0.0


# ---------------------------------------------------------------------------
# 3. Equal split → high contradiction near 1.0
# ---------------------------------------------------------------------------


class TestEqualSplit:
    """Equal positive and negative evidence produces maximum entropy (H=1.0),
    modulated by the volume factor."""

    def test_equal_split_moderate_mass(self):
        """LLR +2.0 and -2.0: E_pos=2, E_neg=2, E_total=4.
        H_conflict = 1.0 (50/50 split).
        volume_factor = 1 - exp(-4/3) ≈ 0.7364.
        contradiction ≈ 0.7364.
        """
        clusters = [_make_cluster(2.0), _make_cluster(-2.0)]
        result = compute_v3_contradiction(clusters)
        expected_volume = 1.0 - math.exp(-4.0 / 3.0)
        expected = 1.0 * expected_volume  # H_conflict = 1.0 for equal split
        assert result == pytest.approx(expected, abs=1e-4)

    def test_equal_split_large_mass(self):
        """LLR +5.0 and -5.0: E_total=10, volume_factor → ~0.964.
        contradiction near 1.0.
        """
        clusters = [_make_cluster(5.0), _make_cluster(-5.0)]
        result = compute_v3_contradiction(clusters)
        expected_volume = 1.0 - math.exp(-10.0 / 3.0)
        expected = 1.0 * expected_volume
        assert result == pytest.approx(expected, abs=1e-4)
        assert result > 0.9  # Near 1.0 due to large evidence mass


# ---------------------------------------------------------------------------
# 4. E_total = 0 → contradiction = 0.0
# ---------------------------------------------------------------------------


class TestZeroEvidence:
    """When all cluster LLRs are zero, E_total=0, contradiction is 0."""

    def test_all_zero_llrs(self):
        clusters = [_make_cluster(0.0), _make_cluster(0.0), _make_cluster(0.0)]
        assert compute_v3_contradiction(clusters) == 0.0

    def test_single_zero_llr(self):
        clusters = [_make_cluster(0.0)]
        assert compute_v3_contradiction(clusters) == 0.0


# ---------------------------------------------------------------------------
# 5. Empty clusters → contradiction = 0.0
# ---------------------------------------------------------------------------


class TestEmptyClusters:
    """Empty cluster list returns 0.0."""

    def test_empty_list(self):
        assert compute_v3_contradiction([]) == 0.0


# ---------------------------------------------------------------------------
# 6. Small evidence mass → suppressed score (volume_factor effect)
# ---------------------------------------------------------------------------


class TestSmallEvidenceMass:
    """Small E_total leads to a small volume_factor that suppresses the score."""

    def test_tiny_equal_split(self):
        """LLR +0.1 and -0.1: E_total=0.2.
        H_conflict = 1.0 (equal split).
        volume_factor = 1 - exp(-0.2/3) ≈ 0.0645.
        contradiction ≈ 0.0645 (suppressed).
        """
        clusters = [_make_cluster(0.1), _make_cluster(-0.1)]
        result = compute_v3_contradiction(clusters)
        expected_volume = 1.0 - math.exp(-0.2 / 3.0)
        expected = 1.0 * expected_volume
        assert result == pytest.approx(expected, abs=1e-4)
        # Confirm suppression: score well below 0.1
        assert result < 0.1


# ---------------------------------------------------------------------------
# 7. Large evidence mass → volume_factor approaches 1.0
# ---------------------------------------------------------------------------


class TestLargeEvidenceMass:
    """Large E_total pushes volume_factor near 1.0, so score ≈ H_conflict."""

    def test_large_equal_split(self):
        """LLR +5.0 and -5.0: E_total=10, volume_factor ≈ 0.964.
        contradiction ≈ 0.964 (near maximum).
        """
        clusters = [_make_cluster(5.0), _make_cluster(-5.0)]
        result = compute_v3_contradiction(clusters)
        # Volume factor should be very close to 1.0
        volume_factor = 1.0 - math.exp(-10.0 / 3.0)
        assert volume_factor > 0.95
        assert result > 0.95

    def test_asymmetric_large_mass(self):
        """LLR +4.0 and -1.0: E_pos=4, E_neg=1, E_total=5.
        f_pos=0.8, f_neg=0.2.
        H_conflict = -0.8×log2(0.8) - 0.2×log2(0.2) ≈ 0.7219.
        volume_factor = 1 - exp(-5/3) ≈ 0.8111.
        contradiction ≈ 0.586.
        """
        clusters = [_make_cluster(4.0), _make_cluster(-1.0)]
        result = compute_v3_contradiction(clusters)
        f_pos = 4.0 / 5.0
        f_neg = 1.0 / 5.0
        h_conflict = -f_pos * math.log2(f_pos) - f_neg * math.log2(f_neg)
        volume_factor = 1.0 - math.exp(-5.0 / 3.0)
        expected = h_conflict * volume_factor
        assert result == pytest.approx(expected, abs=1e-4)
