"""Tests for the scene resolver module."""

from __future__ import annotations

from pathlib import Path

import pytest

from godotiq.parsers.tscn_parser import (
    ExtResource,
    SceneHeader,
    TscnNode,
    TscnScene,
    parse_tscn,
)
from godotiq.parsers.scene_resolver import (
    ResolvedScene,
    WorldNode,
    _clone_node,
    _resolve_res_path,
    calculate_world_transforms,
    find_nodes_by_group,
    find_nodes_by_type,
    find_nodes_in_radius,
    resolve_scene,
)


# ---------------------------------------------------------------------------
# Unit Tests -- res:// Path Resolution
# ---------------------------------------------------------------------------


class TestResolveResPath:
    def test_resolve_res_path_basic(self, tmp_path: Path) -> None:
        """res://scenes/main.tscn + project_root -> project_root/scenes/main.tscn"""
        result = _resolve_res_path("res://scenes/main.tscn", tmp_path)
        assert result == tmp_path / "scenes" / "main.tscn"

    def test_resolve_res_path_nested(self, tmp_path: Path) -> None:
        """res://scenes/equipment/fdm_printer.tscn -> correct nested path"""
        result = _resolve_res_path("res://scenes/equipment/fdm_printer.tscn", tmp_path)
        assert result == tmp_path / "scenes" / "equipment" / "fdm_printer.tscn"


# ---------------------------------------------------------------------------
# Unit Tests -- Node Cloning
# ---------------------------------------------------------------------------


class TestCloneNode:
    def test_clone_node_independent(self) -> None:
        """Cloned node modifications don't affect original."""
        original = TscnNode(
            name="test",
            type="Node3D",
            properties={"key": "value"},
            groups=["group1"],
        )
        clone = _clone_node(original)

        clone.properties["new_key"] = "new_value"
        clone.groups.append("group2")

        assert "new_key" not in original.properties
        assert "group2" not in original.groups

    def test_clone_node_deep(self) -> None:
        """Clone a node with 3 levels of children. Verify independence."""
        grandchild = TscnNode(name="grandchild", type="Node3D")
        child = TscnNode(name="child", type="Node3D", children=[grandchild])
        root = TscnNode(name="root", type="Node3D", children=[child])

        clone = _clone_node(root)
        clone.children[0].children[0].name = "modified"

        assert root.children[0].children[0].name == "grandchild"


# ---------------------------------------------------------------------------
# Integration Tests -- Instance Resolution
# ---------------------------------------------------------------------------


class TestResolveScene:
    def test_resolve_no_instances(self, sample_project_root: Path) -> None:
        """fdm_printer.tscn has no .tscn instances. Resolved = original structure."""
        parsed = parse_tscn(
            str(sample_project_root / "scenes" / "equipment" / "fdm_printer.tscn")
        )
        resolved = resolve_scene(parsed, sample_project_root)

        assert len(resolved.unresolved) == 0
        # 6 nodes: FDMPrinter + PrinterModel + NozzleMesh + WorkPoint + StatusLight + FilamentSpool
        assert resolved.total_nodes == 6

    def test_resolve_with_instance(self, sample_project_root: Path) -> None:
        """main.tscn instances fdm_printer.tscn. After resolution, children expanded."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)

        # main: Main, Equipment, FDMPrinter_1 (3 original)
        # + children from fdm_printer: PrinterModel, NozzleMesh, WorkPoint, StatusLight, FilamentSpool (5 added)
        assert resolved.total_nodes == 8
        assert len(resolved.instance_sources) > 0
        assert len(resolved.unresolved) == 0

    def test_property_override_transform(self, sample_project_root: Path) -> None:
        """Instance node's transform is overridden to (-36, 0, -2)."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)

        # Find the FDMPrinter_1 node
        fdm_node = None
        for node in _flatten_tree(resolved.root):
            if node.name == "FDMPrinter_1":
                fdm_node = node
                break

        assert fdm_node is not None
        assert fdm_node.position == pytest.approx((-36.0, 0.0, -2.0))

    def test_instance_type_copied(self, sample_project_root: Path) -> None:
        """Instance node with empty type gets type from instanced root."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)

        fdm_node = None
        for node in _flatten_tree(resolved.root):
            if node.name == "FDMPrinter_1":
                fdm_node = node
                break

        assert fdm_node is not None
        assert fdm_node.type == "Node3D"

    def test_unresolved_instances(self, tmp_path: Path) -> None:
        """Instance pointing to non-existent file -> listed in unresolved."""
        scene = TscnScene(
            header=SceneHeader(),
            ext_resources={
                "1": ExtResource(id="1", type="PackedScene", path="res://nonexistent.tscn")
            },
            sub_resources={},
            root=TscnNode(
                name="Root",
                type="Node3D",
                children=[
                    TscnNode(name="Missing", type="", instance="1"),
                ],
            ),
            nodes=[],
            connections=[],
        )
        resolved = resolve_scene(scene, tmp_path)

        assert "res://nonexistent.tscn" in resolved.unresolved
        # Node still in tree
        assert resolved.root.children[0].name == "Missing"

    def test_max_depth_limit(self, sample_project_root: Path) -> None:
        """With max_depth=0, no instances are expanded."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root, max_depth=0)

        # Should still have original 3 nodes, FDMPrinter_1 not expanded
        assert resolved.total_nodes == 3

    def test_non_tscn_instance_noted(self, sample_project_root: Path) -> None:
        """Instance pointing to .glb is recorded but not expanded."""
        parsed = parse_tscn(
            str(sample_project_root / "scenes" / "equipment" / "fdm_printer.tscn")
        )
        resolved = resolve_scene(parsed, sample_project_root)

        # PrinterModel instances a .glb -- should be in instance_sources
        glb_found = any(
            v.endswith(".glb") for v in resolved.instance_sources.values()
        )
        assert glb_found


# ---------------------------------------------------------------------------
# World Transform Tests
# ---------------------------------------------------------------------------


def _make_transform_dict(raw: tuple) -> dict:
    """Create a transform property dict with raw key."""
    from godotiq.parsers.value_parser import decompose_transform3d
    return decompose_transform3d(raw)


def _make_resolved(root: TscnNode, instance_sources: dict | None = None) -> ResolvedScene:
    """Create a minimal ResolvedScene for testing."""
    def _count(n: TscnNode) -> int:
        return 1 + sum(_count(c) for c in n.children)

    return ResolvedScene(
        source_path="",
        root=root,
        total_nodes=_count(root),
        instance_sources=instance_sources or {},
        unresolved=[],
    )


class TestWorldTransformMath:
    def test_compose_identity(self) -> None:
        """Identity composed with identity yields identity."""
        root = TscnNode(name="Root", type="Node3D", children=[
            TscnNode(name="Child", type="Node3D"),
        ])
        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        for wn in world_nodes:
            assert wn.world_position == pytest.approx((0, 0, 0))
            assert wn.world_rotation == pytest.approx((0, 0, 0))
            assert wn.world_scale == pytest.approx((1, 1, 1))

    def test_compose_translation(self) -> None:
        """Two pure translations compose by adding origins."""
        root = TscnNode(
            name="Root", type="Node3D",
            properties={"transform": _make_transform_dict(
                (1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, 0)
            )},
            position=(10.0, 0.0, 0.0),
            children=[
                TscnNode(
                    name="Child", type="Node3D",
                    properties={"transform": _make_transform_dict(
                        (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5)
                    )},
                    position=(0.0, 0.0, 5.0),
                ),
            ],
        )
        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        child_wn = world_nodes[1]
        assert child_wn.world_position == pytest.approx((10, 0, 5))

    def test_compose_rotation_translation(self) -> None:
        """Parent rotated 90 deg Y, child at local (0,0,1) -> world (1,0,0)."""
        # 90 deg Y rotation: col0=(0,0,-1), col1=(0,1,0), col2=(1,0,0)
        root = TscnNode(
            name="Root", type="Node3D",
            properties={"transform": _make_transform_dict(
                (0, 0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0)
            )},
        )
        child = TscnNode(
            name="Child", type="Node3D",
            properties={"transform": _make_transform_dict(
                (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1)
            )},
            position=(0.0, 0.0, 1.0),
        )
        root.children = [child]
        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        child_wn = world_nodes[1]
        assert child_wn.world_position == pytest.approx((1, 0, 0), abs=1e-6)

    def test_world_position_deep_nesting(self) -> None:
        """4 levels of pure translations: positions sum up."""
        def make_node(name, tx, ty, tz):
            return TscnNode(
                name=name, type="Node3D",
                properties={"transform": _make_transform_dict(
                    (1, 0, 0, 0, 1, 0, 0, 0, 1, tx, ty, tz)
                )},
                position=(tx, ty, tz),
            )

        d3 = make_node("D3", 0, 0, 1)
        d2 = make_node("D2", 0, 1, 0)
        d2.children = [d3]
        d1 = make_node("D1", 1, 0, 0)
        d1.children = [d2]
        root = make_node("Root", 0, 0, 0)
        root.children = [d1]

        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        deepest = world_nodes[3]  # D3
        assert deepest.world_position == pytest.approx((1, 1, 1))

    def test_world_position_with_scale(self) -> None:
        """Parent scaled 2x, child at local (1,0,0) -> world (2,0,0)."""
        root = TscnNode(
            name="Root", type="Node3D",
            properties={"transform": _make_transform_dict(
                (2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0)
            )},
        )
        child = TscnNode(
            name="Child", type="Node3D",
            properties={"transform": _make_transform_dict(
                (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0)
            )},
            position=(1.0, 0.0, 0.0),
        )
        root.children = [child]
        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        child_wn = world_nodes[1]
        assert child_wn.world_position == pytest.approx((2, 0, 0))

    def test_world_position_resolved_instance(self, sample_project_root: Path) -> None:
        """Resolved main.tscn: WorkPoint world pos = FDMPrinter pos + local offset."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)
        world_nodes = calculate_world_transforms(resolved)

        # Find WorkPoint
        wp = None
        for wn in world_nodes:
            if wn.node.name == "WorkPoint":
                wp = wn
                break

        assert wp is not None
        # FDMPrinter_1 at (-36, 0, -2), WorkPoint local at (0, 0, 1.2)
        # world = (-36, 0, -2) + (0, 0, 1.2) = (-36, 0, -0.8)
        assert wp.world_position == pytest.approx((-36.0, 0.0, -0.8))

    def test_is_instance_root_populated(self, sample_project_root: Path) -> None:
        """Instance root has is_instance_root=True and correct source."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)
        world_nodes = calculate_world_transforms(resolved)

        fdm_wn = None
        for wn in world_nodes:
            if wn.node.name == "FDMPrinter_1":
                fdm_wn = wn
                break

        assert fdm_wn is not None
        assert fdm_wn.is_instance_root is True
        assert fdm_wn.instance_source is not None
        assert "fdm_printer.tscn" in fdm_wn.instance_source


# ---------------------------------------------------------------------------
# Query Function Tests
# ---------------------------------------------------------------------------


class TestQueryFunctions:
    def test_find_nodes_by_type(self, sample_project_root: Path) -> None:
        """Find Node3D nodes in resolved scene."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)
        world_nodes = calculate_world_transforms(resolved)

        node3d_nodes = find_nodes_by_type(world_nodes, "Node3D")
        assert len(node3d_nodes) > 0
        assert all(wn.node.type == "Node3D" for wn in node3d_nodes)

    def test_find_nodes_by_group(self, sample_project_root: Path) -> None:
        """Find nodes in 'interactable' group."""
        parsed = parse_tscn(
            str(sample_project_root / "scenes" / "equipment" / "fdm_printer.tscn")
        )
        resolved = resolve_scene(parsed, sample_project_root)
        world_nodes = calculate_world_transforms(resolved)

        interactable = find_nodes_by_group(world_nodes, "interactable")
        assert len(interactable) == 1
        assert interactable[0].node.name == "FDMPrinter"

    def test_find_nodes_in_radius_includes_nearby(self, sample_project_root: Path) -> None:
        """Node within radius is included."""
        parsed = parse_tscn(str(sample_project_root / "scenes" / "main.tscn"))
        resolved = resolve_scene(parsed, sample_project_root)
        world_nodes = calculate_world_transforms(resolved)

        # FDMPrinter_1 is at (-36, 0, -2), search near it
        nearby = find_nodes_in_radius(world_nodes, (-36, 0, -2), 2.0)
        names = [wn.node.name for wn in nearby]
        assert "FDMPrinter_1" in names

    def test_find_nodes_in_radius_excludes_far(self) -> None:
        """Nodes far from center with small radius: empty result."""
        root = TscnNode(
            name="Root", type="Node3D",
            properties={"transform": _make_transform_dict(
                (1, 0, 0, 0, 1, 0, 0, 0, 1, 100, 100, 100)
            )},
            position=(100.0, 100.0, 100.0),
        )
        resolved = _make_resolved(root)
        world_nodes = calculate_world_transforms(resolved)

        result = find_nodes_in_radius(world_nodes, (0, 0, 0), 1.0)
        assert len(result) == 0


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _flatten_tree(node: TscnNode) -> list[TscnNode]:
    """Flatten a node tree into a list."""
    result = [node]
    for child in node.children:
        result.extend(_flatten_tree(child))
    return result
