"""Tests for spatial_validator module (Section 01) and node_ops integration (Section 02)."""

import pytest
from unittest.mock import AsyncMock, patch

from godotiq.tools.bridge.spatial_validator import (
    validate_move,
    validate_scale,
    _get_name_prefix,
    _extract_nodes,
)
from godotiq.tools.bridge.node_ops import node_ops


def _scene_data(nodes):
    return {"nodes": nodes}


# --- _get_name_prefix tests ---


class TestGetNamePrefix:
    def test_numbered_suffix(self):
        assert _get_name_prefix("Shelf_01") == "Shelf_"

    def test_same_prefix_different_number(self):
        assert _get_name_prefix("Shelf_02") == "Shelf_"

    def test_short_alpha_suffix(self):
        assert _get_name_prefix("PrinterSlot_P3") == "PrinterSlot_"

    def test_long_alpha_suffix_returns_empty(self):
        assert _get_name_prefix("Wall_East") == ""

    def test_no_underscore_returns_empty(self):
        assert _get_name_prefix("MainCamera") == ""


# --- _extract_nodes tests ---


class TestExtractNodes:
    def test_extracts_nodes_from_key(self):
        nodes = [{"name": "A", "type": "Node3D", "position": [0, 0, 0]}]
        assert _extract_nodes({"nodes": nodes}) == nodes

    def test_returns_empty_for_empty_dict(self):
        assert _extract_nodes({}) == []

    def test_handles_full_field_names(self):
        nodes = [{"name": "W", "type": "StaticBody3D", "position": [1, 2, 3], "scale": [1, 1, 1]}]
        result = _extract_nodes({"nodes": nodes})
        assert result[0]["name"] == "W"
        assert result[0]["scale"] == [1, 1, 1]


# --- validate_move tests ---


class TestValidateMove:
    def test_empty_scene_data_returns_ok(self):
        result = validate_move([1, 0, 0], "Foo", _scene_data([]))
        assert result["severity"] == "OK"
        assert result["valid"] is True

    def test_invalid_position_returns_blocked(self):
        result = validate_move([1], "Foo", _scene_data([]))
        assert result["severity"] == "BLOCKED"
        assert result["valid"] is False

    def test_far_from_walls_returns_ok(self):
        nodes = [{"name": "Wall_East", "type": "StaticBody3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([10, 0, 10], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "OK"
        assert result["valid"] is True

    def test_too_close_to_wall_blocked(self):
        nodes = [{"name": "Wall_East", "type": "StaticBody3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"
        assert result["valid"] is False
        assert "Wall_East" in result["message"]

    def test_warning_near_wall(self):
        nodes = [{"name": "Wall_East", "type": "StaticBody3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.3, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "WARNING"

    def test_self_node_excluded(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [5, 0, 0], "depth": 1},
        ]
        result = validate_move([5, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "OK"

    def test_overlap_same_depth_blocked(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "depth": 1},
            {"name": "Box", "type": "MeshInstance3D", "position": [1.0, 0, 0], "depth": 1},
        ]
        result = validate_move([1.01, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"
        assert "overlap" in result["message"].lower()

    def test_overlap_different_depth_ok(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "depth": 1},
            {"name": "Box", "type": "MeshInstance3D", "position": [1, 0, 0], "depth": 2},
        ]
        # Target node at depth 1, other node at depth 2 -> no overlap check
        result = validate_move([1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "OK"

    def test_sibling_outlier_warning(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [1, 0, 0], "depth": 1},
            {"name": "Shelf_02", "type": "MeshInstance3D", "position": [2, 0, 0], "depth": 1},
            {"name": "Shelf_03", "type": "MeshInstance3D", "position": [3, 0, 0], "depth": 1},
        ]
        result = validate_move([50, 0, 0], "Shelf_04", _scene_data(nodes))
        assert result["severity"] == "WARNING"
        assert "sibling" in result["message"].lower()

    def test_wall_y_diff_skipped(self):
        nodes = [{"name": "Wall_East", "type": "StaticBody3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 5, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "OK"

    def test_wall_identified_by_name(self):
        nodes = [{"name": "Wall_East", "type": "Node3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"

    def test_wall_identified_by_floor_name(self):
        nodes = [{"name": "Floor", "type": "Node3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"

    def test_wall_identified_by_ceiling_name(self):
        nodes = [{"name": "Ceiling", "type": "Node3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"

    def test_wall_identified_by_ground_name(self):
        nodes = [{"name": "Ground", "type": "Node3D", "position": [0, 0, 0], "depth": 1}]
        result = validate_move([0.1, 0, 0], "Shelf_01", _scene_data(nodes))
        assert result["severity"] == "BLOCKED"


# --- validate_scale tests ---


class TestValidateScale:
    def test_consistent_scale_ok(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_02", "type": "MeshInstance3D", "position": [1, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_03", "type": "MeshInstance3D", "position": [2, 0, 0], "scale": [1, 1, 1]},
        ]
        result = validate_scale([1.0, 1.0, 1.0], "Shelf_04", _scene_data(nodes))
        assert result["severity"] == "OK"

    def test_outlier_scale_warning(self):
        nodes = [
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_02", "type": "MeshInstance3D", "position": [1, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_03", "type": "MeshInstance3D", "position": [2, 0, 0], "scale": [1, 1, 1]},
        ]
        result = validate_scale([0.025, 0.025, 0.025], "Shelf_04", _scene_data(nodes))
        assert result["severity"] == "WARNING"

    def test_no_siblings_ok(self):
        nodes = [
            {"name": "Other", "type": "MeshInstance3D", "position": [0, 0, 0], "scale": [1, 1, 1]},
        ]
        result = validate_scale([5.0, 5.0, 5.0], "MainCamera", _scene_data(nodes))
        assert result["severity"] == "OK"

    def test_invalid_scale_blocked(self):
        result = validate_scale([1.0], "Foo", _scene_data([]))
        assert result["severity"] == "BLOCKED"
        assert result["valid"] is False


# --- node_ops integration tests (Section 02) ---


class TestNodeOpsValidation:
    """Tests for node_ops integration with spatial validation."""

    @pytest.fixture
    def mock_bridge(self):
        bridge = AsyncMock()
        bridge.request.return_value = {"results": [{"op": "move", "node": "Shelf_01", "status": "ok"}]}
        return bridge

    @pytest.fixture
    def wall_scene_data(self):
        return _scene_data([
            {"name": "Wall_East", "type": "StaticBody3D", "position": [5.1, 0, 0], "depth": 1},
        ])

    @pytest.fixture
    def warning_scene_data(self):
        return _scene_data([
            {"name": "Wall_East", "type": "StaticBody3D", "position": [0.3, 0, 0], "depth": 1},
        ])

    @pytest.fixture
    def sibling_scene_data(self):
        return _scene_data([
            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_02", "type": "MeshInstance3D", "position": [1, 0, 0], "scale": [1, 1, 1]},
            {"name": "Shelf_03", "type": "MeshInstance3D", "position": [2, 0, 0], "scale": [1, 1, 1]},
        ])

    async def test_no_validate_flag_bypasses_validation(self, mock_bridge):
        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0]}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=_scene_data([
                {"name": "Wall_East", "type": "StaticBody3D", "position": [5.1, 0, 0], "depth": 1},
            ]))
        assert "results" in result
        assert "validation" not in result
        mock_bridge.request.assert_called_once()

    async def test_no_scene_data_bypasses_validation(self, mock_bridge):
        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=None)
        assert "results" in result
        mock_bridge.request.assert_called_once()

    async def test_blocked_prevents_bridge_call(self, mock_bridge, wall_scene_data):
        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=wall_scene_data)
        assert result["status"] == "BLOCKED"
        assert "validation" in result
        assert result["operations_blocked"] == 1
        mock_bridge.request.assert_not_called()

    async def test_warning_allows_bridge_call(self, mock_bridge, warning_scene_data):
        ops = [{"op": "move", "node": "Shelf_01", "position": [0, 0, 0], "validate": True}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=warning_scene_data)
        assert "results" in result
        assert "validation" in result
        assert any(v["severity"] == "WARNING" for v in result["validation"])
        mock_bridge.request.assert_called_once()

    async def test_mixed_batch_atomic_blocking(self, mock_bridge, wall_scene_data):
        ops = [
            {"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True},
            {"op": "move", "node": "Shelf_02", "position": [10, 0, 10]},
        ]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=wall_scene_data)
        assert result["status"] == "BLOCKED"
        mock_bridge.request.assert_not_called()

    async def test_add_child_with_position_triggers_validation(self, mock_bridge, wall_scene_data):
        ops = [{"op": "add_child", "parent": "Root", "scene": "res://foo.tscn",
                "position": [5.0, 0, 0], "validate": True}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=wall_scene_data)
        assert result["status"] == "BLOCKED"

    async def test_scale_op_triggers_validate_scale(self, mock_bridge, sibling_scene_data):
        ops = [{"op": "scale", "node": "Shelf_04", "scale": [0.01, 0.01, 0.01], "validate": True}]
        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
            result = await node_ops(operations=ops, scene_data=sibling_scene_data)
        assert "validation" in result
        assert any(v["severity"] == "WARNING" for v in result["validation"])
        mock_bridge.request.assert_called_once()
