# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
"""
Standalone property-based tests for UV edge matching functionality.

Feature: vrm-uv-edge-merger
Property 5: UV Edge Matching Precision

These tests can run without Blender dependencies.
"""
from dataclasses import dataclass
from typing import Optional

from hypothesis import given, settings
from hypothesis import strategies as st


# UV坐标精度（小数位数）- copied from utils.py
UV_PRECISION = 6


def normalize_uv_pair(
    uv1: tuple[float, float], uv2: tuple[float, float]
) -> tuple[tuple[float, float], tuple[float, float]]:
    """
    标准化UV坐标对（较小的在前）

    通过将较小的UV坐标放在前面，确保相同的边无论顶点顺序如何都能匹配。
    使用6位小数精度进行比较和存储。

    Args:
        uv1: 第一个UV坐标 (u, v)
        uv2: 第二个UV坐标 (u, v)

    Returns:
        标准化后的UV对 ((u1,v1), (u2,v2))，较小的坐标在前
    """
    # 四舍五入到指定精度
    uv1_rounded = (round(uv1[0], UV_PRECISION), round(uv1[1], UV_PRECISION))
    uv2_rounded = (round(uv2[0], UV_PRECISION), round(uv2[1], UV_PRECISION))

    # 比较并排序，较小的在前
    if uv1_rounded <= uv2_rounded:
        return (uv1_rounded, uv2_rounded)
    return (uv2_rounded, uv1_rounded)


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})"
        )

    @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_precision_rounding_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 result should have coordinates
        rounded to exactly 6 decimal places.
        """
        normalized = normalize_uv_pair(uv1, uv2)

        # Check that all coordinates are rounded to 6 decimal places
        for uv in normalized:
            for coord in uv:
                # Verify rounding by checking round(coord, 6) == coord
                assert round(coord, UV_PRECISION) == coord, (
                    f"Coordinate {coord} is not rounded to {UV_PRECISION} decimal places"
                )


def select_material_faces_impl(
    polygons_material_indices: list[int],
    material_index: int,
) -> set[int]:
    """
    获取指定材质索引的所有面的索引集合

    这是一个纯函数实现，用于测试材质面选择的完整性。

    Args:
        polygons_material_indices: 每个面的材质索引列表
        material_index: 目标材质索引

    Returns:
        使用该材质的面索引集合
    """
    result: set[int] = set()
    for i, mat_idx in enumerate(polygons_material_indices):
        if mat_idx == material_index:
            result.add(i)
    return result


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.
        """
        # Call the function under test
        result = select_material_faces_impl(material_indices, 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.
        """
        # 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_impl(material_indices, 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"
        )

    @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_no_duplicates_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, the selected face indices should be unique (no duplicates).
        """
        result = select_material_faces_impl(material_indices, target_material_index)

        # Since result is a set, it inherently has no duplicates
        # But we verify the count matches expected
        expected_count = sum(
            1 for mat_idx in material_indices if mat_idx == target_material_index
        )

        assert len(result) == expected_count, (
            f"Expected {expected_count} faces, got {len(result)}. "
            f"Material indices: {material_indices}, target: {target_material_index}"
        )

    @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_valid_indices_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, all returned face indices should be valid (within bounds).
        """
        result = select_material_faces_impl(material_indices, target_material_index)

        # All indices should be valid
        for face_idx in result:
            assert 0 <= face_idx < len(material_indices), (
                f"Invalid face index {face_idx} for mesh with "
                f"{len(material_indices)} faces"
            )


def find_body_mesh_impl(children: list[tuple[str, str]]) -> bool:
    """
    检查子对象列表中是否存在名为"Body"的MESH对象

    Args:
        children: 子对象列表，每个元素为 (name, type) 元组

    Returns:
        如果存在名为"Body"的MESH对象返回True，否则返回False
    """
    for name, obj_type in children:
        if name == "Body" and obj_type == "MESH":
            return True
    return False


def find_face_mesh_impl(children: list[tuple[str, str]]) -> bool:
    """
    检查子对象列表中是否存在名为"Face"的MESH对象

    Args:
        children: 子对象列表，每个元素为 (name, type) 元组

    Returns:
        如果存在名为"Face"的MESH对象返回True，否则返回False
    """
    for name, obj_type in children:
        if name == "Face" and obj_type == "MESH":
            return True
    return False


def is_valid_vrm_structure(children: list[tuple[str, str]]) -> bool:
    """
    检查子对象列表是否构成有效的VRM结构

    Args:
        children: 子对象列表，每个元素为 (name, type) 元组

    Returns:
        如果同时存在Body和Face MESH对象返回True，否则返回False
    """
    return find_body_mesh_impl(children) and find_face_mesh_impl(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.
        """
        # Check if Body and Face meshes exist
        has_body_mesh = any(
            name == "Body" and obj_type == "MESH"
            for name, obj_type in children_specs
        )
        has_face_mesh = any(
            name == "Face" and obj_type == "MESH"
            for name, obj_type in children_specs
        )

        # Test find_body_mesh_impl
        body_found = find_body_mesh_impl(children_specs)
        assert body_found == has_body_mesh, (
            f"find_body_mesh_impl mismatch: expected {has_body_mesh}, "
            f"got {body_found} for {children_specs}"
        )

        # Test find_face_mesh_impl
        face_found = find_face_mesh_impl(children_specs)
        assert face_found == has_face_mesh, (
            f"find_face_mesh_impl mismatch: expected {has_face_mesh}, "
            f"got {face_found} 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 = is_valid_vrm_structure(children_specs)
        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 = [
            ("Body", "MESH"),
            ("Face", "MESH"),
            ("Hair", "MESH"),
        ]

        assert find_body_mesh_impl(children), "Body mesh should be found"
        assert find_face_mesh_impl(children), "Face mesh should be found"
        assert is_valid_vrm_structure(children), "VRM structure should be valid"

    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 = [
            ("Face", "MESH"),
            ("Hair", "MESH"),
        ]

        assert not find_body_mesh_impl(children), "Body mesh should not be found"
        assert find_face_mesh_impl(children), "Face mesh should be found"
        assert not is_valid_vrm_structure(children), "VRM structure should be invalid"

    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 = [
            ("Body", "MESH"),
            ("Hair", "MESH"),
        ]

        assert find_body_mesh_impl(children), "Body mesh should be found"
        assert not find_face_mesh_impl(children), "Face mesh should not be found"
        assert not is_valid_vrm_structure(children), "VRM structure should be invalid"

    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 = [
            ("Body", "EMPTY"),  # Not a mesh
            ("Face", "MESH"),
        ]

        assert not find_body_mesh_impl(children), "Non-mesh Body should not be found"
        assert find_face_mesh_impl(children), "Face mesh should be found"
        assert not is_valid_vrm_structure(children), "VRM structure should be invalid"

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

        assert find_body_mesh_impl(children), "Body mesh should be found"
        assert not find_face_mesh_impl(children), "Non-mesh Face should not be found"
        assert not is_valid_vrm_structure(children), "VRM structure should be invalid"

    def test_vrm_detection_with_empty_children(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
        Empty children list should result in invalid VRM detection.
        """
        children: list[tuple[str, str]] = []

        assert not find_body_mesh_impl(children), "Body should not be found in empty list"
        assert not find_face_mesh_impl(children), "Face should not be found in empty list"
        assert not is_valid_vrm_structure(children), "VRM structure should be invalid"


@dataclass
class MockOperationState:
    """Mock operation state for testing state restoration."""
    original_mode: str
    original_active_object: Optional[str]  # Object name
    original_selected_objects: list[str]  # Object names
    processed_materials: list[str]


def save_state_impl(
    current_mode: str,
    active_object_name: Optional[str],
    selected_object_names: list[str],
) -> MockOperationState:
    """
    保存当前状态的纯函数实现

    Args:
        current_mode: 当前Blender模式
        active_object_name: 当前活动对象名称
        selected_object_names: 当前选中对象名称列表

    Returns:
        保存的状态对象
    """
    return MockOperationState(
        original_mode=current_mode,
        original_active_object=active_object_name,
        original_selected_objects=list(selected_object_names),
        processed_materials=[],
    )


def restore_state_impl(
    state: MockOperationState,
    available_objects: set[str],
) -> tuple[str, Optional[str], list[str]]:
    """
    恢复状态的纯函数实现

    Args:
        state: 保存的状态
        available_objects: 当前可用的对象名称集合

    Returns:
        (恢复的模式, 恢复的活动对象, 恢复的选中对象列表)
    """
    # 恢复选中对象（只恢复仍然存在的对象）
    restored_selected = [
        name for name in state.original_selected_objects
        if name in available_objects
    ]

    # 恢复活动对象（只有在对象仍然存在时才恢复）
    restored_active = None
    if state.original_active_object and state.original_active_object in available_objects:
        restored_active = state.original_active_object

    # 恢复模式
    restored_mode = state.original_mode

    return (restored_mode, restored_active, restored_selected)


class TestStateRestorationOnError:
    """
    Property 7: State Restoration on Error
    *For any* error during processing, the system SHALL restore the original Blender
    state (mode, active object, selection) before reporting the error.
    **Validates: Requirements 7.4**
    """

    @given(
        st.sampled_from(["OBJECT", "EDIT", "SCULPT", "VERTEX_PAINT", "WEIGHT_PAINT"]),
        st.text(min_size=1, max_size=20).filter(lambda x: x.strip()) | st.none(),
        st.lists(
            st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
            min_size=0,
            max_size=5,
        ),
    )
    @settings(max_examples=100)
    def test_state_save_and_restore_property(
        self,
        original_mode: str,
        active_object: Optional[str],
        selected_objects: list[str],
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
        For any initial state, saving and then restoring should return to the
        original state (assuming all objects still exist).
        """
        # Save state
        state = save_state_impl(original_mode, active_object, selected_objects)

        # Verify state was saved correctly
        assert state.original_mode == original_mode
        assert state.original_active_object == active_object
        assert state.original_selected_objects == selected_objects

        # Create available objects set (all objects exist)
        available_objects = set(selected_objects)
        if active_object:
            available_objects.add(active_object)

        # Restore state
        restored_mode, restored_active, restored_selected = restore_state_impl(
            state, available_objects
        )

        # Verify restoration
        assert restored_mode == original_mode, (
            f"Mode not restored: expected {original_mode}, got {restored_mode}"
        )
        assert restored_active == active_object, (
            f"Active object not restored: expected {active_object}, got {restored_active}"
        )
        assert set(restored_selected) == set(selected_objects), (
            f"Selected objects not restored: expected {selected_objects}, "
            f"got {restored_selected}"
        )

    @given(
        st.sampled_from(["OBJECT", "EDIT", "SCULPT"]),
        st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
        st.lists(
            st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
            min_size=1,
            max_size=5,
        ),
        st.sets(
            st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
            min_size=0,
            max_size=3,
        ),
    )
    @settings(max_examples=100)
    def test_state_restore_with_deleted_objects_property(
        self,
        original_mode: str,
        active_object: str,
        selected_objects: list[str],
        deleted_objects: set[str],
    ) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
        When some objects have been deleted, restoration should only restore
        objects that still exist.
        """
        # Save state
        state = save_state_impl(original_mode, active_object, selected_objects)

        # Create available objects (some may have been deleted)
        all_objects = set(selected_objects)
        all_objects.add(active_object)
        available_objects = all_objects - deleted_objects

        # Restore state
        restored_mode, restored_active, restored_selected = restore_state_impl(
            state, available_objects
        )

        # Mode should always be restored
        assert restored_mode == original_mode

        # Active object should only be restored if it still exists
        if active_object in deleted_objects:
            assert restored_active is None, (
                f"Deleted active object should not be restored: {active_object}"
            )
        else:
            assert restored_active == active_object

        # Selected objects should only include those that still exist
        for obj_name in restored_selected:
            assert obj_name in available_objects, (
                f"Restored selected object {obj_name} should be in available objects"
            )
            assert obj_name not in deleted_objects, (
                f"Deleted object {obj_name} should not be in restored selection"
            )

    def test_state_restore_with_empty_state(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
        Restoring an empty state should work without errors.
        """
        state = save_state_impl("OBJECT", None, [])
        available_objects: set[str] = set()

        restored_mode, restored_active, restored_selected = restore_state_impl(
            state, available_objects
        )

        assert restored_mode == "OBJECT"
        assert restored_active is None
        assert restored_selected == []

    def test_state_processed_materials_tracking(self) -> None:
        """
        Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
        The state should track processed materials for error reporting.
        """
        state = save_state_impl("OBJECT", "Armature", ["Body", "Face"])

        # Simulate processing materials
        state.processed_materials.append("Material1")
        state.processed_materials.append("Material2")

        assert len(state.processed_materials) == 2
        assert "Material1" in state.processed_materials
        assert "Material2" in state.processed_materials
