"""Extended tests for file_tools module."""

import importlib
from pathlib import Path

import pytest

file_tools = importlib.import_module("gjalla_precommit.tools.file_tools")


class TestIsImageFile:
    """Tests for is_image_file function."""

    def test_png_is_image(self):
        """PNG files should be detected as images."""
        assert file_tools.is_image_file("screenshot.png") is True

    def test_jpg_is_image(self):
        """JPG files should be detected as images."""
        assert file_tools.is_image_file("photo.jpg") is True
        assert file_tools.is_image_file("photo.jpeg") is True

    def test_gif_is_image(self):
        """GIF files should be detected as images."""
        assert file_tools.is_image_file("animation.gif") is True

    def test_svg_is_image(self):
        """SVG files should be detected as images."""
        assert file_tools.is_image_file("icon.svg") is True

    def test_webp_is_image(self):
        """WebP files should be detected as images."""
        assert file_tools.is_image_file("modern.webp") is True

    def test_py_not_image(self):
        """Python files should not be detected as images."""
        assert file_tools.is_image_file("main.py") is False

    def test_txt_not_image(self):
        """Text files should not be detected as images."""
        assert file_tools.is_image_file("readme.txt") is False

    def test_case_insensitive(self):
        """Image detection should be case-insensitive."""
        assert file_tools.is_image_file("PHOTO.PNG") is True
        assert file_tools.is_image_file("Photo.JPG") is True


class TestIsBinaryFile:
    """Tests for _is_binary_file function."""

    def test_text_file_not_binary(self, temp_repo):
        """Text files should not be detected as binary."""
        text_file = temp_repo / "text.txt"
        text_file.write_text("Hello, world!\n")
        assert file_tools._is_binary_file(text_file) is False

    def test_binary_file_detected(self, temp_repo):
        """Binary files should be detected."""
        binary_file = temp_repo / "binary.bin"
        binary_file.write_bytes(b"\x00\x01\x02\x03\x04")
        assert file_tools._is_binary_file(binary_file) is True

    def test_nonexistent_file(self, temp_repo):
        """Should handle nonexistent files gracefully."""
        result = file_tools._is_binary_file(temp_repo / "nonexistent.txt")
        assert result is False


class TestIsHiddenFile:
    """Tests for _is_hidden_file function."""

    def test_dotfile_is_hidden(self):
        """Dotfiles should be detected as hidden."""
        assert file_tools._is_hidden_file(Path(".gitignore")) is True
        assert file_tools._is_hidden_file(Path(".env")) is True

    def test_regular_file_not_hidden(self):
        """Regular files should not be detected as hidden."""
        assert file_tools._is_hidden_file(Path("main.py")) is False
        assert file_tools._is_hidden_file(Path("README.md")) is False


class TestResolveSafePath:
    """Tests for _resolve_safe_path function."""

    def test_relative_path_within_repo(self, temp_repo):
        """Relative paths within repo should be accepted."""
        result = file_tools._resolve_safe_path("README.md", temp_repo)
        assert isinstance(result, Path)
        assert result.name == "README.md"

    def test_path_traversal_rejected(self, temp_repo):
        """Path traversal should be rejected."""
        result = file_tools._resolve_safe_path("../../../etc/passwd", temp_repo)
        assert isinstance(result, str)
        assert "Error" in result

    def test_absolute_path_outside_rejected(self, temp_repo):
        """Absolute paths outside repo should be rejected."""
        result = file_tools._resolve_safe_path("/etc/passwd", temp_repo)
        assert isinstance(result, str)
        assert "Error" in result or "outside" in result.lower()

    def test_absolute_path_inside_accepted(self, temp_repo):
        """Absolute paths inside repo should be accepted."""
        abs_path = temp_repo / "README.md"
        result = file_tools._resolve_safe_path(str(abs_path), temp_repo)
        assert isinstance(result, Path)

    def test_symlink_escape_rejected(self, temp_repo):
        """Symlinks escaping repo should be rejected."""
        # Create a symlink pointing outside repo
        outside_dir = temp_repo.parent / "outside"
        outside_dir.mkdir(exist_ok=True)
        (outside_dir / "secret.txt").write_text("secret")

        symlink = temp_repo / "escape_link"
        try:
            symlink.symlink_to(outside_dir / "secret.txt")
            result = file_tools._resolve_safe_path("escape_link", temp_repo, check_symlinks=True)
            assert isinstance(result, str)
            assert "Error" in result or "symlink" in result.lower()
        except OSError:
            pytest.skip("Symlink creation not supported")


class TestReadText:
    """Tests for read_text function."""

    def test_read_text_success(self, temp_repo):
        """Should read text file successfully."""
        content = file_tools.read_text("README.md", temp_repo)
        assert "# Test Repo" in content

    def test_read_text_not_found(self, temp_repo):
        """Should return error for missing file."""
        content = file_tools.read_text("nonexistent.txt", temp_repo)
        assert "Error" in content
        assert "not found" in content.lower()

    def test_read_text_path_traversal(self, temp_repo):
        """Should reject path traversal."""
        content = file_tools.read_text("../../../etc/passwd", temp_repo)
        assert "Error" in content

    def test_read_text_image_file(self, temp_repo):
        """Should reject image files."""
        (temp_repo / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n")
        content = file_tools.read_text("image.png", temp_repo)
        assert "Error" in content
        assert "image" in content.lower()

    def test_read_text_too_large(self, temp_repo):
        """Should reject files exceeding size limit."""
        large_file = temp_repo / "large.txt"
        large_file.write_text("x" * (file_tools.MAX_FILE_SIZE + 1000))
        content = file_tools.read_text("large.txt", temp_repo)
        assert "Error" in content
        assert "too large" in content.lower()

    def test_read_text_fallback_encoding(self, temp_repo):
        """Should fall back to latin-1 when UTF-8 decoding fails."""
        payload = bytes([0xFF, 0xFE])
        test_file = temp_repo / "latin1.txt"
        test_file.write_bytes(payload)

        content = file_tools.read_text("latin1.txt", temp_repo)

        expected = payload.decode(file_tools.FALLBACK_ENCODING)
        assert content == expected

    def test_read_text_directory_error(self, temp_repo):
        """Should return error when path is a directory."""
        directory = temp_repo / "folder"
        directory.mkdir()
        content = file_tools.read_text("folder", temp_repo)
        assert "Error" in content
        assert "not a file" in content.lower()

    def test_read_text_permission_denied(self, temp_repo, monkeypatch):
        """Should return error on permission denied."""
        target = temp_repo / "deny.txt"
        target.write_text("data")
        original_read_text = Path.read_text

        def _deny(self, *args, **kwargs):
            if self.name == "deny.txt":
                raise PermissionError("nope")
            return original_read_text(self, *args, **kwargs)

        monkeypatch.setattr(Path, "read_text", _deny)

        content = file_tools.read_text("deny.txt", temp_repo)
        assert "Error" in content
        assert "permission denied" in content.lower()

    def test_read_text_stat_error(self, temp_repo, monkeypatch):
        """Should return error when stat fails."""
        target = temp_repo / "statfail.txt"
        target.write_text("data")
        original_exists = Path.exists
        original_is_file = Path.is_file
        original_stat = Path.stat

        def _exists(self):
            if self.name == "statfail.txt":
                return True
            return original_exists(self)

        def _is_file(self):
            if self.name == "statfail.txt":
                return True
            return original_is_file(self)

        def _stat(self, *args, **kwargs):
            if self.name == "statfail.txt":
                raise OSError("boom")
            return original_stat(self, *args, **kwargs)

        monkeypatch.setattr(file_tools, "_resolve_safe_path", lambda *_args, **_kwargs: target)
        monkeypatch.setattr(Path, "exists", _exists)
        monkeypatch.setattr(Path, "is_file", _is_file)
        monkeypatch.setattr(Path, "stat", _stat)

        content = file_tools.read_text("statfail.txt", temp_repo)
        assert "Error" in content
        assert "cannot stat file" in content.lower()

    def test_read_text_fallback_decode_error(self, temp_repo, monkeypatch):
        """Should return error when fallback decoding also fails."""
        target = temp_repo / "bad.txt"
        target.write_bytes(b"\xff")
        original_read_text = Path.read_text

        def _raise_decode(self, encoding=None, *args, **kwargs):
            if self.name == "bad.txt":
                raise UnicodeDecodeError(encoding or "utf-8", b"\xff", 0, 1, "boom")
            return original_read_text(self, encoding=encoding, *args, **kwargs)

        monkeypatch.setattr(Path, "read_text", _raise_decode)

        content = file_tools.read_text("bad.txt", temp_repo)

        assert "Error" in content
        assert "cannot decode" in content.lower()


class TestReadFile:
    """Tests for read_file async function."""

    @pytest.mark.asyncio
    async def test_read_file_success(self, temp_repo):
        """Should read file successfully."""
        result = await file_tools.read_file("README.md", temp_repo)
        assert result.success is True
        assert "# Test Repo" in result.data

    @pytest.mark.asyncio
    async def test_read_file_not_found(self, temp_repo):
        """Should return error for missing file."""
        result = await file_tools.read_file("nonexistent.txt", temp_repo)
        assert result.success is False
        assert "not found" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_binary_rejected(self, temp_repo):
        """Should reject binary files."""
        binary_file = temp_repo / "binary.bin"
        binary_file.write_bytes(b"\x00\x01\x02\x03")
        result = await file_tools.read_file("binary.bin", temp_repo)
        assert result.success is False
        assert "binary" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_max_lines(self, temp_repo):
        """Should respect max_lines limit."""
        many_lines = temp_repo / "many_lines.txt"
        many_lines.write_text("\n".join(f"Line {i}" for i in range(1000)))
        result = await file_tools.read_file("many_lines.txt", temp_repo, max_lines=10)
        assert result.success is True
        # Should have at most 10 lines
        assert result.data.count("\n") <= 10

    @pytest.mark.asyncio
    async def test_read_file_path_traversal(self, temp_repo):
        """Should reject path traversal."""
        result = await file_tools.read_file("../../../etc/passwd", temp_repo)
        assert result.success is False
        assert "outside" in result.error.lower() or "traversal" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_absolute_outside(self, temp_repo):
        """Should reject absolute paths outside repo."""
        result = await file_tools.read_file("/etc/passwd", temp_repo)
        assert result.success is False

    @pytest.mark.asyncio
    async def test_read_file_hidden_warning(self, temp_repo):
        """Should handle hidden files (with warning)."""
        (temp_repo / ".hidden").write_text("hidden content")
        result = await file_tools.read_file(".hidden", temp_repo, silent=True)
        assert result.success is True
        assert "hidden content" in result.data

    @pytest.mark.asyncio
    async def test_read_file_fallback_encoding(self, temp_repo):
        """Should fall back to latin-1 on decode errors."""
        payload = bytes([0xFF, 0xFE])
        (temp_repo / "latin1.txt").write_bytes(payload)

        result = await file_tools.read_file("latin1.txt", temp_repo)

        expected = payload.decode(file_tools.FALLBACK_ENCODING)
        assert result.success is True
        assert result.data == expected

    @pytest.mark.asyncio
    async def test_read_file_truncates_characters(self, temp_repo):
        """Should truncate long files at MAX_CHARS."""
        path = temp_repo / "long.txt"
        path.write_text("x" * (file_tools.MAX_CHARS + 5))

        result = await file_tools.read_file(
            "long.txt", temp_repo, max_lines=file_tools.MAX_CHARS
        )

        assert result.success is True
        assert result.error == f"Truncated at {file_tools.MAX_CHARS} characters"
        assert len(result.data) == file_tools.MAX_CHARS

    @pytest.mark.asyncio
    async def test_read_file_image_rejected(self, temp_repo):
        """Should reject image files."""
        (temp_repo / "image.png").write_bytes(b"\x89PNG\r\n\x1a\n")

        result = await file_tools.read_file("image.png", temp_repo)

        assert result.success is False
        assert "image" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_not_a_file(self, temp_repo):
        """Should reject directory paths."""
        (temp_repo / "folder").mkdir()

        result = await file_tools.read_file("folder", temp_repo)

        assert result.success is False
        assert "not a file" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_permission_denied(self, temp_repo, monkeypatch):
        """Should return permission error when os.access denies read."""
        target = temp_repo / "nope.txt"
        target.write_text("data")
        monkeypatch.setattr(file_tools.os, "access", lambda *_args, **_kwargs: False)

        result = await file_tools.read_file("nope.txt", temp_repo, silent=True)

        assert result.success is False
        assert "permission denied" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_stat_permission_error(self, temp_repo, monkeypatch):
        """Should return error when stat raises PermissionError."""
        target = temp_repo / "locked.txt"
        target.write_text("data")
        original_exists = Path.exists
        original_is_file = Path.is_file
        original_stat = Path.stat

        def _exists(self):
            if self.name == "locked.txt":
                return True
            return original_exists(self)

        def _is_file(self):
            if self.name == "locked.txt":
                return True
            return original_is_file(self)

        def _stat(self, *args, **kwargs):
            if self.name == "locked.txt":
                raise PermissionError("no access")
            return original_stat(self, *args, **kwargs)

        monkeypatch.setattr(file_tools, "_resolve_safe_path", lambda *_args, **_kwargs: target)
        monkeypatch.setattr(Path, "exists", _exists)
        monkeypatch.setattr(Path, "is_file", _is_file)
        monkeypatch.setattr(Path, "stat", _stat)

        result = await file_tools.read_file("locked.txt", temp_repo, silent=True)

        assert result.success is False
        assert "permission denied" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_file_os_access_error(self, temp_repo, monkeypatch):
        """Should ignore os.access errors and attempt read."""
        def _raise_access(*_args, **_kwargs):
            raise OSError("access failed")

        monkeypatch.setattr(file_tools.os, "access", _raise_access)

        result = await file_tools.read_file("README.md", temp_repo, silent=True)

        assert result.success is True
        assert "# Test Repo" in result.data

    @pytest.mark.asyncio
    async def test_read_file_permission_error_during_read(self, temp_repo, monkeypatch):
        """Should return permission error when read_text raises."""
        target = temp_repo / "deny_read.txt"
        target.write_text("data")
        original_read_text = Path.read_text

        def _raise_perm(self, encoding=None, *args, **kwargs):
            if self.name == "deny_read.txt":
                raise PermissionError("no read")
            return original_read_text(self, encoding=encoding, *args, **kwargs)

        monkeypatch.setattr(Path, "read_text", _raise_perm)

        result = await file_tools.read_file("deny_read.txt", temp_repo, silent=True)

        assert result.success is False
        assert "permission denied" in result.error.lower()


class TestReadFileChunked:
    """Tests for read_file_chunked async function."""

    @pytest.mark.asyncio
    async def test_read_chunked_first_chunk(self, temp_repo):
        """Should read first chunk correctly."""
        lines_file = temp_repo / "lines.txt"
        lines_file.write_text("\n".join(f"Line {i}" for i in range(100)))

        result = await file_tools.read_file_chunked(
            "lines.txt", temp_repo, start_line=1, max_lines=10
        )
        assert result.success is True
        assert "Line 0" in result.data
        assert "Lines 1-10" in result.data

    @pytest.mark.asyncio
    async def test_read_chunked_middle_chunk(self, temp_repo):
        """Should read middle chunk correctly."""
        lines_file = temp_repo / "lines.txt"
        lines_file.write_text("\n".join(f"Line {i}" for i in range(100)))

        result = await file_tools.read_file_chunked(
            "lines.txt", temp_repo, start_line=50, max_lines=10
        )
        assert result.success is True
        assert "Line 49" in result.data
        assert "Lines 50-" in result.data

    @pytest.mark.asyncio
    async def test_read_chunked_has_navigation(self, temp_repo):
        """Should include navigation hints."""
        lines_file = temp_repo / "lines.txt"
        lines_file.write_text("\n".join(f"Line {i}" for i in range(100)))

        result = await file_tools.read_file_chunked(
            "lines.txt", temp_repo, start_line=50, max_lines=10
        )
        assert result.success is True
        # Should have prev/next navigation hints
        assert "Previous chunk" in result.data or "Next chunk" in result.data

    @pytest.mark.asyncio
    async def test_read_chunked_beyond_file(self, temp_repo):
        """Should return error when start_line beyond file."""
        lines_file = temp_repo / "short.txt"
        lines_file.write_text("Line 1\nLine 2\n")

        result = await file_tools.read_file_chunked(
            "short.txt", temp_repo, start_line=100, max_lines=10
        )
        assert result.success is False
        assert "beyond" in result.error.lower()

    @pytest.mark.asyncio
    async def test_read_chunked_start_line_clamped(self, temp_repo):
        """Should clamp start_line to 1 when below range."""
        lines_file = temp_repo / "short.txt"
        lines_file.write_text("Line 1\nLine 2\n")

        result = await file_tools.read_file_chunked(
            "short.txt", temp_repo, start_line=0, max_lines=1
        )

        assert result.success is True
        assert "Lines 1-1" in result.data
        assert "Line 1" in result.data


class TestListFiles:
    """Tests for list_files async function."""

    @pytest.mark.asyncio
    async def test_list_files_all(self, temp_repo):
        """Should list all files in directory."""
        result = await file_tools.list_files(".", temp_repo)
        assert result.success is True
        assert "README.md" in result.data

    @pytest.mark.asyncio
    async def test_list_files_pattern(self, temp_repo):
        """Should filter by pattern."""
        (temp_repo / "file.py").write_text("# Python")
        (temp_repo / "file.txt").write_text("Text")

        result = await file_tools.list_files(".", temp_repo, pattern="*.py")
        assert result.success is True
        assert any("file.py" in f for f in result.data)
        assert not any("file.txt" in f for f in result.data)

    @pytest.mark.asyncio
    async def test_list_files_not_found(self, temp_repo):
        """Should return error for missing directory."""
        result = await file_tools.list_files("nonexistent", temp_repo)
        assert result.success is False
        assert "not found" in result.error.lower()

    @pytest.mark.asyncio
    async def test_list_files_not_directory(self, temp_repo):
        """Should return error for non-directory."""
        result = await file_tools.list_files("README.md", temp_repo)
        assert result.success is False
        assert "not a directory" in result.error.lower()

    @pytest.mark.asyncio
    async def test_list_files_path_outside_repo(self, temp_repo):
        """Should reject path traversal outside repo."""
        result = await file_tools.list_files("../outside", temp_repo)
        assert result.success is False
        assert "outside" in result.error.lower()

    @pytest.mark.asyncio
    async def test_list_files_glob_error(self, temp_repo, monkeypatch):
        """Should return error when globbing fails."""
        original_glob = Path.glob

        def _glob(self, pattern):
            if self.name == "repo":
                raise OSError("glob failed")
            return original_glob(self, pattern)

        monkeypatch.setattr(Path, "glob", _glob)

        result = await file_tools.list_files(".", temp_repo)
        assert result.success is False
        assert "cannot list directory" in result.error.lower()


class TestWriteFile:
    """Tests for write_file async function."""

    @pytest.mark.asyncio
    async def test_write_file_success(self, temp_repo):
        """Should write file successfully."""
        result = await file_tools.write_file("new_file.txt", "Hello, world!", temp_repo)
        assert result.success is True
        assert (temp_repo / "new_file.txt").read_text() == "Hello, world!"

    @pytest.mark.asyncio
    async def test_write_file_path_traversal(self, temp_repo):
        """Should reject path traversal."""
        result = await file_tools.write_file("../outside.txt", "content", temp_repo)
        assert result.success is False

    @pytest.mark.asyncio
    async def test_write_file_image_rejected(self, temp_repo):
        """Should reject writing to image files."""
        result = await file_tools.write_file("image.png", "not an image", temp_repo)
        assert result.success is False
        assert "image" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_creates_parent(self, temp_repo):
        """Should create parent directories."""
        # This test verifies the directory exists check
        result = await file_tools.write_file(
            "nonexistent_dir/file.txt", "content", temp_repo
        )
        # Should fail because parent doesn't exist
        assert result.success is False
        assert "parent" in result.error.lower() or "directory" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_overwrites(self, temp_repo):
        """Should overwrite existing files."""
        (temp_repo / "existing.txt").write_text("old content")
        result = await file_tools.write_file("existing.txt", "new content", temp_repo)
        assert result.success is True
        assert (temp_repo / "existing.txt").read_text() == "new content"

    @pytest.mark.asyncio
    async def test_write_file_retries_on_permission_error(self, temp_repo, monkeypatch):
        """Should retry and succeed after transient permission errors."""
        target = temp_repo / "retry.txt"
        attempts = {"count": 0}
        original_write_text = Path.write_text

        def flaky_write_text(self, content, encoding=None):
            if str(self) == str(target):
                attempts["count"] += 1
                if attempts["count"] < 3:
                    raise PermissionError("locked")
            return original_write_text(self, content, encoding=encoding)

        monkeypatch.setattr(Path, "write_text", flaky_write_text)

        result = await file_tools.write_file(
            "retry.txt", "ok", temp_repo, max_retries=3, initial_delay=0
        )

        assert result.success is True
        assert attempts["count"] == 3
        assert target.read_text() == "ok"

    @pytest.mark.asyncio
    async def test_write_file_to_directory_rejected(self, temp_repo):
        """Should reject writing to a directory path."""
        target = temp_repo / "dir"
        target.mkdir()

        result = await file_tools.write_file("dir", "content", temp_repo)

        assert result.success is False
        assert "directory" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_parent_permission_denied(self, temp_repo, monkeypatch):
        """Should return permission error when parent directory is not writable."""
        monkeypatch.setattr(file_tools.os, "access", lambda *_args, **_kwargs: False)

        result = await file_tools.write_file("blocked.txt", "content", temp_repo, silent=True)

        assert result.success is False
        assert "permission denied" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_unicode_encode_error(self, temp_repo, monkeypatch):
        """Should return error when encoding fails."""
        target = temp_repo / "encode.txt"
        original_write_text = Path.write_text

        def _raise_encode(self, content, encoding=None):
            if self.name == "encode.txt":
                raise UnicodeEncodeError("utf-8", "x", 0, 1, "boom")
            return original_write_text(self, content, encoding=encoding)

        monkeypatch.setattr(Path, "write_text", _raise_encode)

        result = await file_tools.write_file("encode.txt", "x", temp_repo, silent=True)

        assert result.success is False
        assert "cannot encode content" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_os_error(self, temp_repo, monkeypatch):
        """Should return error on generic OS failures."""
        target = temp_repo / "oserror.txt"
        original_write_text = Path.write_text

        def _raise_oserror(self, content, encoding=None):
            if self.name == "oserror.txt":
                raise OSError("disk full")
            return original_write_text(self, content, encoding=encoding)

        monkeypatch.setattr(Path, "write_text", _raise_oserror)

        result = await file_tools.write_file("oserror.txt", "content", temp_repo, silent=True)

        assert result.success is False
        assert "cannot write file" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_absolute_outside_repo(self, temp_repo):
        """Should reject absolute paths outside repo."""
        outside_path = temp_repo.parent / "outside.txt"
        result = await file_tools.write_file(str(outside_path), "content", temp_repo)

        assert result.success is False
        assert "outside" in result.error.lower()

    @pytest.mark.asyncio
    async def test_write_file_zero_retries(self, temp_repo):
        """Should fail gracefully when retries are disabled."""
        result = await file_tools.write_file(
            "no-retry.txt", "content", temp_repo, max_retries=0, silent=True
        )

        assert result.success is False
        assert "failed to write file" in result.error.lower()


class TestFileExists:
    """Tests for file_exists legacy function."""

    def test_file_exists_true(self, temp_repo):
        """Should return True for existing file."""
        assert file_tools.file_exists(str(temp_repo / "README.md")) is True

    def test_file_exists_false(self, temp_repo):
        """Should return False for nonexistent file."""
        assert file_tools.file_exists(str(temp_repo / "nonexistent.txt")) is False


class TestGetFileInfo:
    """Tests for get_file_info legacy function."""

    def test_get_file_info_exists(self, temp_repo):
        """Should return info for existing file."""
        info = file_tools.get_file_info(str(temp_repo / "README.md"))
        assert info["exists"] is True
        assert info["is_file"] is True
        assert info["is_dir"] is False
        assert "size" in info
        assert "modified" in info

    def test_get_file_info_not_exists(self, temp_repo):
        """Should return exists=False for nonexistent file."""
        info = file_tools.get_file_info(str(temp_repo / "nonexistent.txt"))
        assert info["exists"] is False
