"""Tests for inter-rater reliability metrics."""

import pytest

from sltk.data.core import Segment, SegmentList
from sltk.linguistics.reliability import (
    BoundaryAgreementResult,
    ConfusionMatrixResult,
    KappaResult,
    LabelAgreementResult,
    ReliabilityResult,
    analyze_reliability_by_tier,
    annotation_confusion_matrix,
    boundary_agreement,
    cohens_kappa,
    fleiss_kappa,
    krippendorff_alpha,
    temporal_label_agreement,
)


class TestCohensKappa:
    """Tests for Cohen's Kappa calculation."""

    def test_perfect_agreement(self):
        """Test kappa with perfect agreement."""
        rater1 = ["A", "B", "C", "A", "B"]
        rater2 = ["A", "B", "C", "A", "B"]

        result = cohens_kappa(rater1, rater2)

        assert result.kappa == 1.0
        assert result.observed_agreement == 1.0
        assert result.interpretation == "almost_perfect"
        assert result.n_items == 5

    def test_no_agreement(self):
        """Test kappa with no agreement beyond chance."""
        # Construct a case where observed equals expected
        rater1 = ["A", "A", "B", "B"]
        rater2 = ["A", "B", "A", "B"]

        result = cohens_kappa(rater1, rater2)

        # Some agreement but not perfect
        assert result.kappa < 1.0
        assert result.observed_agreement == 0.5

    def test_moderate_agreement(self):
        """Test kappa with moderate agreement."""
        rater1 = ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"]
        rater2 = ["A", "B", "A", "A", "A", "B", "B", "B", "A", "B"]

        result = cohens_kappa(rater1, rater2)

        assert 0.2 < result.kappa < 0.8
        assert result.interpretation in ["fair", "moderate", "substantial"]

    def test_single_category(self):
        """Test kappa when both raters use only one category."""
        rater1 = ["A", "A", "A", "A"]
        rater2 = ["A", "A", "A", "A"]

        result = cohens_kappa(rater1, rater2)

        assert result.kappa == 1.0
        assert result.n_categories == 1

    def test_length_mismatch_error(self):
        """Test that mismatched lengths raise ValueError."""
        rater1 = ["A", "B", "C"]
        rater2 = ["A", "B"]

        with pytest.raises(ValueError, match="same length"):
            cohens_kappa(rater1, rater2)

    def test_empty_labels_error(self):
        """Test that empty lists raise ValueError."""
        with pytest.raises(ValueError, match="empty"):
            cohens_kappa([], [])

    def test_negative_kappa(self):
        """Test that kappa can be negative (worse than chance)."""
        # Deliberately opposite agreement
        rater1 = ["A", "A", "B", "B"]
        rater2 = ["B", "B", "A", "A"]

        result = cohens_kappa(rater1, rater2)

        assert result.kappa < 0
        assert result.interpretation == "poor"

    def test_to_dict(self):
        """Test JSON serialization."""
        rater1 = ["A", "B", "A", "B"]
        rater2 = ["A", "B", "A", "B"]

        result = cohens_kappa(rater1, rater2)
        d = result.to_dict()

        assert isinstance(d, dict)
        assert "kappa" in d
        assert "interpretation" in d
        assert isinstance(d["kappa"], float)


class TestFleissKappa:
    """Tests for Fleiss' Kappa calculation."""

    def test_perfect_agreement_three_raters(self):
        """Test Fleiss' kappa with perfect agreement among 3 raters."""
        rater1 = ["A", "B", "C", "A", "B"]
        rater2 = ["A", "B", "C", "A", "B"]
        rater3 = ["A", "B", "C", "A", "B"]

        result = fleiss_kappa([rater1, rater2, rater3])

        assert result.kappa == 1.0
        assert result.interpretation == "almost_perfect"
        assert result.n_items == 5

    def test_moderate_agreement(self):
        """Test Fleiss' kappa with moderate agreement."""
        rater1 = ["A", "B", "A", "B", "A"]
        rater2 = ["A", "B", "B", "B", "A"]
        rater3 = ["A", "A", "A", "B", "A"]

        result = fleiss_kappa([rater1, rater2, rater3])

        # Should have moderate to substantial agreement
        assert 0.0 < result.kappa < 1.0

    def test_single_rater_error(self):
        """Test that single rater raises ValueError."""
        with pytest.raises(ValueError, match="at least 2 raters"):
            fleiss_kappa([["A", "B"]])

    def test_empty_ratings_error(self):
        """Test that empty ratings raise ValueError."""
        with pytest.raises(ValueError, match="empty"):
            fleiss_kappa([[], []])

    def test_length_mismatch_error(self):
        """Test that mismatched lengths raise ValueError."""
        rater1 = ["A", "B", "C"]
        rater2 = ["A", "B"]

        with pytest.raises(ValueError, match="items, expected"):
            fleiss_kappa([rater1, rater2])

    def test_two_raters_matches_cohens(self):
        """Test that Fleiss with 2 raters gives similar results to Cohen's."""
        rater1 = ["A", "B", "A", "B", "C", "A", "B", "C"]
        rater2 = ["A", "B", "A", "A", "C", "A", "C", "C"]

        fleiss_result = fleiss_kappa([rater1, rater2])
        cohens_result = cohens_kappa(rater1, rater2)

        # Should be close (not exact due to algorithm differences)
        assert abs(fleiss_result.kappa - cohens_result.kappa) < 0.15

    def test_single_category(self):
        """Test Fleiss' kappa when all raters use only one category."""
        rater1 = ["A", "A", "A"]
        rater2 = ["A", "A", "A"]
        rater3 = ["A", "A", "A"]

        result = fleiss_kappa([rater1, rater2, rater3])

        assert result.kappa == 1.0


class TestKrippendorffAlpha:
    """Tests for Krippendorff's Alpha calculation."""

    def test_perfect_agreement_nominal(self):
        """Test alpha with perfect agreement on nominal data."""
        rater1 = ["A", "B", "C", "A", "B"]
        rater2 = ["A", "B", "C", "A", "B"]
        rater3 = ["A", "B", "C", "A", "B"]

        alpha = krippendorff_alpha([rater1, rater2, rater3], level="nominal")

        assert alpha == 1.0

    def test_handles_missing_data(self):
        """Test alpha handles None values correctly."""
        rater1 = ["A", "B", None, "A", "B"]
        rater2 = ["A", "B", "C", "A", None]
        rater3 = ["A", None, "C", "A", "B"]

        alpha = krippendorff_alpha([rater1, rater2, rater3], level="nominal")

        # Should still compute a valid value
        assert -1.0 <= alpha <= 1.0

    def test_all_missing_error(self):
        """Test that all missing data raises ValueError."""
        rater1 = [None, None, None]
        rater2 = [None, None, None]

        with pytest.raises(ValueError, match="No valid"):
            krippendorff_alpha([rater1, rater2])

    def test_ordinal_level(self):
        """Test alpha with ordinal data."""
        # Ratings on a 1-5 scale
        rater1 = ["1", "2", "3", "4", "5"]
        rater2 = ["1", "2", "3", "4", "5"]

        alpha = krippendorff_alpha([rater1, rater2], level="ordinal")

        assert alpha == 1.0

    def test_single_rater_error(self):
        """Test that single rater raises ValueError."""
        with pytest.raises(ValueError, match="at least 2 raters"):
            krippendorff_alpha([["A", "B"]])

    def test_no_pairs_error(self):
        """Test error when no items have 2+ ratings."""
        # Each item only rated by one person
        rater1 = ["A", None, None]
        rater2 = [None, "B", None]

        with pytest.raises(ValueError, match="at least 2 raters"):
            krippendorff_alpha([rater1, rater2])


class TestBoundaryAgreement:
    """Tests for boundary agreement calculation."""

    def test_perfect_boundaries(self):
        """Test perfect boundary agreement."""
        segs = SegmentList(
            [
                Segment(0, 1, "A"),
                Segment(1.5, 2.5, "B"),
            ]
        )

        result = boundary_agreement(segs, segs)

        assert result.precision == 1.0
        assert result.recall == 1.0
        assert result.f1 == 1.0
        assert result.n_matched == 4  # 0, 1, 1.5, 2.5

    def test_boundaries_within_tolerance(self):
        """Test boundaries that match within tolerance."""
        segs1 = SegmentList([Segment(0, 1, "A")])
        segs2 = SegmentList([Segment(0.05, 1.05, "A")])

        result = boundary_agreement(segs1, segs2, tolerance_sec=0.1)

        assert result.precision == 1.0
        assert result.recall == 1.0
        assert result.f1 == 1.0

    def test_boundaries_outside_tolerance(self):
        """Test boundaries outside tolerance."""
        segs1 = SegmentList([Segment(0, 1, "A")])
        segs2 = SegmentList([Segment(0.5, 1.5, "A")])

        result = boundary_agreement(segs1, segs2, tolerance_sec=0.1)

        assert result.precision == 0.0
        assert result.recall == 0.0
        assert result.f1 == 0.0

    def test_partial_match(self):
        """Test partial boundary matching."""
        segs1 = SegmentList([Segment(0, 1, "A"), Segment(2, 3, "B")])
        segs2 = SegmentList([Segment(0, 1, "A"), Segment(2.5, 3.5, "B")])

        result = boundary_agreement(segs1, segs2, tolerance_sec=0.1)

        # 0, 1 match; 2, 3 don't match 2.5, 3.5
        assert 0 < result.f1 < 1.0

    def test_empty_segments(self):
        """Test with empty segment lists."""
        empty = SegmentList([])
        non_empty = SegmentList([Segment(0, 1, "A")])

        # Both empty
        result = boundary_agreement(empty, empty)
        assert result.f1 == 1.0

        # One empty
        result = boundary_agreement(empty, non_empty)
        assert result.f1 == 0.0

    def test_tier_filter(self):
        """Test filtering by tier."""
        segs1 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 5, "X", tier="sentence"),
            ]
        )
        segs2 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 6, "Y", tier="sentence"),
            ]
        )

        # Filter to gloss tier only
        result = boundary_agreement(segs1, segs2, tier="gloss")
        assert result.f1 == 1.0

        # Sentence tier has different boundaries
        result = boundary_agreement(segs1, segs2, tier="sentence")
        assert result.f1 < 1.0

    def test_to_dict(self):
        """Test JSON serialization."""
        segs = SegmentList([Segment(0, 1, "A")])
        result = boundary_agreement(segs, segs)
        d = result.to_dict()

        assert isinstance(d, dict)
        assert "precision" in d
        assert "recall" in d
        assert "f1" in d


class TestTemporalLabelAgreement:
    """Tests for temporal label agreement calculation."""

    def test_perfect_agreement(self):
        """Test perfect temporal and label agreement."""
        segs1 = SegmentList(
            [
                Segment(0, 1, "HELLO"),
                Segment(1.5, 2.5, "WORLD"),
            ]
        )
        segs2 = SegmentList(
            [
                Segment(0, 1, "HELLO"),
                Segment(1.5, 2.5, "WORLD"),
            ]
        )

        result = temporal_label_agreement(segs1, segs2)

        assert result.kappa == 1.0
        assert result.agreement_rate == 1.0
        assert result.n_aligned_pairs == 2
        assert result.n_label_matches == 2

    def test_aligned_different_labels(self):
        """Test temporally aligned segments with different labels."""
        segs1 = SegmentList([Segment(0, 1, "HELLO")])
        segs2 = SegmentList([Segment(0.1, 1.1, "HI")])

        result = temporal_label_agreement(segs1, segs2, tolerance_sec=0.2)

        assert result.n_aligned_pairs == 1
        assert result.n_label_matches == 0
        assert result.agreement_rate == 0.0

    def test_unaligned_segments(self):
        """Test with segments that don't align temporally."""
        segs1 = SegmentList([Segment(0, 1, "A")])
        segs2 = SegmentList([Segment(5, 6, "B")])

        result = temporal_label_agreement(segs1, segs2, tolerance_sec=0.5)

        assert result.n_aligned_pairs == 0
        assert result.unaligned_rater1 == 1
        assert result.unaligned_rater2 == 1

    def test_empty_segments(self):
        """Test with empty segment lists."""
        empty = SegmentList([])
        non_empty = SegmentList([Segment(0, 1, "A")])

        # Both empty
        result = temporal_label_agreement(empty, empty)
        assert result.kappa == 1.0

        # One empty
        result = temporal_label_agreement(empty, non_empty)
        assert result.n_aligned_pairs == 0

    def test_tier_filter(self):
        """Test filtering by tier."""
        segs1 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 1, "X", tier="other"),
            ]
        )
        segs2 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 1, "Y", tier="other"),
            ]
        )

        result = temporal_label_agreement(segs1, segs2, tier="gloss")
        assert result.n_aligned_pairs == 1
        assert result.n_label_matches == 1

    def test_label_pairs_returned(self):
        """Test that label pairs are included in result."""
        segs1 = SegmentList(
            [
                Segment(0, 1, "A"),
                Segment(2, 3, "B"),
            ]
        )
        segs2 = SegmentList(
            [
                Segment(0, 1, "A"),
                Segment(2, 3, "C"),
            ]
        )

        result = temporal_label_agreement(segs1, segs2)

        assert ("A", "A") in result.label_pairs
        assert ("B", "C") in result.label_pairs

    def test_to_dict(self):
        """Test JSON serialization."""
        segs = SegmentList([Segment(0, 1, "A")])
        result = temporal_label_agreement(segs, segs)
        d = result.to_dict()

        assert isinstance(d, dict)
        assert "kappa" in d
        assert "agreement_rate" in d


class TestAnalyzeReliabilityByTier:
    """Tests for per-tier reliability analysis."""

    def test_single_tier(self):
        """Test analysis with single tier."""
        ann1 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(1.5, 2.5, "B", tier="gloss"),
            ]
        )
        ann2 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(1.5, 2.5, "B", tier="gloss"),
            ]
        )

        results = analyze_reliability_by_tier([ann1, ann2])

        assert "gloss" in results
        assert results["gloss"].kappa == 1.0
        assert results["gloss"].boundary_f1 == 1.0

    def test_multiple_tiers(self):
        """Test analysis with multiple tiers."""
        ann1 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 5, "declarative", tier="sentence"),
            ]
        )
        ann2 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 5, "declarative", tier="sentence"),
            ]
        )

        results = analyze_reliability_by_tier([ann1, ann2])

        assert "gloss" in results
        assert "sentence" in results

    def test_tier_filter(self):
        """Test filtering to specific tiers."""
        ann1 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 5, "X", tier="other"),
            ]
        )
        ann2 = SegmentList(
            [
                Segment(0, 1, "A", tier="gloss"),
                Segment(0, 5, "Y", tier="other"),
            ]
        )

        results = analyze_reliability_by_tier([ann1, ann2], tiers=["gloss"])

        assert "gloss" in results
        assert "other" not in results

    def test_three_annotators(self):
        """Test with three annotators (pairwise averaging)."""
        ann1 = SegmentList([Segment(0, 1, "A", tier="gloss")])
        ann2 = SegmentList([Segment(0, 1, "A", tier="gloss")])
        ann3 = SegmentList([Segment(0, 1, "B", tier="gloss")])

        results = analyze_reliability_by_tier([ann1, ann2, ann3])

        # ann1-ann2: perfect, ann1-ann3: no match, ann2-ann3: no match
        assert "gloss" in results
        # Kappa should be averaged across pairs

    def test_single_annotator_error(self):
        """Test that single annotator raises ValueError."""
        ann1 = SegmentList([Segment(0, 1, "A")])

        with pytest.raises(ValueError, match="at least 2"):
            analyze_reliability_by_tier([ann1])

    def test_empty_tier_skipped(self):
        """Test that empty tiers are skipped."""
        ann1 = SegmentList([Segment(0, 1, "A", tier="gloss")])
        ann2 = SegmentList([Segment(0, 1, "A", tier="gloss")])

        results = analyze_reliability_by_tier([ann1, ann2], tiers=["gloss", "nonexistent"])

        assert "gloss" in results
        assert "nonexistent" not in results

    def test_to_dict(self):
        """Test JSON serialization."""
        ann = SegmentList([Segment(0, 1, "A", tier="gloss")])
        results = analyze_reliability_by_tier([ann, ann])

        for tier, result in results.items():
            d = result.to_dict()
            assert isinstance(d, dict)
            assert "kappa" in d
            assert "boundary_f1" in d


class TestConfusionMatrix:
    """Tests for confusion matrix generation."""

    def test_perfect_agreement(self):
        """Test confusion matrix with perfect agreement."""
        labels = ["A", "B", "C", "A", "B", "C"]

        result = annotation_confusion_matrix(labels, labels)

        assert result.agreement_rate == 1.0
        assert result.n_items == 6
        assert len(result.most_confused_pairs) == 0

    def test_with_disagreements(self):
        """Test confusion matrix with disagreements."""
        rater1 = ["A", "B", "A", "B"]
        rater2 = ["A", "B", "B", "A"]

        result = annotation_confusion_matrix(rater1, rater2)

        assert result.agreement_rate == 0.5
        assert len(result.most_confused_pairs) > 0

        # Check that A-B and B-A confusion are captured
        confused_labels = {(p[0], p[1]) for p in result.most_confused_pairs}
        assert ("A", "B") in confused_labels or ("B", "A") in confused_labels

    def test_matrix_structure(self):
        """Test that matrix has correct structure."""
        rater1 = ["A", "B", "C"]
        rater2 = ["A", "B", "C"]

        result = annotation_confusion_matrix(rater1, rater2)

        # Matrix should be n_labels x n_labels
        n = len(result.labels)
        assert len(result.matrix) == n
        for row in result.matrix:
            assert len(row) == n

        # Diagonal should sum to n_items for perfect agreement
        diagonal_sum = sum(result.matrix[i][i] for i in range(n))
        assert diagonal_sum == result.n_items

    def test_sorted_confused_pairs(self):
        """Test that confused pairs are sorted by frequency."""
        rater1 = ["A", "A", "A", "B", "B"]
        rater2 = ["B", "B", "B", "C", "C"]

        result = annotation_confusion_matrix(rater1, rater2)

        # A->B confusion (3 times) should come before B->C (2 times)
        if len(result.most_confused_pairs) >= 2:
            assert result.most_confused_pairs[0][2] >= result.most_confused_pairs[1][2]

    def test_length_mismatch_error(self):
        """Test that mismatched lengths raise ValueError."""
        with pytest.raises(ValueError, match="same length"):
            annotation_confusion_matrix(["A", "B"], ["A"])

    def test_empty_labels_error(self):
        """Test that empty lists raise ValueError."""
        with pytest.raises(ValueError, match="empty"):
            annotation_confusion_matrix([], [])

    def test_to_dict(self):
        """Test JSON serialization."""
        rater1 = ["A", "B", "A"]
        rater2 = ["A", "B", "B"]

        result = annotation_confusion_matrix(rater1, rater2)
        d = result.to_dict()

        assert isinstance(d, dict)
        assert "matrix" in d
        assert "labels" in d
        assert "agreement_rate" in d
        assert "most_confused_pairs" in d
        assert isinstance(d["most_confused_pairs"], list)


class TestKappaInterpretation:
    """Tests for kappa interpretation thresholds."""

    def test_interpretation_thresholds(self):
        """Test that interpretation follows Landis & Koch thresholds."""
        from sltk.linguistics.reliability import _interpret_kappa

        assert _interpret_kappa(-0.1) == "poor"
        assert _interpret_kappa(0.05) == "slight"
        assert _interpret_kappa(0.25) == "fair"
        assert _interpret_kappa(0.45) == "moderate"
        assert _interpret_kappa(0.65) == "substantial"
        assert _interpret_kappa(0.85) == "almost_perfect"
        assert _interpret_kappa(1.0) == "almost_perfect"


class TestEdgeCases:
    """Tests for edge cases and boundary conditions."""

    def test_single_item(self):
        """Test with single item."""
        result = cohens_kappa(["A"], ["A"])
        assert result.kappa == 1.0
        assert result.n_items == 1

    def test_all_same_label_both_raters(self):
        """Test when both raters use same label for everything."""
        result = cohens_kappa(["X", "X", "X"], ["X", "X", "X"])
        assert result.kappa == 1.0

    def test_binary_labels(self):
        """Test with binary labels."""
        rater1 = ["yes", "no", "yes", "no", "yes"]
        rater2 = ["yes", "no", "no", "no", "yes"]

        result = cohens_kappa(rater1, rater2)

        # Should handle binary case correctly
        assert -1.0 <= result.kappa <= 1.0
        assert result.n_categories == 2

    def test_many_categories(self):
        """Test with many categories."""
        categories = [f"cat_{i}" for i in range(100)]
        rater1 = categories * 2
        rater2 = categories * 2

        result = cohens_kappa(rater1, rater2)

        assert result.kappa == 1.0
        assert result.n_categories == 100

    def test_overlapping_segments_alignment(self):
        """Test segment alignment with overlapping segments."""
        segs1 = SegmentList(
            [
                Segment(0, 2, "A"),
                Segment(1, 3, "B"),  # Overlaps with A
            ]
        )
        segs2 = SegmentList(
            [
                Segment(0.5, 2.5, "A"),
                Segment(1.5, 3.5, "B"),
            ]
        )

        result = temporal_label_agreement(segs1, segs2, tolerance_sec=0.5)

        # Should still align based on midpoints
        assert result.n_aligned_pairs >= 1
