"""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 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()


