"""Tests for godotiq_scene_map tool."""

from __future__ import annotations

import pytest

from godotiq.session import GodotIQSession
from godotiq.tools.spatial import _build_scene_map


class TestSceneMapBasic:
    def test_map_basic(self, session: GodotIQSession) -> None:
        """Basic scene map returns expected structure."""
        result = _build_scene_map(session, "res://scenes/equipment/fdm_printer.tscn")
        assert result["scene"] == "res://scenes/equipment/fdm_printer.tscn"
        assert result["total_nodes"] >= 4
        # Without focus: only top-level nodes (depth <= 1)
        for node in result["nodes"]:
            assert node["depth"] <= 1

    def test_map_no_focus_has_subtree_count(self, session: GodotIQSession) -> None:
        """Without focus, nodes include subtree_count."""
        result = _build_scene_map(session, "res://scenes/equipment/fdm_printer.tscn")
        for node in result["nodes"]:
            assert "subtree_count" in node

    def test_map_no_focus_capped(self, session: GodotIQSession) -> None:
        """Without focus, max 50 nodes returned."""
        result = _build_scene_map(session, "res://scenes/equipment/fdm_printer.tscn")
        assert len(result["nodes"]) <= 50

    def test_map_node_positions(self, session: GodotIQSession) -> None:
        """Nodes have world-space positions."""
        result = _build_scene_map(session, "res://scenes/equipment/fdm_printer.tscn")
        for node in result["nodes"]:
            assert "position" in node
            assert len(node["position"]) == 3

    def test_map_main_scene_with_instances(self, session: GodotIQSession) -> None:
        """Main scene resolves instances and shows top-level structure."""
        result = _build_scene_map(session, "res://scenes/main.tscn")
        assert result["total_nodes"] >= 2
        # Without focus: only top-level nodes
        node_names = [n["name"] for n in result["nodes"]]
        assert "Main" in node_names or any("Equipment" in n for n in node_names)


class TestSceneMapFocus:
    def test_map_with_focus(self, session: GodotIQSession) -> None:
        """Focus on a node filters to nearby nodes."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="WorkPoint",
            radius=5.0,
        )
        assert "focus_node" in result
        # All nodes should have distance_to_focus
        for node in result["nodes"]:
            assert "distance_to_focus" in node

    def test_map_radius_filter(self, session: GodotIQSession) -> None:
        """Radius parameter limits returned nodes."""
        result_wide = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
        )
        result_narrow = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=0.1,
        )
        assert len(result_wide["nodes"]) >= len(result_narrow["nodes"])

    def test_map_focus_capped(self, session: GodotIQSession) -> None:
        """With focus, max 30 nodes returned, sorted by distance."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
        )
        assert len(result["nodes"]) <= 30
        # Should be sorted by distance
        distances = [n["distance_to_focus"] for n in result["nodes"]]
        assert distances == sorted(distances)

    def test_map_focus_no_subtree_count(self, session: GodotIQSession) -> None:
        """With focus, nodes should NOT have subtree_count."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
        )
        for node in result["nodes"]:
            assert "subtree_count" not in node

    def test_map_direction(self, session: GodotIQSession) -> None:
        """Direction from focus is computed."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
        )
        directions = set()
        for node in result["nodes"]:
            if "direction_from_focus" in node:
                directions.add(node["direction_from_focus"])
        # Should have at least one direction (nearby, above, etc.)
        assert len(directions) >= 1


class TestSceneMapDetail:
    def test_map_brief(self, session: GodotIQSession) -> None:
        """Brief detail returns minimal data."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            detail="brief",
        )
        assert "scene" in result
        assert "nodes" in result
        # Brief should NOT have spatial_summary
        assert "spatial_summary" not in result

    def test_map_full(self, session: GodotIQSession) -> None:
        """Full detail returns all data including distance matrix."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            detail="full",
        )
        assert "type_summary" in result

    def test_map_groups_summary(self, session: GodotIQSession) -> None:
        """Groups summary counts nodes per group."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            detail="normal",
        )
        assert "groups_summary" in result
        # FDMPrinter root node is in "interactable" group
        assert "interactable" in result["groups_summary"]


class TestSceneMapInclude:
    def test_include_by_type(self, session: GodotIQSession) -> None:
        """Include filter by node type."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            include=["Node3D"],
        )
        for node in result["nodes"]:
            assert node["type"] == "Node3D"

    def test_include_by_group(self, session: GodotIQSession) -> None:
        """Include filter by group name."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            include=["interactable"],
        )
        assert len(result["nodes"]) >= 1
        assert result["nodes"][0]["name"] == "FDMPrinter"


class TestSceneMapFuzzyFocus:
    """Tests for 4-tier fuzzy focus node search."""

    def test_focus_by_name(self, session: GodotIQSession) -> None:
        """Focus by node name finds deep node even without full path."""
        # WorkPoint is at Equipment/FDMPrinter_1/WorkPoint in main.tscn
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="WorkPoint",
            radius=100.0,
        )
        assert "focus_node" in result
        assert "WorkPoint" in result["focus_node"]

    def test_focus_case_insensitive(self, session: GodotIQSession) -> None:
        """Case-insensitive search finds node regardless of casing."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="workpoint",
            radius=100.0,
        )
        assert "focus_node" in result
        assert result["focus_node"] == "WorkPoint"

    def test_focus_substring(self, session: GodotIQSession) -> None:
        """Substring search finds node when focus is part of name."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="Work",
            radius=100.0,
        )
        assert "focus_node" in result
        assert result["focus_node"] == "WorkPoint"

    def test_focus_alternatives(self, session: GodotIQSession) -> None:
        """Multiple substring matches produce focus_alternatives field."""
        # In main.tscn, "Printer" substring matches FDMPrinter_1 and PrinterModel
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="Printer",
            radius=100.0,
        )
        assert "focus_node" in result
        assert "focus_alternatives" in result
        assert len(result["focus_alternatives"]) >= 1
        assert len(result["focus_alternatives"]) <= 5

    def test_focus_exact_path_priority(self, session: GodotIQSession) -> None:
        """Exact full_path match takes priority over name match."""
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="Equipment/FDMPrinter_1/WorkPoint",
            radius=100.0,
        )
        assert "focus_node" in result
        assert result["focus_node"] == "Equipment/FDMPrinter_1/WorkPoint"
        # No alternatives for exact path match
        assert "focus_alternatives" not in result


class TestSceneMapDetailCapsAndTokenHints:
    """Detail-dependent nearby node caps and token hints."""

    def test_scene_map_nearby_cap_brief(self, session: GodotIQSession) -> None:
        """Brief detail caps nearby nodes at 10."""
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="Main",
            radius=100.0,
            detail="brief",
        )
        assert len(result["nodes"]) <= 10
        assert "nearby_count" in result
        assert result["token_hint"] == "brief"

    def test_scene_map_nearby_cap_normal(self, session: GodotIQSession) -> None:
        """Normal detail caps nearby nodes at 20."""
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="Main",
            radius=100.0,
            detail="normal",
        )
        assert len(result["nodes"]) <= 20
        assert "nearby_count" in result
        assert result["token_hint"] == "normal"

    def test_scene_map_brief_compact_output(self, session: GodotIQSession) -> None:
        """Brief focus mode nearby nodes have only compact keys."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
            detail="brief",
        )
        allowed_keys = {"name", "type", "distance_to_focus", "direction_from_focus"}
        for node in result["nodes"]:
            assert set(node.keys()) == allowed_keys, (
                f"Brief focus node has unexpected keys: {set(node.keys()) - allowed_keys}"
            )

    def test_scene_map_capped_shows_total(self, session: GodotIQSession) -> None:
        """When nearby_capped is True, nearby_total_in_radius is present."""
        result = _build_scene_map(
            session,
            "res://scenes/main.tscn",
            focus="Main",
            radius=100.0,
            detail="brief",
        )
        if result.get("nearby_capped"):
            assert "nearby_total_in_radius" in result
            assert result["nearby_total_in_radius"] > 10
        else:
            # Not enough nodes to trigger cap — verify key is absent
            assert "nearby_total_in_radius" not in result

    def test_scene_map_normal_compact_output(self, session: GodotIQSession) -> None:
        """Normal focus mode nearby nodes include groups and has_script."""
        result = _build_scene_map(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            focus="FDMPrinter",
            radius=100.0,
            detail="normal",
        )
        allowed_keys = {"name", "type", "distance_to_focus", "direction_from_focus", "groups", "has_script"}
        for node in result["nodes"]:
            assert set(node.keys()) == allowed_keys, (
                f"Normal focus node has unexpected keys: {set(node.keys()) - allowed_keys}"
            )


class TestSceneMapErrors:
    def test_map_missing_scene(self, session: GodotIQSession) -> None:
        """Missing scene returns error dict."""
        result = _build_scene_map(session, "res://nonexistent.tscn")
        assert "error" in result
