"""Unit tests for evidence offset validity, support rate, and related metrics.

Validates: Requirements 16.3, 16.4
"""
from __future__ import annotations

from services.intelligence_pipeline_v3.evaluation.evidence_metrics import (
    EvidenceMetricsResult,
    EvidenceSpan,
    ExtractionResult,
    FieldType,
    compute_coverage_score,
    compute_offset_validity,
    compute_orphan_rate,
    compute_per_field_support,
    compute_support_rate,
    compute_unsupported_claim_rate,
    evaluate_evidence,
)

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

SOURCE_TEXT = "Apple reported revenue of $94.8 billion for Q3 2024. Tim Cook said growth was strong."


def _span(span_id: str, text: str, start: int, end: int) -> EvidenceSpan:
    return EvidenceSpan(span_id=span_id, text=text, start_char=start, end_char=end)


def _item(
    item_id: str,
    field_type: FieldType,
    evidence_ids: list[str] | None = None,
    required_fields: list[str] | None = None,
    supported_fields: list[str] | None = None,
) -> ExtractionResult:
    return ExtractionResult(
        item_id=item_id,
        field_type=field_type,
        evidence_ids=evidence_ids or [],
        required_fields=required_fields or [],
        supported_fields=supported_fields or [],
    )


# ---------------------------------------------------------------------------
# Offset Validity
# ---------------------------------------------------------------------------


class TestOffsetValidity:
    def test_all_valid(self) -> None:
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "revenue", 15, 22),
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 1.0
        assert valid == 2
        assert total == 2

    def test_one_invalid_text_mismatch(self) -> None:
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "WRONG", 15, 22),  # text doesn't match source
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 0.5
        assert valid == 1
        assert total == 2

    def test_offset_out_of_bounds(self) -> None:
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "text", 1000, 1004),  # beyond source length
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 0.5
        assert valid == 1
        assert total == 2

    def test_negative_offsets(self) -> None:
        spans = [
            _span("s1", "Apple", -1, 5),
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 0.0
        assert valid == 0
        assert total == 1

    def test_start_greater_than_end(self) -> None:
        spans = [
            _span("s1", "Apple", 5, 0),
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 0.0
        assert valid == 0
        assert total == 1

    def test_empty_spans(self) -> None:
        rate, valid, total = compute_offset_validity([], SOURCE_TEXT)
        assert rate == 1.0
        assert valid == 0
        assert total == 0

    def test_empty_text_span_at_boundary(self) -> None:
        # An empty span (start == end) should match empty string
        spans = [_span("s1", "", 5, 5)]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 1.0
        assert valid == 1

    def test_all_invalid(self) -> None:
        spans = [
            _span("s1", "WRONG", 0, 5),
            _span("s2", "ALSO_WRONG", 10, 20),
        ]
        rate, valid, total = compute_offset_validity(spans, SOURCE_TEXT)
        assert rate == 0.0
        assert valid == 0
        assert total == 2


# ---------------------------------------------------------------------------
# Support Rate
# ---------------------------------------------------------------------------


class TestSupportRate:
    def test_all_supported(self) -> None:
        valid_ids = {"s1", "s2"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=["s2"]),
        ]
        rate, supported, total = compute_support_rate(items, valid_ids)
        assert rate == 1.0
        assert supported == 2
        assert total == 2

    def test_none_supported(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s99"]),
            _item("i2", FieldType.fact, evidence_ids=["s100"]),
        ]
        rate, supported, total = compute_support_rate(items, valid_ids)
        assert rate == 0.0
        assert supported == 0
        assert total == 2

    def test_partial_support(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=["s99"]),
        ]
        rate, supported, total = compute_support_rate(items, valid_ids)
        assert rate == 0.5
        assert supported == 1
        assert total == 2

    def test_item_with_multiple_evidence_one_valid(self) -> None:
        valid_ids = {"s2"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1", "s2"]),
        ]
        rate, supported, total = compute_support_rate(items, valid_ids)
        assert rate == 1.0
        assert supported == 1

    def test_empty_items(self) -> None:
        rate, supported, total = compute_support_rate([], {"s1"})
        assert rate == 1.0
        assert supported == 0
        assert total == 0

    def test_item_with_no_evidence_ids(self) -> None:
        valid_ids = {"s1"}
        items = [_item("i1", FieldType.entity, evidence_ids=[])]
        rate, supported, total = compute_support_rate(items, valid_ids)
        assert rate == 0.0
        assert supported == 0


# ---------------------------------------------------------------------------
# Coverage Score
# ---------------------------------------------------------------------------


class TestCoverageScore:
    def test_full_coverage(self) -> None:
        items = [
            _item(
                "i1", FieldType.entity,
                required_fields=["name", "type"],
                supported_fields=["name", "type"],
            ),
        ]
        score = compute_coverage_score(items)
        assert score == 1.0

    def test_partial_coverage(self) -> None:
        items = [
            _item(
                "i1", FieldType.entity,
                required_fields=["name", "type", "value"],
                supported_fields=["name"],
            ),
        ]
        score = compute_coverage_score(items)
        assert abs(score - 1 / 3) < 1e-9

    def test_no_coverage(self) -> None:
        items = [
            _item(
                "i1", FieldType.entity,
                required_fields=["name", "type"],
                supported_fields=[],
            ),
        ]
        score = compute_coverage_score(items)
        assert score == 0.0

    def test_no_required_fields_full_coverage(self) -> None:
        items = [
            _item("i1", FieldType.entity, required_fields=[], supported_fields=[]),
        ]
        score = compute_coverage_score(items)
        assert score == 1.0

    def test_average_across_items(self) -> None:
        items = [
            _item(
                "i1", FieldType.entity,
                required_fields=["name", "type"],
                supported_fields=["name", "type"],
            ),  # 1.0
            _item(
                "i2", FieldType.fact,
                required_fields=["value", "unit"],
                supported_fields=["value"],
            ),  # 0.5
        ]
        score = compute_coverage_score(items)
        assert abs(score - 0.75) < 1e-9

    def test_empty_items(self) -> None:
        score = compute_coverage_score([])
        assert score == 1.0

    def test_supported_field_not_in_required(self) -> None:
        # Extra supported fields beyond required don't inflate the score
        items = [
            _item(
                "i1", FieldType.entity,
                required_fields=["name"],
                supported_fields=["name", "extra_field"],
            ),
        ]
        score = compute_coverage_score(items)
        assert score == 1.0


# ---------------------------------------------------------------------------
# Orphan Rate
# ---------------------------------------------------------------------------


class TestOrphanRate:
    def test_no_orphans(self) -> None:
        spans = [_span("s1", "Apple", 0, 5), _span("s2", "revenue", 15, 22)]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=["s2"]),
        ]
        rate, count = compute_orphan_rate(spans, items)
        assert rate == 0.0
        assert count == 0

    def test_all_orphans(self) -> None:
        spans = [_span("s1", "Apple", 0, 5), _span("s2", "revenue", 15, 22)]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s99"]),
        ]
        rate, count = compute_orphan_rate(spans, items)
        assert rate == 1.0
        assert count == 2

    def test_partial_orphans(self) -> None:
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "revenue", 15, 22),
            _span("s3", "Q3 2024", 43, 50),
        ]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
        ]
        rate, count = compute_orphan_rate(spans, items)
        assert abs(rate - 2 / 3) < 1e-9
        assert count == 2

    def test_empty_spans(self) -> None:
        items = [_item("i1", FieldType.entity, evidence_ids=["s1"])]
        rate, count = compute_orphan_rate([], items)
        assert rate == 0.0
        assert count == 0

    def test_empty_items_all_orphans(self) -> None:
        spans = [_span("s1", "Apple", 0, 5)]
        rate, count = compute_orphan_rate(spans, [])
        assert rate == 1.0
        assert count == 1

    def test_shared_evidence(self) -> None:
        # Multiple items referencing the same span - span is not orphan
        spans = [_span("s1", "Apple", 0, 5)]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.sentiment, evidence_ids=["s1"]),
        ]
        rate, count = compute_orphan_rate(spans, items)
        assert rate == 0.0
        assert count == 0


# ---------------------------------------------------------------------------
# Per-Field Support
# ---------------------------------------------------------------------------


class TestPerFieldSupport:
    def test_all_types_supported(self) -> None:
        valid_ids = {"s1", "s2", "s3", "s4"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.event, evidence_ids=["s2"]),
            _item("i3", FieldType.fact, evidence_ids=["s3"]),
            _item("i4", FieldType.sentiment, evidence_ids=["s4"]),
        ]
        result = compute_per_field_support(items, valid_ids)
        assert result["entity"] == 1.0
        assert result["event"] == 1.0
        assert result["fact"] == 1.0
        assert result["sentiment"] == 1.0

    def test_mixed_support(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.entity, evidence_ids=["s99"]),
            _item("i3", FieldType.fact, evidence_ids=["s1"]),
        ]
        result = compute_per_field_support(items, valid_ids)
        assert result["entity"] == 0.5
        assert result["fact"] == 1.0

    def test_empty_items(self) -> None:
        result = compute_per_field_support([], {"s1"})
        assert result == {}

    def test_single_type(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.sentiment, evidence_ids=["s1"]),
            _item("i2", FieldType.sentiment, evidence_ids=["s1"]),
        ]
        result = compute_per_field_support(items, valid_ids)
        assert len(result) == 1
        assert result["sentiment"] == 1.0


# ---------------------------------------------------------------------------
# Unsupported Claim Rate
# ---------------------------------------------------------------------------


class TestUnsupportedClaimRate:
    def test_all_supported(self) -> None:
        valid_ids = {"s1", "s2"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=["s2"]),
        ]
        rate = compute_unsupported_claim_rate(items, valid_ids)
        assert rate == 0.0

    def test_all_unsupported_no_evidence(self) -> None:
        items = [
            _item("i1", FieldType.entity, evidence_ids=[]),
            _item("i2", FieldType.fact, evidence_ids=[]),
        ]
        rate = compute_unsupported_claim_rate(items, {"s1"})
        assert rate == 1.0

    def test_all_unsupported_invalid_evidence(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s99"]),
            _item("i2", FieldType.fact, evidence_ids=["s100"]),
        ]
        rate = compute_unsupported_claim_rate(items, valid_ids)
        assert rate == 1.0

    def test_partial_unsupported(self) -> None:
        valid_ids = {"s1"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=[]),
            _item("i3", FieldType.event, evidence_ids=["s99"]),
        ]
        rate = compute_unsupported_claim_rate(items, valid_ids)
        assert abs(rate - 2 / 3) < 1e-9

    def test_empty_items(self) -> None:
        rate = compute_unsupported_claim_rate([], {"s1"})
        assert rate == 0.0

    def test_mixed_evidence_one_valid(self) -> None:
        valid_ids = {"s2"}
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1", "s2"]),
        ]
        rate = compute_unsupported_claim_rate(items, valid_ids)
        assert rate == 0.0


# ---------------------------------------------------------------------------
# Full Evaluation
# ---------------------------------------------------------------------------


class TestEvaluateEvidence:
    def test_perfect_evaluation(self) -> None:
        source = "Apple reported revenue of $94.8 billion"
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "$94.8 billion", 26, 39),
        ]
        items = [
            _item(
                "i1", FieldType.entity, evidence_ids=["s1"],
                required_fields=["name"], supported_fields=["name"],
            ),
            _item(
                "i2", FieldType.fact, evidence_ids=["s2"],
                required_fields=["value", "unit"], supported_fields=["value", "unit"],
            ),
        ]
        result = evaluate_evidence(spans, source, items)

        assert isinstance(result, EvidenceMetricsResult)
        assert result.validity_rate == 1.0
        assert result.support_rate == 1.0
        assert result.coverage_score == 1.0
        assert result.orphan_rate == 0.0
        assert result.unsupported_claim_rate == 0.0
        assert result.total_spans == 2
        assert result.valid_spans == 2
        assert result.total_items == 2
        assert result.supported_items == 2
        assert result.orphan_spans == 0

    def test_evaluation_with_invalid_spans(self) -> None:
        source = "Apple reported revenue"
        spans = [
            _span("s1", "Apple", 0, 5),  # valid
            _span("s2", "WRONG", 6, 14),  # invalid text
        ]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.fact, evidence_ids=["s2"]),
        ]
        result = evaluate_evidence(spans, source, items)

        assert result.validity_rate == 0.5
        assert result.support_rate == 0.5  # only i1 has valid evidence
        assert result.unsupported_claim_rate == 0.5

    def test_evaluation_with_orphans(self) -> None:
        source = "Apple reported revenue of $94.8 billion"
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "revenue", 15, 22),
            _span("s3", "$94.8 billion", 26, 39),
        ]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
        ]
        result = evaluate_evidence(spans, source, items)

        assert result.validity_rate == 1.0
        assert result.support_rate == 1.0
        assert abs(result.orphan_rate - 2 / 3) < 1e-9
        assert result.orphan_spans == 2

    def test_evaluation_empty_inputs(self) -> None:
        result = evaluate_evidence([], "", [])

        assert result.validity_rate == 1.0
        assert result.support_rate == 1.0
        assert result.coverage_score == 1.0
        assert result.orphan_rate == 0.0
        assert result.unsupported_claim_rate == 0.0
        assert result.total_spans == 0
        assert result.total_items == 0

    def test_per_field_support_in_report(self) -> None:
        source = "Apple reported strong growth in Q3"
        spans = [
            _span("s1", "Apple", 0, 5),
            _span("s2", "strong growth", 15, 28),
        ]
        items = [
            _item("i1", FieldType.entity, evidence_ids=["s1"]),
            _item("i2", FieldType.sentiment, evidence_ids=["s2"]),
            _item("i3", FieldType.fact, evidence_ids=["s99"]),  # unsupported
        ]
        result = evaluate_evidence(spans, source, items)

        assert result.per_field_support["entity"] == 1.0
        assert result.per_field_support["sentiment"] == 1.0
        assert result.per_field_support["fact"] == 0.0

    def test_result_model_fields(self) -> None:
        result = EvidenceMetricsResult(
            validity_rate=0.9,
            support_rate=0.8,
            coverage_score=0.85,
            orphan_rate=0.1,
            per_field_support={"entity": 0.9, "fact": 0.7},
            unsupported_claim_rate=0.2,
            total_spans=10,
            valid_spans=9,
            total_items=5,
            supported_items=4,
            orphan_spans=1,
        )
        assert result.validity_rate == 0.9
        assert result.per_field_support["entity"] == 0.9
        assert result.orphan_spans == 1
