"""Coverage tests for security/path_validator.py.

Focuses on adversarial inputs and uncovered branches:
- Null bytes in all methods
- Path traversal attempts (../)
- URL-encoded traversal
- validate_local_filesystem_path: control chars, ~ expansion,
  restricted dirs, relative paths
- validate_safe_path: extension filtering, None result
- sanitize_for_filesystem_ops: non-absolute path
- validate_config_path: restricted prefixes, absolute paths,
  missing config files
- validate_model_path: missing/non-file model
- validate_data_path success/failure
"""

from pathlib import Path
from unittest.mock import patch

import pytest

from local_deep_research.security.path_validator import PathValidator

MODULE = "local_deep_research.security.path_validator"


# ---------------------------------------------------------------------------
# validate_safe_path
# ---------------------------------------------------------------------------


class TestValidateSafePath:
    def test_null_byte_raises(self):
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_safe_path("file\x00name.txt", "/tmp")

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

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

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

    def test_double_dot_deeply_nested_allowed_by_werkzeug(self):
        """werkzeug safe_join resolves a/b/../../etc/passwd within the base, so it's
        accepted (it resolves to base/etc/passwd, not an escape). No exception expected."""
        result = PathValidator.validate_safe_path(
            "a/b/../../c/file.txt", "/tmp"
        )
        assert result is not None

    def test_valid_relative_path_accepted(self, tmp_path):
        result = PathValidator.validate_safe_path(
            "subdir/file.txt", str(tmp_path)
        )
        assert result is not None
        assert "subdir/file.txt" in str(result)

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

    def test_correct_extension_accepted(self, tmp_path):
        result = PathValidator.validate_safe_path(
            "config.json",
            str(tmp_path),
            required_extensions=(".json", ".yaml"),
        )
        assert result.suffix == ".json"

    def test_whitespace_stripped_from_input(self, tmp_path):
        result = PathValidator.validate_safe_path("  file.txt  ", str(tmp_path))
        assert result is not None


# ---------------------------------------------------------------------------
# validate_local_filesystem_path
# ---------------------------------------------------------------------------


class TestValidateLocalFilesystemPath:
    def test_null_byte_raises(self):
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_local_filesystem_path("/tmp/file\x00.txt")

    def test_control_characters_raise(self):
        with pytest.raises(ValueError, match="Control characters"):
            PathValidator.validate_local_filesystem_path("/tmp/file\x01.txt")

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

    def test_tilde_expansion(self, tmp_path):
        """~ is expanded to home directory."""
        with patch.object(Path, "home", return_value=tmp_path):
            result = PathValidator.validate_local_filesystem_path("~/subdir")
        assert str(tmp_path) in str(result)

    def test_tilde_only_expansion(self, tmp_path):
        """~ alone expands to home directory."""
        with patch.object(Path, "home", return_value=tmp_path):
            result = PathValidator.validate_local_filesystem_path("~")
        assert str(tmp_path) in str(result)

    def test_restricted_etc_blocked(self):
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path("/etc/passwd")

    def test_restricted_proc_blocked(self):
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path("/proc/cpuinfo")

    def test_restricted_sys_blocked(self):
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path("/sys/kernel")

    def test_restricted_dev_blocked(self):
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path("/dev/null")

    def test_home_directory_allowed(self, tmp_path):
        """User home dir is not restricted."""
        result = PathValidator.validate_local_filesystem_path(str(tmp_path))
        assert result is not None

    def test_empty_string_raises(self):
        with pytest.raises(ValueError, match="Invalid path input"):
            PathValidator.validate_local_filesystem_path("")

    def test_none_raises(self):
        with pytest.raises(ValueError, match="Invalid path input"):
            PathValidator.validate_local_filesystem_path(None)

    def test_relative_path_resolves(self, tmp_path):
        """Relative paths are resolved against cwd."""
        with patch("os.getcwd", return_value=str(tmp_path)):
            result = PathValidator.validate_local_filesystem_path("subdir")
        assert result is not None

    def test_custom_restricted_dirs(self, tmp_path):
        """Custom restricted_dirs are respected."""
        restricted = tmp_path / "secret"
        restricted.mkdir()
        with pytest.raises(ValueError, match="system directories"):
            PathValidator.validate_local_filesystem_path(
                str(restricted), restricted_dirs=[restricted]
            )


# ---------------------------------------------------------------------------
# sanitize_for_filesystem_ops
# ---------------------------------------------------------------------------


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

    def test_absolute_path_returned(self, tmp_path):
        result = PathValidator.sanitize_for_filesystem_ops(tmp_path)
        assert result.is_absolute()
        assert str(tmp_path) in str(result)


# ---------------------------------------------------------------------------
# validate_config_path
# ---------------------------------------------------------------------------


class TestValidateConfigPath:
    def test_null_byte_raises(self):
        with pytest.raises(ValueError, match="Null bytes"):
            PathValidator.validate_config_path("config\x00.json")

    def test_empty_string_raises(self):
        with pytest.raises(ValueError, match="Invalid config path input"):
            PathValidator.validate_config_path("")

    def test_none_raises(self):
        with pytest.raises(ValueError, match="Invalid config path input"):
            PathValidator.validate_config_path(None)

    def test_double_dot_traversal_blocked(self):
        with pytest.raises(ValueError, match="traversal"):
            PathValidator.validate_config_path("../../etc/passwd.json")

    def test_restricted_etc_prefix_blocked(self):
        with pytest.raises(ValueError, match="restricted system directory"):
            PathValidator.validate_config_path("/etc/shadow.json")

    def test_restricted_proc_prefix_blocked(self):
        with pytest.raises(ValueError, match="restricted system directory"):
            PathValidator.validate_config_path("proc/something")

    def test_absolute_path_invalid_extension_blocked(self):
        """Absolute path starting with / causes safe_join(/, path) -> None -> error."""
        # safe_join("/", "/some/path") returns None for any absolute path
        # so any absolute /path causes "Invalid absolute path" or a traversal error
        with pytest.raises(ValueError):
            PathValidator.validate_config_path("/some/config.exe")

    def test_absolute_path_causes_safe_join_failure(self):
        """validate_config_path with /absolute/path raises ValueError since
        safe_join('/','/'+ path) returns None (werkzeug treats it as traversal)."""
        with pytest.raises(ValueError):
            PathValidator.validate_config_path("/home/user/config.json")

    def test_relative_path_with_valid_extension_accepted(self, tmp_path):
        """Relative config path with valid extension goes through validate_safe_path."""
        # Create a json file inside tmp_path so the path is valid
        config_file = tmp_path / "my_config.json"
        config_file.write_text("{}")
        result = PathValidator.validate_config_path(
            "my_config.json", config_root=str(tmp_path)
        )
        assert result.suffix == ".json"


# ---------------------------------------------------------------------------
# validate_model_path
# ---------------------------------------------------------------------------


class TestValidateModelPath:
    def test_missing_model_file_raises(self, tmp_path):
        with pytest.raises(ValueError, match="not found"):
            PathValidator.validate_model_path(
                "nonexistent.gguf", model_root=str(tmp_path)
            )

    def test_path_is_directory_not_file_raises(self, tmp_path):
        subdir = tmp_path / "models"
        subdir.mkdir()
        with pytest.raises(ValueError, match="not a file|not found"):
            PathValidator.validate_model_path(
                "models", model_root=str(tmp_path)
            )

    def test_valid_model_file_accepted(self, tmp_path):
        model = tmp_path / "model.gguf"
        model.write_bytes(b"fake gguf data")
        result = PathValidator.validate_model_path(
            "model.gguf", model_root=str(tmp_path)
        )
        assert result == model

    def test_path_traversal_in_model_path_blocked(self, tmp_path):
        with pytest.raises(ValueError, match="traversal|Invalid path"):
            PathValidator.validate_model_path(
                "../etc/passwd", model_root=str(tmp_path)
            )


# ---------------------------------------------------------------------------
# validate_data_path
# ---------------------------------------------------------------------------


class TestValidateDataPath:
    def test_traversal_blocked(self, tmp_path):
        with pytest.raises(ValueError, match="traversal|Invalid path"):
            PathValidator.validate_data_path("../../secret", str(tmp_path))

    def test_valid_data_path_accepted(self, tmp_path):
        result = PathValidator.validate_data_path("data/file.db", str(tmp_path))
        assert result is not None
        assert "data/file.db" in str(result)

    def test_null_byte_blocked(self, tmp_path):
        with pytest.raises(ValueError, match="Null bytes|Invalid path"):
            PathValidator.validate_data_path("file\x00.db", str(tmp_path))
