"""Tests for the inter-annotator agreement metrics."""

from __future__ import annotations

import pytest

from services.intelligence_pipeline_v3.gold_corpus.agreement import (
    AgreementThresholds,
    FieldAgreement,
    InterAnnotatorReport,
    compute_cohens_kappa,
    compute_weighted_kappa,
)

# ---------------------------------------------------------------------------
# Tests — compute_cohens_kappa
# ---------------------------------------------------------------------------


class TestCohensKappa:
    def test_perfect_agreement(self) -> None:
        a = ["pos", "neg", "neutral", "pos", "neg"]
        b = ["pos", "neg", "neutral", "pos", "neg"]
        kappa = compute_cohens_kappa(a, b)
        assert abs(kappa - 1.0) < 1e-10

    def test_no_agreement_beyond_chance(self) -> None:
        # If both annotators use the same marginal distribution but disagree
        # on specific items, kappa should be around 0
        a = ["pos"] * 50 + ["neg"] * 50
        b = ["neg"] * 50 + ["pos"] * 50
        kappa = compute_cohens_kappa(a, b)
        assert kappa < 0.0  # Worse than chance

    def test_moderate_agreement(self) -> None:
        # 80% agreement with 2 categories
        a = ["pos", "pos", "neg", "pos", "neg", "neg", "pos", "pos", "neg", "pos"]
        b = ["pos", "pos", "neg", "pos", "neg", "neg", "pos", "neg", "neg", "pos"]
        kappa = compute_cohens_kappa(a, b)
        # Should be positive but less than 1
        assert 0.0 < kappa < 1.0

    def test_raises_on_empty(self) -> None:
        with pytest.raises(ValueError, match="empty"):
            compute_cohens_kappa([], [])

    def test_raises_on_length_mismatch(self) -> None:
        with pytest.raises(ValueError, match="equal length"):
            compute_cohens_kappa(["a", "b"], ["a"])

    def test_single_category_returns_one(self) -> None:
        # All same category = trivially perfect
        a = ["pos", "pos", "pos"]
        b = ["pos", "pos", "pos"]
        kappa = compute_cohens_kappa(a, b)
        assert abs(kappa - 1.0) < 1e-10

    def test_known_kappa_value(self) -> None:
        # Classic example: 2 raters, 2 categories, known kappa
        # 50 items: 20 agree pos, 15 agree neg, 10 A=pos B=neg, 5 A=neg B=pos
        a = ["pos"] * 20 + ["neg"] * 15 + ["pos"] * 10 + ["neg"] * 5
        b = ["pos"] * 20 + ["neg"] * 15 + ["neg"] * 10 + ["pos"] * 5
        kappa = compute_cohens_kappa(a, b)
        # p_o = 35/50 = 0.7
        # p_e = (30/50 * 25/50) + (20/50 * 25/50) = 0.3 + 0.2 = 0.5
        # kappa = (0.7 - 0.5) / (1 - 0.5) = 0.4
        assert abs(kappa - 0.4) < 1e-10


# ---------------------------------------------------------------------------
# Tests — compute_weighted_kappa
# ---------------------------------------------------------------------------


class TestWeightedKappa:
    def test_perfect_agreement(self) -> None:
        a = ["low", "medium", "high", "low", "medium"]
        b = ["low", "medium", "high", "low", "medium"]
        kappa = compute_weighted_kappa(a, b, ["low", "medium", "high"])
        assert abs(kappa - 1.0) < 1e-10

    def test_adjacent_disagreement_less_penalized_than_distant(self) -> None:
        # Mix of agreement and disagreement where adjacent is closer
        a = ["low", "medium", "high", "low", "medium", "high", "low", "medium"]
        # Adjacent disagreements (off by 1)
        b_adjacent = ["medium", "medium", "high", "medium", "medium", "high", "medium", "medium"]
        # Distant disagreements (off by 2)
        b_distant = ["high", "medium", "high", "high", "medium", "high", "high", "medium"]

        categories = ["low", "medium", "high"]
        kappa_adjacent = compute_weighted_kappa(a, b_adjacent, categories)
        kappa_distant = compute_weighted_kappa(a, b_distant, categories)

        # Adjacent disagreement should give higher kappa (less penalty)
        assert kappa_adjacent > kappa_distant

    def test_linear_vs_quadratic(self) -> None:
        a = ["low", "low", "medium", "high", "high"]
        b = ["medium", "high", "medium", "low", "medium"]
        categories = ["low", "medium", "high"]

        linear = compute_weighted_kappa(a, b, categories, weight_type="linear")
        quadratic = compute_weighted_kappa(a, b, categories, weight_type="quadratic")

        # Both should be numbers, quadratic penalizes large distances more
        assert isinstance(linear, float)
        assert isinstance(quadratic, float)

    def test_raises_on_empty(self) -> None:
        with pytest.raises(ValueError, match="empty"):
            compute_weighted_kappa([], [])

    def test_raises_on_length_mismatch(self) -> None:
        with pytest.raises(ValueError, match="equal length"):
            compute_weighted_kappa(["a", "b"], ["a"])

    def test_raises_on_invalid_weight_type(self) -> None:
        with pytest.raises(ValueError, match="weight_type"):
            compute_weighted_kappa(["a"], ["a"], weight_type="cubic")

    def test_raises_on_unknown_category(self) -> None:
        with pytest.raises(ValueError, match="not in ordered_categories"):
            compute_weighted_kappa(
                ["a", "b"], ["a", "c"], ordered_categories=["a", "b"]
            )

    def test_auto_determines_categories(self) -> None:
        a = ["high", "low", "medium"]
        b = ["high", "medium", "medium"]
        # Should not raise when ordered_categories is None
        kappa = compute_weighted_kappa(a, b)
        assert isinstance(kappa, float)

    def test_single_category_returns_one(self) -> None:
        kappa = compute_weighted_kappa(["a", "a"], ["a", "a"], ["a"])
        assert abs(kappa - 1.0) < 1e-10


# ---------------------------------------------------------------------------
# Tests — AgreementThresholds
# ---------------------------------------------------------------------------


class TestAgreementThresholds:
    def test_default_thresholds(self) -> None:
        thresholds = AgreementThresholds()
        assert thresholds.entities == 0.80
        assert thresholds.events == 0.80
        assert thresholds.relations == 0.70
        assert thresholds.sentiment == 0.70

    def test_custom_thresholds(self) -> None:
        thresholds = AgreementThresholds(entities=0.90, sentiment=0.75)
        assert thresholds.entities == 0.90
        assert thresholds.sentiment == 0.75


# ---------------------------------------------------------------------------
# Tests — InterAnnotatorReport
# ---------------------------------------------------------------------------


class TestInterAnnotatorReport:
    def test_report_construction(self) -> None:
        fields = [
            FieldAgreement(
                field_name="entities",
                kappa=0.85,
                threshold=0.80,
                meets_threshold=True,
                n_items=100,
                agreement_rate=0.90,
            ),
            FieldAgreement(
                field_name="relations",
                kappa=0.65,
                threshold=0.70,
                meets_threshold=False,
                n_items=50,
                agreement_rate=0.72,
            ),
        ]
        report = InterAnnotatorReport(
            annotator_a="annotator_1",
            annotator_b="annotator_2",
            n_documents=25,
            field_agreements=fields,
            overall_kappa=0.75,
            all_thresholds_met=False,
        )
        assert report.n_documents == 25
        assert not report.all_thresholds_met
        assert report.field_agreements[0].meets_threshold
        assert not report.field_agreements[1].meets_threshold
