# GDScript Parser — TDD Test Plan

Mirrors the structure of `claude-plan.md`. For each implementation section, defines what tests to write FIRST using pytest in `tests/test_parsers/test_gd_parser.py`.

**Testing framework:** pytest with `asyncio_mode = "auto"`
**Fixtures:** Real .gd files in `tests/fixtures/` + inline strings via `_parse_gd_content()`
**Convention:** Class-based test groups, `test_<feature>_<scenario>()` naming

---

## 3. Data Model Tests

### 3.1 GdSignal

```python
# Test: GdSignal default values (empty name, empty args, line 0)
# Test: GdSignal stores (name, type) tuples for typed args
# Test: GdSignal with custom type like "Printer.State" stored as-is
```

### 3.2 GdFunction

```python
# Test: GdFunction default values
# Test: GdFunction arg triples (name, type, default) — all empty strings when absent
# Test: GdFunction is_virtual and is_private both True for underscore-prefixed names
# Test: GdFunction is_static flag
# Test: GdFunction end_line >= line
```

### 3.3 GdVariable

```python
# Test: GdVariable default values (scope="class")
# Test: GdVariable with annotation stores raw annotation text
# Test: GdVariable is_const flag
# Test: GdVariable is_static flag
```

---

## 4. State Machine — Class Declarations

### TestClassDeclarations

```python
# Test: parse class_name from "class_name EconomyManagerClass" → class_name="EconomyManagerClass"
# Test: parse extends from "extends Node" → extends="Node"
# Test: parse combined "class_name Item extends Node" → both set
# Test: parse extends with file path "extends \"res://path.gd\"" → extends='"res://path.gd"'
# Test: minimal file "extends Node3D" only → extends set, everything else empty
# Test: empty file → all fields empty, no exceptions
# Test: missing file → raises FileNotFoundError
```

---

## 4. State Machine — Signal Parsing

### TestSignalParsing

```python
# Test: signal without args "signal health_depleted" → args=[]
# Test: signal with untyped args "signal foo(a, b)" → [("a", ""), ("b", "")]
# Test: signal with typed args "signal cash_changed(amount: float, delta: float)"
#       → [("amount", "float"), ("delta", "float")]
# Test: signal with custom type "signal any_state_changed(id: String, old: Printer.State, new: Printer.State)"
#       → type stored as "Printer.State"
# Test: multiple signals in one file → correct count
# Test: signal line numbers match source
```

**Integration test:** economy_manager.gd has 3 signals (cash_changed, transaction_recorded, insufficient_funds)

---

## 4. State Machine — Function Parsing

### TestFunctionParsing

```python
# Test: basic func "func _ready() -> void:" → name="_ready", return_type="void", args=[]
# Test: func with typed params and default "func add_cash(amount: float, description: String = \"\") -> void:"
#       → args=[("amount", "float", ""), ("description", "String", '""')]
# Test: func return bool "func can_afford(amount: float) -> bool:" → return_type="bool"
# Test: static func "static func sum2(a, b):" → is_static=True
# Test: virtual func "_ready" → is_virtual=True, is_private=True
# Test: non-virtual func "add_cash" → is_virtual=False, is_private=False
# Test: func with no return type "func foo():" → return_type=""
# Test: func with complex return type "func get_idle_printers() -> Array[Printer]:"
#       → return_type="Array[Printer]"
# Test: func arg with parens in default "func f(v: Vector2 = Vector2(1, 2)):"
#       → depth-aware splitting produces correct arg triple
# Test: end_line for function — last non-blank, non-comment line in body
# Test: end_line for last function in file — last non-blank line of file
# Test: one-liner func "func square(a): return a * a" → end_line == line
```

**Integration test:** economy_manager.gd has 8 functions

---

## 4. State Machine — Variable Parsing

### TestVariableParsing

```python
# Test: typed var "var cash: float = 100.0" → type_hint="float", default_value="100.0"
# Test: untyped var "var printers: Dictionary = {}" → type_hint="Dictionary", default_value="{}"
# Test: const "const ANSWER = 42" → is_const=True, default_value="42"
# Test: typed const "const LEVEL_XP: Array[int] = [...]" → is_const=True, type_hint="Array[int]"
# Test: @export same-line "@export var speed: float = 5.0" → annotation="@export"
# Test: @export_range same-line "@export_range(0, 100) var hp: int" → annotation="@export_range(0, 100)"
# Test: @onready same-line "@onready var sprite := $Sprite2D" → annotation="@onready"
# Test: static var "static var balance: int = 0" → is_static=True
# Test: scope tracking — class-level var → scope="class"
# Test: scope tracking — var inside function body NOT captured
# Test: multiline const array — bracket depth tracking produces complete default_value
# Test: multiline var dict (session_stats pattern) — IN_MULTILINE_VALUE triggers for var too
# Test: property getter/setter "var health: int:" — captured as variable, body skipped
# Test: inline comment stripped — "var speed: float = 5.0  # fast" → default_value="5.0"
# Test: @export_group standalone — recognized and discarded, no buffer corruption
```

**Integration test:** progression_manager.gd has const LEVEL_XP (multiline array) and UNLOCK_TABLE (multiline dict)

---

## 4. State Machine — Enum Parsing

### TestEnumParsing

```python
# Test: named enum "enum GameState { PLAYING, PAUSED, DAILY_SUMMARY }"
#       → name="GameState", values={"PLAYING": None, "PAUSED": None, "DAILY_SUMMARY": None}
# Test: enum with explicit values "enum { A, B = 5, C }" → B=5, C=None
# Test: enum with negative values "enum { X = -1 }" → X=-1
# Test: anonymous enum "enum { A, B }" → name=""
# Test: multiline enum — bracket accumulation
# Test: enum line number
```

**Integration test:** game_manager.gd has enum GameState

---

## 4. State Machine — Signal Connections

### TestSignalConnections

```python
# Test: direct connect "signal.connect(_handler)" → target="_handler", has_bind=False
# Test: connect with bind "signal.connect(_handler.bind(arg))" → has_bind=True, bind_args="arg"
# Test: connect with $Node "$Timer.timeout.connect(_on_timeout)" → signal_source="$Timer.timeout"
# Test: connect with lambda "level_up.connect(func(_l, _u): fired[0] = true)"
#       → target starts with "func"
# Test: depth-aware parsing — connect call with trailing code
# Test: signal_source captures full chain "printer.print_started"
```

**Integration test:** printer_manager.gd has 4 connect calls in register_printer()

---

## 4. State Machine — Signal Emissions

### TestSignalEmissions

```python
# Test: emit no args "health_depleted.emit()" → arg_count=0
# Test: emit 2 args "cash_changed.emit(cash, amount)" → arg_count=2
# Test: emit 3 args "any_print_failed.emit(id, product, reason)" → arg_count=3
# Test: emit with nested parens in args — depth-aware counting
```

**Integration test:** economy_manager.gd has multiple .emit() calls

---

## 4. State Machine — Autoload References

### TestAutoloadRefs

```python
# Test: direct autoload "EconomyManager.spend_cash(...)" with known_autoloads=["EconomyManager"]
#       → pattern="direct", autoload_name="EconomyManager"
# Test: no detection without known_autoloads → direct refs not captured
# Test: has_node pattern 'has_node("/root/ProgressionManager")' → pattern="has_node"
# Test: get_node pattern 'get_node("/root/ProgressionManager")' → pattern="get_node"
# Test: get_node_or_null pattern → pattern="get_node_or_null"
# Test: has_node and get_node detected even without known_autoloads
# Test: multiple autoload refs on different lines each create separate entries
```

**Integration test:** game_manager.gd references EconomyManager, ResearchManager, ProgressionManager

---

## 4. State Machine — Preload/Load

### TestPreloadLoad

```python
# Test: preload 'preload("res://scenes/player.tscn")' → is_preload=True, path="res://scenes/player.tscn"
# Test: load 'load("res://data/items.tres")' → is_preload=False
# Test: preload inside const creates BOTH GdVariable and GdPreloadRef
# Test: preload in function body also creates GdPreloadRef
```

---

## 4. State Machine — Other Patterns

### TestIsInstanceValid

```python
# Test: is_instance_valid detected and line recorded
# Test: multiple is_instance_valid calls → multiple line entries
```

**Integration test:** printer_manager.gd has many is_instance_valid() calls

### TestInnerClasses

```python
# Test: inner class "class Circle extends Shape:" → "Circle" in inner_classes
# Test: inner class body skipped — vars/funcs inside NOT added to outer class
# Test: inner class without extends "class Something:" → name captured
```

---

## Full Integration Tests

### TestIntegration

```python
# Test: economy_manager.gd — class_name="EconomyManagerClass", extends="Node",
#       3 signals, 8 functions, 5 class variables, multiple .emit() calls
# Test: printer_manager.gd — signals with Printer.State type, is_instance_valid usage,
#       signal connections in register_printer, get_node_or_null patterns
# Test: game_manager.gd — enum GameState, autoload refs (EconomyManager, ResearchManager),
#       has_node/get_node patterns, multiline var session_stats, is_instance_valid
# Test: progression_manager.gd — multiline const LEVEL_XP, multiline const UNLOCK_TABLE,
#       lambda in connect, 4 signals
# Test: minimal files (printer.gd, worker.gd) — just extends, everything else empty
# Test: parse all fixtures without exceptions (parametrized)
```

---

## Edge Case Tests

### TestEdgeCases

```python
# Test: empty string content → empty GdScript, no exceptions
# Test: file with only comments → empty GdScript
# Test: malformed line does not raise — silently skipped
# Test: unclosed multiline construct at EOF — closes with accumulated content
# Test: annotation buffer not corrupted by @rpc or @warning_ignore before func
# Test: @export_group does not corrupt annotation buffer
```
