"""
Red Team Security Tests - Traversal Attack Attempts

This test file attempts to BREAK OUT of the sandbox using various attack vectors.
These are adversarial tests designed to verify the Iron Vault's security guarantees.

ATTACK VECTORS TESTED:
1. Directory traversal with ../
2. Null byte injection
3. Symlink attacks
4. Absolute path overrides
5. Unicode/encoding attacks
6. Race conditions (TOCTOU)

All tests MUST FAIL to escape the sandbox. If any test passes (i.e., escapes),
that is a CRITICAL security vulnerability.
"""

from __future__ import annotations

from pathlib import Path

import pytest


class TestDirectoryTraversal:
    """Test directory traversal attack prevention."""

    def test_simple_dotdot_blocked(self, temp_sandbox: Path) -> None:
        """Simple ../ traversal is blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="Path escapes sandbox"):
            resolve_safe_path("../etc/passwd", root=temp_sandbox)

    def test_multiple_dotdot_blocked(self, temp_sandbox: Path) -> None:
        """Multiple ../ sequences are blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="Path escapes sandbox"):
            resolve_safe_path("../../../../../../etc/passwd", root=temp_sandbox)

    def test_dotdot_in_middle_blocked(self, temp_sandbox: Path) -> None:
        """../ in the middle of path is blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="Path escapes sandbox"):
            resolve_safe_path("subdir/../../etc/passwd", root=temp_sandbox)

    def test_dotdot_at_end_blocked(self, temp_sandbox: Path) -> None:
        """../ at end escaping sandbox is blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="Path escapes sandbox"):
            resolve_safe_path("a/b/c/../../../../../../../tmp", root=temp_sandbox)

    def test_encoded_dotdot_not_exploitable(self, temp_sandbox: Path) -> None:
        """URL-encoded ../ doesn't bypass validation."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        # %2e%2e = .. URL encoded - but Python Path handles this literally
        # This should create a literal directory named "%2e%2e" not escape
        result = resolve_safe_path("%2e%2e/etc/passwd", root=temp_sandbox)
        # Should be inside sandbox, not escaped
        assert str(result).startswith(str(temp_sandbox))


class TestNullByteInjection:
    """Test null byte injection attack prevention."""

    def test_null_byte_in_path_blocked(self, temp_sandbox: Path) -> None:
        """Null byte in path is rejected."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="null byte"):
            resolve_safe_path("file.txt\x00.jpg", root=temp_sandbox)

    def test_null_byte_traversal_blocked(self, temp_sandbox: Path) -> None:
        """Null byte combined with traversal is rejected."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="null byte"):
            resolve_safe_path("../etc/passwd\x00.txt", root=temp_sandbox)

    def test_null_byte_at_start_blocked(self, temp_sandbox: Path) -> None:
        """Null byte at start of path is rejected."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        with pytest.raises(SandboxViolation, match="null byte"):
            resolve_safe_path("\x00/etc/passwd", root=temp_sandbox)


class TestSymlinkAttacks:
    """Test symlink-based escape attempts."""

    def test_symlink_to_outside_blocked(self, temp_sandbox: Path) -> None:
        """Symlink pointing outside sandbox is rejected."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        # Create symlink pointing outside sandbox
        symlink = temp_sandbox / "evil_link"
        symlink.symlink_to("/etc/passwd")

        # Symlinks to outside are blocked - either by symlink check or escape detection
        with pytest.raises(SandboxViolation):
            resolve_safe_path("evil_link", root=temp_sandbox, allow_symlinks=False)

    def test_symlink_inside_sandbox_allowed_when_enabled(self, temp_sandbox: Path) -> None:
        """Symlink inside sandbox is allowed when allow_symlinks=True."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        # Create a file and symlink to it within sandbox
        real_file = temp_sandbox / "real.txt"
        real_file.write_text("content")
        symlink = temp_sandbox / "link.txt"
        symlink.symlink_to(real_file)

        # Should work with allow_symlinks=True
        result = resolve_safe_path("link.txt", root=temp_sandbox, allow_symlinks=True)
        assert result.exists()

    def test_symlink_escaping_via_target_blocked(self, temp_sandbox: Path) -> None:
        """Symlink with target that escapes sandbox is rejected."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        # Create symlink with target that escapes
        symlink = temp_sandbox / "escape_link"
        symlink.symlink_to("/tmp")

        with pytest.raises(SandboxViolation):
            resolve_safe_path("escape_link", root=temp_sandbox, allow_symlinks=True)

    def test_chained_symlinks_blocked(self, temp_sandbox: Path) -> None:
        """Chain of symlinks eventually escaping is blocked."""
        from aperion_fsal.core.sandbox import SandboxViolation, resolve_safe_path

        # Create chain: link1 -> link2 -> /etc
        link2 = temp_sandbox / "link2"
        link2.symlink_to("/etc")
        link1 = temp_sandbox / "link1"
        link1.symlink_to(link2)

        with pytest.raises(SandboxViolation):
            resolve_safe_path("link1", root=temp_sandbox, allow_symlinks=True)


class TestAbsolutePathOverride:
    """Test absolute path override attempts."""

    def test_absolute_path_normalized_to_sandbox(self, temp_sandbox: Path) -> None:
        """Absolute paths are normalized to be relative to sandbox."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        result = resolve_safe_path("/etc/passwd", root=temp_sandbox)
        # Should resolve to sandbox/etc/passwd, not /etc/passwd
        assert str(result).startswith(str(temp_sandbox))
        assert result == temp_sandbox / "etc" / "passwd"

    def test_absolute_path_cannot_escape(self, temp_sandbox: Path) -> None:
        """Absolute paths cannot access files outside sandbox."""
        from aperion_fsal.core.safeio import read

        # Even if /etc/passwd exists on the system, this should look
        # for sandbox/etc/passwd which doesn't exist
        with pytest.raises(FileNotFoundError):
            read("/etc/passwd", root=temp_sandbox)

    def test_windows_style_path_handled(self, temp_sandbox: Path) -> None:
        """Windows-style paths are handled safely."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        # This should not escape on any platform
        result = resolve_safe_path("C:\\Windows\\System32", root=temp_sandbox)
        assert str(result).startswith(str(temp_sandbox))


class TestSpecialPaths:
    """Test special path handling."""

    def test_dot_path_stays_in_sandbox(self, temp_sandbox: Path) -> None:
        """Single dot path stays in sandbox root."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        result = resolve_safe_path(".", root=temp_sandbox)
        assert result == temp_sandbox

    def test_empty_path_component(self, temp_sandbox: Path) -> None:
        """Empty path components don't cause issues."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        result = resolve_safe_path("a//b//c", root=temp_sandbox)
        assert str(result).startswith(str(temp_sandbox))

    def test_very_long_path(self, temp_sandbox: Path) -> None:
        """Very long paths don't cause issues."""
        from aperion_fsal.core.sandbox import resolve_safe_path

        long_path = "/".join(["a"] * 100)
        result = resolve_safe_path(long_path, root=temp_sandbox)
        assert str(result).startswith(str(temp_sandbox))


class TestAPILevelTraversalPrevention:
    """Test that API endpoints properly reject traversal attacks."""

    def test_api_read_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/read rejects traversal attempts."""
        response = client.post("/v1/fs/read", json={"path": "../../../etc/passwd"})
        assert response.status_code == 403
        assert "escapes sandbox" in response.json()["detail"].lower()

    def test_api_write_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/write rejects traversal attempts."""
        import base64

        response = client.post(
            "/v1/fs/write",
            json={
                "path": "../../../tmp/evil.txt",
                "content_b64": base64.b64encode(b"hacked").decode(),
            },
        )
        assert response.status_code == 403
        assert "escapes sandbox" in response.json()["detail"].lower()

    def test_api_list_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/list rejects traversal attempts."""
        response = client.post("/v1/fs/list", json={"path": "../../../etc"})
        # Should be 403 (escape) or 404 (not found within sandbox)
        assert response.status_code in (403, 404)

    def test_api_delete_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/delete rejects traversal attempts."""
        response = client.post("/v1/fs/delete", json={"path": "../../../tmp/test.txt"})
        assert response.status_code in (403, 404)

    def test_api_mkdir_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/mkdir rejects traversal attempts."""
        response = client.post("/v1/fs/mkdir", json={"path": "../../../tmp/evil_dir"})
        assert response.status_code == 403

    def test_api_move_src_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/move rejects source traversal."""
        response = client.post(
            "/v1/fs/move",
            json={"src": "../../../etc/passwd", "dst": "passwd.txt"},
        )
        assert response.status_code in (403, 404)

    def test_api_move_dst_traversal_blocked(self, client, temp_sandbox: Path) -> None:
        """POST /v1/fs/move rejects destination traversal."""
        # Create a file first
        (temp_sandbox / "test.txt").write_text("test")

        response = client.post(
            "/v1/fs/move",
            json={"src": "test.txt", "dst": "../../../tmp/stolen.txt"},
        )
        assert response.status_code == 403


class TestAuthenticationBypass:
    """Test authentication cannot be bypassed."""

    def test_auth_required_when_token_set(self, auth_client: tuple) -> None:
        """Requests without auth are rejected when token is configured."""
        client, token = auth_client

        # Request without auth header
        response = client.post("/v1/fs/list", json={"path": "."})
        assert response.status_code == 401

    def test_wrong_token_rejected(self, auth_client: tuple) -> None:
        """Requests with wrong token are rejected."""
        client, token = auth_client

        response = client.post(
            "/v1/fs/list",
            json={"path": "."},
            headers={"Authorization": "Bearer wrong-token"},
        )
        assert response.status_code == 403

    def test_correct_token_accepted(self, auth_client: tuple, temp_sandbox: Path) -> None:
        """Requests with correct token are accepted."""
        client, token = auth_client

        response = client.post(
            "/v1/fs/list",
            json={"path": "."},
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 200


class TestSandboxRootEnforcement:
    """Test sandbox root enforcement."""

    def test_production_mode_requires_fsal_root(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Production mode fails without FSAL_ROOT."""
        from aperion_fsal.core.sandbox import SandboxNotConfigured, get_sandbox_root

        # Clear all sandbox env vars and set to production mode
        for var in [
            "FSAL_ROOT",
            "APERION_FSAL_SANDBOX",
            "TEST_SANDBOX",
            "APERION_SANDBOX",
            "FSAL_SANDBOX",
        ]:
            monkeypatch.delenv(var, raising=False)
        monkeypatch.setenv("APERION_ENV", "production")

        with pytest.raises(SandboxNotConfigured):
            get_sandbox_root()

    def test_dev_mode_allows_cwd_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Dev mode allows CWD fallback."""
        from aperion_fsal.core.sandbox import get_sandbox_root

        # Clear all sandbox env vars but set dev mode
        for var in [
            "FSAL_ROOT",
            "APERION_FSAL_SANDBOX",
            "TEST_SANDBOX",
            "APERION_SANDBOX",
            "FSAL_SANDBOX",
        ]:
            monkeypatch.delenv(var, raising=False)
        monkeypatch.setenv("APERION_ENV", "dev")

        # Should not raise, returns CWD
        root = get_sandbox_root()
        assert root == Path.cwd().resolve()
