"""Tests for the v3 annotation schema, validators, and safety gates."""

from __future__ import annotations

import pytest
from pydantic import ValidationError as PydanticValidationError

from services.intelligence_pipeline_v3.schemas.annotations import (
    AmbiguityType,
    AnnotatedDocument,
    AnnotationMetadata,
    CompanySentimentAnnotation,
    EntityAnnotation,
    EntityType,
    EventClass,
    EvidenceSpanAnnotation,
    RelationAnnotation,
    RelationType,
    SentimentLabel,
)
from services.intelligence_pipeline_v3.schemas.safety import (
    SAFETY_CRITICAL_FIELDS,
    SafetyCriticalField,
    check_safety_gates,
)
from services.intelligence_pipeline_v3.schemas.samples import (
    SAMPLE_BUILDERS,
    build_sample_earnings_beat,
    build_sample_macro_event,
    build_sample_multi_company_competitive,
)
from services.intelligence_pipeline_v3.schemas.validators import (
    validate_annotation,
)

# ---------------------------------------------------------------------------
# Schema model tests
# ---------------------------------------------------------------------------


class TestEvidenceSpan:
    def test_valid_span(self):
        span = EvidenceSpanAnnotation(
            start_char=0, end_char=10, text="Apple Inc."
        )
        assert span.start_char == 0
        assert span.end_char == 10

    def test_end_must_exceed_start(self):
        with pytest.raises(PydanticValidationError):
            EvidenceSpanAnnotation(start_char=10, end_char=5, text="x")

    def test_equal_start_end_rejected(self):
        with pytest.raises(PydanticValidationError):
            EvidenceSpanAnnotation(start_char=5, end_char=5, text="x")

    def test_negative_start_rejected(self):
        with pytest.raises(PydanticValidationError):
            EvidenceSpanAnnotation(start_char=-1, end_char=5, text="hello")


class TestEntityAnnotation:
    def test_requires_evidence(self):
        with pytest.raises(PydanticValidationError):
            EntityAnnotation(
                entity_type=EntityType.COMPANY,
                literal_text="Apple",
                evidence_ids=[],
                confidence=0.9,
            )

    def test_confidence_bounds(self):
        with pytest.raises(PydanticValidationError):
            EntityAnnotation(
                entity_type=EntityType.COMPANY,
                literal_text="Apple",
                evidence_ids=["ev-1"],
                confidence=1.5,
            )


class TestCompanySentiment:
    def test_valid_sentiment(self):
        sent = CompanySentimentAnnotation(
            company_entity_id="ent-1",
            label=SentimentLabel.POSITIVE,
            positive_probability=0.8,
            negative_probability=0.1,
            neutral_probability=0.1,
            evidence_ids=["ev-1"],
            confidence=0.9,
        )
        assert sent.label == SentimentLabel.POSITIVE

    def test_probabilities_must_sum_to_one(self):
        with pytest.raises(PydanticValidationError, match="sum to"):
            CompanySentimentAnnotation(
                company_entity_id="ent-1",
                label=SentimentLabel.POSITIVE,
                positive_probability=0.5,
                negative_probability=0.1,
                neutral_probability=0.1,
                evidence_ids=["ev-1"],
                confidence=0.9,
            )

    def test_allows_small_rounding_error(self):
        # 0.33 + 0.33 + 0.34 = 1.0 exactly, but 0.333+0.333+0.334=1.0 too
        sent = CompanySentimentAnnotation(
            company_entity_id="ent-1",
            label=SentimentLabel.NEUTRAL,
            positive_probability=0.33,
            negative_probability=0.33,
            neutral_probability=0.34,
            evidence_ids=["ev-1"],
            confidence=0.8,
        )
        assert sent.label == SentimentLabel.NEUTRAL


class TestEventAnnotation:
    def test_all_event_classes_defined(self):
        expected = {
            "earnings_beat", "earnings_miss", "guidance_raise", "guidance_cut",
            "ma_announcement", "legal_regulatory", "product_launch", "supply_chain",
            "rating_change", "management_change", "macro_event", "dividend_change",
            "buyback",
        }
        actual = {e.value for e in EventClass}
        assert actual == expected


class TestRelationAnnotation:
    def test_all_relation_types_defined(self):
        expected = {"directly_affects", "inferred_exposure", "competes_with", "supplies"}
        actual = {r.value for r in RelationType}
        assert actual == expected


# ---------------------------------------------------------------------------
# Validator tests
# ---------------------------------------------------------------------------


class TestValidator:
    def test_all_samples_valid(self):
        for builder in SAMPLE_BUILDERS:
            doc = builder()
            result = validate_annotation(doc)
            assert result.valid, f"Sample {doc.document_id} failed: {[e.message for e in result.errors]}"

    def test_detects_invalid_evidence_reference(self):
        doc = build_sample_earnings_beat()
        # Add an entity with a bad evidence reference
        doc.entities.append(
            EntityAnnotation(
                entity_type=EntityType.PERSON,
                literal_text="Tim Cook",
                evidence_ids=["nonexistent-id"],
                confidence=0.9,
            )
        )
        result = validate_annotation(doc)
        assert not result.valid
        assert any("nonexistent-id" in e.message for e in result.errors)

    def test_detects_offset_beyond_text(self):
        source = "Short text."
        doc = AnnotatedDocument(
            document_id="test-doc",
            document_type="article",
            source_text=source,
            metadata=AnnotationMetadata(annotator_id="test"),
            evidence_spans=[
                EvidenceSpanAnnotation(
                    id="ev-bad",
                    start_char=0,
                    end_char=999,
                    text="Short text.",
                )
            ],
        )
        result = validate_annotation(doc)
        assert not result.valid
        assert any("exceeds source_text length" in e.message for e in result.errors)

    def test_detects_text_mismatch(self):
        source = "Apple Inc. beat expectations."
        doc = AnnotatedDocument(
            document_id="test-doc",
            document_type="article",
            source_text=source,
            metadata=AnnotationMetadata(annotator_id="test"),
            evidence_spans=[
                EvidenceSpanAnnotation(
                    id="ev-mismatch",
                    start_char=0,
                    end_char=10,
                    text="Google LLC",  # Doesn't match source
                )
            ],
        )
        result = validate_annotation(doc)
        assert not result.valid
        assert any("does not match" in e.message for e in result.errors)

    def test_warns_on_orphaned_evidence(self):
        source = "Some text here."
        doc = AnnotatedDocument(
            document_id="test-doc",
            document_type="article",
            source_text=source,
            metadata=AnnotationMetadata(annotator_id="test"),
            evidence_spans=[
                EvidenceSpanAnnotation(
                    id="ev-orphan",
                    start_char=0,
                    end_char=4,
                    text="Some",
                )
            ],
        )
        result = validate_annotation(doc)
        assert result.valid  # Warnings don't invalidate
        assert result.warning_count > 0
        assert any("not referenced" in w.message for w in result.warnings)

    def test_detects_invalid_relation_target(self):
        doc = build_sample_multi_company_competitive()
        doc.relations.append(
            RelationAnnotation(
                relation_type=RelationType.SUPPLIES,
                source_id="ent-101",
                target_id="nonexistent-entity",
                evidence_ids=["ev-101"],
                confidence=0.8,
            )
        )
        result = validate_annotation(doc)
        assert not result.valid
        assert any("nonexistent-entity" in e.message for e in result.errors)


# ---------------------------------------------------------------------------
# Safety gate tests
# ---------------------------------------------------------------------------


class TestSafetyGates:
    def test_all_fields_have_thresholds(self):
        for field in SafetyCriticalField:
            assert field in SAFETY_CRITICAL_FIELDS

    def test_passing_metrics(self):
        metrics = {
            SafetyCriticalField.COMPANY_IDENTITY: {"precision": 0.96, "recall": 0.91, "f1": 0.93},
            SafetyCriticalField.EVENT_CLASS: {"macro_f1": 0.87, "per_class_min_f1": 0.72},
            SafetyCriticalField.SENTIMENT_DIRECTION: {"macro_f1": 0.86, "direction_accuracy": 0.91},
            SafetyCriticalField.NUMERIC_FACT_VALUE: {"exact_match": 0.82, "tolerance_match_5pct": 0.93},
            SafetyCriticalField.DIRECT_EFFECT_ATTRIBUTION: {"precision": 0.94, "recall": 0.89},
            SafetyCriticalField.EVIDENCE_SUPPORT: {"support_rate": 0.96, "offset_validity": 0.99},
            SafetyCriticalField.CONFIDENCE_CALIBRATION: {"ece": 0.04, "brier_score": 0.12},
        }
        results = check_safety_gates(metrics)
        assert all(r.passed for r in results), [
            f"{r.field.value}.{r.metric_name}: {r.actual_value} vs {r.required_value}"
            for r in results if not r.passed
        ]

    def test_failing_metrics(self):
        metrics = {
            SafetyCriticalField.COMPANY_IDENTITY: {"precision": 0.80, "recall": 0.70, "f1": 0.75},
        }
        results = check_safety_gates(metrics)
        # All company_identity checks should fail
        company_results = [r for r in results if r.field == SafetyCriticalField.COMPANY_IDENTITY]
        assert all(not r.passed for r in company_results)

    def test_missing_metric_fails(self):
        metrics = {
            SafetyCriticalField.COMPANY_IDENTITY: {"precision": 0.96},  # Missing recall and f1
        }
        results = check_safety_gates(metrics)
        company_results = [r for r in results if r.field == SafetyCriticalField.COMPANY_IDENTITY]
        missing = [r for r in company_results if not r.passed]
        assert len(missing) >= 2  # recall and f1 are missing

    def test_lower_is_better_fields(self):
        """ECE and Brier score are lower-is-better metrics."""
        metrics = {
            SafetyCriticalField.CONFIDENCE_CALIBRATION: {"ece": 0.10, "brier_score": 0.25},
        }
        results = check_safety_gates(metrics)
        cal_results = [r for r in results if r.field == SafetyCriticalField.CONFIDENCE_CALIBRATION]
        assert all(not r.passed for r in cal_results)
        assert all(r.is_lower_better for r in cal_results)


# ---------------------------------------------------------------------------
# Sample annotation tests
# ---------------------------------------------------------------------------


class TestSampleAnnotations:
    def test_earnings_beat_structure(self):
        doc = build_sample_earnings_beat()
        assert doc.document_type == "article"
        assert len(doc.entities) == 1
        assert doc.entities[0].canonical_name == "AAPL"
        assert len(doc.events) == 2
        assert doc.events[0].event_class == EventClass.EARNINGS_BEAT
        assert doc.events[1].event_class == EventClass.DIVIDEND_CHANGE
        assert len(doc.numeric_facts) == 2
        assert len(doc.sentiments) == 1
        assert doc.sentiments[0].label == SentimentLabel.POSITIVE
        assert len(doc.direct_effects) == 1
        assert len(doc.ambiguity_markers) == 0

    def test_multi_company_has_ambiguity(self):
        doc = build_sample_multi_company_competitive()
        assert len(doc.ambiguity_markers) == 1
        assert doc.ambiguity_markers[0].ambiguity_type == AmbiguityType.CONFLICTING_SENTIMENT
        assert len(doc.inferred_exposures) == 1
        assert len(doc.relations) == 1
        assert doc.relations[0].relation_type == RelationType.COMPETES_WITH

    def test_macro_event_no_primary_company(self):
        doc = build_sample_macro_event()
        assert doc.document_type == "macro_event"
        assert doc.events[0].event_class == EventClass.MACRO_EVENT
        assert doc.events[0].primary_company_ids == []
        assert len(doc.sentiments) == 0
