"""
Tests for Docker Sandbox.

This module provides tests for the DockerSandbox class, including
configuration, execution, and error handling scenarios.
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from torivers_sdk.testing.docker_sandbox import (
    DockerSandbox,
    DockerSandboxConfig,
    DockerSandboxError,
    SandboxResult,
)

# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def sandbox_config() -> DockerSandboxConfig:
    """Create a test sandbox configuration."""
    return DockerSandboxConfig(
        memory_limit="256m",
        cpu_count=0.5,
        timeout=60,
        network_mode="none",
        mock_credentials=True,
    )


@pytest.fixture
def mock_docker_client():
    """Create a mock Docker client."""
    with patch("docker.from_env") as mock_from_env:
        mock_client = MagicMock()
        mock_from_env.return_value = mock_client
        yield mock_client


@pytest.fixture
def temp_automation_dir():
    """Create a temporary automation directory."""
    with tempfile.TemporaryDirectory() as temp_dir:
        # Create minimal automation structure
        main_py = Path(temp_dir) / "main.py"
        main_py.write_text(
            '''
"""Test automation."""
from typing import Any

class TestAutomation:
    """Test automation class."""

    def validate_input(self, input_data: dict) -> None:
        pass

    def build_graph(self):
        from langgraph.graph import StateGraph
        from typing_extensions import TypedDict

        class State(TypedDict):
            input_data: dict
            output_data: dict

        graph = StateGraph(State)
        return graph

    def on_startup(self) -> None:
        pass

    def on_shutdown(self) -> None:
        pass

automation = TestAutomation()
'''
        )

        automation_yaml = Path(temp_dir) / "automation.yaml"
        automation_yaml.write_text(
            """
name: test-automation
version: 1.0.0
description: Test automation
"""
        )

        yield temp_dir


# =============================================================================
# DockerSandboxConfig Tests
# =============================================================================


class TestDockerSandboxConfig:
    """Test suite for DockerSandboxConfig."""

    def test_default_config(self) -> None:
        """Test default configuration values."""
        config = DockerSandboxConfig()

        assert config.memory_limit == "512m"
        assert config.cpu_count == 1.0
        assert config.timeout == 300
        assert config.network_mode == "bridge"
        assert config.mock_credentials is True
        assert config.image == "torivers/automation-sandbox:latest"
        assert config.auto_pull is True
        assert config.remove_container is True

    def test_custom_config(self) -> None:
        """Test custom configuration values."""
        config = DockerSandboxConfig(
            memory_limit="1g",
            cpu_count=2.0,
            timeout=600,
            network_mode="none",
            mock_credentials=False,
            image="custom/image:v1",
            auto_pull=False,
            environment={"CUSTOM_VAR": "value"},
        )

        assert config.memory_limit == "1g"
        assert config.cpu_count == 2.0
        assert config.timeout == 600
        assert config.network_mode == "none"
        assert config.mock_credentials is False
        assert config.image == "custom/image:v1"
        assert config.auto_pull is False
        assert config.environment == {"CUSTOM_VAR": "value"}


# =============================================================================
# SandboxResult Tests
# =============================================================================


class TestSandboxResult:
    """Test suite for SandboxResult."""

    def test_successful_result(self) -> None:
        """Test creating a successful result."""
        result = SandboxResult(
            success=True,
            output={"key": "value"},
            logs="Execution complete",
            exit_code=0,
            duration_seconds=5.0,
            memory_peak_mb=128.5,
            tokens_used=100,
        )

        assert result.success is True
        assert result.output == {"key": "value"}
        assert result.logs == "Execution complete"
        assert result.exit_code == 0
        assert result.duration_seconds == 5.0
        assert result.memory_peak_mb == 128.5
        assert result.tokens_used == 100

    def test_failed_result(self) -> None:
        """Test creating a failed result."""
        result = SandboxResult(
            success=False,
            output={},
            logs="Error occurred",
            exit_code=1,
            error="Execution failed",
        )

        assert result.success is False
        assert result.exit_code == 1
        assert result.error == "Execution failed"

    def test_get_output(self) -> None:
        """Test get_output helper method."""
        result = SandboxResult(
            success=True,
            output={"data": "test", "count": 42},
            logs="",
            exit_code=0,
        )

        assert result.get_output("data") == "test"
        assert result.get_output("count") == 42
        assert result.get_output("missing") is None
        assert result.get_output("missing", "default") == "default"


# =============================================================================
# DockerSandbox Tests
# =============================================================================


class TestDockerSandbox:
    """Test suite for DockerSandbox."""

    def test_init_default_config(self) -> None:
        """Test initialization with default config."""
        sandbox = DockerSandbox()
        assert sandbox._config.memory_limit == "512m"
        assert sandbox._client is None

    def test_init_custom_config(self, sandbox_config: DockerSandboxConfig) -> None:
        """Test initialization with custom config."""
        sandbox = DockerSandbox(sandbox_config)
        assert sandbox._config.memory_limit == "256m"
        assert sandbox._config.cpu_count == 0.5

    def test_is_available_with_docker(self, mock_docker_client: MagicMock) -> None:
        """Test is_available when Docker is running."""
        mock_docker_client.ping.return_value = True

        sandbox = DockerSandbox()
        assert sandbox.is_available() is True

    def test_is_available_without_docker(self) -> None:
        """Test is_available when Docker is not running."""
        with patch("docker.from_env") as mock_from_env:
            from docker.errors import DockerException

            mock_from_env.side_effect = DockerException("Connection refused")

            sandbox = DockerSandbox()
            assert sandbox.is_available() is False

    def test_get_client_creates_client(self, mock_docker_client: MagicMock) -> None:
        """Test _get_client creates and caches client."""
        mock_docker_client.ping.return_value = True

        sandbox = DockerSandbox()
        client1 = sandbox._get_client()
        client2 = sandbox._get_client()

        assert client1 is client2  # Same cached instance

    def test_get_client_raises_on_failure(self) -> None:
        """Test _get_client raises DockerSandboxError on failure."""
        with patch("docker.from_env") as mock_from_env:
            from docker.errors import DockerException

            mock_from_env.side_effect = DockerException("Connection refused")

            sandbox = DockerSandbox()
            with pytest.raises(DockerSandboxError) as exc_info:
                sandbox._get_client()

            assert "Failed to connect to Docker daemon" in str(exc_info.value)


class TestDockerSandboxExecution:
    """Test suite for DockerSandbox execution."""

    @pytest.mark.asyncio
    async def test_run_invalid_path(self) -> None:
        """Test run with invalid automation path."""
        sandbox = DockerSandbox()
        result = await sandbox.run(
            automation_path="/nonexistent/path",
            input_data={"test": "data"},
        )

        assert result.success is False
        assert "does not exist" in result.error

    @pytest.mark.asyncio
    async def test_run_with_mock_container(
        self,
        mock_docker_client: MagicMock,
        temp_automation_dir: str,
    ) -> None:
        """Test run with mocked Docker container."""
        # Set up mock container
        mock_container = MagicMock()
        mock_container.id = "test-container-123"
        mock_container.wait.return_value = {"StatusCode": 0}
        mock_container.logs.return_value = b"Execution successful"
        mock_container.stats.return_value = {
            "memory_stats": {"max_usage": 128 * 1024 * 1024}
        }

        mock_docker_client.containers.run.return_value = mock_container
        mock_docker_client.images.get.return_value = MagicMock()
        mock_docker_client.ping.return_value = True

        # Create output file that would be written by container
        sandbox = DockerSandbox()

        # Mock the _read_output to return expected data
        with patch.object(
            sandbox,
            "_read_output",
            return_value={
                "output_data": {
                    "_version": 1,
                    "_blocks": [
                        {
                            "type": "text",
                            "label": "Result",
                            "content": "success",
                            "format": "plain",
                        }
                    ],
                },
                "tokens_used": 50,
            },
        ):
            result = await sandbox.run(
                automation_path=temp_automation_dir,
                input_data={"query": "test"},
            )

        assert result.exit_code == 0
        assert result.container_id == "test-container-123"
        assert "result" in result.output

    @pytest.mark.asyncio
    async def test_run_timeout(
        self,
        mock_docker_client: MagicMock,
        temp_automation_dir: str,
    ) -> None:
        """Test run with container timeout."""
        mock_container = MagicMock()
        mock_container.id = "test-container-timeout"
        mock_container.wait.side_effect = Exception("Timeout waiting for container")
        mock_container.logs.return_value = b"Timed out"

        mock_docker_client.containers.run.return_value = mock_container
        mock_docker_client.images.get.return_value = MagicMock()
        mock_docker_client.ping.return_value = True

        config = DockerSandboxConfig(timeout=1)
        sandbox = DockerSandbox(config)

        result = await sandbox.run(
            automation_path=temp_automation_dir,
            input_data={"query": "test"},
        )

        assert result.success is False
        assert "failed" in result.error.lower()
        mock_container.kill.assert_called_once()


class TestDockerSandboxImageManagement:
    """Test suite for Docker image management."""

    def test_ensure_image_exists(self, mock_docker_client: MagicMock) -> None:
        """Test _ensure_image when image exists."""
        mock_docker_client.images.get.return_value = MagicMock()
        mock_docker_client.ping.return_value = True

        sandbox = DockerSandbox()
        sandbox._ensure_image()  # Should not raise

        mock_docker_client.images.get.assert_called_with(sandbox._config.image)

    def test_ensure_image_auto_pull(self, mock_docker_client: MagicMock) -> None:
        """Test _ensure_image auto-pulls missing image."""
        from docker.errors import ImageNotFound

        mock_docker_client.images.get.side_effect = ImageNotFound("Image not found")
        mock_docker_client.images.pull.return_value = MagicMock()
        mock_docker_client.ping.return_value = True

        sandbox = DockerSandbox()
        sandbox._ensure_image()

        mock_docker_client.images.pull.assert_called_once()

    def test_ensure_image_no_auto_pull(self, mock_docker_client: MagicMock) -> None:
        """Test _ensure_image raises when auto_pull disabled."""
        from docker.errors import ImageNotFound

        mock_docker_client.images.get.side_effect = ImageNotFound("Image not found")
        mock_docker_client.ping.return_value = True

        config = DockerSandboxConfig(auto_pull=False)
        sandbox = DockerSandbox(config)

        with pytest.raises(DockerSandboxError) as exc_info:
            sandbox._ensure_image()

        assert "not found" in str(exc_info.value)

    def test_build_image_invalid_path(self, mock_docker_client: MagicMock) -> None:
        """Test build_image with invalid Dockerfile path."""
        mock_docker_client.ping.return_value = True

        sandbox = DockerSandbox()

        with pytest.raises(DockerSandboxError) as exc_info:
            sandbox.build_image(dockerfile_path="/nonexistent/path")

        assert "not found" in str(exc_info.value)


# =============================================================================
# Integration Tests (require Docker)
# =============================================================================


@pytest.mark.integration
class TestDockerSandboxIntegration:
    """Integration tests that require Docker to be running.

    These tests require:
    1. Docker to be installed and running
    2. The sandbox image to be built or available

    Run with: pytest -m integration tests/test_docker_sandbox.py
    """

    @pytest.fixture
    def docker_and_image_available(self) -> bool:
        """Check if Docker and sandbox image are available."""
        sandbox = DockerSandbox()
        if not sandbox.is_available():
            return False
        # Check if image is available (don't auto-pull for tests)
        try:
            config = DockerSandboxConfig(auto_pull=False)
            test_sandbox = DockerSandbox(config)
            test_sandbox._ensure_image()
            return True
        except DockerSandboxError:
            return False

    @pytest.mark.asyncio
    async def test_real_docker_execution(
        self,
        docker_and_image_available: bool,
        temp_automation_dir: str,
    ) -> None:
        """Test real Docker execution (skipped if Docker/image unavailable)."""
        if not docker_and_image_available:
            pytest.skip(
                "Docker is not available or sandbox image not built. "
                "Build with: docker build -t torivers/automation-sandbox:latest "
                "src/torivers_sdk/testing/docker/sandbox/"
            )

        sandbox = DockerSandbox()

        # This test requires the sandbox image to be built
        # It's primarily for manual testing
        result = await sandbox.run(
            automation_path=temp_automation_dir,
            input_data={"test": "data"},
        )

        # Just verify we got a result
        assert isinstance(result, SandboxResult)
        assert result.duration_seconds >= 0
