"""
Tests for Container Sandbox Module

Tests the container security and network isolation functionality.
"""

import subprocess
from unittest.mock import AsyncMock, patch

import pytest

from verlex_server.gateway.backend.sandbox import (
    ContainerSandbox,
    NetworkConfig,
    NetworkManager,
    NetworkMode,
    SandboxConfig,
    SecurityLevel,
    SecurityProfile,
    generate_seccomp_profile,
    save_seccomp_profile,
)


def is_docker_available() -> bool:
    """Check if Docker is available and running."""
    try:
        result = subprocess.run(
            ["docker", "info"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=5,
        )
        return result.returncode == 0
    except (subprocess.SubprocessError, FileNotFoundError):
        return False


# Skip marker for tests that require Docker
requires_docker = pytest.mark.skipif(
    not is_docker_available(),
    reason="Docker is not available or not running"
)


class TestSecurityProfile:
    """Tests for SecurityProfile."""

    def test_default_values(self):
        """Test default security profile values."""
        profile = SecurityProfile()

        assert profile.level == SecurityLevel.STANDARD
        assert "ALL" in profile.drop_capabilities
        assert profile.no_new_privileges is True
        assert profile.pids_limit == 100

    def test_minimal_profile(self):
        """Test minimal security profile."""
        profile = SecurityProfile.minimal()

        assert profile.level == SecurityLevel.MINIMAL
        assert len(profile.drop_capabilities) == 0
        assert profile.no_new_privileges is False
        assert profile.pids_limit == 0

    def test_standard_profile(self):
        """Test standard security profile."""
        profile = SecurityProfile.standard()

        assert profile.level == SecurityLevel.STANDARD
        assert "ALL" in profile.drop_capabilities
        assert "CHOWN" in profile.add_capabilities
        assert profile.pids_limit == 256

    def test_strict_profile(self):
        """Test strict security profile."""
        profile = SecurityProfile.strict()

        assert profile.level == SecurityLevel.STRICT
        assert len(profile.add_capabilities) == 0
        assert profile.read_only_rootfs is True
        assert profile.user == "65534:65534"
        assert profile.pids_limit == 64

    def test_to_docker_args_minimal(self):
        """Test Docker args generation for minimal profile."""
        profile = SecurityProfile.minimal()
        args = profile.to_docker_args()

        # Minimal should have few restrictions
        assert "--read-only" not in args
        assert "--pids-limit" not in args  # pids_limit is 0

    def test_to_docker_args_standard(self):
        """Test Docker args generation for standard profile."""
        profile = SecurityProfile.standard()
        args = profile.to_docker_args()

        assert "--cap-drop" in args
        assert "ALL" in args
        assert "--cap-add" in args
        assert "--security-opt" in args
        assert "no-new-privileges:true" in args
        assert "--pids-limit" in args
        assert "256" in args

    def test_to_docker_args_strict(self):
        """Test Docker args generation for strict profile."""
        profile = SecurityProfile.strict()
        args = profile.to_docker_args()

        assert "--read-only" in args
        assert "--user" in args
        assert "65534:65534" in args
        assert "--tmpfs" in args

    def test_ulimits_in_args(self):
        """Test ulimits are included in Docker args."""
        profile = SecurityProfile(
            ulimits={
                "nofile": {"soft": 1024, "hard": 4096},
                "nproc": {"soft": 100, "hard": 200},
            }
        )
        args = profile.to_docker_args()

        assert "--ulimit" in args
        assert "nofile=1024:4096" in args
        assert "nproc=100:200" in args


class TestNetworkConfig:
    """Tests for NetworkConfig."""

    def test_default_values(self):
        """Test default network config values."""
        config = NetworkConfig()

        assert config.mode == NetworkMode.ISOLATED
        assert config.network_name == "gateway-jobs"
        assert len(config.allowed_egress) > 0
        assert "pypi.org" in config.allowed_egress

    def test_to_docker_args_none(self):
        """Test Docker args for no network."""
        config = NetworkConfig(mode=NetworkMode.NONE)
        args = config.to_docker_args()

        assert "--network" in args
        assert "none" in args

    def test_to_docker_args_isolated(self):
        """Test Docker args for isolated network."""
        config = NetworkConfig(
            mode=NetworkMode.ISOLATED,
            network_name="my-network",
        )
        args = config.to_docker_args()

        assert "--network" in args
        assert "my-network" in args

    def test_to_docker_args_with_dns(self):
        """Test Docker args with custom DNS."""
        config = NetworkConfig(
            dns_servers=["8.8.8.8", "8.8.4.4"],
        )
        args = config.to_docker_args()

        assert "--dns" in args
        assert "8.8.8.8" in args
        assert "8.8.4.4" in args


class TestSandboxConfig:
    """Tests for SandboxConfig."""

    def test_default_values(self):
        """Test default sandbox config."""
        config = SandboxConfig()

        assert config.security.level == SecurityLevel.STANDARD
        assert config.network.mode == NetworkMode.ISOLATED

    def test_to_docker_args(self):
        """Test combined Docker args."""
        config = SandboxConfig()
        args = config.to_docker_args()

        # Should include both security and network args
        assert "--cap-drop" in args
        assert "--network" in args

    def test_filter_env_vars_allowed(self):
        """Test environment variable filtering - allowed."""
        config = SandboxConfig()

        env = {
            "GATEWAY_JOB_ID": "123",
            "PYTHONPATH": "/app",
            "PATH": "/usr/bin",
            "HOME": "/home/user",
        }

        filtered = config.filter_env_vars(env)

        assert "GATEWAY_JOB_ID" in filtered
        assert "PYTHONPATH" in filtered
        assert "PATH" in filtered
        assert "HOME" in filtered

    def test_filter_env_vars_blocked(self):
        """Test environment variable filtering - blocked."""
        config = SandboxConfig()

        env = {
            "AWS_ACCESS_KEY_ID": "secret",
            "AWS_SECRET_ACCESS_KEY": "supersecret",
            "GOOGLE_APPLICATION_CREDENTIALS": "/path/to/creds",
            "GATEWAY_JOB_ID": "123",  # This should pass
        }

        filtered = config.filter_env_vars(env)

        assert "AWS_ACCESS_KEY_ID" not in filtered
        assert "AWS_SECRET_ACCESS_KEY" not in filtered
        assert "GOOGLE_APPLICATION_CREDENTIALS" not in filtered
        assert "GATEWAY_JOB_ID" in filtered

    def test_writable_mounts(self):
        """Test writable mount configuration."""
        config = SandboxConfig(
            writable_mounts={
                "/tmp/checkpoints": "/checkpoints",
                "/tmp/results": "/results",
            }
        )

        args = config.to_docker_args()

        assert "-v" in args
        # Check mount is included
        mount_args = [args[i+1] for i, a in enumerate(args) if a == "-v" and i+1 < len(args)]
        assert any("/tmp/checkpoints:/checkpoints:rw" in m for m in mount_args)


class TestNetworkManager:
    """Tests for NetworkManager."""

    @pytest.fixture
    def manager(self):
        """Create network manager."""
        return NetworkManager()

    @pytest.mark.asyncio
    async def test_create_job_network(self, manager):
        """Test job network creation (mocked)."""
        with patch('asyncio.create_subprocess_exec') as mock_exec:
            mock_proc = AsyncMock()
            mock_proc.returncode = 0
            mock_proc.communicate = AsyncMock(return_value=(b"network_id", b""))
            mock_exec.return_value = mock_proc

            network_name = await manager.create_job_network("job-123")

            assert network_name == "gateway-job-job-123"
            assert "job-123" in manager._networks

    @pytest.mark.asyncio
    async def test_delete_job_network(self, manager):
        """Test job network deletion (mocked)."""
        # Add a network first
        manager._networks["job-123"] = {"name": "gateway-job-job-123"}

        with patch('asyncio.create_subprocess_exec') as mock_exec:
            mock_proc = AsyncMock()
            mock_proc.returncode = 0
            mock_proc.wait = AsyncMock(return_value=0)
            mock_exec.return_value = mock_proc

            result = await manager.delete_job_network("job-123")

            assert result is True
            assert "job-123" not in manager._networks

    @pytest.mark.asyncio
    async def test_ensure_gateway_network_exists(self, manager):
        """Test ensuring gateway network exists."""
        with patch('asyncio.create_subprocess_exec') as mock_exec:
            # First call - network exists
            mock_proc = AsyncMock()
            mock_proc.returncode = 0
            mock_proc.wait = AsyncMock(return_value=0)
            mock_exec.return_value = mock_proc

            network_name = await manager.ensure_gateway_network()

            assert network_name == "gateway-internal"


class TestContainerSandbox:
    """Tests for ContainerSandbox."""

    @pytest.fixture
    def sandbox(self):
        """Create container sandbox."""
        return ContainerSandbox()

    @requires_docker
    @pytest.mark.asyncio
    async def test_prepare_sandbox_standard(self, sandbox):
        """Test preparing standard sandbox."""
        with patch.object(sandbox.network_manager, 'create_job_network') as mock_create:
            mock_create.return_value = "gateway-job-job-123"

            config = await sandbox.prepare_sandbox(
                job_id="job-123",
                security_level="standard",
                network_mode="isolated",
            )

            assert config.security.level == SecurityLevel.STANDARD
            assert config.network.mode == NetworkMode.ISOLATED
            assert "job-123" in sandbox._sandboxes

    @requires_docker
    @pytest.mark.asyncio
    async def test_prepare_sandbox_strict(self, sandbox):
        """Test preparing strict sandbox."""
        with patch.object(sandbox.network_manager, 'create_job_network') as mock_create:
            mock_create.return_value = "gateway-job-job-456"

            config = await sandbox.prepare_sandbox(
                job_id="job-456",
                security_level="strict",
                network_mode="none",
            )

            assert config.security.level == SecurityLevel.STRICT
            assert config.network.mode == NetworkMode.NONE

    @requires_docker
    @pytest.mark.asyncio
    async def test_prepare_sandbox_with_mounts(self, sandbox):
        """Test preparing sandbox with checkpoint and result dirs."""
        with patch.object(sandbox.network_manager, 'create_job_network') as mock_create:
            mock_create.return_value = "gateway-job-job-789"

            config = await sandbox.prepare_sandbox(
                job_id="job-789",
                checkpoint_dir="/tmp/ckpt",
                result_dir="/tmp/results",
            )

            assert "/tmp/ckpt" in config.writable_mounts
            assert "/tmp/results" in config.writable_mounts
            assert config.writable_mounts["/tmp/ckpt"] == "/checkpoints"
            assert config.writable_mounts["/tmp/results"] == "/results"

    @requires_docker
    @pytest.mark.asyncio
    async def test_cleanup_sandbox(self, sandbox):
        """Test sandbox cleanup."""
        # Set up a sandbox
        with patch.object(sandbox.network_manager, 'create_job_network') as mock_create:
            mock_create.return_value = "gateway-job-cleanup-test"
            await sandbox.prepare_sandbox("cleanup-test")

        assert "cleanup-test" in sandbox._sandboxes

        # Clean it up
        with patch.object(sandbox.network_manager, 'delete_job_network') as mock_delete:
            mock_delete.return_value = True
            await sandbox.cleanup_sandbox("cleanup-test")

        assert "cleanup-test" not in sandbox._sandboxes


class TestSeccompProfile:
    """Tests for seccomp profile generation."""

    def test_generate_seccomp_profile_default(self):
        """Test default seccomp profile generation."""
        profile = generate_seccomp_profile()

        assert profile["defaultAction"] == "SCMP_ACT_ERRNO"
        assert "SCMP_ARCH_X86_64" in profile["architectures"]
        assert len(profile["syscalls"]) == 1
        assert profile["syscalls"][0]["action"] == "SCMP_ACT_ALLOW"

        # Check common syscalls are allowed
        allowed = profile["syscalls"][0]["names"]
        assert "read" in allowed
        assert "write" in allowed
        assert "open" in allowed
        assert "socket" in allowed  # Network allowed by default

    def test_generate_seccomp_profile_no_network(self):
        """Test seccomp profile without network."""
        profile = generate_seccomp_profile(allow_network=False)

        allowed = profile["syscalls"][0]["names"]

        # Network syscalls should be removed
        assert "socket" not in allowed
        assert "connect" not in allowed
        assert "bind" not in allowed

        # But other syscalls should still be there
        assert "read" in allowed
        assert "write" in allowed

    def test_generate_seccomp_profile_strict(self):
        """Test strict seccomp profile."""
        profile = generate_seccomp_profile(strict=True)

        allowed = profile["syscalls"][0]["names"]

        # Dangerous syscalls should be removed
        assert "mount" not in allowed
        assert "reboot" not in allowed
        assert "kexec_load" not in allowed

    def test_save_seccomp_profile(self, tmp_path):
        """Test saving seccomp profile to file."""
        path = str(tmp_path / "seccomp.json")

        result = save_seccomp_profile(path)

        assert result == path

        import json
        with open(path) as f:
            profile = json.load(f)

        assert "defaultAction" in profile
        assert "syscalls" in profile
