"""Tests for path security guards."""

import pytest
import os
from pathlib import Path

from skop_runner.file_manager import FileManager


@pytest.fixture
def file_manager(tmp_path):
    """Create a file manager with temporary directory."""
    return FileManager(str(tmp_path))


class TestPathGuards:
    """Tests for path traversal protection."""

    def test_reject_absolute_path_unix(self, file_manager):
        """Unix-style absolute paths should be rejected."""
        with pytest.raises(ValueError, match="Absolute paths not allowed"):
            file_manager.resolve_path("/etc/passwd")

    @pytest.mark.skipif(os.name != "nt", reason="Windows-style paths only on Windows")
    def test_reject_absolute_path_windows(self, file_manager):
        """Windows-style absolute paths should be rejected."""
        with pytest.raises(ValueError, match="Absolute paths not allowed"):
            file_manager.resolve_path("C:\\Windows\\System32")

    def test_reject_null_byte(self, file_manager):
        """Null bytes should be rejected."""
        with pytest.raises(ValueError, match="null byte"):
            file_manager.resolve_path("file\x00.txt")

    def test_reject_null_byte_in_directory(self, file_manager):
        """Null bytes in directory portion should be rejected."""
        with pytest.raises(ValueError, match="null byte"):
            file_manager.resolve_path("dir\x00name/file.txt")

    def test_reject_parent_traversal_simple(self, file_manager):
        """Simple parent traversal should be rejected."""
        with pytest.raises(ValueError, match="traversal"):
            file_manager.resolve_path("../etc/passwd")

    def test_reject_parent_traversal_double(self, file_manager):
        """Double parent traversal should be rejected."""
        with pytest.raises(ValueError, match="traversal"):
            file_manager.resolve_path("../../etc/passwd")

    def test_reject_parent_traversal_hidden(self, file_manager):
        """Hidden traversal in middle of path should be rejected."""
        with pytest.raises(ValueError, match="traversal"):
            file_manager.resolve_path("foo/bar/../../../etc/passwd")

    def test_allow_parent_traversal_to_root(self, file_manager, tmp_path):
        """Traversal that resolves to project root is allowed."""
        # foo/.. normalizes to . which is the project root - this is safe
        (tmp_path / "foo").mkdir()
        result = file_manager.resolve_path("foo/..")
        assert result == tmp_path

    def test_reject_dotdot_alone(self, file_manager):
        """Just '..' should be rejected."""
        with pytest.raises(ValueError, match="traversal"):
            file_manager.resolve_path("..")

    def test_allow_normal_path(self, file_manager, tmp_path):
        """Normal relative paths should work."""
        subdir = tmp_path / "subdir"
        subdir.mkdir()

        result = file_manager.resolve_path("subdir")
        assert result == subdir

    def test_allow_nested_path(self, file_manager, tmp_path):
        """Nested paths should work."""
        nested = tmp_path / "a" / "b" / "c"
        nested.mkdir(parents=True)

        result = file_manager.resolve_path("a/b/c")
        assert result == nested

    def test_allow_dot_for_root(self, file_manager, tmp_path):
        """Dot should resolve to project root."""
        result = file_manager.resolve_path(".")
        assert result == tmp_path

    def test_allow_empty_for_root(self, file_manager, tmp_path):
        """Empty string should resolve to project root."""
        result = file_manager.resolve_path("")
        assert result == tmp_path

    def test_allow_none_for_root(self, file_manager, tmp_path):
        """None should resolve to project root."""
        result = file_manager.resolve_path(None)
        assert result == tmp_path

    def test_allow_file_with_dots_in_name(self, file_manager, tmp_path):
        """Files with dots in name should be allowed."""
        test_file = tmp_path / "file.test.txt"
        test_file.touch()

        result = file_manager.resolve_path("file.test.txt")
        assert result == test_file

    def test_allow_dotfile(self, file_manager, tmp_path):
        """Dotfiles should be allowed (single dot prefix)."""
        dotfile = tmp_path / ".gitignore"
        dotfile.touch()

        result = file_manager.resolve_path(".gitignore")
        assert result == dotfile

    def test_allow_name_starting_with_double_dot(self, file_manager, tmp_path):
        """Entries prefixed with '..' but still inside project should work."""
        hidden = tmp_path / "..cache"
        hidden.mkdir()

        result = file_manager.resolve_path("..cache")
        assert result == hidden

    def test_allow_nested_name_starting_with_double_dot(self, file_manager, tmp_path):
        """Nested paths with '..' in segment names should work."""
        nested = tmp_path / "configs" / "..templates"
        nested.mkdir(parents=True)
        target = nested / "file.txt"
        target.touch()

        result = file_manager.resolve_path("configs/..templates/file.txt")
        assert result == target

    def test_reject_traversal_with_backslash(self, file_manager):
        """Windows-style traversal with backslashes should be rejected."""
        # os.path.normpath converts backslashes on all platforms
        with pytest.raises(ValueError, match="traversal|Absolute"):
            file_manager.resolve_path("..\\..\\etc\\passwd")


@pytest.mark.skipif(not hasattr(os, "symlink"), reason="Symlinks not supported on this platform")
class TestSymlinkSecurity:
    """Tests for symlink handling."""

    def test_internal_symlink_allowed(self, file_manager, tmp_path):
        """Symlinks within project should be followed."""
        target = tmp_path / "target.txt"
        target.write_text("content")

        link = tmp_path / "link.txt"
        link.symlink_to(target)

        result = file_manager.resolve_path("link.txt")
        assert result == target

    def test_external_symlink_blocked(self, file_manager, tmp_path, tmp_path_factory):
        """Symlinks pointing outside project should be blocked."""
        external_dir = tmp_path_factory.mktemp("external_symlink_dir")
        link = tmp_path / "evil_link"
        outside = external_dir / "outside_dir"
        outside.mkdir()
        link.symlink_to(outside)

        with pytest.raises(ValueError, match="escapes"):
            file_manager.resolve_path("evil_link/passwd")

    def test_external_symlink_file_blocked(self, file_manager, tmp_path, tmp_path_factory):
        """File symlinks pointing outside project should be blocked."""
        external_dir = tmp_path_factory.mktemp("external_symlink_file")
        outside_file = external_dir / "passwd"
        outside_file.write_text("root:x")
        link = tmp_path / "evil_link"
        link.symlink_to(outside_file)

        with pytest.raises(ValueError, match="escapes"):
            file_manager.resolve_path("evil_link")

    @pytest.mark.asyncio
    async def test_external_symlink_not_listed(self, file_manager, tmp_path, tmp_path_factory):
        """External symlinks should not appear in directory listing."""
        # Create an external symlink target outside the project
        external_dir = tmp_path_factory.mktemp("external_symlink_listing")
        link = tmp_path / "external"
        link.symlink_to(external_dir)

        # Create a normal file
        normal = tmp_path / "normal.txt"
        normal.write_text("content")

        items = await file_manager.list_directory()
        names = [item["name"] for item in items]

        assert "normal.txt" in names
        # External symlink should be skipped
        assert "external" not in names

    @pytest.mark.asyncio
    async def test_internal_symlink_listed(self, file_manager, tmp_path):
        """Internal symlinks should appear in directory listing."""
        # Create target and internal symlink
        target = tmp_path / "target.txt"
        target.write_text("content")

        link = tmp_path / "internal_link"
        link.symlink_to(target)

        items = await file_manager.list_directory()
        names = [item["name"] for item in items]

        assert "target.txt" in names
        assert "internal_link" in names

    @pytest.mark.asyncio
    async def test_symlink_info_included_when_requested(self, file_manager, tmp_path):
        """Symlink info should be included when requested."""
        target = tmp_path / "target.txt"
        target.write_text("content")

        link = tmp_path / "link.txt"
        link.symlink_to(target)

        items = await file_manager.list_directory(include_symlink_info=True)

        link_item = next(i for i in items if i["name"] == "link.txt")
        target_item = next(i for i in items if i["name"] == "target.txt")

        assert link_item.get("isSymlink") is True
        assert "isSymlink" not in target_item

    @pytest.mark.asyncio
    async def test_symlink_info_not_included_by_default(self, file_manager, tmp_path):
        """Symlink info should not be included by default."""
        target = tmp_path / "target.txt"
        target.write_text("content")

        link = tmp_path / "link.txt"
        link.symlink_to(target)

        items = await file_manager.list_directory()

        link_item = next(i for i in items if i["name"] == "link.txt")
        assert "isSymlink" not in link_item


class TestEdgeCases:
    """Tests for edge cases and unusual inputs."""

    def test_path_with_spaces(self, file_manager, tmp_path):
        """Paths with spaces should work."""
        test_file = tmp_path / "file with spaces.txt"
        test_file.touch()

        result = file_manager.resolve_path("file with spaces.txt")
        assert result == test_file

    def test_path_with_unicode(self, file_manager, tmp_path):
        """Paths with unicode characters should work."""
        filename = "naïve-文件.txt"
        test_file = tmp_path / filename
        test_file.touch()

        result = file_manager.resolve_path(filename)
        assert result == test_file

    def test_deeply_nested_path(self, file_manager, tmp_path):
        """Deeply nested paths should work."""
        deep = tmp_path / "a" / "b" / "c" / "d" / "e" / "f"
        deep.mkdir(parents=True)
        test_file = deep / "file.txt"
        test_file.touch()

        result = file_manager.resolve_path("a/b/c/d/e/f/file.txt")
        assert result == test_file

    def test_relative_internal_traversal_allowed(self, file_manager, tmp_path):
        """Internal traversal that stays within project should work."""
        # Create structure: project/a/b/file.txt
        nested = tmp_path / "a" / "b"
        nested.mkdir(parents=True)
        test_file = nested / "file.txt"
        test_file.touch()

        # Access via a/b/../b/file.txt (stays within project)
        result = file_manager.resolve_path("a/b/../b/file.txt")
        assert result == test_file
