from __future__ import annotations

from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest

from atlas_runner.docker_backend import (
    DockerBackend,
    _parse_container_status,
    _parse_docker_ts,
)
from atlas_runner.models import ContainerState, Mount, RunSpec

# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------


def _make_inspect(
    *,
    cid: str = "abc123",
    name: str = "/test",
    status: str = "running",
    exit_code: int = 0,
    started_at: str = "2026-01-01T00:00:00Z",
    finished_at: str = "0001-01-01T00:00:00Z",
) -> dict:
    return {
        "Id": cid,
        "Name": name,
        "State": {
            "Status": status,
            "ExitCode": exit_code,
            "StartedAt": started_at,
            "FinishedAt": finished_at,
        },
    }


@pytest.fixture()
def mock_cli() -> MagicMock:
    return MagicMock()


@pytest.fixture()
def backend(mock_cli: MagicMock) -> DockerBackend:
    b = DockerBackend()
    b._cli = mock_cli
    return b


# ------------------------------------------------------------------
# _parse_docker_ts
# ------------------------------------------------------------------


class TestParseDockerTs:
    def test_zero_value_returns_none(self) -> None:
        assert _parse_docker_ts("0001-01-01T00:00:00Z") is None

    def test_empty_string_returns_none(self) -> None:
        assert _parse_docker_ts("") is None

    def test_normal_timestamp(self) -> None:
        result = _parse_docker_ts("2026-01-15T10:30:00Z")
        assert result == datetime(2026, 1, 15, 10, 30, tzinfo=UTC)

    def test_nanosecond_truncation(self) -> None:
        result = _parse_docker_ts("2026-01-15T10:30:00.123456789Z")
        assert result is not None
        assert result.microsecond == 123456

    def test_microsecond_precision(self) -> None:
        result = _parse_docker_ts("2026-01-15T10:30:00.123456Z")
        assert result is not None
        assert result.microsecond == 123456

    def test_short_fractional(self) -> None:
        result = _parse_docker_ts("2026-01-15T10:30:00.12Z")
        assert result is not None
        assert result.microsecond == 120000


# ------------------------------------------------------------------
# _parse_container_status
# ------------------------------------------------------------------


class TestParseContainerStatus:
    def test_running_container(self) -> None:
        data = _make_inspect(status="running", started_at="2026-01-01T00:00:00Z")
        status = _parse_container_status(data)
        assert status.container_id == "abc123"
        assert status.name == "test"  # leading / stripped
        assert status.state == ContainerState.RUNNING
        assert status.started_at == datetime(2026, 1, 1, tzinfo=UTC)
        assert status.finished_at is None  # zero-value

    def test_exited_container(self) -> None:
        data = _make_inspect(
            status="exited",
            exit_code=1,
            started_at="2026-01-01T00:00:00Z",
            finished_at="2026-01-01T00:05:00Z",
        )
        status = _parse_container_status(data)
        assert status.state == ContainerState.EXITED
        assert status.exit_code == 1
        assert status.finished_at == datetime(2026, 1, 1, 0, 5, tzinfo=UTC)


# ------------------------------------------------------------------
# _build_run_args
# ------------------------------------------------------------------


class TestBuildRunArgs:
    def test_basic(self) -> None:
        spec = RunSpec(name="c1", image="alpine", command=["echo", "hi"])
        args = DockerBackend._build_run_args(spec)
        assert args[:3] == ["-d", "--name", "c1"]
        # Image before command
        idx = args.index("alpine")
        assert args[idx + 1 :] == ["echo", "hi"]

    def test_shell_mode(self) -> None:
        spec = RunSpec(name="c1", image="alpine", shell="echo hi && ls")
        args = DockerBackend._build_run_args(spec)
        idx = args.index("alpine")
        assert args[idx + 1 :] == ["sh", "-c", "echo hi && ls"]

    def test_labels(self) -> None:
        spec = RunSpec(
            name="c1", image="alpine", labels={"project": "atlas", "env": "dev"}
        )
        args = DockerBackend._build_run_args(spec)
        assert "--label" in args
        assert "project=atlas" in args
        assert "env=dev" in args

    def test_gpu(self) -> None:
        spec = RunSpec(name="c1", image="alpine", gpus=[0, 3])
        args = DockerBackend._build_run_args(spec)
        gpu_idx = args.index("--gpus")
        assert args[gpu_idx + 1] == "device=0,3"
        # NVIDIA_DRIVER_CAPABILITIES should be auto-injected
        assert "-e" in args
        env_args = [args[i + 1] for i, a in enumerate(args) if a == "-e"]
        env_dict = dict(e.split("=", 1) for e in env_args)
        assert env_dict["NVIDIA_DRIVER_CAPABILITIES"] == "compute,utility"
        # NVIDIA_VISIBLE_DEVICES must NOT be set — it conflicts with --gpus
        assert "NVIDIA_VISIBLE_DEVICES" not in env_dict

    def test_gpu_does_not_mutate_spec_env(self) -> None:
        spec = RunSpec(name="c1", image="alpine", gpus=[0])
        original_env = dict(spec.env)
        DockerBackend._build_run_args(spec)
        assert spec.env == original_env

    def test_cpu_cores(self) -> None:
        spec = RunSpec(name="c1", image="alpine", cpu_cores=4)
        args = DockerBackend._build_run_args(spec)
        idx = args.index("--cpus")
        assert args[idx + 1] == "4"

    def test_cpuset(self) -> None:
        spec = RunSpec(name="c1", image="alpine", cpuset="0-3")
        args = DockerBackend._build_run_args(spec)
        idx = args.index("--cpuset-cpus")
        assert args[idx + 1] == "0-3"

    def test_memory(self) -> None:
        spec = RunSpec(name="c1", image="alpine", memory_gb=16)
        args = DockerBackend._build_run_args(spec)
        idx = args.index("--memory")
        assert args[idx + 1] == "16g"

    @patch("atlas_runner.docker_backend.os.makedirs")
    def test_scratch(self, mock_makedirs: MagicMock) -> None:
        spec = RunSpec(name="c1", image="alpine", scratch="/tmp/scratch")
        args = DockerBackend._build_run_args(spec)
        mock_makedirs.assert_called_once_with("/tmp/scratch", exist_ok=True)
        assert "-v" in args
        assert "/tmp/scratch:/scratch" in args

    def test_mounts(self) -> None:
        spec = RunSpec(
            name="c1",
            image="alpine",
            mounts=[
                Mount(src="/data", dst="/mnt/data", ro=True),
                Mount(src="/logs", dst="/mnt/logs"),
            ],
        )
        args = DockerBackend._build_run_args(spec)
        assert "/data:/mnt/data:ro" in args
        assert "/logs:/mnt/logs" in args

    def test_security_defaults(self) -> None:
        spec = RunSpec(name="c1", image="alpine")
        args = DockerBackend._build_run_args(spec)
        assert "--cap-drop" in args
        assert "ALL" in args
        assert "--security-opt" in args
        assert "no-new-privileges" in args

    def test_security_disabled(self) -> None:
        spec = RunSpec(
            name="c1", image="alpine", cap_drop_all=False, no_new_privileges=False
        )
        args = DockerBackend._build_run_args(spec)
        assert "--cap-drop" not in args
        assert "--security-opt" not in args

    def test_network(self) -> None:
        spec = RunSpec(name="c1", image="alpine", network="host")
        args = DockerBackend._build_run_args(spec)
        idx = args.index("--network")
        assert args[idx + 1] == "host"

    def test_workdir(self) -> None:
        spec = RunSpec(name="c1", image="alpine", workdir="/app")
        args = DockerBackend._build_run_args(spec)
        idx = args.index("--workdir")
        assert args[idx + 1] == "/app"

    def test_env(self) -> None:
        spec = RunSpec(name="c1", image="alpine", env={"FOO": "bar"})
        args = DockerBackend._build_run_args(spec)
        assert "-e" in args
        assert "FOO=bar" in args

    def test_ports(self) -> None:
        spec = RunSpec(name="c1", image="alpine", ports={"8080": 80})
        args = DockerBackend._build_run_args(spec)
        assert "-p" in args
        assert "8080:80" in args


# ------------------------------------------------------------------
# DockerBackend methods
# ------------------------------------------------------------------


class TestDockerBackendRun:
    def test_idempotent_existing_container(
        self, backend: DockerBackend, mock_cli: MagicMock
    ) -> None:
        mock_cli.container_exists.return_value = True
        mock_cli.inspect.return_value = _make_inspect(
            cid="existing123",
            name="/mycontainer",
            started_at="2026-01-01T00:00:00Z",
        )
        spec = RunSpec(name="mycontainer", image="alpine")
        result = backend.run(spec)

        assert result.container_id == "existing123"
        assert result.name == "mycontainer"
        mock_cli.run.assert_not_called()

    def test_creates_new_container(
        self, backend: DockerBackend, mock_cli: MagicMock
    ) -> None:
        mock_cli.container_exists.return_value = False
        mock_cli.run.return_value = "new456"
        mock_cli.inspect.return_value = _make_inspect(
            cid="new456",
            name="/test",
            started_at="2026-06-01T12:00:00Z",
        )
        spec = RunSpec(name="test", image="alpine", command=["echo"])
        result = backend.run(spec)

        assert result.container_id == "new456"
        mock_cli.run.assert_called_once()


class TestDockerBackendDelegation:
    def test_login(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        backend.login("reg", "user", "pass")
        mock_cli.login.assert_called_once_with("reg", "user", "pass")

    def test_pull(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        backend.pull("alpine")
        mock_cli.pull.assert_called_once_with("alpine")

    def test_stop(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        backend.stop("c1")
        mock_cli.stop.assert_called_once_with("c1")

    def test_rm(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        backend.rm("c1")
        mock_cli.rm.assert_called_once_with("c1")

    def test_logs(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        mock_cli.logs.return_value = "output"
        assert backend.logs("c1") == "output"

    def test_inspect(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        mock_cli.inspect.return_value = _make_inspect()
        status = backend.inspect("test")
        assert status.container_id == "abc123"
        assert status.state == ContainerState.RUNNING

    def test_ps(self, backend: DockerBackend, mock_cli: MagicMock) -> None:
        mock_cli.ps.return_value = [{"ID": "a1"}, {"ID": "b2"}]
        mock_cli.inspect.side_effect = [
            _make_inspect(cid="a1", name="/c1"),
            _make_inspect(cid="b2", name="/c2"),
        ]
        results = backend.ps()
        assert len(results) == 2
        assert results[0].container_id == "a1"
        assert results[1].container_id == "b2"
