"""Tests for script_ops filesystem tool."""

from __future__ import annotations

import pytest
from pathlib import Path

from godotiq.tools.bridge.script_ops import script_ops, _resolve_path, _is_protected, _validate_gdscript


class TestResolvePath:
    """Path resolution and containment validation."""

    def test_res_prefix_resolves_relative_to_project_root(self, tmp_path: Path, monkeypatch) -> None:
        """_resolve_path with res:// prefix resolves relative to GODOTIQ_PROJECT_ROOT."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = _resolve_path("res://scripts/player.gd")
        assert result == tmp_path / "scripts" / "player.gd"

    def test_relative_path_resolves_relative_to_project_root(self, tmp_path: Path, monkeypatch) -> None:
        """_resolve_path with relative path resolves relative to GODOTIQ_PROJECT_ROOT."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = _resolve_path("scripts/player.gd")
        assert result == tmp_path / "scripts" / "player.gd"

    def test_path_traversal_raises_error(self, tmp_path: Path, monkeypatch) -> None:
        """_resolve_path with ../../etc/passwd raises error (containment check)."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        with pytest.raises(ValueError, match="outside project"):
            _resolve_path("../../etc/passwd")

    def test_res_traversal_raises_error(self, tmp_path: Path, monkeypatch) -> None:
        """_resolve_path with res://../.. traversal raises error."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        with pytest.raises(ValueError, match="outside project"):
            _resolve_path("res://../../etc/passwd")


class TestIsProtected:
    """Protected file pattern matching."""

    def test_project_godot_is_protected(self) -> None:
        """_is_protected returns True for project.godot."""
        assert _is_protected("project.godot") is True

    def test_godot_dir_is_protected(self) -> None:
        """_is_protected returns True for paths inside .godot/."""
        assert _is_protected(".godot/imported/thing.res") is True

    def test_import_files_are_protected(self) -> None:
        """_is_protected returns True for .import files."""
        assert _is_protected("icon.png.import") is True

    def test_saves_dir_is_protected(self) -> None:
        """_is_protected returns True for paths inside saves/."""
        assert _is_protected("saves/game.sav") is True

    def test_regular_gd_file_not_protected(self) -> None:
        """_is_protected returns False for regular .gd files."""
        assert _is_protected("scripts/player.gd") is False


class TestReadMode:
    """Read operation tests."""

    async def test_read_returns_content_and_metadata(self, tmp_path: Path, monkeypatch) -> None:
        """Read mode returns file content, line count, and size."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("extends Node\n\nfunc _ready():\n\tpass\n", encoding="utf-8")
        result = await script_ops(op="read", path="test.gd")
        assert result["status"] == "OK"
        assert "extends Node" in result["content"]
        assert result["lines"] == 4
        assert result["size_bytes"] > 0

    async def test_read_missing_file_returns_error(self, tmp_path: Path, monkeypatch) -> None:
        """Read mode raises error for missing file."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="read", path="nonexistent.gd")
        assert result["status"] == "ERROR"
        assert "not found" in result["error"].lower() or "Not found" in result["error"]


class TestWriteMode:
    """Write operation tests."""

    async def test_write_creates_file(self, tmp_path: Path, monkeypatch) -> None:
        """Write mode creates/overwrites file with content."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="write", path="new_script.gd", content="extends Node\n", validate_before_write=False)
        assert result["status"] == "OK"
        assert result["written"] is True
        assert (tmp_path / "new_script.gd").read_text() == "extends Node\n"

    async def test_write_rejects_protected_files(self, tmp_path: Path, monkeypatch) -> None:
        """Write mode rejects protected files."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="write", path="project.godot", content="test")
        assert result["status"] == "BLOCKED"

    async def test_write_with_validation(self, tmp_path: Path, monkeypatch) -> None:
        """Write mode with validate_before_write=True runs GDScript validation."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        content = "extends Node\nvar speed = 10\n"
        result = await script_ops(op="write", path="test.gd", content=content, validate_before_write=True)
        assert result["status"] == "OK"
        # validation_issues may or may not be empty depending on the content
        assert "validation_issues" in result

    async def test_write_dry_run_does_not_write(self, tmp_path: Path, monkeypatch) -> None:
        """Write mode with dry_run=True validates but doesn't write."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="write", path="test.gd", content="extends Node\n", dry_run=True, validate_before_write=False)
        assert result["status"] == "DRY_RUN"
        assert result["written"] is False
        assert not (tmp_path / "test.gd").exists()

    async def test_write_path_traversal_blocked(self, tmp_path: Path, monkeypatch) -> None:
        """Write mode path traversal blocked."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="write", path="../../etc/evil.gd", content="test")
        assert result["status"] == "ERROR"


class TestPatchMode:
    """Patch operation tests."""

    async def test_patch_single_replacement(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode applies single find-and-replace."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("var speed = 10\n", encoding="utf-8")
        result = await script_ops(
            op="patch", path="test.gd",
            patches=[{"search": "speed = 10", "replace": "speed: float = 20.0"}],
            validate_before_write=False,
        )
        assert result["status"] == "OK"
        assert result["written"] is True
        assert "speed: float = 20.0" in f.read_text()

    async def test_patch_multiple_sequential(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode applies multiple patches sequentially."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("var a = 1\nvar b = 2\n", encoding="utf-8")
        result = await script_ops(
            op="patch", path="test.gd",
            patches=[
                {"search": "var a = 1", "replace": "var a: int = 1"},
                {"search": "var b = 2", "replace": "var b: int = 2"},
            ],
            validate_before_write=False,
        )
        assert result["status"] == "OK"
        content = f.read_text()
        assert "var a: int = 1" in content
        assert "var b: int = 2" in content

    async def test_patch_rejects_non_unique_search(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode rejects non-unique search string (appears 0 or 2+ times)."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("pass\npass\n", encoding="utf-8")
        result = await script_ops(
            op="patch", path="test.gd",
            patches=[{"search": "pass", "replace": "return"}],
            validate_before_write=False,
        )
        assert result["status"] == "ERROR"
        assert len(result["failed"]) > 0

    async def test_patch_line_numbers_before_replacement(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode returns line numbers calculated BEFORE replacement."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("line1\nTARGET\nline3\n", encoding="utf-8")
        result = await script_ops(
            op="patch", path="test.gd",
            patches=[{"search": "TARGET", "replace": "REPLACED"}],
            validate_before_write=False,
        )
        assert result["status"] == "OK"
        assert result["applied"][0]["line"] == 2

    async def test_patch_rejects_protected_files(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode rejects protected files."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "project.godot"
        f.write_text("config_version=5\n", encoding="utf-8")
        result = await script_ops(
            op="patch", path="project.godot",
            patches=[{"search": "5", "replace": "4"}],
        )
        assert result["status"] == "BLOCKED"

    async def test_patch_empty_patches_returns_no_changes(self, tmp_path: Path, monkeypatch) -> None:
        """Patch mode with empty patches list returns unchanged file."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        f = tmp_path / "test.gd"
        f.write_text("extends Node\n", encoding="utf-8")
        result = await script_ops(op="patch", path="test.gd", patches=[])
        assert result["written"] is False or result["status"] == "ERROR"


class TestCreateMode:
    """Create operation tests."""

    async def test_create_new_file(self, tmp_path: Path, monkeypatch) -> None:
        """Create mode creates new file."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="create", path="new.gd", content="extends Node\n")
        assert result["status"] == "OK"
        assert result["created"] is True
        assert (tmp_path / "new.gd").exists()

    async def test_create_fails_if_exists(self, tmp_path: Path, monkeypatch) -> None:
        """Create mode fails if file already exists."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        (tmp_path / "existing.gd").write_text("old", encoding="utf-8")
        result = await script_ops(op="create", path="existing.gd", content="new")
        assert result["status"] == "ERROR"
        assert "exists" in result["error"].lower()

    async def test_create_rejects_protected_paths(self, tmp_path: Path, monkeypatch) -> None:
        """Create mode rejects protected paths."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="create", path=".godot/cache.gd", content="test")
        assert result["status"] == "BLOCKED"

    async def test_create_dry_run_does_not_write(self, tmp_path: Path, monkeypatch) -> None:
        """Create mode with dry_run=True does not create the file."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="create", path="new.gd", content="extends Node\n", dry_run=True)
        assert result["status"] == "DRY_RUN"
        assert result["created"] is False
        assert not (tmp_path / "new.gd").exists()


class TestUnknownOp:
    """Unknown operation handling."""

    async def test_unknown_op_returns_error(self, tmp_path: Path, monkeypatch) -> None:
        """Unknown operation returns error status."""
        monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))
        result = await script_ops(op="delete", path="test.gd")
        assert result["status"] == "ERROR"
        assert "Unknown" in result["error"]


class TestValidation:
    """GDScript convention validation."""

    def test_validate_warns_on_missing_type_hints(self) -> None:
        """_validate_gdscript checks for type hints (basic convention check)."""
        content = "extends Node\nvar speed = 10\n"
        issues = _validate_gdscript(content, "test.gd")
        assert len(issues) > 0
        assert any(i["rule"] == "type_hint" for i in issues)
