"""Tests for godotiq_suggest_scale tool.

Sprint 3c — Scale suggestion with prioritized tier matching.

Design Doc: Sprint 3c — Suggest Scale
"""

from __future__ import annotations

from godotiq.session import GodotIQSession
from godotiq.tools.assets import _build_suggest_scale


class TestSuggestScaleExactMatch:
    """Exact filename match tier takes priority over directory matches."""

    def test_exact_match_excludes_directory_siblings(self, session: GodotIQSession) -> None:
        """printer_standard.glb should match only PrinterModel (0.35), not FilamentSpool (0.15)."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/printer_standard.glb",
        )
        assert result["recommended_scale"] is not None
        # Exact match: only PrinterModel at 0.35, not median with FilamentSpool
        assert result["recommended_scale"] == [0.35, 0.35, 0.35]
        assert result["match_tier"] == "exact_filename"
        assert len(result["reference_models"]) == 1
        assert result["reference_models"][0]["name"] == "PrinterModel"

    def test_exact_match_filament_spool(self, session: GodotIQSession) -> None:
        """filament_spool.glb should match only FilamentSpool (0.15)."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/filament_spool.glb",
        )
        assert result["recommended_scale"] == [0.15, 0.15, 0.15]
        assert result["match_tier"] == "exact_filename"
        assert len(result["reference_models"]) == 1
        assert result["reference_models"][0]["name"] == "FilamentSpool"


class TestSuggestScaleDirectoryFallback:
    """Same-directory tier used only when no exact match exists."""

    def test_directory_fallback(self, session: GodotIQSession) -> None:
        """A new model in the printers dir (no exact match) falls back to directory tier."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/new_printer.glb",
        )
        assert result["match_tier"] == "same_directory"
        assert result["recommended_scale"] is not None
        # Directory tier includes both PrinterModel and FilamentSpool
        assert len(result["reference_models"]) >= 2


class TestSuggestScaleNoReferences:
    """No similar models returns null scale with suggestion."""

    def test_suggest_no_references(self, session: GodotIQSession) -> None:
        """No similar models returns null scale with suggestion."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/some_unique_model.glb",
        )
        assert result["recommended_scale"] is None
        assert result["match_tier"] == "none"
        assert "suggestion" in result or "note" in result


class TestSuggestScaleNearNode:
    """near_node parameter provides position suggestion."""

    def test_suggest_near_node(self, session: GodotIQSession) -> None:
        """near_node provides position suggestion."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/printer_standard.glb",
            near_node="WorkPoint",
        )
        assert "position_suggestion" in result
        assert result["position_suggestion"] is not None
        assert len(result["position_suggestion"]) == 3

    def test_suggest_near_node_fuzzy(self, session: GodotIQSession) -> None:
        """near_node uses fuzzy search (case-insensitive, substring)."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/printer_standard.glb",
            near_node="workpoint",
        )
        assert "position_suggestion" in result
        assert result["position_suggestion"] is not None


class TestSuggestScaleReasoning:
    """Reasoning field explains the calculation."""

    def test_suggest_scale_reasoning(self, session: GodotIQSession) -> None:
        """Reasoning explains median calculation."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/printers/printer_standard.glb",
        )
        assert "reasoning" in result
        assert isinstance(result["reasoning"], str)
        assert len(result["reasoning"]) > 0
        assert "median" in result["reasoning"].lower() or "reference" in result["reasoning"].lower()


class TestSuggestScaleRecursiveSearch:
    """Model search looks inside recursively instanced scenes."""

    def test_recursive_model_search(self, session: GodotIQSession) -> None:
        """Models in recursively instanced scenes are found."""
        result = _build_suggest_scale(
            session,
            "res://scenes/main.tscn",
            "res://assets/models/printers/printer_standard.glb",
        )
        assert len(result["reference_models"]) >= 1
        assert result["recommended_scale"] is not None
        assert result["match_tier"] == "exact_filename"


class TestSuggestScaleExtResourceProperty:
    """Models referenced as ext_resource properties are found."""

    def test_ext_resource_property_from_subscene(self, session: GodotIQSession) -> None:
        """Models referenced as mesh/property ext_resources are found in sub-scenes."""
        result = _build_suggest_scale(
            session,
            "res://scenes/main.tscn",
            "res://assets/models/parts/nozzle_tip.glb",
        )
        assert len(result["reference_models"]) >= 1
        assert result["recommended_scale"] is not None
        assert result["match_tier"] == "exact_filename"

    def test_ext_resource_property_direct_scene(self, session: GodotIQSession) -> None:
        """Models referenced as mesh/property ext_resources are found in the direct scene."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            "res://assets/models/parts/nozzle_tip.glb",
        )
        assert len(result["reference_models"]) >= 1
        assert result["recommended_scale"] is not None
        assert result["match_tier"] == "exact_filename"


class TestSuggestScaleMissingScene:
    """Non-existent scene returns error dict."""

    def test_suggest_missing_scene(self, session: GodotIQSession) -> None:
        """Error dict for missing scene."""
        result = _build_suggest_scale(
            session,
            "res://nonexistent.tscn",
            "res://assets/models/printers/printer_standard.glb",
        )
        assert "error" in result


class TestSuggestScaleReferenceMode:
    """Reference mode: model=None, search by intended_use."""

    def test_suggest_scale_reference_mode(self, session: GodotIQSession) -> None:
        """Reference mode finds nodes matching intended_use and returns scale stats."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="printer",
        )
        assert result["mode"] == "reference"
        assert result["matches_found"] > 0
        assert result["recommended_scale"] is not None
        assert len(result["recommended_scale"]) == 3
        assert "references" in result
        assert len(result["references"]) > 0

    def test_suggest_scale_reference_mode_no_match(self, session: GodotIQSession) -> None:
        """Reference mode with no matching nodes returns matches_found=0."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="dragon",
        )
        assert result["mode"] == "reference"
        assert result["matches_found"] == 0
        assert result["recommended_scale"] is None

    def test_suggest_scale_both_empty_error(self, session: GodotIQSession) -> None:
        """model=None and intended_use='' returns an error."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="",
        )
        assert "error" in result

    def test_suggest_scale_reference_mode_missing_scene(self, session: GodotIQSession) -> None:
        """Reference mode with nonexistent scene returns error dict."""
        result = _build_suggest_scale(
            session,
            "res://nonexistent.tscn",
            model=None,
            intended_use="printer",
        )
        assert "error" in result

    def test_suggest_scale_empty_string_model_triggers_reference_mode(self, session: GodotIQSession) -> None:
        """model='' should behave same as model=None (enter reference mode)."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model="",
            intended_use="printer",
        )
        assert result["mode"] == "reference"
        assert result["matches_found"] > 0
        assert result["recommended_scale"] is not None

    def test_suggest_scale_model_still_works(self, session: GodotIQSession) -> None:
        """Existing model-based behavior is unchanged, now includes mode='model'."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model="res://assets/models/printers/printer_standard.glb",
        )
        assert result["mode"] == "model"
        assert result["recommended_scale"] is not None
        assert result["match_tier"] == "exact_filename"


class TestSuggestScaleReferenceModeExcludesUI:
    """Reference mode skips Control-derived nodes (Panel, Button, etc.)."""

    def test_suggest_scale_reference_mode_excludes_ui_nodes(self) -> None:
        """Reference mode skips Control-derived nodes (Panel, Button, etc.)."""
        from godotiq.parsers.tscn_parser import TscnNode
        from godotiq.parsers.scene_resolver import WorldNode
        from godotiq.tools.assets import _reference_mode

        panel = WorldNode(
            node=TscnNode(name="printer_info_panel", type="PanelContainer"),
            world_position=(0.0, 0.0, 0.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(1.0, 1.0, 1.0),
            full_path="UI/printer_info_panel",
            depth=2,
            is_instance_root=False,
            instance_source=None,
        )
        button = WorldNode(
            node=TscnNode(name="printer_buy_button", type="Button"),
            world_position=(0.0, 0.0, 0.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(2.0, 2.0, 2.0),
            full_path="UI/printer_buy_button",
            depth=2,
            is_instance_root=False,
            instance_source=None,
        )
        control = WorldNode(
            node=TscnNode(name="printer_control", type="Control"),
            world_position=(0.0, 0.0, 0.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(3.0, 3.0, 3.0),
            full_path="UI/printer_control",
            depth=2,
            is_instance_root=False,
            instance_source=None,
        )
        visual = WorldNode(
            node=TscnNode(name="PrinterModel", type="MeshInstance3D"),
            world_position=(2.0, 0.0, 3.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(0.35, 0.35, 0.35),
            full_path="Room/PrinterModel",
            depth=2,
            is_instance_root=True,
            instance_source="res://assets/models/printers/printer_standard.glb",
        )
        result = _reference_mode(
            "res://scenes/main.tscn", "printer", [panel, button, control, visual]
        )
        assert result["matches_found"] == 1
        assert result["references"][0]["name"] == "PrinterModel"
        assert result["recommended_scale"] == [0.35, 0.35, 0.35]


class TestSuggestScaleReferenceModeGLBPriority:
    """When GLB models exist among matches, recommended_scale uses only GLB scales."""

    def test_suggest_scale_reference_mode_prioritizes_glb(self) -> None:
        """When GLB models exist among matches, recommended_scale uses only GLB scales."""
        from godotiq.parsers.tscn_parser import TscnNode
        from godotiq.parsers.scene_resolver import WorldNode
        from godotiq.tools.assets import _reference_mode

        glb_node = WorldNode(
            node=TscnNode(name="printer_glb", type="Node3D"),
            world_position=(0.0, 0.0, 0.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(0.5, 0.5, 0.5),
            full_path="Room/printer_glb",
            depth=2,
            is_instance_root=True,
            instance_source="res://assets/models/printers/printer.glb",
        )
        non_glb = WorldNode(
            node=TscnNode(name="printer_light", type="OmniLight3D"),
            world_position=(0.0, 2.0, 0.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(10.0, 10.0, 10.0),
            full_path="Room/printer_light",
            depth=2,
            is_instance_root=False,
            instance_source=None,
        )
        result = _reference_mode(
            "res://scenes/main.tscn", "printer", [glb_node, non_glb]
        )
        assert result["matches_found"] == 2
        assert result["recommendation_source"] == "glb_models"
        # recommended_scale from GLB node only (0.5), not the light (10.0)
        assert result["recommended_scale"] == [0.5, 0.5, 0.5]


class TestSuggestScaleReferenceModeVisualFilter:
    """Reference mode excludes non-visual marker/placeholder nodes."""

    def test_suggest_scale_reference_mode_excludes_markers(self) -> None:
        """Reference mode skips Marker3D/bare Node3D nodes, only returns visual geometry."""
        from godotiq.parsers.tscn_parser import TscnNode
        from godotiq.parsers.scene_resolver import WorldNode
        from godotiq.tools.assets import _reference_mode

        marker = WorldNode(
            node=TscnNode(name="printer_slot_1", type="Marker3D"),
            world_position=(2.0, 0.0, 3.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(1.0, 1.0, 1.0),
            full_path="Room/printer_slot_1",
            depth=2,
            is_instance_root=False,
            instance_source=None,
        )
        visual = WorldNode(
            node=TscnNode(name="PrinterModel", type="Node3D"),
            world_position=(2.0, 0.0, 3.0),
            world_rotation=(0.0, 0.0, 0.0),
            world_scale=(0.35, 0.35, 0.35),
            full_path="Room/PrinterModel",
            depth=2,
            is_instance_root=True,
            instance_source="res://scenes/equipment/fdm_printer.tscn",
        )
        result = _reference_mode(
            "res://scenes/main.tscn", "printer", [marker, visual]
        )
        assert result["matches_found"] == 1
        assert result["references"][0]["name"] == "PrinterModel"
        assert result["recommended_scale"] == [0.35, 0.35, 0.35]
