"""Tests for to_absolute_path() path-traversal guard in library routes.

Covers three behaviours added/changed by the GCode viewer PR:

1. Relative paths that escape base_dir are rejected with ValueError.
2. Path.is_relative_to() is used instead of startswith(str(base)),
   avoiding the /data/app vs /data/app_evil prefix-confusion bug.
3. Legacy absolute paths (pre-migration DB rows) are returned verbatim
   instead of raising ValueError.
"""

from pathlib import Path
from unittest.mock import patch

import pytest

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _call(relative_path, base_dir):
    """Call to_absolute_path with base_dir patched to *base_dir*."""
    from backend.app.api.routes.library import to_absolute_path

    with patch("backend.app.api.routes.library.app_settings") as mock_settings:
        mock_settings.base_dir = str(base_dir)
        return to_absolute_path(relative_path)


# ---------------------------------------------------------------------------
# None / empty guard
# ---------------------------------------------------------------------------


class TestNullInputs:
    def test_none_returns_none(self, tmp_path):
        assert _call(None, tmp_path) is None

    def test_empty_string_returns_none(self, tmp_path):
        assert _call("", tmp_path) is None


# ---------------------------------------------------------------------------
# Relative path traversal guard
# ---------------------------------------------------------------------------


class TestRelativePathTraversal:
    def test_normal_relative_path_resolves(self, tmp_path):
        """A safe relative path resolves to base_dir / rel."""
        base = tmp_path / "data"
        base.mkdir()
        result = _call("files/model.gcode", base)
        assert result == (base / "files" / "model.gcode").resolve()

    def test_traversal_via_dotdot_raises(self, tmp_path):
        """../etc/passwd must be rejected."""
        base = tmp_path / "data"
        base.mkdir()
        with pytest.raises(ValueError, match="escapes base directory"):
            _call("../etc/passwd", base)

    def test_traversal_via_nested_dotdot_raises(self, tmp_path):
        """files/../../etc/passwd must be rejected."""
        base = tmp_path / "data"
        base.mkdir()
        with pytest.raises(ValueError, match="escapes base directory"):
            _call("files/../../etc/passwd", base)

    def test_prefix_confusion_is_blocked(self, tmp_path):
        """Ensure /data/app_evil/secret is not permitted when base is /data/app.

        A naive startswith(str(base)) check would allow this because
        '/data/app_evil'.startswith('/data/app') is True.
        Path.is_relative_to() must be used instead.
        """
        # Simulate: base = /tmp/.../data_app, sibling = /tmp/.../data_app_evil
        base = tmp_path / "data_app"
        sibling = tmp_path / "data_app_evil"
        base.mkdir()
        sibling.mkdir()

        # Construct a relative path that resolves into the *sibling* dir.
        # from base: ../data_app_evil/secret
        with pytest.raises(ValueError, match="escapes base directory"):
            _call("../data_app_evil/secret", base)


# ---------------------------------------------------------------------------
# Legacy absolute path pass-through
# ---------------------------------------------------------------------------


class TestLegacyAbsolutePaths:
    def test_absolute_path_inside_base_is_returned(self, tmp_path):
        """An absolute path that happens to be inside base_dir is returned as-is."""
        base = tmp_path / "data"
        base.mkdir()
        abs_path = str(base / "archive" / "old.3mf")
        result = _call(abs_path, base)
        assert result == Path(abs_path).resolve()

    def test_absolute_path_outside_base_is_returned(self, tmp_path):
        """An absolute path outside base_dir is returned verbatim (legacy compat).

        Pre-migration DB rows may store absolute paths that predate the
        base_dir layout. These must NOT raise ValueError; callers are
        responsible for further existence checks.
        """
        base = tmp_path / "data"
        base.mkdir()
        outside = tmp_path / "old_archive" / "legacy.3mf"
        result = _call(str(outside), base)
        assert result == outside.resolve()
