from __future__ import annotations

import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest

from services.tools.manage_probuilder import (
    manage_probuilder,
    ALL_ACTIONS,
    SHAPE_ACTIONS,
    MESH_ACTIONS,
    VERTEX_ACTIONS,
    SELECTION_ACTIONS,
    UV_MATERIAL_ACTIONS,
    QUERY_ACTIONS,
    SMOOTHING_ACTIONS,
    UTILITY_ACTIONS,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def mock_unity(monkeypatch):
    """Patch Unity transport layer and return captured call dict."""
    captured: dict[str, object] = {}

    async def fake_send(send_fn, unity_instance, tool_name, params):
        captured["unity_instance"] = unity_instance
        captured["tool_name"] = tool_name
        captured["params"] = params
        return {"success": True, "message": "ok"}

    monkeypatch.setattr(
        "services.tools.manage_probuilder.get_unity_instance_from_context",
        AsyncMock(return_value="unity-instance-1"),
    )
    monkeypatch.setattr(
        "services.tools.manage_probuilder.send_with_unity_instance",
        fake_send,
    )
    return captured


# ---------------------------------------------------------------------------
# Action list completeness
# ---------------------------------------------------------------------------

def test_all_actions_is_union_of_sub_lists():
    expected = set(
        ["ping"] + SHAPE_ACTIONS + MESH_ACTIONS + VERTEX_ACTIONS + SELECTION_ACTIONS
        + UV_MATERIAL_ACTIONS + QUERY_ACTIONS + SMOOTHING_ACTIONS + UTILITY_ACTIONS
    )
    assert set(ALL_ACTIONS) == expected


def test_no_duplicate_actions():
    assert len(ALL_ACTIONS) == len(set(ALL_ACTIONS))


# ---------------------------------------------------------------------------
# Invalid / missing action
# ---------------------------------------------------------------------------

def test_unknown_action_returns_error(mock_unity):
    result = asyncio.run(
        manage_probuilder(SimpleNamespace(), action="nonexistent_action")
    )
    assert result["success"] is False
    assert "Unknown action" in result["message"]
    assert "tool_name" not in mock_unity  # Should NOT call Unity


def test_empty_action_returns_error(mock_unity):
    result = asyncio.run(
        manage_probuilder(SimpleNamespace(), action="")
    )
    assert result["success"] is False


# ---------------------------------------------------------------------------
# Shape creation
# ---------------------------------------------------------------------------

def test_create_shape_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="create_shape",
            properties={"shapeType": "Cube", "size": [2, 2, 2]},
        )
    )
    assert result["success"] is True
    assert mock_unity["tool_name"] == "manage_probuilder"
    assert mock_unity["params"]["action"] == "create_shape"
    assert mock_unity["params"]["properties"]["shapeType"] == "Cube"


def test_create_shape_with_target(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="create_shape",
            target="MyParent",
            properties={"shapeType": "Torus"},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["target"] == "MyParent"


def test_create_poly_shape_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="create_poly_shape",
            properties={
                "points": [[0, 0, 0], [5, 0, 0], [5, 0, 5], [0, 0, 5]],
                "extrudeHeight": 3.0,
            },
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "create_poly_shape"
    assert mock_unity["params"]["properties"]["extrudeHeight"] == 3.0


# ---------------------------------------------------------------------------
# Mesh editing
# ---------------------------------------------------------------------------

def test_extrude_faces_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="extrude_faces",
            target="MyCube",
            properties={"faceIndices": [0, 1], "distance": 1.5},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "extrude_faces"
    assert mock_unity["params"]["target"] == "MyCube"
    assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1]
    assert mock_unity["params"]["properties"]["distance"] == 1.5


def test_bevel_edges_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="bevel_edges",
            target="MyCube",
            properties={"edgeIndices": [0, 2], "amount": 0.2},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "bevel_edges"
    assert mock_unity["params"]["properties"]["amount"] == 0.2


def test_delete_faces_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="delete_faces",
            target="MyCube",
            properties={"faceIndices": [3]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "delete_faces"


def test_subdivide_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="subdivide",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "subdivide"


def test_combine_meshes_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="combine_meshes",
            properties={"targets": ["Cube1", "Cube2"]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "combine_meshes"


# ---------------------------------------------------------------------------
# Vertex operations
# ---------------------------------------------------------------------------

def test_move_vertices_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="move_vertices",
            target="MyCube",
            properties={"vertexIndices": [0, 1, 2], "offset": [0, 1, 0]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "move_vertices"
    assert mock_unity["params"]["properties"]["offset"] == [0, 1, 0]


# ---------------------------------------------------------------------------
# UV & materials
# ---------------------------------------------------------------------------

def test_set_face_material_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="set_face_material",
            target="MyCube",
            properties={"faceIndices": [0], "materialPath": "Assets/Materials/Red.mat"},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "set_face_material"
    assert mock_unity["params"]["properties"]["materialPath"] == "Assets/Materials/Red.mat"


def test_set_face_uvs_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="set_face_uvs",
            target="MyCube",
            properties={"faceIndices": [0, 1], "scale": [2, 2], "rotation": 45},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "set_face_uvs"


# ---------------------------------------------------------------------------
# Query
# ---------------------------------------------------------------------------

def test_get_mesh_info_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="get_mesh_info",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "get_mesh_info"
    assert mock_unity["params"]["target"] == "MyCube"


def test_convert_to_probuilder_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="convert_to_probuilder",
            target="StandardMesh",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "convert_to_probuilder"


# ---------------------------------------------------------------------------
# Search method passthrough
# ---------------------------------------------------------------------------

def test_search_method_passed_through(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="get_mesh_info",
            target="-12345",
            search_method="by_id",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["searchMethod"] == "by_id"


# ---------------------------------------------------------------------------
# Ping
# ---------------------------------------------------------------------------

def test_ping_sends_to_unity(mock_unity):
    result = asyncio.run(
        manage_probuilder(SimpleNamespace(), action="ping")
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "ping"


# ---------------------------------------------------------------------------
# All actions are lowercase-normalized
# ---------------------------------------------------------------------------

def test_action_case_insensitive(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="Create_Shape",
            properties={"shapeType": "Cube"},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "create_shape"


# ---------------------------------------------------------------------------
# Non-dict result from Unity
# ---------------------------------------------------------------------------

def test_non_dict_result_wrapped(monkeypatch):
    async def fake_send(send_fn, unity_instance, tool_name, params):
        return "unexpected string result"

    monkeypatch.setattr(
        "services.tools.manage_probuilder.get_unity_instance_from_context",
        AsyncMock(return_value="unity-instance-1"),
    )
    monkeypatch.setattr(
        "services.tools.manage_probuilder.send_with_unity_instance",
        fake_send,
    )

    result = asyncio.run(
        manage_probuilder(SimpleNamespace(), action="ping")
    )
    assert result["success"] is False
    assert "unexpected string result" in result["message"]


# ---------------------------------------------------------------------------
# New action categories
# ---------------------------------------------------------------------------

def test_smoothing_actions_in_all():
    for action in SMOOTHING_ACTIONS:
        assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"


def test_utility_actions_in_all():
    for action in UTILITY_ACTIONS:
        assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"


# ---------------------------------------------------------------------------
# Smoothing actions
# ---------------------------------------------------------------------------

def test_auto_smooth_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="auto_smooth",
            target="MyCube",
            properties={"angleThreshold": 45},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "auto_smooth"
    assert mock_unity["params"]["target"] == "MyCube"
    assert mock_unity["params"]["properties"]["angleThreshold"] == 45


def test_set_smoothing_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="set_smoothing",
            target="MyCube",
            properties={"faceIndices": [0, 1, 2], "smoothingGroup": 1},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "set_smoothing"
    assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1, 2]
    assert mock_unity["params"]["properties"]["smoothingGroup"] == 1


# ---------------------------------------------------------------------------
# Mesh utility actions
# ---------------------------------------------------------------------------

def test_center_pivot_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="center_pivot",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "center_pivot"
    assert mock_unity["params"]["target"] == "MyCube"


def test_freeze_transform_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="freeze_transform",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "freeze_transform"


def test_validate_mesh_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="validate_mesh",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "validate_mesh"


def test_repair_mesh_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="repair_mesh",
            target="MyCube",
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "repair_mesh"


# ---------------------------------------------------------------------------
# get_mesh_info include parameter passthrough
# ---------------------------------------------------------------------------

def test_get_mesh_info_include_param_passthrough(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="get_mesh_info",
            target="MyCube",
            properties={"include": "faces"},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "get_mesh_info"
    assert mock_unity["params"]["properties"]["include"] == "faces"


# ---------------------------------------------------------------------------
# New actions: mesh editing additions
# ---------------------------------------------------------------------------

def test_duplicate_and_flip_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="duplicate_and_flip",
            target="MyCube",
            properties={"faceIndices": [0, 1]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "duplicate_and_flip"
    assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1]


def test_create_polygon_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="create_polygon",
            target="MyCube",
            properties={"vertexIndices": [0, 1, 2, 3], "unordered": True},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "create_polygon"
    assert mock_unity["params"]["properties"]["vertexIndices"] == [0, 1, 2, 3]


# ---------------------------------------------------------------------------
# New actions: vertex operations
# ---------------------------------------------------------------------------

def test_weld_vertices_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="weld_vertices",
            target="MyCube",
            properties={"vertexIndices": [0, 1, 2], "radius": 0.05},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "weld_vertices"
    assert mock_unity["params"]["properties"]["radius"] == 0.05


def test_insert_vertex_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="insert_vertex",
            target="MyCube",
            properties={"edge": {"a": 0, "b": 1}, "point": [0.5, 0, 0]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "insert_vertex"
    assert mock_unity["params"]["properties"]["edge"] == {"a": 0, "b": 1}


def test_append_vertices_to_edge_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="append_vertices_to_edge",
            target="MyCube",
            properties={"edgeIndices": [0, 1], "count": 3},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "append_vertices_to_edge"
    assert mock_unity["params"]["properties"]["count"] == 3


# ---------------------------------------------------------------------------
# New actions: selection
# ---------------------------------------------------------------------------

def test_select_faces_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="select_faces",
            target="MyCube",
            properties={"direction": "up", "tolerance": 0.9},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "select_faces"
    assert mock_unity["params"]["properties"]["direction"] == "up"
    assert mock_unity["params"]["properties"]["tolerance"] == 0.9


def test_selection_actions_in_all():
    for action in SELECTION_ACTIONS:
        assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"


# ---------------------------------------------------------------------------
# New actions: utility
# ---------------------------------------------------------------------------

def test_set_pivot_sends_correct_params(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="set_pivot",
            target="MyCube",
            properties={"position": [1.5, 0, 2.3]},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "set_pivot"
    assert mock_unity["params"]["properties"]["position"] == [1.5, 0, 2.3]


# ---------------------------------------------------------------------------
# Edge specification by vertex pairs
# ---------------------------------------------------------------------------

def test_bevel_edges_with_vertex_pairs(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="bevel_edges",
            target="MyCube",
            properties={"edges": [{"a": 0, "b": 1}, {"a": 2, "b": 3}], "amount": 0.15},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["action"] == "bevel_edges"
    assert mock_unity["params"]["properties"]["edges"] == [{"a": 0, "b": 1}, {"a": 2, "b": 3}]


def test_extrude_edges_with_vertex_pairs(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="extrude_edges",
            target="MyCube",
            properties={"edges": [{"a": 0, "b": 1}], "distance": 0.5},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["properties"]["edges"] == [{"a": 0, "b": 1}]


# ---------------------------------------------------------------------------
# Detach faces with deleteSourceFaces
# ---------------------------------------------------------------------------

def test_detach_faces_with_delete_source(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="detach_faces",
            target="MyCube",
            properties={"faceIndices": [0], "deleteSourceFaces": True},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["properties"]["deleteSourceFaces"] is True


# ---------------------------------------------------------------------------
# Bridge edges with allowNonManifold
# ---------------------------------------------------------------------------

def test_bridge_edges_with_allow_non_manifold(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="bridge_edges",
            target="MyCube",
            properties={
                "edgeA": {"a": 0, "b": 1},
                "edgeB": {"a": 2, "b": 3},
                "allowNonManifold": True,
            },
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["properties"]["allowNonManifold"] is True


# ---------------------------------------------------------------------------
# Merge vertices with collapseToFirst
# ---------------------------------------------------------------------------

def test_merge_vertices_with_collapse_to_first(mock_unity):
    result = asyncio.run(
        manage_probuilder(
            SimpleNamespace(),
            action="merge_vertices",
            target="MyCube",
            properties={"vertexIndices": [0, 1], "collapseToFirst": True},
        )
    )
    assert result["success"] is True
    assert mock_unity["params"]["properties"]["collapseToFirst"] is True
