"""Unit tests for v3 multiplicative confidence and data quality.

Tests for compute_v3_confidence, compute_v3_data_quality, and
should_force_informational_v3 functions.

Requirements validated: 8.1–8.5, 17.1–17.8
"""
from __future__ import annotations

import math
from datetime import datetime, timezone

import pytest

from services.aggregation.scoring import EvidenceUnit
from services.aggregation.worker import (
    compute_v3_confidence,
    compute_v3_data_quality,
    should_force_informational_v3,
)

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

_NOW = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)


def _make_unit(
    layer: str = "company",
    extraction_conf: float = 0.8,
    impact: float = 0.7,
) -> EvidenceUnit:
    """Create a minimal EvidenceUnit for testing."""
    return EvidenceUnit(
        symbol="AAPL",
        layer=layer,
        event_type="earnings",
        source_id="doc-1",
        source_group="company",
        timestamp=_NOW,
        horizon="7d",
        direction=1,
        sentiment_strength=0.8,
        impact=impact,
        extraction_conf=extraction_conf,
        source_cred=0.85,
        novelty=0.9,
        event_base_rate=0.25,
        cluster_id="test-cluster",
    )


# ---------------------------------------------------------------------------
# Test: Zero data quality → zero confidence
# Requirement: 8.1, 8.5
# ---------------------------------------------------------------------------


class TestZeroDataQualityConfidence:
    """When data_quality = 0.0, confidence must be 0.0."""

    def test_zero_data_quality_produces_zero_confidence(self):
        confidence = compute_v3_confidence(
            n_eff_total=5.0,
            q_values=[0.8, 0.7],
            llrs=[1.0, 0.5],
            strength=0.6,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.0,
        )
        assert confidence == 0.0

    def test_near_zero_data_quality_suppresses_confidence(self):
        """Very low data_quality → near-zero confidence."""
        confidence = compute_v3_confidence(
            n_eff_total=10.0,
            q_values=[0.9, 0.9],
            llrs=[1.5, 1.5],
            strength=0.8,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.01,
        )
        assert confidence < 0.05


# ---------------------------------------------------------------------------
# Test: Full contradiction (1.0) → zero confidence
# Requirement: 8.4
# ---------------------------------------------------------------------------


class TestFullContradictionConfidence:
    """When contradiction = 1.0, confidence must be 0.0."""

    def test_full_contradiction_produces_zero_confidence(self):
        confidence = compute_v3_confidence(
            n_eff_total=10.0,
            q_values=[0.9, 0.8],
            llrs=[1.0, 1.2],
            strength=0.7,
            regime_confidence_mult=1.0,
            contradiction=1.0,
            data_quality=0.9,
        )
        assert confidence == 0.0

    def test_high_contradiction_suppresses_confidence(self):
        """Contradiction = 0.9 → confidence heavily suppressed."""
        conf_no_contra = compute_v3_confidence(
            n_eff_total=5.0,
            q_values=[0.8],
            llrs=[1.0],
            strength=0.6,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.8,
        )
        conf_high_contra = compute_v3_confidence(
            n_eff_total=5.0,
            q_values=[0.8],
            llrs=[1.0],
            strength=0.6,
            regime_confidence_mult=1.0,
            contradiction=0.9,
            data_quality=0.8,
        )
        assert conf_high_contra < conf_no_contra * 0.15


# ---------------------------------------------------------------------------
# Test: Low n_eff → suppressed C_evidence
# Requirement: 8.2
# ---------------------------------------------------------------------------


class TestLowNEffConfidence:
    """Low n_eff_total → C_evidence is suppressed."""

    def test_very_low_n_eff_suppresses_c_evidence(self):
        """n_eff_total=0.5 → C_evidence = 1 - exp(-0.1) ≈ 0.095."""
        # C_evidence = 1 - exp(-0.5 / 5.0) = 1 - exp(-0.1) ≈ 0.0952
        expected_c_evidence = 1.0 - math.exp(-0.1)
        assert expected_c_evidence == pytest.approx(0.0952, rel=1e-2)

        confidence = compute_v3_confidence(
            n_eff_total=0.5,
            q_values=[0.8],
            llrs=[1.0],
            strength=0.8,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.9,
        )
        # Confidence is bounded by C_evidence ≈ 0.095
        assert confidence < 0.15

    def test_high_n_eff_yields_higher_confidence(self):
        """Higher n_eff → higher C_evidence → higher overall confidence."""
        conf_low = compute_v3_confidence(
            n_eff_total=1.0,
            q_values=[0.8],
            llrs=[1.0],
            strength=0.6,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.8,
        )
        conf_high = compute_v3_confidence(
            n_eff_total=10.0,
            q_values=[0.8],
            llrs=[1.0],
            strength=0.6,
            regime_confidence_mult=1.0,
            contradiction=0.0,
            data_quality=0.8,
        )
        assert conf_high > conf_low


# ---------------------------------------------------------------------------
# Test: Data quality computed from known inputs
# Requirement: 17.1, 17.2, 17.3, 17.4, 17.5, 17.6
# ---------------------------------------------------------------------------


class TestDataQualityComputation:
    """Verify data quality formula with known inputs."""

    def test_high_quality_inputs(self):
        """Zero failure, fresh signal, many sources → high quality."""
        units = [_make_unit() for _ in range(10)]
        dq = compute_v3_data_quality(
            units=units,
            extraction_failure_rate=0.0,
            age_newest_hours=1.0,
            n_source_types=4,
        )
        # Q_parse=1.0, Q_fresh=exp(-1/168)≈0.994, Q_coverage=1-exp(-2)≈0.865,
        # Q_diversity=min(1, log2(5)/log2(4))=1.0
        assert dq > 0.60

    def test_high_extraction_failure_rate(self):
        """extraction_failure_rate=0.8 → Q_parse=0.2 → low quality."""
        units = [_make_unit() for _ in range(5)]
        dq = compute_v3_data_quality(
            units=units,
            extraction_failure_rate=0.8,
            age_newest_hours=1.0,
            n_source_types=3,
        )
        # Q_parse = 0.2 → heavily suppresses data_quality
        assert dq < 0.30

    def test_zero_sources_zero_diversity(self):
        """No source types → Q_diversity = 0 → data_quality = 0."""
        units = [_make_unit() for _ in range(5)]
        dq = compute_v3_data_quality(
            units=units,
            extraction_failure_rate=0.0,
            age_newest_hours=1.0,
            n_source_types=0,
        )
        assert dq == 0.0

    def test_empty_units_low_coverage(self):
        """No valid units → Q_coverage = 1 - exp(0) = 0 → data_quality = 0."""
        dq = compute_v3_data_quality(
            units=[],
            extraction_failure_rate=0.0,
            age_newest_hours=1.0,
            n_source_types=3,
        )
        assert dq == 0.0

    def test_stale_signal_decays_quality(self):
        """Very old signal → Q_fresh low → suppresses data_quality."""
        units = [_make_unit() for _ in range(5)]
        dq_fresh = compute_v3_data_quality(
            units=units,
            extraction_failure_rate=0.0,
            age_newest_hours=1.0,
            n_source_types=3,
        )
        dq_stale = compute_v3_data_quality(
            units=units,
            extraction_failure_rate=0.0,
            age_newest_hours=500.0,
            n_source_types=3,
        )
        assert dq_stale < dq_fresh


# ---------------------------------------------------------------------------
# Test: Force informational mode
# Requirement: 17.7, 17.8
# ---------------------------------------------------------------------------


class TestForceInformational:
    """Test should_force_informational_v3 forcing conditions."""

    def test_data_quality_below_threshold_forces(self):
        """data_quality < 0.50 → forces informational."""
        units = [_make_unit() for _ in range(5)]
        forced, reason = should_force_informational_v3(
            data_quality=0.49,
            units=units,
            extraction_failure_rate=0.0,
        )
        assert forced is True
        assert reason == "data_quality_below_threshold"

    def test_data_quality_at_threshold_does_not_force(self):
        """data_quality = 0.50 → does NOT force informational."""
        units = [_make_unit() for _ in range(5)]
        forced, reason = should_force_informational_v3(
            data_quality=0.50,
            units=units,
            extraction_failure_rate=0.0,
        )
        assert forced is False

    def test_insufficient_evidence_forces(self):
        """N_valid < 2 → forces informational."""
        units = [_make_unit()]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.0,
        )
        assert forced is True
        assert reason == "insufficient_evidence_count"

    def test_high_extraction_failure_forces(self):
        """extraction_failure_rate > 0.50 → Q_parse < 0.50 → forces."""
        units = [_make_unit() for _ in range(5)]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.51,
        )
        assert forced is True
        assert reason == "extraction_parse_rate_below_threshold"

    def test_only_macro_signals_forces(self):
        """Only macro/competitive evidence (no company) → forces."""
        units = [_make_unit(layer="macro"), _make_unit(layer="competitive")]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.0,
        )
        assert forced is True
        assert reason == "macro_competitive_only_evidence"

    def test_only_macro_with_macro_only_enabled_does_not_force(self):
        """Only macro evidence WITH macro_only_enabled → does NOT force."""
        units = [_make_unit(layer="macro"), _make_unit(layer="macro")]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.0,
            macro_only_enabled=True,
        )
        assert forced is False

    def test_mixed_signals_does_not_force(self):
        """Company + macro evidence → does NOT force."""
        units = [_make_unit(layer="company"), _make_unit(layer="macro")]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.0,
        )
        assert forced is False

    def test_good_inputs_do_not_force(self):
        """All good → no forcing."""
        units = [_make_unit() for _ in range(5)]
        forced, reason = should_force_informational_v3(
            data_quality=0.80,
            units=units,
            extraction_failure_rate=0.1,
        )
        assert forced is False
        assert reason == ""
