"""Tests for the GodotIQ session layer."""

from __future__ import annotations

from pathlib import Path

import pytest

from godotiq.session import GodotIQSession, discover_project_root
from godotiq.parsers.gd_parser import GdScript
from godotiq.parsers.tscn_parser import TscnScene


@pytest.fixture
def sample_root() -> Path:
    return Path(__file__).resolve().parent.parent / "fixtures" / "sample_project"


@pytest.fixture
def session(sample_root: Path) -> GodotIQSession:
    s = GodotIQSession(sample_root)
    s.load()
    return s


class TestSessionLoadsProject:
    def test_session_loads_project(self, session: GodotIQSession) -> None:
        """Session.load() populates the project index."""
        assert session._index is not None
        assert session._index.project_name == "Sample Project"
        assert session._index.total_scenes >= 2
        assert session._index.total_scripts >= 4

    def test_session_finds_autoloads(self, session: GodotIQSession) -> None:
        """Session discovers all autoloads from project.godot."""
        assert session._index is not None
        assert "GameManager" in session._index.autoloads
        assert "EconomyManager" in session._index.autoloads
        assert "ProgressionManager" in session._index.autoloads

    def test_session_parses_gd_with_autoloads(self, session: GodotIQSession) -> None:
        """Session parses all .gd files with known autoload names."""
        assert len(session._gd_scripts) >= 4
        for path, gd in session._gd_scripts.items():
            assert isinstance(gd, GdScript)

    def test_session_autoload_pattern1(self, session: GodotIQSession) -> None:
        """Pattern 1 autoload detection works when session provides known_autoloads."""
        prog_path = "res://scripts/autoloads/progression_manager.gd"
        gd = session.get_script(prog_path)
        assert gd is not None
        autoload_names = [ref.autoload_name for ref in gd.autoload_refs]
        assert "GameManager" in autoload_names
        assert "EconomyManager" in autoload_names


class TestSessionQueries:
    def test_session_get_script(self, session: GodotIQSession) -> None:
        """get_script() returns parsed GdScript for valid res:// path."""
        gd = session.get_script("res://scripts/autoloads/economy_manager.gd")
        assert gd is not None
        assert gd.class_name == "EconomyManagerClass"

    def test_session_get_script_missing(self, session: GodotIQSession) -> None:
        """get_script() returns None for unknown path."""
        assert session.get_script("res://nonexistent.gd") is None

    def test_session_caches_scenes(self, session: GodotIQSession) -> None:
        """get_scene() returns TscnScene and caches on second call."""
        scene = session.get_scene("res://scenes/main.tscn")
        assert scene is not None
        assert isinstance(scene, TscnScene)
        scene2 = session.get_scene("res://scenes/main.tscn")
        assert scene2 is scene

    def test_signal_definitions(self, session: GodotIQSession) -> None:
        """get_all_signal_definitions() maps signal names to defining scripts."""
        defs = session.get_all_signal_definitions()
        assert "cash_changed" in defs
        assert any("economy_manager" in p for p in defs["cash_changed"])

    def test_signal_emissions(self, session: GodotIQSession) -> None:
        """get_all_signal_emissions() maps signal names to emitting scripts."""
        emissions = session.get_all_signal_emissions()
        assert "cash_changed" in emissions

    def test_signal_connections(self, session: GodotIQSession) -> None:
        """get_all_signal_connections() maps signal source to connecting scripts."""
        connections = session.get_all_signal_connections()
        assert isinstance(connections, dict)

    def test_autoload_dependents(self, session: GodotIQSession) -> None:
        """get_autoload_dependents() returns scripts that reference an autoload."""
        deps = session.get_autoload_dependents("GameManager")
        assert len(deps) >= 1

    def test_resolve_scene_spatial(self, session: GodotIQSession) -> None:
        """resolve_scene_spatial() returns resolved scene with world nodes."""
        result = session.resolve_scene_spatial("res://scenes/main.tscn")
        assert result is not None
        resolved, world_nodes = result
        assert len(world_nodes) > 0
        # Cached on second call
        result2 = session.resolve_scene_spatial("res://scenes/main.tscn")
        assert result2 is result

    def test_resolve_scene_spatial_missing(self, session: GodotIQSession) -> None:
        """resolve_scene_spatial() returns None for missing scene."""
        assert session.resolve_scene_spatial("res://nonexistent.tscn") is None

    def test_to_res_path(self, session: GodotIQSession) -> None:
        """to_res_path() converts filesystem paths to res:// paths."""
        res = session.to_res_path("scripts/autoloads/economy_manager.gd")
        assert res == "res://scripts/autoloads/economy_manager.gd"
        # Already res:// should pass through
        assert session.to_res_path("res://foo.gd") == "res://foo.gd"

    def test_to_res_path_absolute(self, session: GodotIQSession) -> None:
        """to_res_path() converts absolute paths by stripping project root."""
        abs_path = str(session.project_root / "scripts" / "autoloads" / "economy_manager.gd")
        res = session.to_res_path(abs_path)
        assert res == "res://scripts/autoloads/economy_manager.gd"

    def test_resolve_file_path(self, session: GodotIQSession) -> None:
        """resolve_file_path() finds files by res:// or relative path."""
        res = session.resolve_file_path("res://scripts/autoloads/economy_manager.gd")
        assert res == "res://scripts/autoloads/economy_manager.gd"
        assert session.resolve_file_path("res://nonexistent.gd") is None


class TestProjectRootDiscovery:
    def test_discover_from_env(self, sample_root: Path, monkeypatch) -> None:
        """Discovers project root from GODOTIQ_PROJECT_ROOT env var."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(sample_root))
        result = discover_project_root()
        assert result is not None
        assert result == sample_root

    def test_discover_returns_none_when_not_found(self, tmp_path: Path, monkeypatch) -> None:
        """Returns None when no project root can be found."""
        monkeypatch.delenv("GODOTIQ_PROJECT_ROOT", raising=False)
        monkeypatch.chdir(tmp_path)
        result = discover_project_root()
        assert result is None

    def test_discover_from_walk_up(self, sample_root: Path, monkeypatch) -> None:
        """Discovers project root by walking up from a subdirectory."""
        monkeypatch.delenv("GODOTIQ_PROJECT_ROOT", raising=False)
        monkeypatch.chdir(sample_root / "scripts" / "entities")
        result = discover_project_root()
        assert result is not None
        assert result == sample_root
