"""Tests for path traversal prevention in validation module and services."""

from pathlib import Path

import pytest

from hangar.validation import validate_change_name, validate_path_containment


class TestValidateChangeName:
    """validate_change_name rejects path traversal attempts."""

    def test_valid_simple_name(self) -> None:
        assert validate_change_name("my-feature") == "my-feature"

    def test_valid_single_char(self) -> None:
        assert validate_change_name("x") == "x"

    def test_valid_with_numbers(self) -> None:
        assert validate_change_name("feat-123") == "feat-123"

    def test_rejects_dot_dot_slash(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("../etc")

    def test_rejects_absolute_path(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("/etc/passwd")

    def test_rejects_dots(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("foo.bar")

    def test_rejects_spaces(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("foo bar")

    def test_rejects_uppercase(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("MyFeature")

    def test_rejects_empty(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("")

    def test_rejects_slashes(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("foo/bar")

    def test_rejects_encoded_traversal(self) -> None:
        with pytest.raises(ValueError, match="Invalid change name"):
            validate_change_name("..%2F..%2Fetc")


class TestValidatePathContainment:
    """validate_path_containment prevents directory escape."""

    def test_valid_simple_file(self, tmp_path: Path) -> None:
        (tmp_path / "file.md").touch()
        result = validate_path_containment(tmp_path, "file.md")
        assert result == (tmp_path / "file.md").resolve()

    def test_valid_nested_path(self, tmp_path: Path) -> None:
        (tmp_path / "specs").mkdir()
        (tmp_path / "specs" / "spec.md").touch()
        result = validate_path_containment(tmp_path, "specs/spec.md")
        assert result == (tmp_path / "specs" / "spec.md").resolve()

    def test_rejects_dot_dot(self, tmp_path: Path) -> None:
        with pytest.raises(ValueError, match="escapes the allowed directory"):
            validate_path_containment(tmp_path, "../etc/passwd")

    def test_rejects_nested_dot_dot(self, tmp_path: Path) -> None:
        with pytest.raises(ValueError, match="escapes the allowed directory"):
            validate_path_containment(tmp_path, "specs/../../etc/passwd")

    def test_rejects_absolute_path(self, tmp_path: Path) -> None:
        with pytest.raises(ValueError, match="escapes the allowed directory"):
            validate_path_containment(tmp_path, "/etc/passwd")


class TestPathTraversalViaRoutes:
    """Integration tests verifying routes reject path traversal."""

    @pytest.mark.integration
    def test_sync_preview_rejects_traversal(
        self, integration_client, integration_project
    ) -> None:
        """GET /api/sync/{change_name}/preview rejects '../' in change_name."""
        response = integration_client.get("/api/sync/../../../etc/preview")
        # FastAPI may normalize the URL or our validation catches it
        assert response.status_code in (404, 422, 400)

    @pytest.mark.integration
    def test_wizard_state_rejects_traversal(
        self, integration_client, integration_project
    ) -> None:
        """GET /api/wizard/{change_name}/state rejects traversal."""
        response = integration_client.get("/api/wizard/../../../etc/state")
        assert response.status_code in (404, 422, 400)

    @pytest.mark.integration
    def test_specs_artifacts_rejects_traversal(
        self, integration_client, integration_project
    ) -> None:
        """GET /api/specs/{name}/artifacts rejects traversal in name."""
        response = integration_client.get("/api/specs/../../../etc/artifacts")
        assert response.status_code in (404, 422, 400, 500)
