"""Tests for PathValidator — security-sensitive path validation edge cases."""

from pathlib import Path

import pytest

from local_deep_research.security.path_validator import PathValidator


# ===========================================================================
# 1. validate_safe_path
# ===========================================================================


class TestValidateSafePath:
    """Verify validate_safe_path input validation and traversal blocking."""

    def test_non_string_input_rejected(self):
        """Non-string input raises ValueError."""
        with pytest.raises(ValueError, match="Invalid path input"):
            PathValidator.validate_safe_path(123, "/tmp")

    def test_none_input_rejected(self):
        """None input raises ValueError."""
        with pytest.raises(ValueError, match="Invalid path input"):
            PathValidator.validate_safe_path(None, "/tmp")

    def test_empty_string_rejected(self):
        """Empty string raises ValueError."""
        with pytest.raises(ValueError, match="Invalid path input"):
            PathValidator.validate_safe_path("", "/tmp")

    def test_null_bytes_rejected_in_safe_path(self, tmp_path):
        """Null bytes in validate_safe_path raise ValueError."""
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_safe_path("file\x00.txt", str(tmp_path))

    def test_traversal_blocked(self, tmp_path):
        """Path traversal attempt via .. is blocked."""
        with pytest.raises(ValueError):
            PathValidator.validate_safe_path("../../etc/passwd", str(tmp_path))

    def test_valid_relative_path(self, tmp_path):
        """Valid relative path returns resolved Path."""
        result = PathValidator.validate_safe_path(
            "subdir/file.txt", str(tmp_path)
        )
        assert result is not None
        assert str(tmp_path) in str(result)

    def test_extension_check_rejects_wrong_extension(self, tmp_path):
        """Wrong extension raises ValueError."""
        with pytest.raises(ValueError, match="Invalid file type"):
            PathValidator.validate_safe_path(
                "file.txt",
                str(tmp_path),
                required_extensions=(".json", ".yaml"),
            )

    def test_extension_check_accepts_correct_extension(self, tmp_path):
        """Correct extension passes validation."""
        result = PathValidator.validate_safe_path(
            "config.json",
            str(tmp_path),
            required_extensions=(".json", ".yaml"),
        )
        assert result.suffix == ".json"

    def test_extension_case_sensitivity(self, tmp_path):
        """Extension check is case-sensitive — .JSON != .json."""
        with pytest.raises(ValueError, match="Invalid file type"):
            PathValidator.validate_safe_path(
                "config.JSON",
                str(tmp_path),
                required_extensions=(".json",),
            )

    def test_whitespace_stripped(self, tmp_path):
        """Leading/trailing whitespace in input is stripped."""
        result = PathValidator.validate_safe_path("  file.txt  ", str(tmp_path))
        assert result is not None
        assert "file.txt" in str(result)


# ===========================================================================
# 2. validate_local_filesystem_path — home dir expansion
# ===========================================================================


class TestHomeDirExpansion:
    """Verify ~ expansion edge cases."""

    def test_tilde_slash_expands_to_home(self):
        """'~/' expands to home directory."""
        home = str(Path.home())
        result = PathValidator.validate_local_filesystem_path(
            "~/documents",
            restricted_dirs=[],
        )
        assert str(result).startswith(home)

    def test_tilde_alone_expands_to_home(self):
        """'~' alone expands to home directory."""
        home = Path.home().resolve()
        result = PathValidator.validate_local_filesystem_path(
            "~",
            restricted_dirs=[],
        )
        assert result == home

    def test_tilde_with_trailing_slashes(self):
        """'~/' with no relative part yields home directory."""
        home = Path.home().resolve()
        result = PathValidator.validate_local_filesystem_path(
            "~/",
            restricted_dirs=[],
        )
        assert result == home

    def test_null_bytes_rejected(self):
        """Null bytes in path raise ValueError."""
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_local_filesystem_path("/tmp/file\x00.txt")

    def test_control_characters_rejected(self):
        """Control characters raise ValueError."""
        with pytest.raises(ValueError, match="Control characters"):
            PathValidator.validate_local_filesystem_path("/tmp/file\x01name")

    def test_path_traversal_blocked(self):
        """.. in path raises ValueError."""
        with pytest.raises(ValueError, match="traversal"):
            PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd")

    def test_restricted_dir_blocked(self):
        """Access to /etc is blocked by default restrictions."""
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path("/etc/passwd")

    def test_custom_restricted_dir(self, tmp_path):
        """Custom restricted dirs are enforced."""
        restricted = tmp_path / "secret"
        restricted.mkdir()
        target = restricted / "file.txt"
        target.touch()

        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path(
                str(target),
                restricted_dirs=[restricted],
            )


# ===========================================================================
# 3. sanitize_for_filesystem_ops — entirely uncovered
# ===========================================================================


class TestSanitizeForFilesystemOps:
    """Verify sanitize_for_filesystem_ops validation."""

    def test_relative_path_rejected(self):
        """Relative path raises ValueError."""
        with pytest.raises(ValueError, match="must be absolute"):
            PathValidator.sanitize_for_filesystem_ops(Path("relative/path"))

    def test_absolute_path_passes(self, tmp_path):
        """Absolute path is returned as a Path."""
        result = PathValidator.sanitize_for_filesystem_ops(tmp_path)
        assert isinstance(result, Path)
        assert result.is_absolute()

    def test_root_path(self):
        """Root path '/' passes sanitization."""
        result = PathValidator.sanitize_for_filesystem_ops(Path("/"))
        assert result == Path("/")

    def test_deep_path_preserved(self, tmp_path):
        """Deep nested path structure is preserved."""
        deep = tmp_path / "a" / "b" / "c"
        result = PathValidator.sanitize_for_filesystem_ops(deep)
        assert str(result).endswith("a/b/c")


# ===========================================================================
# 4. validate_model_path
# ===========================================================================


class TestValidateModelPath:
    """Verify validate_model_path checks."""

    def test_nonexistent_model_file(self, tmp_path):
        """Non-existent model file raises ValueError."""
        with pytest.raises(ValueError, match="Model file not found"):
            PathValidator.validate_model_path(
                "model.bin", model_root=str(tmp_path)
            )

    def test_directory_not_file_rejected(self, tmp_path):
        """Directory path raises 'not a file' error."""
        subdir = tmp_path / "model_dir"
        subdir.mkdir()
        with pytest.raises(ValueError, match="not a file"):
            PathValidator.validate_model_path(
                "model_dir", model_root=str(tmp_path)
            )

    def test_valid_model_file(self, tmp_path):
        """Valid model file returns resolved path."""
        model_file = tmp_path / "model.gguf"
        model_file.touch()
        result = PathValidator.validate_model_path(
            "model.gguf", model_root=str(tmp_path)
        )
        assert result == model_file.resolve()

    def test_traversal_in_model_path(self, tmp_path):
        """Path traversal in model path is blocked."""
        with pytest.raises(ValueError):
            PathValidator.validate_model_path(
                "../../../etc/passwd", model_root=str(tmp_path)
            )

    def test_model_root_created_if_missing(self, tmp_path):
        """Model root directory is created if it doesn't exist."""
        new_root = tmp_path / "new_models"
        # Will fail because file doesn't exist, but root should be created
        try:
            PathValidator.validate_model_path(
                "test.bin", model_root=str(new_root)
            )
        except ValueError:
            pass
        assert new_root.exists()


# ===========================================================================
# 5. validate_config_path — restricted prefixes and null byte rejection
# ===========================================================================


class TestValidateConfigPath:
    """Verify validate_config_path security checks."""

    def test_non_string_rejected(self):
        """Non-string input raises ValueError."""
        with pytest.raises(ValueError, match="Invalid config path"):
            PathValidator.validate_config_path(123)

    def test_empty_string_rejected(self):
        """Empty string raises ValueError."""
        with pytest.raises(ValueError, match="Invalid config path"):
            PathValidator.validate_config_path("")

    def test_null_bytes_rejected(self, tmp_path):
        """Null bytes in config paths raise ValueError."""
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_config_path(
                "config\x00.json",
                config_root=str(tmp_path),
            )

    def test_traversal_rejected(self):
        """.. in config path raises ValueError."""
        with pytest.raises(ValueError, match="traversal"):
            PathValidator.validate_config_path(
                "../etc/passwd", config_root="/tmp"
            )

    @pytest.mark.parametrize(
        "restricted", ["etc/passwd", "proc/self", "sys/class", "dev/null"]
    )
    def test_restricted_prefixes_blocked(self, restricted):
        """System directory prefixes are blocked."""
        with pytest.raises(ValueError, match="restricted system directory"):
            PathValidator.validate_config_path(f"/{restricted}")

    def test_restricted_prefix_exact_match(self):
        """Exact restricted directory name (no trailing path) is blocked."""
        with pytest.raises(ValueError, match="restricted system directory"):
            PathValidator.validate_config_path("/etc")

    def test_invalid_extension_rejected(self, tmp_path):
        """Non-config extension is rejected."""
        txt_file = tmp_path / "file.txt"
        txt_file.touch()
        with pytest.raises(ValueError, match="Invalid"):
            PathValidator.validate_config_path(
                "file.txt", config_root=str(tmp_path)
            )

    def test_valid_config_extensions(self, tmp_path):
        """All valid config extensions are accepted."""
        for ext in (".json", ".yaml", ".yml", ".toml", ".ini", ".conf"):
            config_file = tmp_path / f"config{ext}"
            config_file.write_text("")
            result = PathValidator.validate_config_path(
                f"config{ext}",
                config_root=str(tmp_path),
            )
            assert result is not None

    def test_absolute_config_path_rejected_by_safe_join(self, tmp_path):
        """Absolute config paths fail safe_join validation (safe_join rejects absolute second args)."""
        config_file = tmp_path / "app.json"
        config_file.write_text("{}")
        # safe_join("/", "/tmp/.../app.json") returns None because the path is absolute
        with pytest.raises(ValueError, match="Invalid absolute path"):
            PathValidator.validate_config_path(str(config_file))

    def test_restricted_prefix_case_insensitive(self):
        """Restricted prefix check is case-insensitive."""
        with pytest.raises(ValueError, match="restricted system directory"):
            PathValidator.validate_config_path("/ETC/passwd")


# ===========================================================================
# 6. validate_data_path
# ===========================================================================


class TestValidateDataPath:
    """Verify validate_data_path basics."""

    def test_valid_data_path(self, tmp_path):
        """Valid relative path returns resolved path."""
        result = PathValidator.validate_data_path("data.db", str(tmp_path))
        assert "data.db" in str(result)

    def test_traversal_in_data_path(self, tmp_path):
        """Path traversal is blocked."""
        with pytest.raises(ValueError):
            PathValidator.validate_data_path("../../etc/passwd", str(tmp_path))
