"""
Tests for the phonology module.

Tests phonological form data model, distance calculations,
minimal pair detection, and inventory analysis.
"""

from collections import Counter

import pytest

from sltk.linguistics import (
    CORE_PARAMETERS,
    EXTENDED_PARAMETERS,
    FingerOrientation,
    HandArrangement,
    Handshape,
    InventoryStats,
    LinguisticSegment,
    Location,
    Movement,
    NonManualSignal,
    PalmOrientation,
    PhonologicalForm,
    analyze_inventory,
    compute_phonological_neighborhood,
    filter_by_parameter,
    find_minimal_pairs,
    find_near_minimal_pairs,
    get_differing_parameters,
    group_by_parameter,
    phonological_distance,
)

# =============================================================================
# Enum Tests
# =============================================================================


class TestHandshapeEnum:
    """Tests for Handshape enum."""

    def test_handshape_values(self):
        """Test that handshape values are defined correctly."""
        assert Handshape.A.value == "A"
        assert Handshape.B_FLAT.value == "B_flat"
        assert Handshape.FIVE.value == "5"
        assert Handshape.ONE.value == "1"

    def test_from_string_direct(self):
        """Test from_string with direct value match."""
        assert Handshape.from_string("A") == Handshape.A
        assert Handshape.from_string("B_flat") == Handshape.B_FLAT

    def test_from_string_name(self):
        """Test from_string with enum name."""
        assert Handshape.from_string("B_FLAT") == Handshape.B_FLAT
        assert Handshape.from_string("FIVE") == Handshape.FIVE

    def test_from_string_case_insensitive(self):
        """Test from_string is case insensitive."""
        assert Handshape.from_string("b_flat") == Handshape.B_FLAT
        assert Handshape.from_string("a") == Handshape.A

    def test_from_string_invalid(self):
        """Test from_string raises ValueError for invalid values."""
        with pytest.raises(ValueError, match="Unknown handshape"):
            Handshape.from_string("INVALID_SHAPE")


class TestLocationEnum:
    """Tests for Location enum."""

    def test_location_values(self):
        """Test that location values are defined correctly."""
        assert Location.FOREHEAD.value == "forehead"
        assert Location.CHEST.value == "chest"
        assert Location.NEUTRAL_SPACE.value == "neutral_space"

    def test_from_string(self):
        """Test from_string conversion."""
        assert Location.from_string("forehead") == Location.FOREHEAD
        assert Location.from_string("NEUTRAL_SPACE") == Location.NEUTRAL_SPACE

    def test_from_string_invalid(self):
        """Test from_string raises ValueError for invalid values."""
        with pytest.raises(ValueError, match="Unknown location"):
            Location.from_string("INVALID_LOCATION")


class TestMovementEnum:
    """Tests for Movement enum."""

    def test_movement_values(self):
        """Test that movement values are defined correctly."""
        assert Movement.LINEAR_UP.value == "linear_up"
        assert Movement.CIRCLE_HORIZONTAL.value == "circle_horizontal"
        assert Movement.HOLD.value == "hold"

    def test_from_string(self):
        """Test from_string conversion."""
        assert Movement.from_string("linear_up") == Movement.LINEAR_UP
        assert Movement.from_string("HOLD") == Movement.HOLD


class TestOrientationEnums:
    """Tests for orientation enums."""

    def test_palm_orientation_values(self):
        """Test palm orientation values."""
        assert PalmOrientation.PALM_UP.value == "palm_up"
        assert PalmOrientation.PALM_IN.value == "palm_in"

    def test_finger_orientation_values(self):
        """Test finger orientation values."""
        assert FingerOrientation.FINGERS_UP.value == "fingers_up"
        assert FingerOrientation.FINGERS_FORWARD.value == "fingers_forward"


class TestHandArrangementEnum:
    """Tests for HandArrangement enum."""

    def test_hand_arrangement_values(self):
        """Test hand arrangement values."""
        assert HandArrangement.ONE_HANDED.value == "one_handed"
        assert HandArrangement.SYMMETRIC.value == "symmetric"
        assert HandArrangement.ALTERNATING.value == "alternating"


class TestNonManualSignalEnum:
    """Tests for NonManualSignal enum."""

    def test_nms_values(self):
        """Test non-manual signal values."""
        assert NonManualSignal.BROW_RAISE.value == "brow_raise"
        assert NonManualSignal.HEAD_NOD.value == "head_nod"
        assert NonManualSignal.MOUTHING.value == "mouthing"


# =============================================================================
# PhonologicalForm Tests
# =============================================================================


class TestPhonologicalForm:
    """Tests for PhonologicalForm dataclass."""

    @pytest.fixture
    def basic_form(self):
        """Create a basic one-handed phonological form."""
        return PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )

    @pytest.fixture
    def two_handed_form(self):
        """Create a two-handed phonological form."""
        return PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,
            handshape_nondom=Handshape.B_FLAT,
            location=Location.NEUTRAL_SPACE,
            movement=Movement.CIRCLE_HORIZONTAL,
            orientation_palm=PalmOrientation.PALM_DOWN,
            orientation_fingers=FingerOrientation.FINGERS_FORWARD,
            hand_arrangement=HandArrangement.SYMMETRIC,
        )

    def test_create_with_enums(self, basic_form):
        """Test creating form with enum values."""
        assert basic_form.handshape_dom == Handshape.A
        assert basic_form.location == Location.CHEST
        assert basic_form.movement == Movement.CONTACT

    def test_create_with_strings(self):
        """Test creating form with string values."""
        form = PhonologicalForm(
            handshape_dom="A",
            location="chest",
            movement="contact",
            orientation_palm="palm_in",
            orientation_fingers="fingers_up",
        )
        assert form.handshape_dom == Handshape.A
        assert form.location == Location.CHEST

    def test_create_with_invalid_string(self):
        """Test that invalid strings are kept as strings."""
        form = PhonologicalForm(
            handshape_dom="CUSTOM_SHAPE",  # Not in standard inventory
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert form.handshape_dom == "CUSTOM_SHAPE"

    def test_is_one_handed(self, basic_form, two_handed_form):
        """Test is_one_handed property."""
        assert basic_form.is_one_handed is True
        assert two_handed_form.is_one_handed is False

    def test_is_two_handed(self, basic_form, two_handed_form):
        """Test is_two_handed property."""
        assert basic_form.is_two_handed is False
        assert two_handed_form.is_two_handed is True

    def test_has_nms(self, basic_form):
        """Test has_nms property."""
        assert basic_form.has_nms is False

        form_with_nms = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
            nms_cooccurring=[NonManualSignal.BROW_RAISE],
        )
        assert form_with_nms.has_nms is True

    def test_parameters(self, basic_form, two_handed_form):
        """Test parameters property."""
        params = basic_form.parameters
        assert "handshape_dom" in params
        assert "location" in params
        assert "movement" in params
        assert "handshape_nondom" not in params

        two_handed_params = two_handed_form.parameters
        assert "handshape_nondom" in two_handed_params
        assert "hand_arrangement" in two_handed_params

    def test_get_parameter_value(self, basic_form):
        """Test get_parameter_value method."""
        assert basic_form.get_parameter_value("handshape_dom") == Handshape.A
        assert basic_form.get_parameter_value("location") == Location.CHEST

    def test_get_parameter_string(self, basic_form):
        """Test get_parameter_string method."""
        assert basic_form.get_parameter_string("handshape_dom") == "A"
        assert basic_form.get_parameter_string("location") == "chest"
        assert basic_form.get_parameter_string("handshape_nondom") == "none"

    def test_to_dict(self, basic_form):
        """Test to_dict serialization."""
        d = basic_form.to_dict()
        assert d["handshape_dom"] == "A"
        assert d["location"] == "chest"
        assert d["handshape_nondom"] is None
        assert d["nms_cooccurring"] == []

    def test_from_dict(self):
        """Test from_dict deserialization."""
        data = {
            "handshape_dom": "A",
            "location": "chest",
            "movement": "contact",
            "orientation_palm": "palm_in",
            "orientation_fingers": "fingers_up",
        }
        form = PhonologicalForm.from_dict(data)
        assert form.handshape_dom == Handshape.A
        assert form.location == Location.CHEST

    def test_to_dict_from_dict_roundtrip(self, basic_form, two_handed_form):
        """Test that to_dict/from_dict is a proper roundtrip."""
        # One-handed form
        d = basic_form.to_dict()
        restored = PhonologicalForm.from_dict(d)
        assert restored == basic_form

        # Two-handed form
        d2 = two_handed_form.to_dict()
        restored2 = PhonologicalForm.from_dict(d2)
        assert restored2 == two_handed_form

    def test_equality(self, basic_form):
        """Test equality comparison."""
        same_form = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert basic_form == same_form

        different_form = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert basic_form != different_form

    def test_hash(self, basic_form):
        """Test that forms can be hashed (for use in sets/dicts)."""
        form_set = {basic_form}
        assert basic_form in form_set

        same_form = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert same_form in form_set

    def test_auto_hand_arrangement(self):
        """Test that hand_arrangement defaults for two-handed signs."""
        form = PhonologicalForm(
            handshape_dom=Handshape.A,
            handshape_nondom=Handshape.B_FLAT,
            location=Location.NEUTRAL_SPACE,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert form.hand_arrangement == HandArrangement.DOMINANT_ACTIVE


# =============================================================================
# LinguisticSegment Tests
# =============================================================================


class TestLinguisticSegment:
    """Tests for LinguisticSegment dataclass."""

    @pytest.fixture
    def basic_segment(self):
        """Create a basic linguistic segment."""
        return LinguisticSegment(
            start=1.0,
            end=2.0,
            label="HELLO",
            tier="glosses",
        )

    @pytest.fixture
    def segment_with_phonology(self):
        """Create a segment with phonological form."""
        form = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,
            location=Location.FOREHEAD,
            movement=Movement.LINEAR_AWAY,
            orientation_palm=PalmOrientation.PALM_OUT,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        return LinguisticSegment(
            start=0.5,
            end=1.5,
            label="HELLO",
            phonological_form=form,
            part_of_speech="noun",
            lemma="HELLO",
        )

    def test_create_basic(self, basic_segment):
        """Test creating a basic segment."""
        assert basic_segment.start == 1.0
        assert basic_segment.end == 2.0
        assert basic_segment.label == "HELLO"
        assert basic_segment.tier == "glosses"

    def test_duration(self, basic_segment):
        """Test duration property."""
        assert basic_segment.duration == 1.0

    def test_midpoint(self, basic_segment):
        """Test midpoint property."""
        assert basic_segment.midpoint == 1.5

    def test_has_phonology(self, basic_segment, segment_with_phonology):
        """Test has_phonology property."""
        assert basic_segment.has_phonology is False
        assert segment_with_phonology.has_phonology is True

    def test_has_morphology(self, basic_segment):
        """Test has_morphology property."""
        assert basic_segment.has_morphology is False

        segment_with_morph = LinguisticSegment(
            start=0.0,
            end=1.0,
            label="TEST",
            morphological_analysis=["TEST", "-PAST"],
        )
        assert segment_with_morph.has_morphology is True

    def test_validation_negative_start(self):
        """Test that negative start time raises ValueError."""
        with pytest.raises(ValueError, match="start must be non-negative"):
            LinguisticSegment(start=-1.0, end=1.0)

    def test_validation_end_before_start(self):
        """Test that end before start raises ValueError."""
        with pytest.raises(ValueError, match="end.*must be >= start"):
            LinguisticSegment(start=2.0, end=1.0)

    def test_to_dict(self, segment_with_phonology):
        """Test to_dict serialization."""
        d = segment_with_phonology.to_dict()
        assert d["start"] == 0.5
        assert d["end"] == 1.5
        assert d["label"] == "HELLO"
        assert d["phonological_form"] is not None
        assert d["phonological_form"]["handshape_dom"] == "B_flat"

    def test_from_dict(self):
        """Test from_dict deserialization."""
        data = {
            "start": 1.0,
            "end": 2.0,
            "label": "TEST",
            "tier": "glosses",
            "part_of_speech": "verb",
        }
        segment = LinguisticSegment.from_dict(data)
        assert segment.start == 1.0
        assert segment.label == "TEST"
        assert segment.part_of_speech == "verb"

    def test_to_dict_from_dict_roundtrip(self, segment_with_phonology):
        """Test roundtrip serialization."""
        d = segment_with_phonology.to_dict()
        restored = LinguisticSegment.from_dict(d)
        assert restored.start == segment_with_phonology.start
        assert restored.end == segment_with_phonology.end
        assert restored.label == segment_with_phonology.label
        assert restored.phonological_form == segment_with_phonology.phonological_form


# =============================================================================
# Phonological Distance Tests
# =============================================================================


class TestPhonologicalDistance:
    """Tests for phonological distance calculations."""

    @pytest.fixture
    def form_a(self):
        """Create a reference form."""
        return PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )

    def test_identical_forms(self, form_a):
        """Test distance is 0 for identical forms."""
        form_b = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert phonological_distance(form_a, form_b) == 0

    def test_one_difference(self, form_a):
        """Test distance is 1 for forms differing in one parameter."""
        form_b = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert phonological_distance(form_a, form_b) == 1

    def test_two_differences(self, form_a):
        """Test distance is 2 for forms differing in two parameters."""
        form_b = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.FOREHEAD,  # Different
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        assert phonological_distance(form_a, form_b) == 2

    def test_all_different(self, form_a):
        """Test maximum distance when all parameters differ."""
        form_b = PhonologicalForm(
            handshape_dom=Handshape.FIVE,  # Different
            location=Location.NEUTRAL_SPACE,  # Different
            movement=Movement.CIRCLE_HORIZONTAL,  # Different
            orientation_palm=PalmOrientation.PALM_DOWN,  # Different
            orientation_fingers=FingerOrientation.FINGERS_FORWARD,  # Different
        )
        assert phonological_distance(form_a, form_b) == 5

    def test_with_nondom(self):
        """Test distance includes non-dominant hand when requested."""
        form_a = PhonologicalForm(
            handshape_dom=Handshape.A,
            handshape_nondom=Handshape.B_FLAT,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        form_b = PhonologicalForm(
            handshape_dom=Handshape.A,
            handshape_nondom=Handshape.FIVE,  # Different
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        # With nondom included (default)
        assert phonological_distance(form_a, form_b, include_nondom=True) == 1
        # Without nondom
        assert phonological_distance(form_a, form_b, include_nondom=False) == 0


class TestGetDifferingParameters:
    """Tests for get_differing_parameters function."""

    def test_no_differences(self):
        """Test empty list for identical forms."""
        form = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        diff = get_differing_parameters(form, form)
        assert diff == []

    def test_one_difference(self):
        """Test single parameter difference."""
        form_a = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        form_b = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        diff = get_differing_parameters(form_a, form_b)
        assert diff == ["handshape_dom"]

    def test_multiple_differences(self):
        """Test multiple parameter differences."""
        form_a = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        form_b = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # Different
            location=Location.FOREHEAD,  # Different
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        diff = get_differing_parameters(form_a, form_b)
        assert "handshape_dom" in diff
        assert "location" in diff
        assert len(diff) == 2


# =============================================================================
# Minimal Pair Detection Tests
# =============================================================================


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

    @pytest.fixture
    def sample_forms(self):
        """Create sample forms for testing."""
        return {
            "MOTHER": PhonologicalForm(
                handshape_dom=Handshape.FIVE,
                location=Location.CHIN,
                movement=Movement.CONTACT_REPEATED,
                orientation_palm=PalmOrientation.PALM_LEFT,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "FATHER": PhonologicalForm(
                handshape_dom=Handshape.FIVE,
                location=Location.FOREHEAD,  # Only location differs
                movement=Movement.CONTACT_REPEATED,
                orientation_palm=PalmOrientation.PALM_LEFT,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "NICE": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,  # Different handshape
                location=Location.CHEST,  # Different location
                movement=Movement.LINEAR_DOWN,  # Different movement
                orientation_palm=PalmOrientation.PALM_UP,  # Different palm
                orientation_fingers=FingerOrientation.FINGERS_FORWARD,  # Different fingers
            ),
        }

    def test_find_minimal_pairs(self, sample_forms):
        """Test finding minimal pairs."""
        pairs = find_minimal_pairs(sample_forms)
        # MOTHER and FATHER should be a minimal pair (differ only in location)
        assert len(pairs) == 1
        pair = pairs[0]
        assert "MOTHER" in pair[:2] or "FATHER" in pair[:2]
        assert pair[2] == "location"

    def test_find_minimal_pairs_empty(self):
        """Test with no minimal pairs."""
        forms = {
            "SIGN1": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHEST,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "SIGN2": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,  # Different
                location=Location.FOREHEAD,  # Different
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
        }
        pairs = find_minimal_pairs(forms)
        assert len(pairs) == 0


class TestNearMinimalPairs:
    """Tests for near-minimal pair detection."""

    @pytest.fixture
    def sample_forms(self):
        """Create sample forms for testing."""
        base = PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        one_diff = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # 1 difference
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        two_diff = PhonologicalForm(
            handshape_dom=Handshape.B_FLAT,  # 1 difference
            location=Location.FOREHEAD,  # 2 differences
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )
        return {"BASE": base, "ONE_DIFF": one_diff, "TWO_DIFF": two_diff}

    def test_find_near_minimal_pairs_dist1(self, sample_forms):
        """Test finding pairs with distance 1."""
        pairs = find_near_minimal_pairs(sample_forms, max_distance=1)
        # BASE-ONE_DIFF (dist=1) and ONE_DIFF-TWO_DIFF (dist=1) should be included
        # BASE-TWO_DIFF has dist=2 so should NOT be included
        assert len(pairs) == 2
        # Verify BASE-TWO_DIFF is not in the results
        pair_names = [(p[0], p[1]) for p in pairs]
        assert ("BASE", "TWO_DIFF") not in pair_names

    def test_find_near_minimal_pairs_dist2(self, sample_forms):
        """Test finding pairs with distance <= 2."""
        pairs = find_near_minimal_pairs(sample_forms, max_distance=2)
        # Should include BASE-ONE_DIFF (dist=1), BASE-TWO_DIFF (dist=2), ONE_DIFF-TWO_DIFF (dist=1)
        assert len(pairs) == 3


# =============================================================================
# Inventory Analysis Tests
# =============================================================================


class TestInventoryStats:
    """Tests for InventoryStats dataclass."""

    @pytest.fixture
    def sample_stats(self):
        """Create sample inventory stats."""
        return InventoryStats(
            total_forms=100,
            handshape_counts=Counter({"A": 30, "B_flat": 25, "5": 20, "1": 15, "other": 10}),
            location_counts=Counter({"chest": 40, "neutral_space": 35, "forehead": 25}),
            movement_counts=Counter({"contact": 50, "linear_away": 30, "hold": 20}),
            palm_orientation_counts=Counter({"palm_in": 60, "palm_out": 40}),
            finger_orientation_counts=Counter({"fingers_up": 70, "fingers_forward": 30}),
            hand_arrangement_counts=Counter(
                {"one_handed": 70, "symmetric": 20, "dominant_active": 10}
            ),
            nms_counts=Counter({"brow_raise": 15, "head_nod": 10}),
            one_handed_count=70,
            two_handed_count=30,
        )

    def test_unique_counts(self, sample_stats):
        """Test unique counts properties."""
        assert sample_stats.unique_handshapes == 5
        assert sample_stats.unique_locations == 3
        assert sample_stats.unique_movements == 3

    def test_ratios(self, sample_stats):
        """Test one-handed/two-handed ratio properties."""
        assert sample_stats.one_handed_ratio == 0.7
        assert sample_stats.two_handed_ratio == 0.3

    def test_most_common(self, sample_stats):
        """Test most_common methods."""
        top_hs = sample_stats.most_common_handshapes(3)
        assert len(top_hs) == 3
        assert top_hs[0][0] == "A"
        assert top_hs[0][1] == 30

    def test_to_dict(self, sample_stats):
        """Test to_dict serialization."""
        d = sample_stats.to_dict()
        assert d["total_forms"] == 100
        assert d["handshape_counts"]["A"] == 30
        assert d["unique_handshapes"] == 5

    def test_from_dict(self, sample_stats):
        """Test from_dict deserialization."""
        d = sample_stats.to_dict()
        restored = InventoryStats.from_dict(d)
        assert restored.total_forms == sample_stats.total_forms
        assert restored.one_handed_count == sample_stats.one_handed_count


class TestAnalyzeInventory:
    """Tests for analyze_inventory function."""

    @pytest.fixture
    def sample_forms(self):
        """Create sample forms for inventory analysis."""
        return [
            PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHEST,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.FOREHEAD,
                movement=Movement.LINEAR_AWAY,
                orientation_palm=PalmOrientation.PALM_OUT,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,
                handshape_nondom=Handshape.B_FLAT,
                location=Location.NEUTRAL_SPACE,
                movement=Movement.CIRCLE_HORIZONTAL,
                orientation_palm=PalmOrientation.PALM_DOWN,
                orientation_fingers=FingerOrientation.FINGERS_FORWARD,
                hand_arrangement=HandArrangement.SYMMETRIC,
            ),
        ]

    def test_analyze_inventory_counts(self, sample_forms):
        """Test that analyze_inventory counts correctly."""
        stats = analyze_inventory(sample_forms)
        assert stats.total_forms == 3
        assert stats.one_handed_count == 2
        assert stats.two_handed_count == 1

    def test_analyze_inventory_handshapes(self, sample_forms):
        """Test handshape counting."""
        stats = analyze_inventory(sample_forms)
        # A appears twice (2 one-handed signs), B_flat appears twice (two-handed sign has both hands)
        assert stats.handshape_counts["A"] == 2
        assert stats.handshape_counts["B_flat"] == 2

    def test_analyze_inventory_empty(self):
        """Test with empty list."""
        stats = analyze_inventory([])
        assert stats.total_forms == 0
        assert stats.one_handed_count == 0
        assert stats.one_handed_ratio == 0.0


# =============================================================================
# Filter and Group Tests
# =============================================================================


class TestFilterByParameter:
    """Tests for filter_by_parameter function."""

    @pytest.fixture
    def sample_forms(self):
        """Create sample forms for filtering."""
        return {
            "SIGN1": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHEST,
                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,
            ),
            "SIGN3": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,
                location=Location.CHEST,
                movement=Movement.LINEAR_AWAY,
                orientation_palm=PalmOrientation.PALM_OUT,
                orientation_fingers=FingerOrientation.FINGERS_FORWARD,
            ),
        }

    def test_filter_by_handshape(self, sample_forms):
        """Test filtering by handshape."""
        filtered = filter_by_parameter(sample_forms, "handshape_dom", Handshape.A)
        assert len(filtered) == 2
        assert "SIGN1" in filtered
        assert "SIGN2" in filtered
        assert "SIGN3" not in filtered

    def test_filter_by_location(self, sample_forms):
        """Test filtering by location."""
        filtered = filter_by_parameter(sample_forms, "location", Location.CHEST)
        assert len(filtered) == 2
        assert "SIGN1" in filtered
        assert "SIGN3" in filtered

    def test_filter_by_string(self, sample_forms):
        """Test filtering by string value."""
        filtered = filter_by_parameter(sample_forms, "location", "chest")
        assert len(filtered) == 2


class TestGroupByParameter:
    """Tests for group_by_parameter function."""

    @pytest.fixture
    def sample_forms(self):
        """Create sample forms for grouping."""
        return {
            "SIGN1": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHEST,
                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,
            ),
            "SIGN3": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,
                location=Location.CHEST,
                movement=Movement.LINEAR_AWAY,
                orientation_palm=PalmOrientation.PALM_OUT,
                orientation_fingers=FingerOrientation.FINGERS_FORWARD,
            ),
        }

    def test_group_by_handshape(self, sample_forms):
        """Test grouping by handshape."""
        groups = group_by_parameter(sample_forms, "handshape_dom")
        assert len(groups) == 2
        assert len(groups["A"]) == 2
        assert len(groups["B_flat"]) == 1

    def test_group_by_location(self, sample_forms):
        """Test grouping by location."""
        groups = group_by_parameter(sample_forms, "location")
        assert len(groups) == 2
        assert len(groups["chest"]) == 2
        assert len(groups["forehead"]) == 1


# =============================================================================
# Phonological Neighborhood Tests
# =============================================================================


class TestPhonologicalNeighborhood:
    """Tests for compute_phonological_neighborhood function."""

    @pytest.fixture
    def target_form(self):
        """Create target form."""
        return PhonologicalForm(
            handshape_dom=Handshape.A,
            location=Location.CHEST,
            movement=Movement.CONTACT,
            orientation_palm=PalmOrientation.PALM_IN,
            orientation_fingers=FingerOrientation.FINGERS_UP,
        )

    @pytest.fixture
    def candidate_forms(self):
        """Create candidate forms at various distances."""
        return {
            "IDENTICAL": PhonologicalForm(
                handshape_dom=Handshape.A,
                location=Location.CHEST,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "DIST_1": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,  # 1 difference
                location=Location.CHEST,
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "DIST_2": PhonologicalForm(
                handshape_dom=Handshape.B_FLAT,  # 1 difference
                location=Location.FOREHEAD,  # 2 differences
                movement=Movement.CONTACT,
                orientation_palm=PalmOrientation.PALM_IN,
                orientation_fingers=FingerOrientation.FINGERS_UP,
            ),
            "DIST_5": PhonologicalForm(
                handshape_dom=Handshape.FIVE,  # Different
                location=Location.NEUTRAL_SPACE,  # Different
                movement=Movement.CIRCLE_HORIZONTAL,  # Different
                orientation_palm=PalmOrientation.PALM_DOWN,  # Different
                orientation_fingers=FingerOrientation.FINGERS_FORWARD,  # Different
            ),
        }

    def test_neighborhood_dist_0(self, target_form, candidate_forms):
        """Test neighborhood at distance 0 (identical only)."""
        neighbors = compute_phonological_neighborhood(target_form, candidate_forms, max_distance=0)
        assert len(neighbors) == 1
        assert "IDENTICAL" in neighbors

    def test_neighborhood_dist_1(self, target_form, candidate_forms):
        """Test neighborhood at distance 1."""
        neighbors = compute_phonological_neighborhood(target_form, candidate_forms, max_distance=1)
        assert len(neighbors) == 2
        assert "IDENTICAL" in neighbors
        assert "DIST_1" in neighbors

    def test_neighborhood_dist_2(self, target_form, candidate_forms):
        """Test neighborhood at distance 2."""
        neighbors = compute_phonological_neighborhood(target_form, candidate_forms, max_distance=2)
        assert len(neighbors) == 3
        assert "DIST_5" not in neighbors

    def test_neighborhood_returns_distance(self, target_form, candidate_forms):
        """Test that neighborhood returns correct distances."""
        neighbors = compute_phonological_neighborhood(target_form, candidate_forms, max_distance=2)
        assert neighbors["IDENTICAL"][1] == 0
        assert neighbors["DIST_1"][1] == 1
        assert neighbors["DIST_2"][1] == 2


# =============================================================================
# Constants Tests
# =============================================================================


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

    def test_core_parameters(self):
        """Test CORE_PARAMETERS constant."""
        assert "handshape_dom" in CORE_PARAMETERS
        assert "location" in CORE_PARAMETERS
        assert "movement" in CORE_PARAMETERS
        assert "orientation_palm" in CORE_PARAMETERS
        assert "orientation_fingers" in CORE_PARAMETERS
        assert "handshape_nondom" not in CORE_PARAMETERS

    def test_extended_parameters(self):
        """Test EXTENDED_PARAMETERS constant."""
        # Extended should include core
        for param in CORE_PARAMETERS:
            assert param in EXTENDED_PARAMETERS
        # Plus additional
        assert "handshape_nondom" in EXTENDED_PARAMETERS
        assert "hand_arrangement" in EXTENDED_PARAMETERS
