"""Golden mapping tests for the v3→v2 compatibility adapter.

Tests cover:
- Every legacy sentiment enum value is reachable
- impact_score stays in [-1, 1]
- impact_horizon is one of the valid strings
- novelty_score stays in [0, 1]
- confidence stays in [0, 1]
- Adapter disabled by default (mode=disabled raises)
- Adapter enabled in replay mode
- model_provider = 'hybrid' is always set
- Lineage includes adapter version
"""

from __future__ import annotations

import pytest

from services.intelligence_pipeline_v3.compatibility.adapter import (
    ADAPTER_VERSION,
    AdapterDisabledError,
    CompatibilityAdapter,
)
from services.intelligence_pipeline_v3.compatibility.config import (
    DEFAULT_ADAPTER_MODE,
    AdapterMode,
    is_adapter_enabled,
)
from services.intelligence_pipeline_v3.compatibility.models import (
    V3CompanySignal,
    V3DirectionProbabilities,
    V3HorizonProbabilities,
    V3IntelligenceRecord,
    V3SentimentDistribution,
    V3StageRun,
)

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


_SENTINEL = object()


def _make_signal(
    *,
    sentiment: V3SentimentDistribution | None = None,
    horizon: V3HorizonProbabilities | None = None,
    direction: V3DirectionProbabilities | None = None,
    expected_magnitude: float | None = None,
    event_classes: list[str] | None | object = _SENTINEL,
) -> V3CompanySignal:
    """Factory for a minimal v3 company signal with overrides."""
    if event_classes is _SENTINEL:
        event_classes = ["earnings_beat"]
    return V3CompanySignal(
        company_id="aaaaaaaa-1111-2222-3333-444444444444",
        ticker="AAPL",
        relevance_probability=0.9,
        event_classes=event_classes or [],
        sentiment=sentiment or V3SentimentDistribution(positive=0.7, negative=0.1, neutral=0.2),
        direction_probabilities=direction or V3DirectionProbabilities(positive=0.6, negative=0.2, neutral=0.2),
        horizon_probabilities=horizon or V3HorizonProbabilities(one_day=0.6, seven_day=0.3, thirty_day=0.1),
        expected_magnitude=expected_magnitude,
        evidence_spans=["span-1", "span-2"],
    )


def _make_v3_record(signals: list[V3CompanySignal] | None = None) -> V3IntelligenceRecord:
    """Factory for a minimal v3 intelligence record."""
    return V3IntelligenceRecord(
        document_id="doc-001",
        document_type="article",
        summary="Test summary",
        macro_themes=["earnings", "technology"],
        novelty_score=0.7,
        confidence=0.85,
        company_signals=signals or [_make_signal()],
        stage_runs=[
            V3StageRun(stage="segmenter", schema_version="1.0.0", duration_ms=50),
            V3StageRun(stage="specialist", model_version="gliner2-large-v1", schema_version="1.0.0", duration_ms=200),
            V3StageRun(stage="sentiment", model_version="finbert-v1", schema_version="1.0.0", duration_ms=100),
        ],
        pipeline_version="3.0.0",
    )


# ---------------------------------------------------------------------------
# Task 22.4: Adapter disabled outside replay/shadow mode
# ---------------------------------------------------------------------------


class TestAdapterDisabled:
    """Verify adapter is gated by mode — disabled by default."""

    def test_default_mode_is_disabled(self) -> None:
        assert DEFAULT_ADAPTER_MODE == AdapterMode.DISABLED

    def test_is_adapter_enabled_false_for_disabled(self) -> None:
        assert is_adapter_enabled(AdapterMode.DISABLED) is False

    def test_is_adapter_enabled_true_for_replay(self) -> None:
        assert is_adapter_enabled(AdapterMode.REPLAY_ONLY) is True

    def test_is_adapter_enabled_true_for_shadow(self) -> None:
        assert is_adapter_enabled(AdapterMode.SHADOW_ONLY) is True

    def test_is_adapter_enabled_true_for_canary(self) -> None:
        assert is_adapter_enabled(AdapterMode.CANARY) is True

    def test_is_adapter_enabled_true_for_production(self) -> None:
        assert is_adapter_enabled(AdapterMode.PRODUCTION) is True

    def test_disabled_adapter_raises_on_map(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.DISABLED)
        with pytest.raises(AdapterDisabledError):
            adapter.map_to_v2(_make_v3_record())

    def test_replay_adapter_succeeds(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2_record, lineage = adapter.map_to_v2(_make_v3_record())
        assert v2_record is not None
        assert lineage is not None

    def test_shadow_adapter_succeeds(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.SHADOW_ONLY)
        v2_record, _lineage = adapter.map_to_v2(_make_v3_record())
        assert v2_record is not None


# ---------------------------------------------------------------------------
# Task 22.1 / 22.2: Mapping and lineage
# ---------------------------------------------------------------------------


class TestModelProviderHybrid:
    """Verify model_provider is always 'hybrid'."""

    def test_model_provider_is_hybrid(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2_record, _lineage = adapter.map_to_v2(_make_v3_record())
        assert v2_record.model_provider == "hybrid"

    def test_model_name_is_pipeline_v3(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2_record, _lineage = adapter.map_to_v2(_make_v3_record())
        assert v2_record.model_name == "intelligence-pipeline-v3"


class TestLineage:
    """Verify lineage records adapter version and stage details."""

    def test_lineage_includes_adapter_version(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        _v2_record, lineage = adapter.map_to_v2(_make_v3_record())
        assert lineage.adapter_version == ADAPTER_VERSION

    def test_lineage_includes_pipeline_version(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        _v2_record, lineage = adapter.map_to_v2(_make_v3_record())
        assert lineage.pipeline_version == "3.0.0"

    def test_lineage_includes_stage_runs(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        _v2_record, lineage = adapter.map_to_v2(_make_v3_record())
        assert len(lineage.stage_runs) == 3
        stages = [sr.stage for sr in lineage.stage_runs]
        assert "segmenter" in stages
        assert "specialist" in stages
        assert "sentiment" in stages

    def test_lineage_links_v3_to_v2(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2_record, lineage = adapter.map_to_v2(_make_v3_record())
        assert lineage.v3_document_id == "doc-001"
        assert lineage.v2_intelligence_id == v2_record.id


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — sentiment enum
# ---------------------------------------------------------------------------


class TestSentimentMapping:
    """Every legacy sentiment enum value (positive/negative/neutral/mixed) is reachable."""

    def test_positive_sentiment(self) -> None:
        signal = _make_signal(
            sentiment=V3SentimentDistribution(positive=0.8, negative=0.1, neutral=0.1)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].sentiment == "positive"

    def test_negative_sentiment(self) -> None:
        signal = _make_signal(
            sentiment=V3SentimentDistribution(positive=0.1, negative=0.8, neutral=0.1)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].sentiment == "negative"

    def test_neutral_sentiment(self) -> None:
        signal = _make_signal(
            sentiment=V3SentimentDistribution(positive=0.1, negative=0.1, neutral=0.8)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].sentiment == "neutral"

    def test_mixed_sentiment(self) -> None:
        signal = _make_signal(
            sentiment=V3SentimentDistribution(positive=0.4, negative=0.4, neutral=0.2)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].sentiment == "mixed"

    def test_mixed_threshold_boundary(self) -> None:
        """Both positive and negative at exactly 0.3 triggers mixed."""
        signal = _make_signal(
            sentiment=V3SentimentDistribution(positive=0.3, negative=0.3, neutral=0.4)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].sentiment == "mixed"


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — impact_score range
# ---------------------------------------------------------------------------


class TestImpactScoreRange:
    """impact_score stays in [-1, 1]."""

    def test_impact_score_from_magnitude(self) -> None:
        signal = _make_signal(expected_magnitude=0.5)
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert -1.0 <= v2.impact_records[0].impact_score <= 1.0
        assert v2.impact_records[0].impact_score == 0.5

    def test_impact_score_clamped_high(self) -> None:
        signal = _make_signal(expected_magnitude=2.5)
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_score == 1.0

    def test_impact_score_clamped_low(self) -> None:
        signal = _make_signal(expected_magnitude=-3.0)
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_score == -1.0

    def test_impact_score_negative_magnitude(self) -> None:
        signal = _make_signal(expected_magnitude=-0.7)
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_score == -0.7

    def test_impact_score_fallback_from_direction(self) -> None:
        """When expected_magnitude is None, derive from direction probabilities."""
        signal = _make_signal(
            expected_magnitude=None,
            direction=V3DirectionProbabilities(positive=0.8, negative=0.1, neutral=0.1),
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        score = v2.impact_records[0].impact_score
        assert -1.0 <= score <= 1.0
        # 0.8 - 0.1 = 0.7
        assert abs(score - 0.7) < 1e-9

    def test_impact_score_zero(self) -> None:
        signal = _make_signal(expected_magnitude=0.0)
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_score == 0.0


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — impact_horizon valid strings
# ---------------------------------------------------------------------------


VALID_HORIZONS = {"intraday", "1d", "7d", "30d", "90d"}


class TestImpactHorizonMapping:
    """impact_horizon is one of the valid legacy strings."""

    def test_intraday_horizon(self) -> None:
        signal = _make_signal(
            horizon=V3HorizonProbabilities(intraday=0.9, one_day=0.05, seven_day=0.05)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon == "intraday"
        assert v2.impact_records[0].impact_horizon in VALID_HORIZONS

    def test_one_day_horizon(self) -> None:
        signal = _make_signal(
            horizon=V3HorizonProbabilities(intraday=0.1, one_day=0.7, seven_day=0.2)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon == "1d"

    def test_seven_day_horizon(self) -> None:
        signal = _make_signal(
            horizon=V3HorizonProbabilities(seven_day=0.8, thirty_day=0.1, ninety_day=0.1)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon == "7d"

    def test_thirty_day_horizon(self) -> None:
        signal = _make_signal(
            horizon=V3HorizonProbabilities(thirty_day=0.9, ninety_day=0.1)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon == "30d"

    def test_ninety_day_horizon(self) -> None:
        signal = _make_signal(
            horizon=V3HorizonProbabilities(ninety_day=0.9)
        )
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon == "90d"

    def test_horizon_always_valid(self) -> None:
        """Default horizon probs still produce a valid string."""
        signal = _make_signal(horizon=V3HorizonProbabilities())
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].impact_horizon in VALID_HORIZONS


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — novelty_score range
# ---------------------------------------------------------------------------


class TestNoveltyScoreRange:
    """novelty_score stays in [0, 1]."""

    def test_novelty_passes_through(self) -> None:
        record = _make_v3_record()
        record.novelty_score = 0.7
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.novelty_score == 0.7
        assert 0.0 <= v2.novelty_score <= 1.0

    def test_novelty_zero(self) -> None:
        record = _make_v3_record()
        record.novelty_score = 0.0
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.novelty_score == 0.0

    def test_novelty_one(self) -> None:
        record = _make_v3_record()
        record.novelty_score = 1.0
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.novelty_score == 1.0


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — confidence range
# ---------------------------------------------------------------------------


class TestConfidenceRange:
    """confidence stays in [0, 1]."""

    def test_confidence_passes_through(self) -> None:
        record = _make_v3_record()
        record.confidence = 0.85
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.confidence == 0.85
        assert 0.0 <= v2.confidence <= 1.0

    def test_confidence_zero(self) -> None:
        record = _make_v3_record()
        record.confidence = 0.0
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.confidence == 0.0

    def test_confidence_one(self) -> None:
        record = _make_v3_record()
        record.confidence = 1.0
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(record)
        assert v2.confidence == 1.0


# ---------------------------------------------------------------------------
# Task 22.3: Golden mapping tests — catalyst type mapping
# ---------------------------------------------------------------------------


class TestCatalystTypeMapping:
    """Event taxonomy maps to legacy catalyst enum values."""

    @pytest.mark.parametrize(
        "event_class,expected_catalyst",
        [
            ("earnings_beat", "earnings"),
            ("earnings_miss", "earnings"),
            ("guidance_raise", "earnings"),
            ("guidance_cut", "earnings"),
            ("product_launch", "product"),
            ("legal_regulatory", "legal"),
            ("ma_announcement", "m_and_a"),
            ("supply_chain", "supply_chain"),
            ("rating_change", "rating_change"),
            ("macro_event", "macro"),
            ("management_change", "other"),
            ("dividend_change", "other"),
            ("buyback", "other"),
        ],
    )
    def test_event_class_to_catalyst(self, event_class: str, expected_catalyst: str) -> None:
        signal = _make_signal(event_classes=[event_class])
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].catalyst_type == expected_catalyst

    def test_unknown_event_class_falls_back_to_other(self) -> None:
        signal = _make_signal(event_classes=["unknown_future_event"])
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].catalyst_type == "other"

    def test_empty_event_classes_falls_back_to_other(self) -> None:
        signal = _make_signal(event_classes=[])
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].catalyst_type == "other"

    def test_first_matching_event_wins(self) -> None:
        """When multiple event classes, first match determines catalyst."""
        signal = _make_signal(event_classes=["product_launch", "earnings_beat"])
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=[signal]))
        assert v2.impact_records[0].catalyst_type == "product"


# ---------------------------------------------------------------------------
# Task 22.1: Field mapping completeness
# ---------------------------------------------------------------------------


class TestFieldMappingCompleteness:
    """Verify all v2 fields are populated from v3 sources."""

    def test_summary_mapped(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record())
        assert v2.summary == "Test summary"

    def test_macro_themes_mapped(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record())
        assert v2.macro_themes == ["earnings", "technology"]

    def test_evidence_spans_mapped(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record())
        assert v2.impact_records[0].evidence_spans == ["span-1", "span-2"]

    def test_relevance_mapped(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record())
        assert v2.impact_records[0].relevance == 0.9

    def test_ticker_mapped(self) -> None:
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record())
        assert v2.impact_records[0].ticker == "AAPL"

    def test_multiple_companies(self) -> None:
        signals = [
            _make_signal(),
            V3CompanySignal(
                company_id="bbbbbbbb-1111-2222-3333-444444444444",
                ticker="MSFT",
                relevance_probability=0.7,
                event_classes=["product_launch"],
                sentiment=V3SentimentDistribution(positive=0.6, negative=0.2, neutral=0.2),
                direction_probabilities=V3DirectionProbabilities(positive=0.5, negative=0.2, neutral=0.3),
                horizon_probabilities=V3HorizonProbabilities(seven_day=0.6, thirty_day=0.4),
                expected_magnitude=0.3,
                evidence_spans=["span-3"],
            ),
        ]
        adapter = CompatibilityAdapter(mode=AdapterMode.REPLAY_ONLY)
        v2, _ = adapter.map_to_v2(_make_v3_record(signals=signals))
        assert len(v2.impact_records) == 2
        tickers = {r.ticker for r in v2.impact_records}
        assert tickers == {"AAPL", "MSFT"}
