"""Integration tests for the v3 calibrated evidence pipeline.

Tests the full pipeline path through pure functions end-to-end:
    raw signals → EvidenceUnit → q_i → LLR → cluster → posterior → recommendation

Also validates feature flag routing and v3 metadata fields.

Requirements validated: 19.1–19.6, 20.1–20.5
"""
from __future__ import annotations

from datetime import datetime, timezone

from services.aggregation.bayesian import V3Posterior, compute_v3_posterior
from services.aggregation.contradiction import compute_v3_contradiction
from services.aggregation.regime import (
    _DEFAULT_V3_UNCERTAINTY,
)
from services.aggregation.scoring import (
    SourceStats,
    compute_llr,
    compute_v3_reliability,
    normalize_company_signal,
)
from services.aggregation.worker import (
    _annotate_pipeline_mode,
    cluster_evidence,
    compute_cluster_llr,
    compute_n_eff,
    compute_v3_confidence,
    compute_v3_data_quality,
    should_force_informational_v3,
)

# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------

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

_DEFAULT_REGIME = _DEFAULT_V3_UNCERTAINTY


def _make_signal(
    sentiment: str = "positive",
    impact: float = 0.7,
    source_id: str = "doc1",
    event_type: str = "earnings",
    source_group: str = "company",
    extraction_conf: float = 0.85,
    source_cred: float = 0.80,
    novelty: float = 0.7,
) -> dict:
    """Build a raw company signal dict for normalization."""
    return {
        "symbol": "AAPL",
        "timestamp": _NOW,
        "source_id": source_id,
        "event_type": event_type,
        "source_group": source_group,
        "horizon": "7d",
        "sentiment": sentiment,
        "sentiment_strength": impact,
        "impact": impact,
        "extraction_conf": extraction_conf,
        "source_cred": source_cred,
        "novelty": novelty,
    }


# ---------------------------------------------------------------------------
# Test 1: Full pipeline path through pure functions
# Requirements: 20.1, 20.2, 20.3, 20.4, 20.5
# ---------------------------------------------------------------------------


def test_full_pipeline_path():
    """End-to-end test: raw signals → EvidenceUnit → q_i → LLR → cluster → posterior."""
    # 1. Create raw signal dicts with opposing sentiments
    signals = [
        _make_signal(
            sentiment="positive",
            impact=0.8,
            source_id="doc1",
            event_type="earnings",
            extraction_conf=0.9,
            source_cred=0.85,
            novelty=0.8,
        ),
        _make_signal(
            sentiment="positive",
            impact=0.6,
            source_id="doc2",
            event_type="earnings",
            extraction_conf=0.75,
            source_cred=0.7,
            novelty=0.6,
        ),
        _make_signal(
            sentiment="negative",
            impact=0.5,
            source_id="doc3",
            event_type="regulatory",
            extraction_conf=0.7,
            source_cred=0.6,
            novelty=0.9,
        ),
    ]

    # 2. Normalize to EvidenceUnit
    units = [normalize_company_signal(s) for s in signals]
    assert all(u is not None for u in units), "All signals should normalize successfully"
    units = [u for u in units if u is not None]  # type narrowing
    assert len(units) == 3

    # 3. Compute q_i (reliability) for each unit
    neutral_stats = SourceStats(source_id="default", hits=0, misses=0)
    q_values = []
    for unit in units:
        reliability = compute_v3_reliability(
            unit=unit,
            source_stats=neutral_stats,
            cluster_position=0,
            reference_time=_NOW,
        )
        q_values.append(reliability.q_i)

    # All q_i should be in [0, 1]
    for q in q_values:
        assert 0.0 <= q <= 1.0, f"q_i out of bounds: {q}"

    # 4. Compute LLR for each unit
    llrs = [compute_llr(unit, q) for unit, q in zip(units, q_values)]
    # Positive sentiment → positive LLR, negative → negative LLR
    assert llrs[0] > 0.0, "Positive signal should produce positive LLR"
    assert llrs[1] > 0.0, "Positive signal should produce positive LLR"
    assert llrs[2] < 0.0, "Negative signal should produce negative LLR"

    # 5. Cluster evidence
    clusters = cluster_evidence(units, llrs)
    assert len(clusters) >= 1, "Should produce at least one cluster"

    # 6. Compute n_eff and cluster_llr for each cluster
    for cluster in clusters:
        cluster.n_eff = compute_n_eff(cluster.llrs)
        cluster.cluster_llr = compute_cluster_llr(cluster.llrs, cluster.n_eff)
        assert cluster.n_eff >= 1.0, "n_eff should be >= 1.0"
        assert -2.5 <= cluster.cluster_llr <= 2.5, "cluster_llr should be clamped"

    # 7. Compute posterior (using default uncertainty regime)
    posterior = compute_v3_posterior(clusters, _DEFAULT_REGIME, p_prior=0.50)
    assert isinstance(posterior, V3Posterior)

    # 8. Assert posterior is valid
    assert 0.0 < posterior.p_up < 1.0, f"P_up should be in (0, 1), got {posterior.p_up}"
    assert 0.0 < posterior.p_down < 1.0, "P_down should be in (0, 1)"
    assert abs(posterior.p_up + posterior.p_down - 1.0) < 1e-9
    assert 0.0 <= posterior.strength <= 1.0
    assert posterior.direction in ("bullish", "bearish", "neutral")
    assert posterior.n_eff_total > 0

    # 9. Compute contradiction
    contradiction = compute_v3_contradiction(clusters)
    # 10. Assert contradiction is bounded
    assert 0.0 <= contradiction <= 1.0

    # 11. Compute data quality
    data_quality = compute_v3_data_quality(
        units=units,
        extraction_failure_rate=0.0,
        age_newest_hours=0.0,
        n_source_types=1,
    )
    assert 0.0 <= data_quality <= 1.0

    # 12. Compute confidence
    confidence = compute_v3_confidence(
        n_eff_total=posterior.n_eff_total,
        q_values=q_values,
        llrs=llrs,
        strength=posterior.strength,
        regime_confidence_mult=_DEFAULT_REGIME.confidence_multiplier,
        contradiction=contradiction,
        data_quality=data_quality,
    )
    assert 0.0 <= confidence <= 1.0

    # With real positive signals we should get a non-trivial posterior
    # (not exactly 0.5 since we have net-positive evidence)
    assert posterior.p_up > 0.50, (
        "Net-positive evidence should push P_up above 0.50"
    )


# ---------------------------------------------------------------------------
# Test 2: Feature flag routing — _annotate_pipeline_mode
# Requirements: 19.1, 19.5
# ---------------------------------------------------------------------------


def test_annotate_pipeline_mode_v3():
    """_annotate_pipeline_mode sets pipeline_mode correctly for v3."""
    from services.shared.schemas import TrendDirection, TrendSummary, TrendWindow

    summary = TrendSummary(
        entity_type="company",
        entity_id="AAPL",
        window=TrendWindow.SEVEN_DAY,
        trend_direction=TrendDirection.BULLISH,
        trend_strength=0.6,
        confidence=0.7,
        top_supporting_evidence=["doc1"],
        top_opposing_evidence=[],
        dominant_catalysts=["earnings"],
        material_risks=[],
        contradiction_score=0.1,
        disagreement_details=[],
        generated_at=_NOW,
    )
    # Initially no market_context
    summary.market_context = {}

    _annotate_pipeline_mode(summary, "v3")
    assert summary.market_context["pipeline_mode"] == "v3"


def test_annotate_pipeline_mode_heuristic():
    """_annotate_pipeline_mode sets pipeline_mode correctly for heuristic."""
    from services.shared.schemas import TrendDirection, TrendSummary, TrendWindow

    summary = TrendSummary(
        entity_type="company",
        entity_id="AAPL",
        window=TrendWindow.SEVEN_DAY,
        trend_direction=TrendDirection.NEUTRAL,
        trend_strength=0.0,
        confidence=0.0,
        top_supporting_evidence=[],
        top_opposing_evidence=[],
        dominant_catalysts=[],
        material_risks=[],
        contradiction_score=0.0,
        disagreement_details=[],
        generated_at=_NOW,
    )
    summary.market_context = {}

    _annotate_pipeline_mode(summary, "heuristic")
    assert summary.market_context["pipeline_mode"] == "heuristic"


# ---------------------------------------------------------------------------
# Test 3: v3 metadata contains expected fields
# Requirements: 20.1, 20.2, 20.3
# ---------------------------------------------------------------------------


def test_v3_metadata_contains_expected_fields():
    """Run through pipeline and verify output metadata dict contains v3 fields."""
    # Build a simple pipeline run
    signals = [
        _make_signal(sentiment="positive", impact=0.7, source_id="a1"),
        _make_signal(sentiment="negative", impact=0.4, source_id="a2", event_type="regulatory"),
    ]
    units = [normalize_company_signal(s) for s in signals]
    units = [u for u in units if u is not None]

    neutral_stats = SourceStats(source_id="default", hits=0, misses=0)
    q_values = []
    for unit in units:
        rel = compute_v3_reliability(
            unit=unit,
            source_stats=neutral_stats,
            cluster_position=0,
            reference_time=_NOW,
        )
        q_values.append(rel.q_i)

    llrs = [compute_llr(u, q) for u, q in zip(units, q_values)]
    clusters = cluster_evidence(units, llrs)
    for c in clusters:
        c.n_eff = compute_n_eff(c.llrs)
        c.cluster_llr = compute_cluster_llr(c.llrs, c.n_eff)

    posterior = compute_v3_posterior(clusters, _DEFAULT_REGIME, p_prior=0.50)
    contradiction = compute_v3_contradiction(clusters)
    data_quality = compute_v3_data_quality(
        units=units,
        extraction_failure_rate=0.0,
        age_newest_hours=0.5,
        n_source_types=1,
    )
    confidence = compute_v3_confidence(
        n_eff_total=posterior.n_eff_total,
        q_values=q_values,
        llrs=llrs,
        strength=posterior.strength,
        regime_confidence_mult=_DEFAULT_REGIME.confidence_multiplier,
        contradiction=contradiction,
        data_quality=data_quality,
    )

    # Build the v3 metadata dict (mirrors _run_v3_pipeline logic)
    v3_metadata = {
        "v3_posterior": {
            "p_up": round(posterior.p_up, 6),
            "p_down": round(posterior.p_down, 6),
            "log_odds": round(posterior.log_odds, 6),
            "strength": round(posterior.strength, 6),
            "confidence": round(confidence, 6),
            "contradiction": round(contradiction, 6),
            "n_eff": round(posterior.n_eff_total, 4),
            "data_quality": round(data_quality, 6),
            "regime": posterior.regime,
        },
        "pipeline_mode": "v3",
        "explainability": {
            "top_positive_clusters": [],
            "top_negative_clusters": [],
            "suppression_reasons": [],
            "risk_adjustments": [],
        },
    }

    # Verify all required v3_posterior keys exist
    required_posterior_keys = {
        "p_up", "p_down", "log_odds", "strength",
        "confidence", "contradiction", "n_eff", "data_quality", "regime",
    }
    assert set(v3_metadata["v3_posterior"].keys()) == required_posterior_keys

    # Verify top-level metadata keys
    assert "pipeline_mode" in v3_metadata
    assert v3_metadata["pipeline_mode"] == "v3"
    assert "explainability" in v3_metadata

    # Verify explainability structure
    explainability = v3_metadata["explainability"]
    assert "top_positive_clusters" in explainability
    assert "top_negative_clusters" in explainability
    assert "suppression_reasons" in explainability
    assert "risk_adjustments" in explainability

    # Verify numeric ranges
    p = v3_metadata["v3_posterior"]
    assert 0.0 < p["p_up"] < 1.0
    assert 0.0 < p["p_down"] < 1.0
    assert abs(p["p_up"] + p["p_down"] - 1.0) < 1e-5
    assert 0.0 <= p["strength"] <= 1.0
    assert 0.0 <= p["confidence"] <= 1.0
    assert 0.0 <= p["contradiction"] <= 1.0
    assert 0.0 <= p["data_quality"] <= 1.0
    assert p["regime"] in ("panic", "trend_following", "mean_reversion", "uncertainty")


# ---------------------------------------------------------------------------
# Test 4: Heuristic fallback function exists and is callable
# Requirements: 19.2, 19.4
# ---------------------------------------------------------------------------


def test_heuristic_fallback_function_exists():
    """Verify the heuristic fallback function exists and is callable."""
    from services.aggregation.worker import _aggregate_company_heuristic

    assert callable(_aggregate_company_heuristic)


def test_v3_read_flag_function_exists():
    """Verify the _read_v3_flag async function exists and is callable."""
    from services.aggregation.worker import _read_v3_flag

    assert callable(_read_v3_flag)


# ---------------------------------------------------------------------------
# Test 5: should_force_informational_v3 routing
# Requirements: 19.3, 19.4
# ---------------------------------------------------------------------------


def test_force_informational_low_data_quality():
    """Low data quality should force informational mode."""
    units = [
        normalize_company_signal(_make_signal(source_id="x1")),
        normalize_company_signal(_make_signal(source_id="x2")),
    ]
    units = [u for u in units if u is not None]

    should_force, reason = should_force_informational_v3(
        data_quality=0.3,
        units=units,
        extraction_failure_rate=0.0,
    )
    assert should_force is True
    assert reason == "data_quality_below_threshold"


def test_no_force_informational_good_quality():
    """Good data quality with sufficient evidence should NOT force informational."""
    units = [
        normalize_company_signal(_make_signal(source_id="x1")),
        normalize_company_signal(_make_signal(source_id="x2")),
        normalize_company_signal(_make_signal(source_id="x3")),
    ]
    units = [u for u in units if u is not None]

    should_force, reason = should_force_informational_v3(
        data_quality=0.75,
        units=units,
        extraction_failure_rate=0.1,
    )
    assert should_force is False
    assert reason == ""


# ---------------------------------------------------------------------------
# Test 6: Pipeline with all-neutral signals produces neutral posterior
# Requirements: 20.1, 20.4
# ---------------------------------------------------------------------------


def test_neutral_signals_produce_neutral_posterior():
    """All neutral signals should produce posterior at P_up ≈ 0.50."""
    signals = [
        _make_signal(sentiment="neutral", impact=0.5, source_id="n1"),
        _make_signal(sentiment="neutral", impact=0.3, source_id="n2"),
    ]
    units = [normalize_company_signal(s) for s in signals]
    units = [u for u in units if u is not None]

    neutral_stats = SourceStats(source_id="default", hits=0, misses=0)
    q_values = []
    for unit in units:
        rel = compute_v3_reliability(
            unit=unit, source_stats=neutral_stats,
            cluster_position=0, reference_time=_NOW,
        )
        q_values.append(rel.q_i)

    llrs = [compute_llr(u, q) for u, q in zip(units, q_values)]
    # All neutral → all LLRs should be 0
    assert all(llr == 0.0 for llr in llrs), "Neutral signals should produce zero LLR"

    clusters = cluster_evidence(units, llrs)
    for c in clusters:
        c.n_eff = compute_n_eff(c.llrs)
        c.cluster_llr = compute_cluster_llr(c.llrs, c.n_eff)

    posterior = compute_v3_posterior(clusters, _DEFAULT_REGIME, p_prior=0.50)
    assert abs(posterior.p_up - 0.50) < 1e-6, (
        f"Neutral evidence should maintain prior, got P_up={posterior.p_up}"
    )
    assert posterior.direction == "neutral"
