from __future__ import annotations

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

import pytest

from atlas_runner.docker_backend import DockerBackend
from atlas_runner.errors import ErrorCode, RunnerError
from atlas_runner.models import ContainerState, ContainerStatus, RunResult, RunSpec
from atlas_runner.runner import Runner, create_backend

# ------------------------------------------------------------------
# create_backend
# ------------------------------------------------------------------


class TestCreateBackend:
    def test_docker(self) -> None:
        backend = create_backend("docker")
        assert isinstance(backend, DockerBackend)

    def test_docker_with_host(self) -> None:
        backend = create_backend("docker", docker_host="tcp://remote:2375")
        assert isinstance(backend, DockerBackend)

    def test_unknown_raises(self) -> None:
        with pytest.raises(RunnerError) as exc_info:
            create_backend("unknown")
        assert exc_info.value.code == ErrorCode.BACKEND_ERROR
        assert "Unknown backend type" in exc_info.value.message


# ------------------------------------------------------------------
# Runner delegation
# ------------------------------------------------------------------


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


@pytest.fixture()
def runner(mock_backend: MagicMock) -> Runner:
    return Runner(backend=mock_backend)


class TestRunnerDelegation:
    def test_login(self, runner: Runner, mock_backend: MagicMock) -> None:
        runner.login("reg", "user", "pass")
        mock_backend.login.assert_called_once_with("reg", "user", "pass")

    def test_pull(self, runner: Runner, mock_backend: MagicMock) -> None:
        runner.pull("alpine")
        mock_backend.pull.assert_called_once_with("alpine")

    def test_run(self, runner: Runner, mock_backend: MagicMock) -> None:
        spec = RunSpec(name="test", image="alpine")
        expected = RunResult(
            container_id="abc", name="test", started_at=datetime(2026, 1, 1, tzinfo=UTC)
        )
        mock_backend.run.return_value = expected
        result = runner.run(spec)
        assert result == expected
        mock_backend.run.assert_called_once_with(spec)

    def test_stop(self, runner: Runner, mock_backend: MagicMock) -> None:
        runner.stop("c1")
        mock_backend.stop.assert_called_once_with("c1")

    def test_status(self, runner: Runner, mock_backend: MagicMock) -> None:
        expected = ContainerStatus(
            container_id="abc",
            name="test",
            state=ContainerState.RUNNING,
        )
        mock_backend.inspect.return_value = expected
        result = runner.status("test")
        assert result == expected
        mock_backend.inspect.assert_called_once_with("test")

    def test_logs(self, runner: Runner, mock_backend: MagicMock) -> None:
        mock_backend.logs.return_value = "output"
        assert runner.logs("c1") == "output"
        mock_backend.logs.assert_called_once_with("c1")

    def test_ps(self, runner: Runner, mock_backend: MagicMock) -> None:
        mock_backend.ps.return_value = []
        assert runner.ps() == []
        mock_backend.ps.assert_called_once()


# ------------------------------------------------------------------
# destroy
# ------------------------------------------------------------------


class TestRunnerDestroy:
    def test_composes_stop_and_rm(
        self, runner: Runner, mock_backend: MagicMock
    ) -> None:
        runner.destroy("c1")
        mock_backend.stop.assert_called_once_with("c1")
        mock_backend.rm.assert_called_once_with("c1")

    def test_stop_called_before_rm(
        self, runner: Runner, mock_backend: MagicMock
    ) -> None:
        call_order: list[str] = []
        mock_backend.stop.side_effect = lambda name: call_order.append("stop")
        mock_backend.rm.side_effect = lambda name: call_order.append("rm")
        runner.destroy("c1")
        assert call_order == ["stop", "rm"]

    def test_destroy_is_idempotent(
        self, runner: Runner, mock_backend: MagicMock
    ) -> None:
        runner.destroy("c1")
        runner.destroy("c1")
        assert mock_backend.stop.call_count == 2
        assert mock_backend.rm.call_count == 2
