"""Tests for file_ops filesystem tool."""

from __future__ import annotations

import pytest

from godotiq.tools.bridge.file_ops import file_ops, _resolve, _is_protected, _matches_type_filter


@pytest.fixture
def project(tmp_path, monkeypatch):
    """Set up a fake Godot project directory with sample files."""
    monkeypatch.setenv("GODOTIQ_PROJECT_ROOT", str(tmp_path))

    # Create project.godot so it looks like a real project
    (tmp_path / "project.godot").write_text("[gd_resource]")

    # Create sample files
    (tmp_path / "main.tscn").write_text("[gd_scene format=3]")
    (tmp_path / "player.gd").write_text("extends Node\nfunc _ready():\n\tpass\n")
    (tmp_path / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)

    # Create subdirectory structure
    scripts_dir = tmp_path / "scripts"
    scripts_dir.mkdir()
    (scripts_dir / "enemy.gd").write_text(
        "extends CharacterBody3D\nvar speed: float = 5.0\n"
    )
    (scripts_dir / "utils.gd").write_text(
        "class_name Utils\nstatic func helper() -> void:\n\tpass\n"
    )

    scenes_dir = tmp_path / "scenes"
    scenes_dir.mkdir()
    (scenes_dir / "level1.tscn").write_text("[gd_scene format=3]")

    # Create excluded directories
    godot_dir = tmp_path / ".godot"
    godot_dir.mkdir()
    (godot_dir / "cache.bin").write_bytes(b"\x00" * 10)

    git_dir = tmp_path / ".git"
    git_dir.mkdir()
    (git_dir / "config").write_text("[core]")

    # Create saves dir (protected)
    saves_dir = tmp_path / "saves"
    saves_dir.mkdir()
    (saves_dir / "save1.tres").write_text("[resource]")

    return tmp_path


class TestResolve:
    """Path resolution and containment checks."""

    async def test_res_prefix(self, project, monkeypatch):
        """res:// paths resolve relative to project root."""
        result = _resolve("res://player.gd")
        assert result == project / "player.gd"

    async def test_relative_path(self, project, monkeypatch):
        """Relative paths resolve against project root."""
        result = _resolve("scripts/enemy.gd")
        assert result == project / "scripts" / "enemy.gd"

    async def test_path_traversal_blocked(self, project, monkeypatch):
        """Path traversal outside project root raises ValueError."""
        with pytest.raises(ValueError, match="outside"):
            _resolve("../../etc/passwd")


class TestIsProtected:
    """Protected file detection."""

    def test_protected_files(self, project):
        """Known protected patterns are correctly identified."""
        root = project
        assert _is_protected(root / "project.godot") is True
        assert _is_protected(root / ".godot" / "cache.bin") is True
        assert _is_protected(root / "something.import") is True
        assert _is_protected(root / "saves" / "save1.tres") is True
        # Not protected
        assert _is_protected(root / "player.gd") is False
        assert _is_protected(root / "main.tscn") is False


class TestListOperation:
    """The 'list' operation."""

    async def test_list_root(self, project):
        """Listing project root returns top-level entries."""
        result = await file_ops(op="list")
        assert "entries" in result
        names = [e["name"] for e in result["entries"]]
        assert "main.tscn" in names
        assert "player.gd" in names

    async def test_list_filter_scripts(self, project):
        """Type filter narrows results to matching extensions."""
        result = await file_ops(op="list", filter="scripts")
        assert all(
            e["name"].endswith(".gd")
            for e in result["entries"]
            if e["type"] == "file"
        )

    async def test_list_recursive(self, project):
        """Recursive listing includes files in subdirectories."""
        result = await file_ops(op="list", recursive=True, filter="scripts")
        names = [e["name"] for e in result["entries"]]
        assert "enemy.gd" in names or any("enemy.gd" in n for n in names)

    async def test_list_excludes_system_dirs(self, project):
        """System/hidden directories are automatically excluded."""
        result = await file_ops(op="list")
        names = [e["name"] for e in result["entries"]]
        assert ".godot" not in names
        assert ".git" not in names

    async def test_list_missing_dir(self, project):
        """Listing a nonexistent directory returns an error."""
        result = await file_ops(op="list", path="nonexistent_dir")
        assert "error" in result


class TestReadOperation:
    """The 'read' operation."""

    async def test_read_text_file(self, project):
        """Reading a text file returns its content."""
        result = await file_ops(op="read", path="player.gd")
        assert result["content"] == "extends Node\nfunc _ready():\n\tpass\n"
        assert result["lines"] == 3

    async def test_read_binary_rejected(self, project):
        """Attempting to read a binary file returns an error."""
        result = await file_ops(op="read", path="icon.png")
        assert "error" in result
        assert "binary" in result["error"].lower()

    async def test_read_missing_file(self, project):
        """Reading a nonexistent file returns an error."""
        result = await file_ops(op="read", path="missing.gd")
        assert "error" in result


class TestWriteOperation:
    """The 'write' operation."""

    async def test_write_creates_file(self, project):
        """Writing to a new path creates the file."""
        result = await file_ops(op="write", path="new_file.cfg", content="[config]\nkey=value\n")
        assert result["op"] == "write"
        assert (project / "new_file.cfg").exists()
        assert (project / "new_file.cfg").read_text() == "[config]\nkey=value\n"

    async def test_write_protected_rejected(self, project):
        """Writing to a protected file returns an error."""
        result = await file_ops(op="write", path="project.godot", content="bad")
        assert "error" in result
        assert "protected" in result["error"].lower()

    async def test_write_traversal_blocked(self, project):
        """Write with path traversal is rejected."""
        result = await file_ops(op="write", path="../../evil.txt", content="bad")
        assert "error" in result


class TestMoveOperation:
    """The 'move' operation."""

    async def test_move_renames_file(self, project):
        """Moving a file to a new name renames it on disk."""
        result = await file_ops(op="move", path="player.gd", destination="player_old.gd")
        assert result.get("success") is True
        assert not (project / "player.gd").exists()
        assert (project / "player_old.gd").exists()

    async def test_move_protected_source_rejected(self, project):
        """Cannot move a protected source file."""
        result = await file_ops(op="move", path="project.godot", destination="renamed.godot")
        assert "error" in result

    async def test_move_protected_destination_rejected(self, project):
        """Cannot move into a protected destination path."""
        result = await file_ops(op="move", path="player.gd", destination="saves/player.gd")
        assert "error" in result

    async def test_move_traversal_blocked(self, project):
        """Path traversal is blocked for both source and destination."""
        result = await file_ops(op="move", path="../../etc/passwd", destination="stolen.txt")
        assert "error" in result
        result2 = await file_ops(op="move", path="player.gd", destination="../../evil.gd")
        assert "error" in result2


class TestDeleteOperation:
    """The 'delete' operation."""

    async def test_delete_removes_file(self, project):
        """Deleting an existing file removes it from disk."""
        result = await file_ops(op="delete", path="player.gd")
        assert result.get("success") is True
        assert not (project / "player.gd").exists()

    async def test_delete_protected_rejected(self, project):
        """Cannot delete a protected file."""
        result = await file_ops(op="delete", path="project.godot")
        assert "error" in result

    async def test_delete_traversal_blocked(self, project):
        """Delete with path traversal is rejected."""
        result = await file_ops(op="delete", path="../../etc/passwd")
        assert "error" in result


class TestSearchOperation:
    """The 'search' operation."""

    async def test_search_finds_matches(self, project):
        """Search returns files containing the query string."""
        result = await file_ops(op="search", query="extends")
        assert len(result["matches"]) >= 2  # player.gd and enemy.gd

    async def test_search_context_lines(self, project):
        """Context lines are included around each match."""
        result = await file_ops(op="search", query="_ready", context_lines=1)
        match = result["matches"][0]
        assert "context_before" in match
        assert "context_after" in match

    async def test_search_with_filter(self, project):
        """Type filter restricts which files are searched."""
        result = await file_ops(op="search", query="format=3", filter="scenes")
        for m in result["matches"]:
            assert m["file"].endswith(".tscn")

    async def test_search_excludes_system_dirs(self, project):
        """System directories are skipped during search."""
        result = await file_ops(op="search", query="core")
        for m in result["matches"]:
            assert ".git" not in m["file"]
            assert ".godot" not in m["file"]

    async def test_search_max_matches_cap(self, project):
        """Results are capped at 50 matches maximum."""
        # Create many files with matching content
        bulk_dir = project / "bulk"
        bulk_dir.mkdir()
        for i in range(60):
            (bulk_dir / f"file_{i}.gd").write_text(f"extends Node  # file {i}\n")

        result = await file_ops(op="search", query="extends Node")
        assert len(result["matches"]) <= 50
        assert result["capped"] is True


class TestTreeOperation:
    """The 'tree' operation."""

    async def test_tree_with_depth(self, project):
        """Tree respects max_depth parameter."""
        result = await file_ops(op="tree", max_depth=1)
        assert result["op"] == "tree"
        tree = result["tree"]
        assert tree["type"] == "dir"
        # At depth 1, subdirectories should have no children key
        for child in tree.get("children", []):
            if child["type"] == "dir":
                assert "children" not in child, f"Subdirectory {child['name']} should not have children at max_depth=1"

    async def test_tree_with_sizes(self, project):
        """File sizes are included when requested."""
        result = await file_ops(op="tree", show_sizes=True)
        tree = result["tree"]
        # Find a file entry and check it has size
        for child in tree.get("children", []):
            if child["type"] == "file":
                assert "size" in child
                break

    async def test_tree_excludes_system_dirs(self, project):
        """System directories (.godot, .git, etc.) are excluded from tree."""
        result = await file_ops(op="tree", max_depth=2)
        tree = result["tree"]
        child_names = [c["name"] for c in tree.get("children", [])]
        assert ".godot" not in child_names
        assert ".git" not in child_names


class TestTypeFilters:
    """Type filter matching logic."""

    def test_scene_filter(self):
        """Scene filter matches .tscn and .scn extensions."""
        assert _matches_type_filter("level.tscn", "scenes") is True
        assert _matches_type_filter("level.scn", "scenes") is True
        assert _matches_type_filter("script.gd", "scenes") is False

    def test_script_filter(self):
        """Script filter matches .gd extension."""
        assert _matches_type_filter("player.gd", "scripts") is True
        assert _matches_type_filter("level.tscn", "scripts") is False

    def test_all_filter(self):
        """The 'all' filter matches any file."""
        assert _matches_type_filter("anything.xyz", "all") is True
        assert _matches_type_filter("script.gd", "all") is True
