# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
"""
Property-based tests for body_mesh utility functions.

Feature: vrm-body-mesh-separator
Feature: vrm-uv-edge-merger
"""
from hypothesis import given, settings
from hypothesis import strategies as st

from .utils import (
    find_body_mesh,
    find_face_mesh,
    generate_unique_name,
    is_skin_material,
    normalize_uv_pair,
    select_material_faces,
)


class TestIsSkinMaterial:
    """
    Property 2: 材质分类正确性
    *For any* 材质名称字符串，`is_skin_material`函数应该返回True当且仅当该名称包含"SKIN"子字符串（不区分大小写）。
    **Validates: Requirements 2.2, 2.3**
    """

    @given(st.text())
    @settings(max_examples=100)
    def test_skin_material_classification_property(self, material_name: str) -> None:
        """
        Feature: vrm-body-mesh-separator, Property 2: 材质分类正确性
        For any material name string, is_skin_material returns True
        if and only if the name contains "SKIN" (case-insensitive).
        """
        result = is_skin_material(material_name)
        expected = "SKIN" in material_name.upper()
        assert result == expected, (
            f"is_skin_material('{material_name}') returned {result}, "
            f"expected {expected}"
        )


class TestGenerateUniqueName:
    """
    Property 6: 唯一命名保证
    *For any* 基础名称和已存在名称集合，`generate_unique_name`返回的名称不应该在已存在名称集合中。
    **Validates: Requirements 4.2**
    """

    @given(
        st.text(min_size=1, max_size=50).filter(lambda x: x.strip()),
        st.sets(st.text(min_size=1, max_size=50), max_size=20),
    )
    @settings(max_examples=100)
    def test_unique_name_generation_property(
        self, base_name: str, existing_names: set[str]
    ) -> None:
        """
        Feature: vrm-body-mesh-separator, Property 6: 唯一命名保证
        For any base name and set of existing names, generate_unique_name
        returns a name that is not in the existing names set.
        """
        result = generate_unique_name(base_name, existing_names)
        assert result not in existing_names, (
            f"generate_unique_name('{base_name}', {existing_names}) "
            f"returned '{result}' which is in existing_names"
        )



class TestNormalizeUvPair:
    """
    Property 4: UV坐标标准化对称性
    *For any* pair of UV coordinates (uv1, uv2), normalizing (uv1, uv2) SHALL produce
    the same result as normalizing (uv2, uv1), ensuring order-independent matching.
    **Validates: Requirements 4.5**
    """

    @given(
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
    )
    @settings(max_examples=100)
    def test_uv_normalization_symmetry_property(
        self, uv1: tuple[float, float], uv2: tuple[float, float]
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 4: UV Coordinate Normalization
        For any pair of UV coordinates, normalize(uv1, uv2) == normalize(uv2, uv1).
        """
        result_forward = normalize_uv_pair(uv1, uv2)
        result_reverse = normalize_uv_pair(uv2, uv1)
        assert result_forward == result_reverse, (
            f"normalize_uv_pair({uv1}, {uv2}) = {result_forward}, "
            f"but normalize_uv_pair({uv2}, {uv1}) = {result_reverse}"
        )


class TestUvEdgeMatchingPrecision:
    """
    Property 5: UV边匹配精度
    *For any* edge with UV coordinates, the matching function SHALL correctly identify
    the edge if its UV coordinates (rounded to 6 decimal places) exist in the target UV set.
    **Validates: Requirements 4.2, 4.3, 4.4**
    """

    @given(
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
        st.floats(min_value=-1e-7, max_value=1e-7, allow_nan=False, allow_infinity=False),
    )
    @settings(max_examples=100)
    def test_uv_matching_precision_property(
        self,
        uv1: tuple[float, float],
        uv2: tuple[float, float],
        noise: float,
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 5: UV Edge Matching Precision
        For any UV coordinate pair, adding noise smaller than 6 decimal precision
        should still result in a match after normalization.
        """
        # Create the normalized reference UV pair
        normalized_ref = normalize_uv_pair(uv1, uv2)

        # Add small noise (within 6 decimal precision tolerance)
        uv1_noisy = (uv1[0] + noise, uv1[1] + noise)
        uv2_noisy = (uv2[0] + noise, uv2[1] + noise)

        # Normalize the noisy pair
        normalized_noisy = normalize_uv_pair(uv1_noisy, uv2_noisy)

        # Create target UV set with the reference
        target_uvs = {normalized_ref}

        # The noisy normalized pair should match the reference
        # because the noise is within 6 decimal precision
        matches = normalized_noisy in target_uvs

        # Calculate expected match based on rounding behavior
        # If noise is small enough, rounding should produce same result
        expected_match = normalized_ref == normalized_noisy

        assert matches == expected_match, (
            f"UV matching inconsistency: "
            f"normalized_ref={normalized_ref}, normalized_noisy={normalized_noisy}, "
            f"noise={noise}, matches={matches}, expected={expected_match}"
        )

    @given(
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
        st.tuples(
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
            st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
        ),
    )
    @settings(max_examples=100)
    def test_uv_matching_set_membership_property(
        self,
        uv1: tuple[float, float],
        uv2: tuple[float, float],
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 5: UV Edge Matching Precision
        For any UV coordinate pair, the normalized pair should be found in a target
        set containing that same normalized pair, regardless of original order.
        """
        # Normalize in both orders
        normalized_forward = normalize_uv_pair(uv1, uv2)
        normalized_reverse = normalize_uv_pair(uv2, uv1)

        # Create target set with forward normalization
        target_uvs = {normalized_forward}

        # Both should match (symmetry property ensures this)
        assert normalized_forward in target_uvs, (
            f"Forward normalized pair {normalized_forward} not found in target set"
        )
        assert normalized_reverse in target_uvs, (
            f"Reverse normalized pair {normalized_reverse} not found in target set "
            f"(forward was {normalized_forward})"
        )


class MockPolygon:
    """Mock polygon for testing material face selection."""

    def __init__(self, material_index: int) -> None:
        self.material_index = material_index


class MockMesh:
    """Mock mesh data for testing material face selection."""

    def __init__(self, polygons: list[MockPolygon]) -> None:
        self.polygons = polygons


class MockMeshObject:
    """Mock mesh object for testing material face selection."""

    def __init__(self, mesh_data: MockMesh) -> None:
        self.type = "MESH"
        self.data = mesh_data


class TestMaterialFaceSelectionCompleteness:
    """
    Property 6: Material Face Selection Completeness
    *For any* mesh and material index, the face selection function SHALL select
    exactly all faces that have the specified material index, and no others.
    **Validates: Requirements 3.2**
    """

    @given(
        st.lists(
            st.integers(min_value=0, max_value=5),
            min_size=1,
            max_size=50,
        ),
        st.integers(min_value=0, max_value=5),
    )
    @settings(max_examples=100)
    def test_material_face_selection_completeness_property(
        self,
        material_indices: list[int],
        target_material_index: int,
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
        For any mesh with faces having various material indices, select_material_faces
        returns exactly the set of face indices that have the specified material index.
        """
        # Create mock polygons with the given material indices
        polygons = [MockPolygon(idx) for idx in material_indices]
        mock_mesh = MockMesh(polygons)
        mock_obj = MockMeshObject(mock_mesh)

        # Call the function under test
        result = select_material_faces(mock_obj, target_material_index)

        # Calculate expected result: indices of faces with the target material
        expected = {
            i for i, mat_idx in enumerate(material_indices)
            if mat_idx == target_material_index
        }

        # Verify completeness: result should contain exactly the expected faces
        assert result == expected, (
            f"select_material_faces returned {result}, expected {expected}. "
            f"Material indices: {material_indices}, target: {target_material_index}"
        )

    @given(
        st.lists(
            st.integers(min_value=0, max_value=5),
            min_size=0,
            max_size=50,
        ),
    )
    @settings(max_examples=100)
    def test_material_face_selection_no_false_positives_property(
        self,
        material_indices: list[int],
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
        For any mesh, selecting faces for a material index that doesn't exist
        should return an empty set.
        """
        # Create mock polygons
        polygons = [MockPolygon(idx) for idx in material_indices]
        mock_mesh = MockMesh(polygons)
        mock_obj = MockMeshObject(mock_mesh)

        # Use a material index that definitely doesn't exist
        non_existent_index = max(material_indices, default=-1) + 10

        # Call the function under test
        result = select_material_faces(mock_obj, non_existent_index)

        # Should return empty set for non-existent material
        assert result == set(), (
            f"select_material_faces returned {result} for non-existent material "
            f"index {non_existent_index}, expected empty set"
        )

    def test_material_face_selection_invalid_object(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
        Selecting faces from None or non-mesh object should return empty set.
        """
        # Test with None
        result_none = select_material_faces(None, 0)
        assert result_none == set(), "Expected empty set for None object"

        # Test with non-mesh object (mock with wrong type)
        class NonMeshObject:
            type = "ARMATURE"
            data = None

        result_non_mesh = select_material_faces(NonMeshObject(), 0)
        assert result_non_mesh == set(), "Expected empty set for non-mesh object"


class MockChild:
    """Mock child object for testing VRM model detection."""

    def __init__(self, name: str, obj_type: str = "MESH") -> None:
        self.name = name
        self.type = obj_type


class MockArmature:
    """Mock armature object for testing VRM model detection."""

    def __init__(self, children: list[MockChild]) -> None:
        self.type = "ARMATURE"
        self.children = children


class TestVRMModelDetectionCorrectness:
    """
    Property 1: VRM Model Detection Correctness
    *For any* Blender object hierarchy, the VRM detection function SHALL return True
    if and only if the hierarchy contains an Armature with both "Body" and "Face"
    mesh children, regardless of the current Blender mode.
    **Validates: Requirements 1.1, 1.2, 1.3, 1.4**
    """

    @given(
        st.lists(
            st.tuples(
                st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
                st.sampled_from(["MESH", "ARMATURE", "EMPTY", "LIGHT", "CAMERA"]),
            ),
            min_size=0,
            max_size=10,
        ),
    )
    @settings(max_examples=100)
    def test_vrm_detection_requires_both_body_and_face(
        self,
        children_specs: list[tuple[str, str]],
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        For any set of child objects, VRM detection returns True if and only if
        both "Body" and "Face" mesh children exist.
        """
        # Create mock children
        children = [MockChild(name, obj_type) for name, obj_type in children_specs]
        mock_armature = MockArmature(children)

        # Check if Body and Face meshes exist
        has_body_mesh = any(
            c.name == "Body" and c.type == "MESH" for c in children
        )
        has_face_mesh = any(
            c.name == "Face" and c.type == "MESH" for c in children
        )

        # Test find_body_mesh
        body_result = find_body_mesh(mock_armature)
        if has_body_mesh:
            assert body_result is not None, (
                f"find_body_mesh should find Body mesh in {children_specs}"
            )
            assert body_result.name == "Body", (
                f"find_body_mesh returned wrong object: {body_result.name}"
            )
        else:
            assert body_result is None, (
                f"find_body_mesh should return None for {children_specs}"
            )

        # Test find_face_mesh
        face_result = find_face_mesh(mock_armature)
        if has_face_mesh:
            assert face_result is not None, (
                f"find_face_mesh should find Face mesh in {children_specs}"
            )
            assert face_result.name == "Face", (
                f"find_face_mesh returned wrong object: {face_result.name}"
            )
        else:
            assert face_result is None, (
                f"find_face_mesh should return None for {children_specs}"
            )

        # VRM detection should be True only if both exist
        expected_vrm_valid = has_body_mesh and has_face_mesh
        actual_vrm_valid = body_result is not None and face_result is not None
        assert actual_vrm_valid == expected_vrm_valid, (
            f"VRM detection mismatch: expected {expected_vrm_valid}, "
            f"got {actual_vrm_valid} for {children_specs}"
        )

    def test_vrm_detection_with_valid_vrm_structure(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        A valid VRM structure with both Body and Face meshes should be detected.
        """
        children = [
            MockChild("Body", "MESH"),
            MockChild("Face", "MESH"),
            MockChild("Hair", "MESH"),
        ]
        mock_armature = MockArmature(children)

        body = find_body_mesh(mock_armature)
        face = find_face_mesh(mock_armature)

        assert body is not None, "Body mesh should be found"
        assert face is not None, "Face mesh should be found"
        assert body.name == "Body"
        assert face.name == "Face"

    def test_vrm_detection_with_missing_body(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        Missing Body mesh should result in invalid VRM detection.
        """
        children = [
            MockChild("Face", "MESH"),
            MockChild("Hair", "MESH"),
        ]
        mock_armature = MockArmature(children)

        body = find_body_mesh(mock_armature)
        face = find_face_mesh(mock_armature)

        assert body is None, "Body mesh should not be found"
        assert face is not None, "Face mesh should be found"

    def test_vrm_detection_with_missing_face(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        Missing Face mesh should result in invalid VRM detection.
        """
        children = [
            MockChild("Body", "MESH"),
            MockChild("Hair", "MESH"),
        ]
        mock_armature = MockArmature(children)

        body = find_body_mesh(mock_armature)
        face = find_face_mesh(mock_armature)

        assert body is not None, "Body mesh should be found"
        assert face is None, "Face mesh should not be found"

    def test_vrm_detection_with_non_mesh_body(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        Body object that is not a MESH should not be detected as Body mesh.
        """
        children = [
            MockChild("Body", "EMPTY"),  # Not a mesh
            MockChild("Face", "MESH"),
        ]
        mock_armature = MockArmature(children)

        body = find_body_mesh(mock_armature)
        face = find_face_mesh(mock_armature)

        assert body is None, "Non-mesh Body should not be found"
        assert face is not None, "Face mesh should be found"

    def test_vrm_detection_with_none_armature(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        None armature should return None for both meshes.
        """
        body = find_body_mesh(None)
        face = find_face_mesh(None)

        assert body is None, "Body should be None for None armature"
        assert face is None, "Face should be None for None armature"
