"""
Tests for Docker Executor

Tests the Docker container execution system.
"""

import os
from unittest.mock import AsyncMock, patch

import pytest

from verlex_server.gateway.backend.docker_executor import (
    ContainerConfig,
    ContainerResult,
    ContainerStatus,
    DockerExecutor,
)


class TestContainerConfig:
    """Tests for ContainerConfig."""

    def test_default_values(self):
        """Test default configuration values."""
        config = ContainerConfig()

        assert config.python_version == "3.12"
        assert config.cpu_limit == 0.0  # 0 = not yet configured by LLM
        assert config.memory_limit == "0"  # 0 = not yet configured by LLM
        assert config.gpu_enabled is False
        assert config.execution_timeout == 3600

    def test_get_base_image_default(self):
        """Test default base image generation."""
        config = ContainerConfig(python_version="3.10")
        assert config.get_base_image() == "python:3.10-slim"

    def test_get_base_image_custom(self):
        """Test custom base image."""
        config = ContainerConfig(base_image="my-custom-image:latest")
        assert config.get_base_image() == "my-custom-image:latest"

    def test_gpu_configuration(self):
        """Test GPU configuration."""
        config = ContainerConfig(
            gpu_enabled=True,
            gpu_count=2,
        )

        assert config.gpu_enabled is True
        assert config.gpu_count == 2


class TestDockerExecutor:
    """Tests for DockerExecutor."""

    @pytest.fixture
    def executor(self):
        """Create a DockerExecutor instance."""
        return DockerExecutor()

    def test_init(self, executor):
        """Test executor initialization."""
        assert executor.docker_host is None
        assert executor.checkpoint_base_path.exists()

    @pytest.mark.asyncio
    async def test_is_docker_available_not_installed(self, executor):
        """Test Docker availability check when not installed."""
        with patch('asyncio.create_subprocess_exec', side_effect=FileNotFoundError):
            result = await executor.is_docker_available()
            assert result is False

    @pytest.mark.asyncio
    async def test_is_docker_available_success(self, executor):
        """Test Docker availability check when installed."""
        mock_proc = AsyncMock()
        mock_proc.returncode = 0
        mock_proc.communicate = AsyncMock(return_value=(b"24.0.0", b""))

        with patch('asyncio.create_subprocess_exec', return_value=mock_proc):
            # Reset cached value
            executor._docker_available = None
            result = await executor.is_docker_available()
            assert result is True

    def test_generate_dockerfile(self, executor):
        """Test Dockerfile generation."""
        config = ContainerConfig(python_version="3.12")
        pip_packages = ["numpy", "pandas"]

        dockerfile = executor._generate_dockerfile(config, pip_packages)

        assert "FROM python:3.12-slim" in dockerfile
        assert "pip install" in dockerfile
        assert "numpy" in dockerfile
        assert "pandas" in dockerfile
        assert "cloudpickle" in dockerfile

    def test_generate_dockerfile_with_requirements(self, executor):
        """Test Dockerfile generation with requirements.txt."""
        config = ContainerConfig()

        dockerfile = executor._generate_dockerfile(
            config,
            pip_packages=[],
            requirements_content="numpy==1.24.0\npandas==2.0.0",
        )

        assert "COPY requirements.txt" in dockerfile
        assert "-r requirements.txt" in dockerfile

    def test_generate_runner_script(self, executor):
        """Test runner script generation."""
        script = executor._generate_runner_script()

        assert "cloudpickle" in script
        assert "function.pkl" in script
        assert "result.pkl" in script
        assert "__main__" in script

    @pytest.mark.asyncio
    async def test_fallback_local_execution(self, executor):
        """Test fallback to local execution."""
        import cloudpickle

        def test_func():
            return 42

        func_bytes = cloudpickle.dumps(test_func)
        output_lines = []

        result = await executor._fallback_local_execution(
            job_id="test-123",
            function_bytes=func_bytes,
            pip_packages=[],
            output_callback=lambda x: output_lines.append(x),
        )

        assert result.status == ContainerStatus.COMPLETED
        assert result.result == 42
        assert result.exit_code == 0

    @pytest.mark.asyncio
    async def test_fallback_local_execution_error(self, executor):
        """Test fallback execution with error."""
        import cloudpickle

        def failing_func():
            raise ValueError("Test error")

        func_bytes = cloudpickle.dumps(failing_func)
        output_lines = []

        result = await executor._fallback_local_execution(
            job_id="test-123",
            function_bytes=func_bytes,
            pip_packages=[],
            output_callback=lambda x: output_lines.append(x),
        )

        assert result.status == ContainerStatus.FAILED
        assert "Test error" in result.error
        assert result.exit_code == 1

    @pytest.mark.asyncio
    async def test_execute_job_no_docker(self, executor):
        """Test job execution when Docker is not available."""
        import cloudpickle

        def test_func():
            return "hello"

        func_bytes = cloudpickle.dumps(test_func)

        # Mock Docker as unavailable
        executor._docker_available = False

        output_lines = []
        result = await executor.execute_job(
            job_id="test-456",
            function_bytes=func_bytes,
            output_callback=lambda x: output_lines.append(x),
        )

        assert result.status == ContainerStatus.COMPLETED
        assert result.result == "hello"

    @pytest.mark.asyncio
    async def test_list_gateway_containers(self, executor):
        """Test listing GateWay containers."""
        mock_proc = AsyncMock()
        mock_proc.returncode = 0
        mock_proc.communicate = AsyncMock(return_value=(
            b'{"id":"abc123","name":"gateway-test","status":"running","created":"2024-01-01"}\n',
            b""
        ))

        with patch('asyncio.create_subprocess_exec', return_value=mock_proc):
            containers = await executor.list_gateway_containers()

            assert len(containers) == 1
            assert containers[0]["name"] == "gateway-test"


class TestContainerResult:
    """Tests for ContainerResult."""

    def test_default_values(self):
        """Test default result values."""
        result = ContainerResult(
            container_id="test-123",
            status=ContainerStatus.PENDING,
        )

        assert result.exit_code is None
        assert result.result is None
        assert result.error is None
        assert result.duration_seconds == 0

    def test_completed_result(self):
        """Test completed result."""
        from datetime import datetime, timedelta

        start = datetime.utcnow()
        end = start + timedelta(seconds=10)

        result = ContainerResult(
            container_id="test-123",
            status=ContainerStatus.COMPLETED,
            exit_code=0,
            result={"answer": 42},
            started_at=start,
            finished_at=end,
            duration_seconds=10.0,
        )

        assert result.status == ContainerStatus.COMPLETED
        assert result.result == {"answer": 42}
        assert result.duration_seconds == 10.0


# Integration test - only runs if Docker is available and GATEWAY_DOCKER_TESTS is set
@pytest.mark.asyncio
@pytest.mark.integration
@pytest.mark.skipif(
    os.environ.get("GATEWAY_DOCKER_TESTS", "").lower() not in ("true", "1", "yes"),
    reason="Docker integration tests require Docker. Set GATEWAY_DOCKER_TESTS=true to run."
)
async def test_real_docker_execution():
    """Integration test with real Docker (requires Docker to be running)."""
    import cloudpickle

    executor = DockerExecutor()

    # Skip if Docker not available
    if not await executor.is_docker_available():
        pytest.skip("Docker not available")

    def simple_job():
        return 1 + 1

    func_bytes = cloudpickle.dumps(simple_job)

    result = await executor.execute_job(
        job_id="integration-test",
        function_bytes=func_bytes,
        pip_packages=[],
        config=ContainerConfig(execution_timeout=60),
    )

    assert result.status == ContainerStatus.COMPLETED
    assert result.result == 2
