"""Tests for the GDScript parser.

Unit tests (inline strings) and integration tests (real fixture files).
"""

from __future__ import annotations

from pathlib import Path

import pytest

from godotiq.parsers.gd_parser import GdScript, _parse_gd_content, parse_gd

FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"
SAMPLE_FIXTURES = FIXTURES / "sample_project" / "scripts" / "entities"


# ---------------------------------------------------------------------------
# TestClassDeclarations
# ---------------------------------------------------------------------------


class TestClassDeclarations:
    """Test class_name and extends parsing at TOP_LEVEL."""

    def test_parse_class_name(self):
        result = _parse_gd_content("class_name EconomyManagerClass\n")
        assert result.class_name == "EconomyManagerClass"

    def test_parse_extends(self):
        result = _parse_gd_content("extends Node\n")
        assert result.extends == "Node"

    def test_parse_combined_class_name_extends(self):
        result = _parse_gd_content("class_name Item extends Node\n")
        assert result.class_name == "Item"
        assert result.extends == "Node"

    def test_parse_extends_file_path(self):
        result = _parse_gd_content('extends "res://path.gd"\n')
        assert result.extends == '"res://path.gd"'

    def test_minimal_file(self):
        result = _parse_gd_content("extends Node3D\n")
        assert result.extends == "Node3D"
        assert result.class_name == ""
        assert result.signals == []
        assert result.functions == []
        assert result.variables == []

    def test_empty_file(self):
        result = _parse_gd_content("")
        assert result.class_name == ""
        assert result.extends == ""

    def test_missing_file_raises(self):
        with pytest.raises(FileNotFoundError):
            parse_gd("/nonexistent/path/script.gd")


# ---------------------------------------------------------------------------
# TestSignalParsing
# ---------------------------------------------------------------------------


class TestSignalParsing:
    """Test signal declaration parsing."""

    def test_signal_no_args(self):
        result = _parse_gd_content("signal health_depleted\n")
        assert len(result.signals) == 1
        assert result.signals[0].name == "health_depleted"
        assert result.signals[0].args == []

    def test_signal_untyped_args(self):
        result = _parse_gd_content("signal foo(a, b)\n")
        assert result.signals[0].args == [("a", ""), ("b", "")]

    def test_signal_typed_args(self):
        result = _parse_gd_content(
            "signal cash_changed(amount: float, delta: float)\n"
        )
        sig = result.signals[0]
        assert sig.args == [("amount", "float"), ("delta", "float")]

    def test_signal_custom_type(self):
        content = "signal any_state_changed(id: String, old: Printer.State, new: Printer.State)\n"
        result = _parse_gd_content(content)
        sig = result.signals[0]
        assert sig.args[1] == ("old", "Printer.State")
        assert sig.args[2] == ("new", "Printer.State")

    def test_multiple_signals(self):
        content = "signal a\nsignal b\nsignal c\n"
        result = _parse_gd_content(content)
        assert len(result.signals) == 3

    def test_signal_line_number(self):
        content = "\n\nsignal foo\n"
        result = _parse_gd_content(content)
        assert result.signals[0].line == 3


# ---------------------------------------------------------------------------
# TestFunctionParsing
# ---------------------------------------------------------------------------


class TestFunctionParsing:
    """Test function declaration and end_line tracking."""

    def test_basic_func(self):
        content = "func _ready() -> void:\n\tpass\n"
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert f.name == "_ready"
        assert f.return_type == "void"
        assert f.args == []

    def test_func_with_typed_params_and_default(self):
        content = 'func add_cash(amount: float, description: String = "") -> void:\n\tpass\n'
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert f.args == [("amount", "float", ""), ("description", "String", '""')]
        assert f.return_type == "void"

    def test_func_return_bool(self):
        content = "func can_afford(amount: float) -> bool:\n\treturn true\n"
        result = _parse_gd_content(content)
        assert result.functions[0].return_type == "bool"

    def test_static_func(self):
        content = "static func sum2(a, b):\n\treturn a + b\n"
        result = _parse_gd_content(content)
        assert result.functions[0].is_static is True

    def test_virtual_func(self):
        content = "func _ready():\n\tpass\n"
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert f.is_virtual is True
        assert f.is_private is True

    def test_non_virtual_func(self):
        content = "func add_cash():\n\tpass\n"
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert f.is_virtual is False
        assert f.is_private is False

    def test_func_no_return_type(self):
        content = "func foo():\n\tpass\n"
        result = _parse_gd_content(content)
        assert result.functions[0].return_type == ""

    def test_func_complex_return_type(self):
        content = "func get_idle_printers() -> Array[Printer]:\n\tpass\n"
        result = _parse_gd_content(content)
        assert result.functions[0].return_type == "Array[Printer]"

    def test_func_arg_with_parens_in_default(self):
        content = "func f(v: Vector2 = Vector2(1, 2)):\n\tpass\n"
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert len(f.args) == 1
        assert f.args[0] == ("v", "Vector2", "Vector2(1, 2)")

    def test_end_line_for_function(self):
        content = "func foo():\n\tvar x = 1\n\treturn x\n\n"
        result = _parse_gd_content(content)
        assert result.functions[0].end_line == 3

    def test_end_line_last_function_in_file(self):
        content = "func foo():\n\tpass\n"
        result = _parse_gd_content(content)
        assert result.functions[0].end_line == 2

    def test_one_liner_func(self):
        content = "func square(a): return a * a\n"
        result = _parse_gd_content(content)
        f = result.functions[0]
        assert f.end_line == f.line


# ---------------------------------------------------------------------------
# TestVariableParsing
# ---------------------------------------------------------------------------


class TestVariableParsing:
    """Test variable and constant declaration parsing."""

    def test_typed_var(self):
        content = "var cash: float = 100.0\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.name == "cash"
        assert v.type_hint == "float"
        assert v.default_value == "100.0"

    def test_const(self):
        content = "const ANSWER = 42\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.is_const is True
        assert v.default_value == "42"

    def test_typed_const(self):
        content = "const LEVEL_XP: Array[int] = [0, 40, 100]\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.is_const is True
        assert v.type_hint == "Array[int]"

    def test_export_same_line(self):
        content = "@export var speed: float = 5.0\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.annotation == "@export"
        assert v.name == "speed"

    def test_export_range_same_line(self):
        content = "@export_range(0, 100) var hp: int\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.annotation == "@export_range(0, 100)"

    def test_onready_same_line(self):
        content = "@onready var sprite := $Sprite2D\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.annotation == "@onready"

    def test_static_var(self):
        content = "static var balance: int = 0\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.is_static is True

    def test_class_scope(self):
        content = "var x: int = 0\n"
        result = _parse_gd_content(content)
        assert result.variables[0].scope == "class"

    def test_local_scope_not_captured(self):
        content = "func foo():\n\tvar local = 1\n"
        result = _parse_gd_content(content)
        assert len(result.variables) == 0

    def test_multiline_const_array(self):
        content = "const XP: Array[int] = [\n\t0,\n\t40,\n\t100,\n]\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.is_const is True
        assert "0" in v.default_value
        assert "100" in v.default_value

    def test_multiline_var_dict(self):
        content = 'var stats: Dictionary = {\n\t"wins": 0,\n\t"losses": 0,\n}\n'
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert "wins" in v.default_value

    def test_property_getter_setter(self):
        content = "var health: int:\n\tget:\n\t\treturn _health\n\tset(value):\n\t\t_health = value\n"
        result = _parse_gd_content(content)
        assert len(result.variables) == 1
        assert result.variables[0].name == "health"
        assert len(result.functions) == 0

    def test_inline_comment_stripped(self):
        content = "var speed: float = 5.0  # movement speed\n"
        result = _parse_gd_content(content)
        assert result.variables[0].default_value == "5.0"

    def test_buffered_annotation_separate_line(self):
        content = "@export\nvar speed: float = 5.0\n"
        result = _parse_gd_content(content)
        v = result.variables[0]
        assert v.annotation == "@export"
        assert v.name == "speed"

    def test_export_group_discarded(self):
        content = '@export_group("Movement")\n@export var speed: float = 5.0\n'
        result = _parse_gd_content(content)
        assert len(result.variables) == 1
        assert result.variables[0].annotation == "@export"


# ---------------------------------------------------------------------------
# TestEnumParsing
# ---------------------------------------------------------------------------


class TestEnumParsing:
    """Test enum declaration parsing."""

    def test_named_enum(self):
        content = "enum GameState { PLAYING, PAUSED, DAILY_SUMMARY }\n"
        result = _parse_gd_content(content)
        e = result.enums[0]
        assert e.name == "GameState"
        assert e.values == {"PLAYING": None, "PAUSED": None, "DAILY_SUMMARY": None}

    def test_enum_explicit_values(self):
        content = "enum { A, B = 5, C }\n"
        result = _parse_gd_content(content)
        assert result.enums[0].values["B"] == 5
        assert result.enums[0].values["C"] is None

    def test_enum_negative_values(self):
        content = "enum { X = -1 }\n"
        result = _parse_gd_content(content)
        assert result.enums[0].values["X"] == -1

    def test_anonymous_enum(self):
        content = "enum { A, B }\n"
        result = _parse_gd_content(content)
        assert result.enums[0].name == ""

    def test_multiline_enum(self):
        content = "enum State {\n\tIDLE,\n\tRUNNING,\n\tSTOPPED,\n}\n"
        result = _parse_gd_content(content)
        e = result.enums[0]
        assert e.name == "State"
        assert "IDLE" in e.values
        assert "STOPPED" in e.values

    def test_enum_line_number(self):
        content = "\nenum Foo { A }\n"
        result = _parse_gd_content(content)
        assert result.enums[0].line == 2


# ---------------------------------------------------------------------------
# TestInnerClasses
# ---------------------------------------------------------------------------


class TestInnerClasses:
    """Test inner class detection and body skipping."""

    def test_inner_class_detected(self):
        content = "class Circle extends Shape:\n\tvar radius: float = 1.0\n\tfunc area():\n\t\treturn PI * radius * radius\n"
        result = _parse_gd_content(content)
        assert "Circle" in result.inner_classes

    def test_inner_class_body_skipped(self):
        content = "class Circle extends Shape:\n\tvar radius: float = 1.0\n\tfunc area():\n\t\tpass\n"
        result = _parse_gd_content(content)
        assert len(result.variables) == 0
        assert len(result.functions) == 0

    def test_inner_class_without_extends(self):
        content = "class Something:\n\tpass\n"
        result = _parse_gd_content(content)
        assert "Something" in result.inner_classes


# ---------------------------------------------------------------------------
# TestEdgeCases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_empty_string(self):
        result = _parse_gd_content("")
        assert result.class_name == ""
        assert result.extends == ""
        assert result.signals == []

    def test_only_comments(self):
        result = _parse_gd_content("# This is a comment\n## Doc comment\n")
        assert result.class_name == ""

    def test_malformed_line_no_raise(self):
        content = "extends Node\n!@#$%^&*()\nvar x: int = 1\n"
        result = _parse_gd_content(content)
        assert result.extends == "Node"
        assert len(result.variables) == 1

    def test_unclosed_multiline_at_eof(self):
        content = "const XP: Array[int] = [\n\t0,\n\t40,\n"
        result = _parse_gd_content(content)
        assert len(result.variables) == 1

    def test_annotation_buffer_not_corrupted_by_rpc(self):
        content = "@rpc\nfunc remote_action():\n\tpass\n@export var x: int = 1\n"
        result = _parse_gd_content(content)
        assert result.variables[0].annotation == "@export"

    def test_export_group_no_buffer_corruption(self):
        content = '@export_group("Group")\nvar plain: int = 1\n'
        result = _parse_gd_content(content)
        assert result.variables[0].annotation == ""


# ---------------------------------------------------------------------------
# TestSignalConnections
# ---------------------------------------------------------------------------


class TestSignalConnections:
    """Tests for .connect() detection inside function bodies."""

    def test_direct_connect(self) -> None:
        content = "func _ready():\n\tmy_signal.connect(_handler)\n"
        result = _parse_gd_content(content)
        assert len(result.signal_connections) == 1
        assert result.signal_connections[0].signal_source == "my_signal"
        assert result.signal_connections[0].target == "_handler"
        assert result.signal_connections[0].has_bind is False

    def test_connect_with_bind(self) -> None:
        content = "func _ready():\n\tmy_signal.connect(_handler.bind(some_arg))\n"
        result = _parse_gd_content(content)
        conn = result.signal_connections[0]
        assert conn.has_bind is True
        assert conn.bind_args == "some_arg"

    def test_connect_with_dollar_node(self) -> None:
        content = "func _ready():\n\t$Timer.timeout.connect(_on_timeout)\n"
        result = _parse_gd_content(content)
        conn = result.signal_connections[0]
        assert conn.signal_source == "$Timer.timeout"
        assert conn.target == "_on_timeout"

    def test_connect_with_lambda(self) -> None:
        content = "func _ready():\n\tlevel_up.connect(func(_l, _u): fired[0] = true)\n"
        result = _parse_gd_content(content)
        conn = result.signal_connections[0]
        assert conn.target.startswith("func")

    def test_depth_aware_connect_parsing(self) -> None:
        content = "func _ready():\n\tif signal.connect(handler) == OK:\n\t\tpass\n"
        result = _parse_gd_content(content)
        conn = result.signal_connections[0]
        assert conn.target == "handler"

    def test_signal_source_captures_full_chain(self) -> None:
        content = "func _ready():\n\tprinter.print_started.connect(_on_print_started)\n"
        result = _parse_gd_content(content)
        conn = result.signal_connections[0]
        assert conn.signal_source == "printer.print_started"

    def test_connect_line_number(self) -> None:
        content = "func _ready():\n\tvar x = 1\n\tmy_signal.connect(_handler)\n"
        result = _parse_gd_content(content)
        assert result.signal_connections[0].line == 3


# ---------------------------------------------------------------------------
# TestSignalEmissions
# ---------------------------------------------------------------------------


class TestSignalEmissions:
    """Tests for .emit() detection inside function bodies."""

    def test_emit_no_args(self) -> None:
        content = "func foo():\n\thealth_depleted.emit()\n"
        result = _parse_gd_content(content)
        assert len(result.signal_emissions) == 1
        assert result.signal_emissions[0].signal_name == "health_depleted"
        assert result.signal_emissions[0].arg_count == 0

    def test_emit_two_args(self) -> None:
        content = "func foo():\n\tcash_changed.emit(cash, amount)\n"
        result = _parse_gd_content(content)
        assert result.signal_emissions[0].arg_count == 2

    def test_emit_three_args(self) -> None:
        content = "func foo():\n\tany_print_failed.emit(id, product, reason)\n"
        result = _parse_gd_content(content)
        assert result.signal_emissions[0].arg_count == 3

    def test_emit_with_nested_parens(self) -> None:
        content = "func foo():\n\tmy_signal.emit(Vector2(1, 2), 3)\n"
        result = _parse_gd_content(content)
        assert result.signal_emissions[0].arg_count == 2


# ---------------------------------------------------------------------------
# TestAutoloadRefs
# ---------------------------------------------------------------------------


class TestAutoloadRefs:
    """Tests for autoload singleton reference detection."""

    def test_direct_autoload_with_known_autoloads(self) -> None:
        content = "func foo():\n\tEconomyManager.spend_cash(100)\n"
        result = _parse_gd_content(content, known_autoloads=["EconomyManager"])
        refs = [r for r in result.autoload_refs if r.pattern == "direct"]
        assert len(refs) >= 1
        assert refs[0].autoload_name == "EconomyManager"
        assert refs[0].access == "spend_cash"

    def test_no_direct_detection_without_known_autoloads(self) -> None:
        content = "func foo():\n\tEconomyManager.spend_cash(100)\n"
        result = _parse_gd_content(content, known_autoloads=None)
        direct = [r for r in result.autoload_refs if r.pattern == "direct"]
        assert len(direct) == 0

    def test_has_node_pattern(self) -> None:
        content = 'func foo():\n\thas_node("/root/ProgressionManager")\n'
        result = _parse_gd_content(content)
        refs = [r for r in result.autoload_refs if r.pattern == "has_node"]
        assert len(refs) == 1
        assert refs[0].autoload_name == "ProgressionManager"

    def test_get_node_pattern(self) -> None:
        content = 'func foo():\n\tget_node("/root/ProgressionManager")\n'
        result = _parse_gd_content(content)
        refs = [r for r in result.autoload_refs if r.pattern == "get_node"]
        assert len(refs) == 1
        assert refs[0].autoload_name == "ProgressionManager"

    def test_get_node_or_null_pattern(self) -> None:
        content = 'func foo():\n\tget_tree().root.get_node_or_null("Main/Systems/PrinterSlotManager")\n'
        result = _parse_gd_content(content)
        refs = [r for r in result.autoload_refs if r.pattern == "get_node_or_null"]
        assert len(refs) == 1

    def test_has_node_detected_without_known_autoloads(self) -> None:
        content = 'func foo():\n\thas_node("/root/SomeManager")\n'
        result = _parse_gd_content(content, known_autoloads=None)
        refs = [r for r in result.autoload_refs if r.pattern == "has_node"]
        assert len(refs) == 1

    def test_multiple_autoload_refs_separate_entries(self) -> None:
        content = "func foo():\n\tEconomyManager.foo()\n\tResearchManager.bar()\n"
        result = _parse_gd_content(
            content, known_autoloads=["EconomyManager", "ResearchManager"]
        )
        direct = [r for r in result.autoload_refs if r.pattern == "direct"]
        assert len(direct) >= 2


# ---------------------------------------------------------------------------
# TestPreloadLoad
# ---------------------------------------------------------------------------


class TestPreloadLoad:
    """Tests for preload() and load() reference detection."""

    def test_preload_detected(self) -> None:
        content = 'const Scene = preload("res://scenes/player.tscn")\n'
        result = _parse_gd_content(content)
        assert len(result.preload_refs) == 1
        assert result.preload_refs[0].path == "res://scenes/player.tscn"
        assert result.preload_refs[0].is_preload is True

    def test_load_detected(self) -> None:
        content = 'func foo():\n\tvar tex = load("res://data/items.tres")\n'
        result = _parse_gd_content(content)
        assert len(result.preload_refs) == 1
        assert result.preload_refs[0].is_preload is False

    def test_preload_in_const_creates_both(self) -> None:
        content = 'const MyScene = preload("res://scenes/player.tscn")\n'
        result = _parse_gd_content(content)
        consts = [v for v in result.variables if v.name == "MyScene"]
        assert len(consts) == 1
        assert len(result.preload_refs) == 1

    def test_preload_in_function_body(self) -> None:
        content = 'func foo():\n\tvar s = preload("res://foo.tscn")\n'
        result = _parse_gd_content(content)
        assert len(result.preload_refs) >= 1


# ---------------------------------------------------------------------------
# TestIsInstanceValid
# ---------------------------------------------------------------------------


class TestIsInstanceValid:
    """Tests for is_instance_valid() detection."""

    def test_is_instance_valid_detected(self) -> None:
        content = "func foo():\n\tif is_instance_valid(printer):\n\t\tpass\n"
        result = _parse_gd_content(content)
        assert 2 in result.is_instance_valid_lines

    def test_multiple_is_instance_valid(self) -> None:
        content = "func foo():\n\tis_instance_valid(a)\n\tis_instance_valid(b)\n"
        result = _parse_gd_content(content)
        assert len(result.is_instance_valid_lines) >= 2


# ---------------------------------------------------------------------------
# TestIntegration — real fixture file tests
# ---------------------------------------------------------------------------


class TestIntegration:
    """Integration tests parsing real .gd fixture files."""

    @pytest.mark.parametrize("fixture_path", [
        FIXTURES / "economy_manager.gd",
        FIXTURES / "printer_manager.gd",
        FIXTURES / "game_manager.gd",
        FIXTURES / "progression_manager.gd",
        SAMPLE_FIXTURES / "printer.gd",
        SAMPLE_FIXTURES / "worker.gd",
    ])
    def test_parse_fixture_no_exceptions(self, fixture_path: Path) -> None:
        """Every fixture file parses without raising."""
        result = parse_gd(str(fixture_path))
        assert isinstance(result, GdScript)

    def test_economy_manager(self) -> None:
        """economy_manager.gd: class_name, extends, signals, functions, variables, emissions."""
        result = parse_gd(str(FIXTURES / "economy_manager.gd"))
        assert result.class_name == "EconomyManagerClass"
        assert result.extends == "Node"
        assert len(result.signals) == 3
        sig_names = [s.name for s in result.signals]
        assert "cash_changed" in sig_names
        assert "transaction_recorded" in sig_names
        assert "insufficient_funds" in sig_names
        # cash_changed has typed args
        cc = [s for s in result.signals if s.name == "cash_changed"][0]
        assert cc.args == [("new_amount", "float"), ("delta", "float")]
        # 8 functions
        assert len(result.functions) == 8
        func_names = [f.name for f in result.functions]
        assert "_ready" in func_names
        assert "add_cash" in func_names
        assert "spend_cash" in func_names
        assert "can_afford" in func_names
        # 5 class variables
        assert len(result.variables) == 5
        cash_var = [v for v in result.variables if v.name == "cash"][0]
        assert cash_var.type_hint == "float"
        assert cash_var.default_value == "100.0"
        # Signal emissions
        assert len(result.signal_emissions) >= 5
        cc_emissions = [e for e in result.signal_emissions if e.signal_name == "cash_changed"]
        assert any(e.arg_count == 2 for e in cc_emissions)

    def test_printer_manager(self) -> None:
        """printer_manager.gd: custom types, is_instance_valid, connections, autoload patterns."""
        result = parse_gd(
            str(FIXTURES / "printer_manager.gd"),
            known_autoloads=["ResearchManager", "SlicerManager"],
        )
        assert result.class_name == "PrinterManagerClass"
        assert result.extends == "Node"
        assert len(result.signals) == 5
        # Printer.State custom type in any_state_changed
        asc = [s for s in result.signals if s.name == "any_state_changed"][0]
        state_args = [a for a in asc.args if a[1] == "Printer.State"]
        assert len(state_args) >= 1
        # is_instance_valid
        assert len(result.is_instance_valid_lines) >= 8
        # Signal connections (4 in register_printer)
        assert len(result.signal_connections) >= 4
        ps_conn = [c for c in result.signal_connections if "print_started" in c.signal_source]
        assert len(ps_conn) >= 1
        assert ps_conn[0].target == "_on_print_started"
        # get_node_or_null refs
        gnor = [r for r in result.autoload_refs if r.pattern == "get_node_or_null"]
        assert len(gnor) >= 2
        # has_node refs
        hn = [r for r in result.autoload_refs if r.pattern == "has_node"]
        assert len(hn) >= 1
        # Signal emissions
        assert len(result.signal_emissions) >= 5

    def test_game_manager(self) -> None:
        """game_manager.gd: enum, autoload refs, has_node/get_node, multiline var."""
        result = parse_gd(
            str(FIXTURES / "game_manager.gd"),
            known_autoloads=["EconomyManager", "ResearchManager", "ProgressionManager"],
        )
        assert result.class_name == "GameManagerClass"
        assert result.extends == "Node"
        # Enum
        assert len(result.enums) == 1
        gs = result.enums[0]
        assert gs.name == "GameState"
        assert "PLAYING" in gs.values
        assert "PAUSED" in gs.values
        assert "DAILY_SUMMARY" in gs.values
        # Signals
        assert len(result.signals) == 3
        # Functions
        assert len(result.functions) >= 10
        # Variables
        assert len(result.variables) >= 7
        ss = [v for v in result.variables if v.name == "session_stats"][0]
        assert ss.type_hint == "Dictionary"
        # Autoload direct refs
        econ = [r for r in result.autoload_refs if r.autoload_name == "EconomyManager" and r.pattern == "direct"]
        assert len(econ) >= 1
        res = [r for r in result.autoload_refs if r.autoload_name == "ResearchManager" and r.pattern == "direct"]
        assert len(res) >= 1
        # has_node and get_node for ProgressionManager
        hn = [r for r in result.autoload_refs if r.autoload_name == "ProgressionManager" and r.pattern == "has_node"]
        assert len(hn) >= 1
        gn = [r for r in result.autoload_refs if r.autoload_name == "ProgressionManager" and r.pattern == "get_node"]
        assert len(gn) >= 1
        # get_node_or_null
        gnor = [r for r in result.autoload_refs if r.pattern == "get_node_or_null"]
        assert len(gnor) >= 1
        # is_instance_valid
        assert len(result.is_instance_valid_lines) >= 1

    def test_progression_manager(self) -> None:
        """progression_manager.gd: multiline const, connect, 4 signals."""
        result = parse_gd(
            str(FIXTURES / "progression_manager.gd"),
            known_autoloads=["GameManager"],
        )
        assert result.class_name == "ProgressionManagerClass"
        assert result.extends == "Node"
        assert len(result.signals) == 4
        sig_names = [s.name for s in result.signals]
        assert "xp_gained" in sig_names
        assert "level_up" in sig_names
        assert "product_unlocked" in sig_names
        assert "feature_unlocked" in sig_names
        # Multiline const LEVEL_XP
        lxp = [v for v in result.variables if v.name == "LEVEL_XP"][0]
        assert lxp.is_const is True
        assert lxp.type_hint == "Array[int]"
        assert lxp.default_value  # non-empty
        # Multiline const UNLOCK_TABLE
        ut = [v for v in result.variables if v.name == "UNLOCK_TABLE"][0]
        assert ut.is_const is True
        assert ut.type_hint == "Dictionary"
        assert ut.default_value  # non-empty
        # Signal connection
        assert len(result.signal_connections) >= 1
        lu_conn = [c for c in result.signal_connections if "level_up" in c.signal_source]
        assert len(lu_conn) >= 1
        # is_instance_valid
        assert len(result.is_instance_valid_lines) >= 2
        # get_node_or_null
        gnor = [r for r in result.autoload_refs if r.pattern == "get_node_or_null"]
        assert len(gnor) >= 1
        # GameManager direct ref
        gm = [r for r in result.autoload_refs if r.autoload_name == "GameManager" and r.pattern == "direct"]
        assert len(gm) >= 1
        # Functions
        assert len(result.functions) >= 14

    def test_minimal_printer_fixture(self) -> None:
        """Minimal printer.gd: only extends Node3D."""
        result = parse_gd(str(SAMPLE_FIXTURES / "printer.gd"))
        assert result.extends == "Node3D"
        assert result.class_name == ""
        assert result.signals == []
        assert result.functions == []
        assert result.variables == []
        assert result.enums == []

    def test_minimal_worker_fixture(self) -> None:
        """Minimal worker.gd: only extends CharacterBody3D."""
        result = parse_gd(str(SAMPLE_FIXTURES / "worker.gd"))
        assert result.extends == "CharacterBody3D"
        assert result.class_name == ""
        assert result.signals == []
        assert result.functions == []
        assert result.variables == []
        assert result.enums == []
