"""
Tests for non-manual signal (NMS) analysis module.
"""

import numpy as np
import pytest

from sltk.data.core import Segment, SegmentList
from sltk.linguistics.nonmanual import (
    GRAMMATICAL_MARKERS,
    # Constants
    NMS_TIERS,
    GrammaticalPattern,
    NMSCooccurrenceResult,
    NMSDurationStats,
    NMSInventoryResult,
    NMSScopeResult,
    NMSTimingResult,
    # Result dataclasses
    ScopeExample,
    _get_nms_tiers,
    _normalize_tier_name,
    # Functions
    analyze_nms_cooccurrence,
    analyze_nms_inventory,
    analyze_nms_scope,
    analyze_nms_timing,
    detect_grammatical_patterns,
    get_grammatical_markers,
    get_standard_nms_tiers,
    nms_duration_stats,
    summarize_nms,
)

# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def simple_segments():
    """Create a simple SegmentList with gloss and NMS tiers."""
    segments = SegmentList(
        [
            # Manual signs (gloss tier)
            Segment(start=0.0, end=0.5, label="WHO", tier="gloss"),
            Segment(start=0.5, end=1.0, label="YOUR", tier="gloss"),
            Segment(start=1.0, end=1.5, label="NAME", tier="gloss"),
            Segment(start=2.0, end=2.5, label="MY", tier="gloss"),
            Segment(start=2.5, end=3.0, label="NAME", tier="gloss"),
            Segment(start=3.0, end=3.8, label="JOHN", tier="gloss"),
            # Eyebrow tier (question marking)
            Segment(start=0.0, end=1.5, label="furrowed", tier="eyebrow"),
            # Head tier
            Segment(start=3.0, end=3.8, label="nod", tier="head"),
            # Mouth tier
            Segment(start=3.0, end=3.8, label="closed", tier="mouth"),
        ]
    )
    return segments


@pytest.fixture
def complex_segments():
    """Create a more complex SegmentList with multiple NMS patterns."""
    segments = SegmentList(
        [
            # Manual signs
            Segment(start=0.0, end=0.3, label="YOU", tier="gloss"),
            Segment(start=0.3, end=0.6, label="GO", tier="gloss"),
            Segment(start=0.6, end=1.0, label="STORE", tier="gloss"),
            Segment(start=1.5, end=2.0, label="NOT", tier="gloss"),
            Segment(start=2.0, end=2.5, label="WANT", tier="gloss"),
            Segment(start=3.0, end=3.3, label="IF", tier="gloss"),
            Segment(start=3.3, end=3.8, label="RAIN", tier="gloss"),
            Segment(start=4.0, end=4.5, label="STAY", tier="gloss"),
            Segment(start=4.5, end=5.0, label="HOME", tier="gloss"),
            # Y/N question (raised brows over clause)
            Segment(start=0.0, end=1.0, label="raised", tier="eyebrow"),
            # Negation (headshake)
            Segment(start=1.5, end=2.5, label="shake", tier="head"),
            # Conditional (raised brows + forward lean)
            Segment(start=3.0, end=5.0, label="raised", tier="eyebrow"),
            Segment(start=3.0, end=3.8, label="forward", tier="body"),
        ]
    )
    return segments


@pytest.fixture
def empty_segments():
    """Create an empty SegmentList."""
    return SegmentList([])


@pytest.fixture
def no_nms_segments():
    """Create a SegmentList with only manual signs."""
    return SegmentList(
        [
            Segment(start=0.0, end=0.5, label="HELLO", tier="gloss"),
            Segment(start=0.5, end=1.0, label="WORLD", tier="gloss"),
        ]
    )


# ============================================================================
# Test Constants and Helpers
# ============================================================================


class TestConstants:
    """Tests for module constants."""

    def test_nms_tiers_structure(self):
        """Test that NMS_TIERS has expected structure."""
        assert isinstance(NMS_TIERS, dict)
        expected_categories = ["eyebrows", "mouth", "head", "eye_gaze", "body"]
        for cat in expected_categories:
            assert cat in NMS_TIERS
            assert isinstance(NMS_TIERS[cat], list)
            assert len(NMS_TIERS[cat]) > 0

    def test_grammatical_markers_structure(self):
        """Test that GRAMMATICAL_MARKERS has expected structure."""
        assert isinstance(GRAMMATICAL_MARKERS, dict)
        expected_types = ["wh_question", "yn_question", "negation", "topic"]
        for ptype in expected_types:
            assert ptype in GRAMMATICAL_MARKERS
            marker = GRAMMATICAL_MARKERS[ptype]
            assert "nms" in marker
            assert "tier_preference" in marker


class TestHelperFunctions:
    """Tests for helper functions."""

    def test_normalize_tier_name_exact_match(self):
        """Test tier name normalization with exact matches."""
        assert _normalize_tier_name("eyebrow") == "eyebrows"
        assert _normalize_tier_name("head") == "head"
        assert _normalize_tier_name("mouth") == "mouth"
        assert _normalize_tier_name("gaze") == "eye_gaze"
        assert _normalize_tier_name("body") == "body"

    def test_normalize_tier_name_partial_match(self):
        """Test tier name normalization with partial matches."""
        assert _normalize_tier_name("eyebrow_movement") == "eyebrows"
        assert _normalize_tier_name("head_mvt") == "head"
        assert _normalize_tier_name("mouthing") == "mouth"

    def test_normalize_tier_name_unknown(self):
        """Test that unknown tier names return None."""
        assert _normalize_tier_name("gloss") is None
        assert _normalize_tier_name("translation") is None
        assert _normalize_tier_name("unknown_tier") is None

    def test_get_nms_tiers_auto_detect(self, simple_segments):
        """Test automatic NMS tier detection."""
        tiers = _get_nms_tiers(simple_segments, None)
        assert "eyebrow" in tiers
        assert "head" in tiers
        assert "mouth" in tiers
        assert "gloss" not in tiers

    def test_get_nms_tiers_explicit(self, simple_segments):
        """Test explicit NMS tier specification."""
        tiers = _get_nms_tiers(simple_segments, ["head"])
        assert tiers == ["head"]

    def test_get_nms_tiers_invalid_explicit(self, simple_segments):
        """Test that invalid explicit tiers are filtered out."""
        tiers = _get_nms_tiers(simple_segments, ["nonexistent"])
        assert tiers == []


# ============================================================================
# Test Dataclasses
# ============================================================================


class TestDataclasses:
    """Tests for result dataclasses."""

    def test_scope_example_to_dict(self):
        """Test ScopeExample serialization."""
        example = ScopeExample(
            nms_label="raised",
            nms_start=0.0,
            nms_end=1.0,
            manual_signs=["WHO", "WHAT"],
            manual_times=[(0.0, 0.5), (0.5, 1.0)],
        )
        d = example.to_dict()
        assert d["nms_label"] == "raised"
        assert d["nms_start"] == 0.0
        assert d["manual_signs"] == ["WHO", "WHAT"]

    def test_grammatical_pattern_properties(self):
        """Test GrammaticalPattern properties."""
        pattern = GrammaticalPattern(
            pattern_type="wh_question",
            start=0.0,
            end=1.5,
            nms_markers=["furrowed"],
            manual_signs=["WHO"],
            confidence=0.8,
        )
        assert pattern.duration == 1.5
        d = pattern.to_dict()
        assert d["duration"] == 1.5

    def test_nms_cooccurrence_result_top_associations(self):
        """Test NMSCooccurrenceResult helper methods."""
        result = NMSCooccurrenceResult(
            manual_to_nms={
                "WHO": [("furrowed", 0.9), ("raised", 0.1)],
            },
            nms_to_manual={
                "furrowed": [("WHO", 0.9), ("WHAT", 0.7)],
            },
            total_manual_signs=10,
            total_nms_tokens=5,
        )
        top = result.top_associations_for_sign("WHO", n=1)
        assert len(top) == 1
        assert top[0][0] == "furrowed"

    def test_nms_scope_result_ratio(self):
        """Test NMSScopeResult single_sign_ratio property."""
        result = NMSScopeResult(
            avg_scope=2.0,
            scope_distribution={1: 5, 2: 3, 3: 2},
            examples=[],
            total_nms_spans=10,
            single_sign_count=5,
            multi_sign_count=5,
        )
        assert result.single_sign_ratio == 0.5


# ============================================================================
# Test Analysis Functions
# ============================================================================


class TestAnalyzeNMSCooccurrence:
    """Tests for analyze_nms_cooccurrence function."""

    def test_basic_cooccurrence(self, simple_segments):
        """Test basic co-occurrence analysis."""
        result = analyze_nms_cooccurrence(
            simple_segments,
            manual_tier="gloss",
        )
        assert isinstance(result, NMSCooccurrenceResult)
        assert result.total_manual_signs > 0
        assert result.total_nms_tokens > 0

        # WHO should co-occur with furrowed
        assert "WHO" in result.manual_to_nms
        nms_for_who = dict(result.manual_to_nms["WHO"])
        assert "furrowed" in nms_for_who

    def test_cooccurrence_with_explicit_tiers(self, simple_segments):
        """Test co-occurrence with explicit NMS tier specification."""
        result = analyze_nms_cooccurrence(
            simple_segments,
            manual_tier="gloss",
            nms_tiers=["eyebrow"],
        )
        # Should only find eyebrow NMS
        all_nms = set()
        for sign_nms in result.manual_to_nms.values():
            all_nms.update(nms for nms, _ in sign_nms)
        # "nod" is from head tier, should not appear
        assert "nod" not in all_nms

    def test_cooccurrence_missing_manual_tier(self, simple_segments):
        """Test graceful handling of missing manual tier."""
        result = analyze_nms_cooccurrence(
            simple_segments,
            manual_tier="nonexistent",
        )
        assert result.total_manual_signs == 0
        assert result.manual_to_nms == {}

    def test_cooccurrence_no_nms_tiers(self, no_nms_segments):
        """Test handling when no NMS tiers are found."""
        result = analyze_nms_cooccurrence(
            no_nms_segments,
            manual_tier="gloss",
        )
        assert result.total_nms_tokens == 0

    def test_cooccurrence_empty_segments(self, empty_segments):
        """Test with empty segment list."""
        result = analyze_nms_cooccurrence(
            empty_segments,
            manual_tier="gloss",
        )
        assert result.total_manual_signs == 0
        assert result.total_nms_tokens == 0

    def test_cooccurrence_serialization(self, simple_segments):
        """Test that result can be serialized to dict."""
        result = analyze_nms_cooccurrence(simple_segments, manual_tier="gloss")
        d = result.to_dict()
        assert isinstance(d, dict)
        assert "manual_to_nms" in d
        assert "total_manual_signs" in d


class TestAnalyzeNMSScope:
    """Tests for analyze_nms_scope function."""

    def test_basic_scope(self, simple_segments):
        """Test basic scope analysis."""
        result = analyze_nms_scope(
            simple_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        assert isinstance(result, NMSScopeResult)
        assert result.total_nms_spans > 0
        # Furrowed eyebrows span WHO, YOUR, NAME (3 signs)
        assert result.avg_scope == 3.0
        assert 3 in result.scope_distribution

    def test_scope_single_sign(self, simple_segments):
        """Test scope for single-sign NMS."""
        result = analyze_nms_scope(
            simple_segments,
            nms_tier="head",
            manual_tier="gloss",
        )
        # Head nod only spans JOHN (1 sign)
        assert result.single_sign_count >= 1

    def test_scope_missing_tier(self, simple_segments):
        """Test graceful handling of missing tier."""
        result = analyze_nms_scope(
            simple_segments,
            nms_tier="nonexistent",
            manual_tier="gloss",
        )
        assert result.total_nms_spans == 0
        assert result.avg_scope == 0.0

    def test_scope_examples(self, complex_segments):
        """Test that scope examples are collected."""
        result = analyze_nms_scope(
            complex_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
            max_examples=5,
        )
        assert len(result.examples) > 0
        for ex in result.examples:
            assert isinstance(ex, ScopeExample)
            assert len(ex.manual_signs) == len(ex.manual_times)

    def test_scope_serialization(self, simple_segments):
        """Test scope result serialization."""
        result = analyze_nms_scope(
            simple_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        d = result.to_dict()
        assert "avg_scope" in d
        assert "single_sign_ratio" in d


class TestDetectGrammaticalPatterns:
    """Tests for detect_grammatical_patterns function."""

    def test_detect_all_patterns(self, complex_segments):
        """Test detection of multiple pattern types."""
        patterns = detect_grammatical_patterns(complex_segments)
        assert isinstance(patterns, list)
        assert len(patterns) > 0
        for p in patterns:
            assert isinstance(p, GrammaticalPattern)
            assert p.confidence >= 0.0
            assert p.confidence <= 1.0

    def test_detect_question_patterns(self, simple_segments):
        """Test detection of question patterns."""
        patterns = detect_grammatical_patterns(
            simple_segments,
            pattern_type="question",
        )
        # Should find WH-question from furrowed brows
        wh_patterns = [p for p in patterns if p.pattern_type == "wh_question"]
        assert len(wh_patterns) > 0

    def test_detect_negation_patterns(self, complex_segments):
        """Test detection of negation patterns."""
        patterns = detect_grammatical_patterns(
            complex_segments,
            pattern_type="negation",
        )
        neg_patterns = [p for p in patterns if p.pattern_type == "negation"]
        assert len(neg_patterns) > 0

    def test_detect_with_confidence_threshold(self, complex_segments):
        """Test pattern detection with confidence threshold."""
        all_patterns = detect_grammatical_patterns(
            complex_segments,
            confidence_threshold=0.0,
        )
        high_conf_patterns = detect_grammatical_patterns(
            complex_segments,
            confidence_threshold=0.8,
        )
        assert len(high_conf_patterns) <= len(all_patterns)

    def test_patterns_sorted_by_time(self, complex_segments):
        """Test that patterns are sorted by start time."""
        patterns = detect_grammatical_patterns(complex_segments)
        for i in range(len(patterns) - 1):
            assert patterns[i].start <= patterns[i + 1].start

    def test_pattern_serialization(self, simple_segments):
        """Test pattern serialization."""
        patterns = detect_grammatical_patterns(simple_segments)
        if patterns:
            d = patterns[0].to_dict()
            assert "pattern_type" in d
            assert "duration" in d


class TestNMSDurationStats:
    """Tests for nms_duration_stats function."""

    def test_basic_duration_stats(self, simple_segments):
        """Test basic duration statistics."""
        stats = nms_duration_stats(simple_segments, nms_tier="eyebrow")
        assert isinstance(stats, NMSDurationStats)
        assert stats.count > 0
        assert stats.mean > 0
        assert stats.min_val <= stats.mean <= stats.max_val

    def test_duration_stats_quartiles(self, complex_segments):
        """Test quartile calculation."""
        stats = nms_duration_stats(complex_segments, nms_tier="eyebrow")
        q1, q2, q3 = stats.quartiles
        assert q1 <= q2 <= q3

    def test_duration_stats_histogram(self, simple_segments):
        """Test histogram data."""
        stats = nms_duration_stats(simple_segments, nms_tier="eyebrow")
        assert len(stats.histogram_bins) == len(stats.histogram_counts) + 1
        assert sum(stats.histogram_counts) == stats.count

    def test_duration_stats_by_label(self, complex_segments):
        """Test per-label statistics."""
        stats = nms_duration_stats(complex_segments, nms_tier="eyebrow")
        assert "raised" in stats.by_label
        assert "mean" in stats.by_label["raised"]

    def test_duration_stats_missing_tier(self, simple_segments):
        """Test with missing tier."""
        stats = nms_duration_stats(simple_segments, nms_tier="nonexistent")
        assert stats.count == 0
        assert stats.mean == 0.0

    def test_duration_stats_serialization(self, simple_segments):
        """Test duration stats serialization."""
        stats = nms_duration_stats(simple_segments, nms_tier="eyebrow")
        d = stats.to_dict()
        assert "mean" in d
        assert "quartiles" in d


class TestAnalyzeNMSTiming:
    """Tests for analyze_nms_timing function."""

    def test_basic_timing(self, simple_segments):
        """Test basic timing analysis."""
        result = analyze_nms_timing(
            simple_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        assert isinstance(result, NMSTimingResult)
        assert result.total_comparisons > 0

    def test_timing_distributions(self, complex_segments):
        """Test timing distribution data."""
        result = analyze_nms_timing(
            complex_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        assert len(result.onset_distribution) == result.total_comparisons
        assert len(result.offset_distribution) == result.total_comparisons

    def test_timing_lead_lag_counts(self, simple_segments):
        """Test lead/lag counting."""
        result = analyze_nms_timing(
            simple_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        total = result.nms_leads_count + result.nms_lags_count + result.coincident_count
        assert total == result.total_comparisons

    def test_timing_missing_tier(self, simple_segments):
        """Test with missing tier."""
        result = analyze_nms_timing(
            simple_segments,
            nms_tier="nonexistent",
            manual_tier="gloss",
        )
        assert result.total_comparisons == 0

    def test_timing_serialization(self, simple_segments):
        """Test timing result serialization."""
        result = analyze_nms_timing(
            simple_segments,
            nms_tier="eyebrow",
            manual_tier="gloss",
        )
        d = result.to_dict()
        assert "avg_onset_diff" in d
        assert "nms_leads_ratio" in d


class TestAnalyzeNMSInventory:
    """Tests for analyze_nms_inventory function."""

    def test_basic_inventory(self, simple_segments):
        """Test basic inventory analysis."""
        result = analyze_nms_inventory(simple_segments)
        assert isinstance(result, NMSInventoryResult)
        assert result.total_tokens > 0
        assert result.total_types > 0

    def test_inventory_tier_inventories(self, simple_segments):
        """Test per-tier inventory breakdown."""
        result = analyze_nms_inventory(simple_segments)
        assert "eyebrow" in result.tier_inventories
        assert "furrowed" in result.tier_inventories["eyebrow"]

    def test_inventory_frequency_distribution(self, complex_segments):
        """Test overall frequency distribution."""
        result = analyze_nms_inventory(complex_segments)
        total_from_dist = sum(result.frequency_distribution.values())
        assert total_from_dist == result.total_tokens

    def test_inventory_top_nms(self, complex_segments):
        """Test top NMS retrieval."""
        result = analyze_nms_inventory(complex_segments)
        top = result.top_nms(5)
        # Should be sorted by frequency
        for i in range(len(top) - 1):
            assert top[i][1] >= top[i + 1][1]

    def test_inventory_cross_tier_combinations(self, complex_segments):
        """Test cross-tier combination detection."""
        result = analyze_nms_inventory(complex_segments)
        # complex_segments has simultaneous NMS on multiple tiers
        # At 3.0-3.8: raised eyebrow + forward body
        if result.cross_tier_combinations:
            combo = result.cross_tier_combinations[0]
            assert "components" in combo
            assert "count" in combo

    def test_inventory_examples(self, simple_segments):
        """Test example collection."""
        result = analyze_nms_inventory(simple_segments, max_examples_per_label=2)
        assert len(result.examples_by_label) > 0
        for label, examples in result.examples_by_label.items():
            assert len(examples) <= 2

    def test_inventory_empty_segments(self, empty_segments):
        """Test with empty segments."""
        result = analyze_nms_inventory(empty_segments)
        assert result.total_tokens == 0
        assert result.total_types == 0

    def test_inventory_serialization(self, simple_segments):
        """Test inventory serialization."""
        result = analyze_nms_inventory(simple_segments)
        d = result.to_dict()
        assert "tier_inventories" in d
        assert "total_tokens" in d


# ============================================================================
# Test Convenience Functions
# ============================================================================


class TestConvenienceFunctions:
    """Tests for convenience functions."""

    def test_get_standard_nms_tiers(self):
        """Test get_standard_nms_tiers returns a copy."""
        tiers = get_standard_nms_tiers()
        assert tiers == NMS_TIERS
        # Modify returned dict shouldn't affect original
        tiers["test"] = ["test"]
        assert "test" not in NMS_TIERS

    def test_get_grammatical_markers(self):
        """Test get_grammatical_markers returns a copy."""
        markers = get_grammatical_markers()
        assert "wh_question" in markers
        # Modify returned dict shouldn't affect original
        markers["test"] = {"nms": []}
        assert "test" not in GRAMMATICAL_MARKERS

    def test_summarize_nms(self, complex_segments):
        """Test summarize_nms convenience function."""
        summary = summarize_nms(complex_segments)
        assert isinstance(summary, dict)
        assert "total_nms_tokens" in summary
        assert "total_nms_types" in summary
        assert "top_nms" in summary
        assert "grammatical_patterns_detected" in summary

    def test_summarize_nms_empty(self, empty_segments):
        """Test summarize_nms with empty segments."""
        summary = summarize_nms(empty_segments)
        assert summary["total_nms_tokens"] == 0


# ============================================================================
# Integration Tests
# ============================================================================


class TestIntegration:
    """Integration tests combining multiple functions."""

    def test_full_analysis_pipeline(self, complex_segments):
        """Test running a full analysis pipeline."""
        # Get inventory first
        inventory = analyze_nms_inventory(complex_segments)
        detected_tiers = list(inventory.tier_inventories.keys())

        # Analyze each tier
        for tier in detected_tiers:
            stats = nms_duration_stats(complex_segments, tier)
            assert stats.nms_tier == tier

            analyze_nms_scope(complex_segments, tier, "gloss")
            analyze_nms_timing(complex_segments, tier, "gloss")

        # Detect patterns
        detect_grammatical_patterns(complex_segments)

        # Get co-occurrence
        analyze_nms_cooccurrence(complex_segments, "gloss")

        # All should succeed without errors
        assert True

    def test_json_serialization_all_results(self, complex_segments):
        """Test that all results can be JSON serialized."""
        import json

        cooc = analyze_nms_cooccurrence(complex_segments, "gloss")
        scope = analyze_nms_scope(complex_segments, "eyebrow", "gloss")
        patterns = detect_grammatical_patterns(complex_segments)
        stats = nms_duration_stats(complex_segments, "eyebrow")
        timing = analyze_nms_timing(complex_segments, "eyebrow", "gloss")
        inventory = analyze_nms_inventory(complex_segments)

        # All should be JSON serializable
        json.dumps(cooc.to_dict())
        json.dumps(scope.to_dict())
        json.dumps([p.to_dict() for p in patterns])
        json.dumps(stats.to_dict())
        json.dumps(timing.to_dict())
        json.dumps(inventory.to_dict())
