"""Tests for linguistic and caption analysis tools."""

from dataclasses import dataclass

import pytest

from sltk.analysis.captions import (
    CaptionStatistics,
    analyze_caption_corpus,
    build_caption_concordance,
    compute_caption_alignment_stats,
    extract_caption_keywords,
    extract_caption_ngrams,
    search_captions,
)
from sltk.analysis.linguistic import (
    ConcordanceLine,
    CooccurrenceResult,
    DurationAnalysis,
    MinimalPair,
    NGram,
    SegmentReference,
    analyze_cooccurrence,
    analyze_duration_patterns,
    analyze_signer_variation,
    build_concordance,
    compute_cooccurrence_matrix,
    extract_ngrams,
    find_minimal_pairs,
)
from sltk.linguistics.phonology import (
    FingerOrientation,
    Handshape,
    Location,
    Movement,
    PalmOrientation,
    PhonologicalForm,
)


@dataclass
class MockSegment:
    """Mock segment for testing."""

    label: str
    start: float
    end: float
    tier: str = "gloss"


class TestConcordance:
    """Tests for concordance building."""

    @pytest.fixture
    def sample_segments(self):
        """Create sample segments for testing."""
        return {
            "file1": [
                MockSegment("A", 0.0, 1.0),
                MockSegment("HELLO", 1.0, 2.0),
                MockSegment("B", 2.0, 3.0),
                MockSegment("HELLO", 3.0, 4.0),
                MockSegment("C", 4.0, 5.0),
            ],
            "file2": [
                MockSegment("X", 0.0, 1.0),
                MockSegment("HELLO", 1.0, 2.0),
                MockSegment("Y", 2.0, 3.0),
            ],
        }

    def test_build_concordance(self, sample_segments):
        """Test basic concordance building."""
        results = build_concordance(sample_segments, "HELLO", context_window=2)

        assert len(results) == 3  # 3 occurrences of HELLO
        assert all(isinstance(r, ConcordanceLine) for r in results)

    def test_concordance_context(self, sample_segments):
        """Test concordance context extraction."""
        results = build_concordance(sample_segments, "HELLO", context_window=1)

        # First occurrence in file1
        first = [r for r in results if r.file_id == "file1"][0]
        assert first.left_context == ["A"]
        assert first.right_context == ["B"]

    def test_case_insensitive(self, sample_segments):
        """Test case-insensitive matching."""
        results = build_concordance(sample_segments, "hello", case_sensitive=False)
        assert len(results) == 3

    def test_kwic_string(self, sample_segments):
        """Test KWIC string formatting."""
        results = build_concordance(sample_segments, "HELLO", context_window=2)
        kwic = results[0].to_kwic_string(context_width=2)

        assert "|" in kwic
        assert "HELLO" in kwic


class TestCooccurrence:
    """Tests for co-occurrence analysis."""

    @pytest.fixture
    def sample_segments(self):
        """Create sample segments."""
        return {
            "file1": [
                MockSegment("A", 0.0, 1.0),
                MockSegment("TARGET", 1.0, 2.0),
                MockSegment("B", 2.0, 3.0),
                MockSegment("TARGET", 3.0, 4.0),
                MockSegment("A", 4.0, 5.0),
            ],
        }

    def test_analyze_cooccurrence(self, sample_segments):
        """Test co-occurrence analysis."""
        result = analyze_cooccurrence(sample_segments, "TARGET", window_size=2)

        assert isinstance(result, CooccurrenceResult)
        assert result.total_occurrences == 2
        assert "a" in result.collocates  # lowercase due to default

    def test_left_right_collocates(self, sample_segments):
        """Test left/right collocate separation."""
        result = analyze_cooccurrence(sample_segments, "TARGET", window_size=1)

        assert result.left_collocates is not None
        assert result.right_collocates is not None

    def test_top_collocates(self, sample_segments):
        """Test getting top collocates."""
        result = analyze_cooccurrence(sample_segments, "TARGET", window_size=2)
        top = result.top_collocates(n=5)

        assert isinstance(top, list)
        assert all(isinstance(t, tuple) and len(t) == 2 for t in top)


class TestNGrams:
    """Tests for n-gram extraction."""

    @pytest.fixture
    def sample_segments(self):
        """Create sample segments."""
        return {
            "file1": [
                MockSegment("A", 0.0, 1.0),
                MockSegment("B", 1.0, 2.0),
                MockSegment("C", 2.0, 3.0),
                MockSegment("A", 3.0, 4.0),
                MockSegment("B", 4.0, 5.0),
                MockSegment("C", 5.0, 6.0),
            ],
        }

    def test_extract_bigrams(self, sample_segments):
        """Test bigram extraction."""
        ngrams = extract_ngrams(sample_segments, n=2, min_frequency=1)

        assert len(ngrams) > 0
        assert all(isinstance(ng, NGram) for ng in ngrams)
        assert all(ng.n == 2 for ng in ngrams)

    def test_extract_trigrams(self, sample_segments):
        """Test trigram extraction."""
        ngrams = extract_ngrams(sample_segments, n=3, min_frequency=1)

        assert all(ng.n == 3 for ng in ngrams)

    def test_min_frequency_filter(self, sample_segments):
        """Test minimum frequency filtering."""
        ngrams_low = extract_ngrams(sample_segments, n=2, min_frequency=1)
        ngrams_high = extract_ngrams(sample_segments, n=2, min_frequency=2)

        assert len(ngrams_high) <= len(ngrams_low)

    def test_ngram_examples(self, sample_segments):
        """Test n-gram example inclusion."""
        ngrams = extract_ngrams(sample_segments, n=2, include_examples=True)

        if ngrams:
            assert len(ngrams[0].examples) > 0


class TestDurationPatterns:
    """Tests for duration pattern analysis."""

    @pytest.fixture
    def sample_segments(self):
        """Create segments with varying durations."""
        return {
            "file1": [
                MockSegment("FAST", 0.0, 0.3),
                MockSegment("FAST", 0.5, 0.8),
                MockSegment("SLOW", 1.0, 2.0),
                MockSegment("SLOW", 2.5, 3.5),
                MockSegment("SLOW", 4.0, 5.0),
            ],
        }

    def test_analyze_duration(self, sample_segments):
        """Test duration pattern analysis."""
        results = analyze_duration_patterns(sample_segments, min_occurrences=2)

        assert "FAST" in results
        assert "SLOW" in results

    def test_duration_statistics(self, sample_segments):
        """Test duration statistics."""
        results = analyze_duration_patterns(sample_segments, min_occurrences=2)
        slow = results["SLOW"]

        assert isinstance(slow, DurationAnalysis)
        assert slow.count == 3
        assert slow.mean_duration == pytest.approx(1.0, rel=0.1)

    def test_single_gloss_analysis(self, sample_segments):
        """Test analyzing single gloss."""
        results = analyze_duration_patterns(sample_segments, gloss="FAST", min_occurrences=1)

        assert len(results) == 1
        assert "FAST" in results


class TestMinimalPairs:
    """Tests for minimal pairs detection."""

    def test_find_minimal_pairs_legacy_mode(self):
        """Test finding minimal pairs using string similarity (legacy mode)."""
        vocab = ["HELLO", "HALLO", "HELP", "WORLD", "WORK"]
        pairs = find_minimal_pairs(vocabulary=vocab, legacy_threshold=0.7)

        assert len(pairs) > 0
        assert all(isinstance(p, MinimalPair) for p in pairs)
        assert all(p.method == "string_similarity" for p in pairs)
        assert all(p.differing_parameter is None for p in pairs)

    def test_legacy_threshold_filtering(self):
        """Test similarity threshold filtering in legacy mode."""
        vocab = ["ABC", "ABD", "XYZ"]
        pairs_low = find_minimal_pairs(vocabulary=vocab, legacy_threshold=0.5)
        pairs_high = find_minimal_pairs(vocabulary=vocab, legacy_threshold=0.9)

        assert len(pairs_low) >= len(pairs_high)

    def test_minimal_pair_dataclass(self):
        """Test MinimalPair dataclass functionality."""
        pair = MinimalPair(
            sign1="MOTHER",
            sign2="FATHER",
            differing_parameter="location",
            distance=1,
            method="phonological",
            examples_sign1=[SegmentReference("file1", 0, 1.0, 1.5)],
            examples_sign2=[SegmentReference("file1", 5, 3.0, 3.5)],
        )

        assert pair.sign1 == "MOTHER"
        assert pair.is_true_minimal_pair
        assert pair.method == "phonological"

        # Test to_dict
        d = pair.to_dict()
        assert d["sign1"] == "MOTHER"
        assert d["differing_parameter"] == "location"
        assert len(d["examples_sign1"]) == 1

    def test_segment_reference_dataclass(self):
        """Test SegmentReference dataclass."""
        ref = SegmentReference(
            file_id="video1",
            segment_idx=5,
            start_time=1.0,
            end_time=2.0,
            tier="gloss",
        )

        assert ref.file_id == "video1"
        d = ref.to_dict()
        assert d["segment_idx"] == 5

    def test_phonological_minimal_pairs(self):
        """Test finding minimal pairs using phonological comparison."""
        # Create forms that differ in exactly one parameter
        mother_form = PhonologicalForm(
            handshape_dom=Handshape.FIVE,
            location=Location.CHIN,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_LEFT,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        father_form = PhonologicalForm(
            handshape_dom=Handshape.FIVE,
            location=Location.FOREHEAD,  # Different location
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_LEFT,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        unrelated_form = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CIRCLE_VERTICAL,
            orientation_palm=PalmOrientation.PALM_DOWN,
            orientation_fingers=FingerOrientation.FINGERS_FORWARD,
        )

        forms = {
            "MOTHER": mother_form,
            "FATHER": father_form,
            "WORK": unrelated_form,
        }

        pairs = find_minimal_pairs(
            phonological_forms=forms,
            use_phonology=True,
            max_distance=1,
        )

        # Should find MOTHER-FATHER as minimal pair
        assert len(pairs) >= 1
        minimal_pair = next((p for p in pairs if {p.sign1, p.sign2} == {"FATHER", "MOTHER"}), None)
        assert minimal_pair is not None
        assert minimal_pair.method == "phonological"
        assert minimal_pair.distance == 1
        assert minimal_pair.differing_parameter == "location"
        assert minimal_pair.is_true_minimal_pair

    def test_near_minimal_pairs(self):
        """Test finding near-minimal pairs (distance > 1)."""
        form1 = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHIN,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        form2 = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.FOREHEAD,  # Different
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )

        forms = {"SIGN1": form1, "SIGN2": form2}

        # With max_distance=1, should not find pair
        pairs_strict = find_minimal_pairs(
            phonological_forms=forms, use_phonology=True, max_distance=1
        )
        assert len(pairs_strict) == 0

        # With max_distance=2, should find pair
        pairs_loose = find_minimal_pairs(
            phonological_forms=forms, use_phonology=True, max_distance=2
        )
        assert len(pairs_loose) == 1
        assert pairs_loose[0].distance == 2
        assert not pairs_loose[0].is_true_minimal_pair

    def test_mixed_mode_with_examples(self):
        """Test mixed mode with segment examples."""
        # Create segments
        segments = {
            "file1": [
                MockSegment("MOTHER", 0.0, 1.0),
                MockSegment("FATHER", 1.0, 2.0),
                MockSegment("HELLO", 2.0, 3.0),
                MockSegment("HALLO", 3.0, 4.0),  # Similar to HELLO
            ],
        }

        # Create phonological forms for some signs
        mother_form = PhonologicalForm(
            handshape_dom=Handshape.FIVE,
            location=Location.CHIN,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_LEFT,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        father_form = PhonologicalForm(
            handshape_dom=Handshape.FIVE,
            location=Location.FOREHEAD,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_LEFT,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )

        forms = {"MOTHER": mother_form, "FATHER": father_form}

        pairs = find_minimal_pairs(
            segments_by_file=segments,
            phonological_forms=forms,
            use_phonology=True,
            max_distance=1,
            legacy_threshold=0.7,
            include_examples=True,
        )

        # Should find both phonological and string similarity pairs
        phon_pairs = [p for p in pairs if p.method == "phonological"]
        string_pairs = [p for p in pairs if p.method == "string_similarity"]

        assert len(phon_pairs) >= 1  # MOTHER-FATHER
        assert len(string_pairs) >= 1  # HELLO-HALLO

        # Check examples are included
        for pair in pairs:
            if pair.sign1 in ["MOTHER", "FATHER"]:
                assert len(pair.examples_sign1) > 0 or len(pair.examples_sign2) > 0

    def test_vocabulary_extraction_from_segments(self):
        """Test vocabulary is correctly extracted from segments."""
        segments = {
            "file1": [
                MockSegment("A", 0.0, 1.0),
                MockSegment("B", 1.0, 2.0),
            ],
            "file2": [
                MockSegment("A", 0.0, 1.0),  # Duplicate
                MockSegment("C", 1.0, 2.0),
            ],
        }

        pairs = find_minimal_pairs(
            segments_by_file=segments,
            legacy_threshold=0.5,
        )

        # Should have processed unique vocabulary: A, B, C
        assert isinstance(pairs, list)

    def test_empty_inputs(self):
        """Test handling of empty inputs."""
        # Empty vocabulary
        pairs = find_minimal_pairs(vocabulary=[])
        assert pairs == []

        # No forms
        pairs = find_minimal_pairs(phonological_forms={})
        assert pairs == []

    def test_use_phonology_false(self):
        """Test that use_phonology=False forces legacy mode."""
        forms = {
            "SIGN1": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHIN,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "SIGN2": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.FOREHEAD,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
        }

        pairs = find_minimal_pairs(
            vocabulary=["SIGN1", "SIGN2"],
            phonological_forms=forms,
            use_phonology=False,  # Force legacy mode
            legacy_threshold=0.5,
        )

        # All pairs should use string similarity
        assert all(p.method == "string_similarity" for p in pairs)


class TestSignerVariation:
    """Tests for signer variation analysis."""

    @pytest.fixture
    def multi_signer_segments(self):
        """Create segments from multiple signers."""
        return {
            "file1": [
                MockSegment("HELLO", 0.0, 0.5),
                MockSegment("HELLO", 1.0, 1.3),
            ],
            "file2": [
                MockSegment("HELLO", 0.0, 0.8),
                MockSegment("HELLO", 1.0, 1.6),
            ],
        }

    @pytest.fixture
    def signer_ids(self):
        """Signer ID mapping."""
        return {"file1": "signer_A", "file2": "signer_B"}

    def test_analyze_variation(self, multi_signer_segments, signer_ids):
        """Test signer variation analysis."""
        result = analyze_signer_variation(multi_signer_segments, "HELLO", signer_ids)

        assert result["gloss"] == "HELLO"
        assert "signer_A" in result["signers"]
        assert "signer_B" in result["signers"]

    def test_variation_counts(self, multi_signer_segments, signer_ids):
        """Test variation occurrence counts."""
        result = analyze_signer_variation(multi_signer_segments, "HELLO", signer_ids)

        assert result["signers"]["signer_A"]["count"] == 2
        assert result["signers"]["signer_B"]["count"] == 2


class TestCooccurrenceMatrix:
    """Tests for co-occurrence matrix computation."""

    @pytest.fixture
    def sample_segments(self):
        """Create sample segments."""
        return {
            "file1": [
                MockSegment("A", 0.0, 1.0),
                MockSegment("B", 1.0, 2.0),
                MockSegment("A", 2.0, 3.0),
                MockSegment("C", 3.0, 4.0),
            ]
            * 5,  # Repeat for frequency
        }

    def test_compute_matrix(self, sample_segments):
        """Test co-occurrence matrix computation."""
        result = compute_cooccurrence_matrix(sample_segments, top_n_glosses=3)

        assert "glosses" in result
        assert "matrix" in result
        assert "frequencies" in result

    def test_matrix_shape(self, sample_segments):
        """Test matrix dimensions."""
        result = compute_cooccurrence_matrix(sample_segments, top_n_glosses=3)
        matrix = result["matrix"]

        n = len(result["glosses"])
        assert len(matrix) == n
        assert all(len(row) == n for row in matrix)


# Caption-based analysis tests


class TestCaptionAnalysis:
    """Tests for caption-based analysis."""

    @pytest.fixture
    def sample_captions(self):
        """Create sample captions."""
        return {
            "video1": [
                {"text": "Hello world", "start": 0.0, "end": 2.0},
                {"text": "How are you doing today", "start": 2.5, "end": 5.0},
            ],
            "video2": [
                {"text": "Welcome to the show", "start": 0.0, "end": 2.0},
                {"text": "Let us begin", "start": 2.5, "end": 4.0},
            ],
        }

    def test_analyze_corpus(self, sample_captions):
        """Test caption corpus analysis."""
        stats = analyze_caption_corpus(sample_captions)

        assert isinstance(stats, CaptionStatistics)
        assert stats.total_captions == 4
        assert stats.total_words > 0

    def test_extract_keywords(self, sample_captions):
        """Test keyword extraction."""
        keywords = extract_caption_keywords(sample_captions, top_n=10)

        assert isinstance(keywords, list)
        assert all(isinstance(k, tuple) and len(k) == 2 for k in keywords)

    def test_search_captions(self, sample_captions):
        """Test caption search."""
        results = search_captions(sample_captions, "hello")

        assert len(results) >= 1
        assert results[0]["text"] == "Hello world"

    def test_search_whole_word(self, sample_captions):
        """Test whole word search."""
        results = search_captions(sample_captions, "how", whole_word=True)
        assert len(results) >= 1

    def test_alignment_stats(self, sample_captions):
        """Test alignment statistics."""
        stats = compute_caption_alignment_stats(sample_captions)

        assert "num_gaps" in stats
        assert "num_overlaps" in stats
        assert "mean_duration" in stats

    def test_caption_concordance(self, sample_captions):
        """Test caption concordance."""
        results = build_caption_concordance(sample_captions, "hello", context_words=3)

        assert len(results) >= 1
        assert "word" in results[0]
        assert "left_context" in results[0]
        assert "right_context" in results[0]

    def test_caption_ngrams(self, sample_captions):
        """Test caption n-gram extraction."""
        ngrams = extract_caption_ngrams(sample_captions, n=2, min_freq=1, exclude_stopwords=False)

        assert isinstance(ngrams, list)
        if ngrams:
            assert isinstance(ngrams[0][0], tuple)
            assert len(ngrams[0][0]) == 2


class TestCaptionStopwords:
    """Tests for stopword handling in caption analysis."""

    @pytest.fixture
    def stopword_heavy_captions(self):
        """Create captions with many stopwords."""
        return {
            "video1": [
                {"text": "The quick brown fox jumps over the lazy dog", "start": 0.0, "end": 3.0},
            ],
        }

    def test_exclude_stopwords(self, stopword_heavy_captions):
        """Test stopword exclusion."""
        with_stopwords = analyze_caption_corpus(stopword_heavy_captions, exclude_stopwords=False)
        without_stopwords = analyze_caption_corpus(stopword_heavy_captions, exclude_stopwords=True)

        assert without_stopwords.total_words < with_stopwords.total_words
        assert "the" not in without_stopwords.word_frequencies


class TestCaptionTimingAnalysis:
    """Tests for caption timing analysis."""

    @pytest.fixture
    def overlapping_captions(self):
        """Create captions with overlaps."""
        return {
            "video1": [
                {"text": "First", "start": 0.0, "end": 2.0},
                {"text": "Second", "start": 1.5, "end": 3.0},  # Overlaps
                {"text": "Third", "start": 3.5, "end": 5.0},  # Gap
            ],
        }

    def test_detect_overlaps(self, overlapping_captions):
        """Test overlap detection."""
        stats = compute_caption_alignment_stats(overlapping_captions)

        assert stats["num_overlaps"] >= 1
        assert stats["num_gaps"] >= 1
