"""Extended tests for security/path_validator.py - targeting untested paths.

Covers:
- sanitize_for_filesystem_ops() (entirely untested)
- validate_local_filesystem_path() relative path handling
- validate_local_filesystem_path() path traversal via .. explicit check
- validate_config_path() absolute path branch
- validate_config_path() non-string input
- validate_config_path() default config_root
- validate_model_path() default model_root
"""

from pathlib import Path
from unittest.mock import patch

import pytest

from local_deep_research.security.path_validator import PathValidator


# ── sanitize_for_filesystem_ops ──────────────────────────────────


class TestSanitizeForFilesystemOps:
    """Tests for sanitize_for_filesystem_ops (lines 225-235, entirely untested)."""

    def test_returns_path_for_valid_absolute(self):
        """Valid absolute path returns a Path object."""
        result = PathValidator.sanitize_for_filesystem_ops(Path("/tmp/test"))
        assert isinstance(result, Path)
        assert str(result).startswith("/")

    def test_preserves_path_components(self):
        """Path components are preserved after sanitization."""
        p = Path("/usr/local/share/data.txt")
        result = PathValidator.sanitize_for_filesystem_ops(p)
        assert result.name == "data.txt"
        assert "local" in str(result)

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

    def test_raises_for_dot_relative(self):
        """Dot-relative path raises ValueError."""
        with pytest.raises(ValueError, match="absolute"):
            PathValidator.sanitize_for_filesystem_ops(Path("./here"))


# ── validate_local_filesystem_path: relative path handling ───────


class TestLocalFilesystemPathRelative:
    """Tests for relative path handling in validate_local_filesystem_path (lines 160-167)."""

    def test_relative_path_resolved_from_cwd(self, tmp_path):
        """Relative path is resolved relative to current working directory."""
        with patch("os.getcwd", return_value=str(tmp_path)):
            result = PathValidator.validate_local_filesystem_path(
                "some_folder", restricted_dirs=[]
            )
            assert result == (tmp_path / "some_folder").resolve()

    def test_relative_nested_path(self, tmp_path):
        """Nested relative path resolves correctly."""
        with patch("os.getcwd", return_value=str(tmp_path)):
            result = PathValidator.validate_local_filesystem_path(
                "a/b/c", restricted_dirs=[]
            )
            assert str(result).endswith("a/b/c")

    def test_path_traversal_in_input_blocked(self):
        """Explicit .. in path input is blocked before resolution."""
        with pytest.raises(ValueError, match="traversal"):
            PathValidator.validate_local_filesystem_path("../escape")

    def test_double_dot_in_middle_blocked(self):
        """Path with .. in the middle is blocked."""
        with pytest.raises(ValueError, match="traversal"):
            PathValidator.validate_local_filesystem_path("safe/../escape")


# ── validate_local_filesystem_path: home expansion edge cases ────


class TestLocalFilesystemPathHomeExpansion:
    """Tests for home directory expansion edge cases."""

    @pytest.mark.skipif(
        Path.home() == Path("/root"),
        reason="Skipping in Docker/CI where home is /root (restricted)",
    )
    def test_bare_tilde_resolves_to_home(self):
        """Bare ~ resolves to home directory."""
        result = PathValidator.validate_local_filesystem_path("~")
        assert result == Path.home().resolve()

    @pytest.mark.skipif(
        Path.home() == Path("/root"),
        reason="Skipping in Docker/CI where home is /root (restricted)",
    )
    def test_tilde_with_relative_part(self):
        """~/path expands home and joins relative part."""
        result = PathValidator.validate_local_filesystem_path("~/some/path")
        expected = (Path.home() / "some/path").resolve()
        assert result == expected


# ── validate_config_path: absolute path branch ──────────────────


class TestConfigPathAbsolute:
    """Tests for absolute config path validation (lines 361-380).

    Note: werkzeug's safe_join("/", "/absolute/path") returns None,
    so absolute paths are always rejected as "Invalid absolute path".
    This test documents that security-conservative behavior.
    """

    def test_absolute_config_rejected_by_safe_join(self, tmp_path):
        """Absolute path is rejected because safe_join returns None."""
        config_file = tmp_path / "settings.json"
        config_file.write_text('{"key": "value"}')

        with pytest.raises(ValueError, match="Invalid absolute path"):
            PathValidator.validate_config_path(str(config_file))

    def test_absolute_config_wrong_extension_also_rejected(self, tmp_path):
        """Absolute path with wrong extension is also rejected (before ext check)."""
        script = tmp_path / "evil.py"
        script.write_text("import os")

        with pytest.raises(ValueError, match="Invalid absolute path"):
            PathValidator.validate_config_path(str(script))

    def test_absolute_config_nonexistent_also_rejected(self, tmp_path):
        """Absolute path to non-existent config is also rejected (before existence check)."""
        with pytest.raises(ValueError, match="Invalid absolute path"):
            PathValidator.validate_config_path(str(tmp_path / "missing.json"))


# ── validate_config_path: input validation ───────────────────────


class TestConfigPathInputValidation:
    """Tests for config path input validation edge cases."""

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

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

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

    def test_sys_prefix_blocked(self):
        """Path starting with sys/ is blocked as restricted."""
        with pytest.raises(ValueError, match="restricted"):
            PathValidator.validate_config_path("/sys/class/net")

    def test_dev_prefix_blocked(self):
        """Path starting with dev/ is blocked as restricted."""
        with pytest.raises(ValueError, match="restricted"):
            PathValidator.validate_config_path("/dev/null")


# ── validate_config_path: default config_root ────────────────────


class TestConfigPathDefaultRoot:
    """Tests for config path with default config_root (lines 383-386)."""

    def test_uses_default_root_when_none(self, tmp_path):
        """When config_root is None, get_data_directory() is used."""
        config_file = tmp_path / "test.json"
        config_file.write_text("{}")

        # get_data_directory is imported locally inside validate_config_path
        with patch(
            "local_deep_research.config.paths.get_data_directory",
            return_value=str(tmp_path),
        ):
            result = PathValidator.validate_config_path("test.json")
            assert result.name == "test.json"


# ── validate_model_path: default model_root ──────────────────────


class TestModelPathDefaultRoot:
    """Tests for model path with default model_root (line 256)."""

    def test_uses_default_root_when_none(self, tmp_path):
        """When model_root is None, get_models_directory() is used."""
        model_file = tmp_path / "model.gguf"
        model_file.write_text("fake model")

        with patch(
            "local_deep_research.security.path_validator.get_models_directory",
            return_value=tmp_path,
        ):
            result = PathValidator.validate_model_path("model.gguf")
            assert result.exists()
            assert result.name == "model.gguf"
