"""Tests for Apple Containers runtime."""

from __future__ import annotations

import json
import platform
import subprocess
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from gaol.models.container import (
    ContainerConfig,
    ContainerStatus,
    NetworkConfig,
    ResourceLimits,
)
from gaol.runtimes.apple import AppleRuntime


@pytest.fixture
def runtime(tmp_path: Path) -> AppleRuntime:
    """Create an AppleRuntime instance with a temp metadata directory."""
    rt = AppleRuntime()
    rt._metadata_dir = tmp_path / "apple" / "containers"
    rt._metadata_dir.mkdir(parents=True, exist_ok=True)
    return rt


@pytest.fixture
def sample_config() -> ContainerConfig:
    """Create a sample container configuration."""
    return ContainerConfig(
        name="test-container",
        distro="trixie",
        profiles=[],
        network=NetworkConfig(ports={8080: 80}),
        environment={"FOO": "bar"},
        volumes={"/host/path": "/container/path"},
        resources=ResourceLimits(cpu_count=2, memory_mb=1024),
    )


class TestAppleRuntimeProperties:
    """Test AppleRuntime basic properties."""

    def test_name(self, runtime: AppleRuntime) -> None:
        """Test runtime name is 'apple'."""
        assert runtime.name == "apple"

    @patch("platform.system")
    @patch("platform.machine")
    @patch("shutil.which")
    def test_is_available_not_macos(
        self,
        mock_which: MagicMock,
        mock_machine: MagicMock,
        mock_system: MagicMock,
        runtime: AppleRuntime,
    ) -> None:
        """Test is_available returns False on non-macOS."""
        mock_system.return_value = "Linux"
        mock_machine.return_value = "x86_64"
        mock_which.return_value = "/usr/bin/container"

        assert runtime.is_available() is False

    @patch("platform.system")
    @patch("platform.machine")
    @patch("shutil.which")
    def test_is_available_not_arm64(
        self,
        mock_which: MagicMock,
        mock_machine: MagicMock,
        mock_system: MagicMock,
        runtime: AppleRuntime,
    ) -> None:
        """Test is_available returns False on Intel Mac."""
        mock_system.return_value = "Darwin"
        mock_machine.return_value = "x86_64"
        mock_which.return_value = "/usr/bin/container"

        assert runtime.is_available() is False

    @patch("platform.system")
    @patch("platform.machine")
    @patch("shutil.which")
    def test_is_available_no_cli(
        self,
        mock_which: MagicMock,
        mock_machine: MagicMock,
        mock_system: MagicMock,
        runtime: AppleRuntime,
    ) -> None:
        """Test is_available returns False when container CLI not installed."""
        mock_system.return_value = "Darwin"
        mock_machine.return_value = "arm64"
        mock_which.return_value = None

        assert runtime.is_available() is False

    @patch("subprocess.run")
    @patch("platform.system")
    @patch("platform.machine")
    @patch("shutil.which")
    def test_is_available_system_not_running(
        self,
        mock_which: MagicMock,
        mock_machine: MagicMock,
        mock_system: MagicMock,
        mock_run: MagicMock,
        runtime: AppleRuntime,
    ) -> None:
        """Test is_available returns False when container system not running."""
        mock_system.return_value = "Darwin"
        mock_machine.return_value = "arm64"
        mock_which.return_value = "/usr/local/bin/container"
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "system", "status"],
            returncode=1,
            stdout="",
            stderr="System not running",
        )

        assert runtime.is_available() is False

    @patch("subprocess.run")
    @patch("platform.system")
    @patch("platform.machine")
    @patch("shutil.which")
    def test_is_available_true(
        self,
        mock_which: MagicMock,
        mock_machine: MagicMock,
        mock_system: MagicMock,
        mock_run: MagicMock,
        runtime: AppleRuntime,
    ) -> None:
        """Test is_available returns True when all conditions met."""
        mock_system.return_value = "Darwin"
        mock_machine.return_value = "arm64"
        mock_which.return_value = "/usr/local/bin/container"
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "system", "status"],
            returncode=0,
            stdout="Running",
            stderr="",
        )

        assert runtime.is_available() is True


class TestAppleRuntimeCreate:
    """Test container creation."""

    @patch("subprocess.run")
    def test_create_basic(
        self,
        mock_run: MagicMock,
        runtime: AppleRuntime,
        sample_config: ContainerConfig,
    ) -> None:
        """Test basic container creation."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "create", "--name", "test-container"],
            returncode=0,
            stdout="abc123def456",
            stderr="",
        )

        container_id = runtime.create(sample_config)

        assert container_id == "abc123def456"
        assert mock_run.called
        call_args = mock_run.call_args[0][0]
        assert "container" in call_args
        assert "create" in call_args
        assert "--name" in call_args
        assert "test-container" in call_args

        # Check metadata was saved
        metadata_path = runtime._metadata_path("test-container")
        assert metadata_path.exists()
        with open(metadata_path) as f:
            metadata = json.load(f)
        assert metadata["name"] == "test-container"

    @patch("subprocess.run")
    def test_create_with_environment(
        self,
        mock_run: MagicMock,
        runtime: AppleRuntime,
        sample_config: ContainerConfig,
    ) -> None:
        """Test container creation with environment variables."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "create"],
            returncode=0,
            stdout="container123",
            stderr="",
        )

        runtime.create(sample_config)

        call_args = mock_run.call_args[0][0]
        assert "-e" in call_args
        env_idx = call_args.index("-e")
        assert call_args[env_idx + 1] == "FOO=bar"

    @patch("subprocess.run")
    def test_create_with_ports(
        self,
        mock_run: MagicMock,
        runtime: AppleRuntime,
        sample_config: ContainerConfig,
    ) -> None:
        """Test container creation with port mappings."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "create"],
            returncode=0,
            stdout="container123",
            stderr="",
        )

        runtime.create(sample_config)

        call_args = mock_run.call_args[0][0]
        assert "-p" in call_args
        port_idx = call_args.index("-p")
        assert call_args[port_idx + 1] == "8080:80"

    @patch("subprocess.run")
    def test_create_with_volumes(
        self,
        mock_run: MagicMock,
        runtime: AppleRuntime,
        sample_config: ContainerConfig,
    ) -> None:
        """Test container creation with volume mounts."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "create"],
            returncode=0,
            stdout="container123",
            stderr="",
        )

        runtime.create(sample_config)

        call_args = mock_run.call_args[0][0]
        assert "-v" in call_args
        vol_idx = call_args.index("-v")
        assert call_args[vol_idx + 1] == "/host/path:/container/path"

    @patch("subprocess.run")
    def test_create_with_resources(
        self,
        mock_run: MagicMock,
        runtime: AppleRuntime,
        sample_config: ContainerConfig,
    ) -> None:
        """Test container creation with resource limits."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "create"],
            returncode=0,
            stdout="container123",
            stderr="",
        )

        runtime.create(sample_config)

        call_args = mock_run.call_args[0][0]
        assert "-c" in call_args
        assert "-m" in call_args
        cpu_idx = call_args.index("-c")
        mem_idx = call_args.index("-m")
        assert call_args[cpu_idx + 1] == "2"
        assert call_args[mem_idx + 1] == "1024m"


class TestAppleRuntimeLifecycle:
    """Test container lifecycle operations."""

    @patch("subprocess.run")
    def test_start(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test starting a container."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "start"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.start("container123")

        call_args = mock_run.call_args[0][0]
        assert call_args == ["container", "start", "container123"]

    @patch("subprocess.run")
    def test_stop(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test stopping a container."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "stop"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.stop("container123", timeout=15)

        call_args = mock_run.call_args[0][0]
        assert call_args == ["container", "stop", "--time", "15", "container123"]

    @patch("subprocess.run")
    def test_destroy(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test destroying a container."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "delete"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.destroy("container123")

        call_args = mock_run.call_args[0][0]
        assert call_args == ["container", "delete", "container123"]

    @patch("subprocess.run")
    def test_destroy_force(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test force destroying a container."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "delete"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.destroy("container123", force=True)

        call_args = mock_run.call_args[0][0]
        assert call_args == ["container", "delete", "--force", "container123"]


class TestAppleRuntimeExec:
    """Test command execution."""

    @patch("subprocess.run")
    def test_exec_basic(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test basic command execution."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "exec"],
            returncode=0,
            stdout="output",
            stderr="",
        )

        exit_code, stdout, stderr = runtime.exec("container123", ["echo", "hello"])

        assert exit_code == 0
        assert stdout == "output"
        assert stderr == ""

        call_args = mock_run.call_args[0][0]
        assert "exec" in call_args
        assert "container123" in call_args
        assert "--" in call_args
        assert "echo" in call_args
        assert "hello" in call_args

    @patch("subprocess.run")
    def test_exec_with_user(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test command execution with user."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "exec"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.exec("container123", ["whoami"], user="root")

        call_args = mock_run.call_args[0][0]
        assert "-u" in call_args
        user_idx = call_args.index("-u")
        assert call_args[user_idx + 1] == "root"

    @patch("subprocess.run")
    def test_exec_with_workdir(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test command execution with working directory."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "exec"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.exec("container123", ["pwd"], workdir="/app")

        call_args = mock_run.call_args[0][0]
        assert "-w" in call_args
        workdir_idx = call_args.index("-w")
        assert call_args[workdir_idx + 1] == "/app"

    @patch("subprocess.run")
    def test_exec_with_environment(
        self, mock_run: MagicMock, runtime: AppleRuntime
    ) -> None:
        """Test command execution with environment."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "exec"],
            returncode=0,
            stdout="",
            stderr="",
        )

        runtime.exec("container123", ["env"], environment={"KEY": "value"})

        call_args = mock_run.call_args[0][0]
        assert "-e" in call_args
        env_idx = call_args.index("-e")
        assert call_args[env_idx + 1] == "KEY=value"


class TestAppleRuntimeList:
    """Test container listing."""

    @patch("subprocess.run")
    def test_list_empty(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test listing with no containers."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "list"],
            returncode=0,
            stdout="[]",
            stderr="",
        )

        containers = runtime.list()
        assert containers == []

    @patch("subprocess.run")
    def test_list_with_containers(
        self, mock_run: MagicMock, runtime: AppleRuntime
    ) -> None:
        """Test listing with containers (only gaol-managed)."""
        # First, create metadata for a container
        metadata = {
            "name": "test-container",
            "id": "abc123",
            "profiles": ["base"],
            "distro": "trixie",
            "image": "debian:trixie",
            "created_at": datetime.now().isoformat(),
            "runtime": "apple",
            "config": {"network": {"ports": {8080: 80}}},
        }
        metadata_path = runtime._metadata_path("test-container")
        with open(metadata_path, "w") as f:
            json.dump(metadata, f)

        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "list"],
            returncode=0,
            stdout=json.dumps(
                [
                    {
                        "Id": "abc123def456",
                        "Name": "test-container",
                        "State": "running",
                        "Image": "debian:trixie",
                        "Created": "2025-01-13T10:00:00Z",
                    }
                ]
            ),
            stderr="",
        )

        containers = runtime.list(all=True)

        assert len(containers) == 1
        assert containers[0].name == "test-container"
        assert containers[0].runtime == "apple"
        assert containers[0].status == ContainerStatus.RUNNING


class TestAppleRuntimeStatus:
    """Test container status."""

    @patch("subprocess.run")
    def test_status(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test getting container status."""
        # Create metadata
        metadata = {
            "name": "test-container",
            "profiles": ["dev"],
            "config": {"network": {"ports": {}}},
        }
        metadata_path = runtime._metadata_path("test-container")
        with open(metadata_path, "w") as f:
            json.dump(metadata, f)

        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "inspect"],
            returncode=0,
            stdout=json.dumps(
                {
                    "Id": "abc123",
                    "Name": "test-container",
                    "State": {"Status": "running"},
                    "Image": "debian:trixie",
                    "Created": "2025-01-13T10:00:00Z",
                }
            ),
            stderr="",
        )

        status = runtime.status("test-container")

        assert status.name == "test-container"
        assert status.runtime == "apple"
        assert status.status == ContainerStatus.RUNNING
        assert status.profiles == ["dev"]

    @patch("subprocess.run")
    def test_status_not_found(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test status for non-existent container."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "inspect"],
            returncode=1,
            stdout="",
            stderr="Container not found",
        )

        with pytest.raises(RuntimeError, match="Container not found"):
            runtime.status("nonexistent")


class TestAppleRuntimeLogs:
    """Test container logs."""

    @patch("subprocess.run")
    def test_logs_basic(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test getting container logs."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "logs"],
            returncode=0,
            stdout="Log line 1\nLog line 2\n",
            stderr="",
        )

        logs = runtime.logs("container123")

        assert "Log line 1" in logs
        assert "Log line 2" in logs

    @patch("subprocess.run")
    def test_logs_with_tail(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test getting last N lines of logs."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "logs"],
            returncode=0,
            stdout="Last line\n",
            stderr="",
        )

        runtime.logs("container123", tail=10)

        call_args = mock_run.call_args[0][0]
        assert "-n" in call_args
        n_idx = call_args.index("-n")
        assert call_args[n_idx + 1] == "10"


class TestAppleRuntimeStats:
    """Test container stats."""

    @patch("subprocess.run")
    def test_stats(self, mock_run: MagicMock, runtime: AppleRuntime) -> None:
        """Test getting container stats."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["container", "stats"],
            returncode=0,
            stdout=json.dumps(
                {
                    "CPUPerc": "15.5%",
                    "MemUsage": "512MB / 1GB",
                    "MemPerc": "50%",
                    "NetIO": "1MB / 2MB",
                    "BlockIO": "100MB / 50MB",
                    "PIDs": 10,
                }
            ),
            stderr="",
        )

        stats = runtime.stats("container123")

        assert stats.container_id == "container123"
        assert stats.cpu_percent == 15.5
        assert stats.memory_percent == 50.0
        assert stats.pids == 10


class TestAppleRuntimePauseUnsupported:
    """Test unsupported pause operations."""

    def test_pause_not_implemented(self, runtime: AppleRuntime) -> None:
        """Test that pause raises NotImplementedError."""
        with pytest.raises(NotImplementedError, match="does not support pause"):
            runtime.pause("container123")

    def test_unpause_not_implemented(self, runtime: AppleRuntime) -> None:
        """Test that unpause raises NotImplementedError."""
        with pytest.raises(NotImplementedError, match="does not support unpause"):
            runtime.unpause("container123")


class TestAppleRuntimeCheckpointUnsupported:
    """Test unsupported checkpoint operations."""

    def test_checkpoint_create_not_implemented(self, runtime: AppleRuntime) -> None:
        """Test that checkpoint_create raises NotImplementedError."""
        with pytest.raises(NotImplementedError, match="not supported"):
            runtime.checkpoint_create("container123", "checkpoint1")

    def test_checkpoint_restore_not_implemented(self, runtime: AppleRuntime) -> None:
        """Test that checkpoint_restore raises NotImplementedError."""
        with pytest.raises(NotImplementedError, match="not supported"):
            runtime.checkpoint_restore("container123", "checkpoint1")

    def test_checkpoint_list_returns_empty(self, runtime: AppleRuntime) -> None:
        """Test that checkpoint_list returns empty list."""
        assert runtime.checkpoint_list("container123") == []

    def test_checkpoint_delete_returns_false(self, runtime: AppleRuntime) -> None:
        """Test that checkpoint_delete returns False."""
        assert runtime.checkpoint_delete("container123", "checkpoint1") is False

    def test_checkpoint_capabilities(self, runtime: AppleRuntime) -> None:
        """Test checkpoint capabilities returns not available."""
        caps = runtime.checkpoint_capabilities()
        assert caps["available"] is False
        assert caps["checkpoint_supported"] is False


class TestAppleRuntimeMetadata:
    """Test metadata operations."""

    def test_save_and_load_metadata(self, runtime: AppleRuntime) -> None:
        """Test saving and loading metadata."""
        from gaol.models.container import ContainerConfig

        config = ContainerConfig(
            name="test",
            distro="trixie",
            profiles=["base", "dev-python"],
        )

        runtime._save_metadata("test", "container123", config)

        metadata = runtime._load_metadata("test")
        assert metadata is not None
        assert metadata["name"] == "test"
        assert metadata["id"] == "container123"
        assert metadata["profiles"] == ["base", "dev-python"]
        assert metadata["distro"] == "trixie"

    def test_delete_metadata(self, runtime: AppleRuntime) -> None:
        """Test deleting metadata."""
        # Create metadata first
        metadata_path = runtime._metadata_path("test")
        with open(metadata_path, "w") as f:
            json.dump({"name": "test"}, f)

        assert metadata_path.exists()

        runtime._delete_metadata("test")

        assert not metadata_path.exists()

    def test_load_nonexistent_metadata(self, runtime: AppleRuntime) -> None:
        """Test loading metadata for non-existent container."""
        metadata = runtime._load_metadata("nonexistent")
        assert metadata is None


class TestAppleRuntimeHelpers:
    """Test helper methods."""

    def test_parse_size_bytes(self, runtime: AppleRuntime) -> None:
        """Test parsing byte sizes."""
        assert runtime._parse_size("100B") == 100
        assert runtime._parse_size("1KB") == 1024
        assert runtime._parse_size("1MB") == 1024**2
        assert runtime._parse_size("1GB") == 1024**3
        assert runtime._parse_size("1.5GB") == int(1.5 * 1024**3)

    def test_parse_size_invalid(self, runtime: AppleRuntime) -> None:
        """Test parsing invalid size."""
        assert runtime._parse_size("invalid") == 0
        assert runtime._parse_size("") == 0

    def test_apple_status_to_status(self, runtime: AppleRuntime) -> None:
        """Test status conversion."""
        assert runtime._apple_status_to_status("created") == ContainerStatus.CREATED
        assert runtime._apple_status_to_status("running") == ContainerStatus.RUNNING
        assert runtime._apple_status_to_status("stopped") == ContainerStatus.STOPPED
        assert runtime._apple_status_to_status("exited") == ContainerStatus.EXITED
        assert runtime._apple_status_to_status("unknown") == ContainerStatus.UNKNOWN

    def test_parse_datetime(self, runtime: AppleRuntime) -> None:
        """Test datetime parsing."""
        dt = runtime._parse_datetime("2025-01-13T10:00:00Z")
        assert dt is not None
        assert dt.year == 2025
        assert dt.month == 1
        assert dt.day == 13

    def test_parse_datetime_none(self, runtime: AppleRuntime) -> None:
        """Test parsing None datetime."""
        assert runtime._parse_datetime(None) is None

    def test_parse_datetime_invalid(self, runtime: AppleRuntime) -> None:
        """Test parsing invalid datetime."""
        assert runtime._parse_datetime("not-a-date") is None


class TestAppleRuntimeRegistration:
    """Test runtime registration."""

    def test_runtime_registered(self) -> None:
        """Test that AppleRuntime is registered in the runtimes module."""
        from gaol.runtimes import AppleRuntime, get_runtime

        runtime = get_runtime("apple")
        assert isinstance(runtime, AppleRuntime)
        assert runtime.name == "apple"

    def test_runtime_in_list(self) -> None:
        """Test that apple runtime is in the available runtimes list."""
        from gaol.runtimes import list_available_runtimes

        runtimes = list_available_runtimes()
        runtime_names = [name for name, _ in runtimes]
        assert "apple" in runtime_names
