"""Tests for SandboxesResource."""

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock
from uuid import UUID, uuid4

import pytest

from mcpgateway_sdk.models.sandbox import ExecResult, Sandbox, SandboxFile
from mcpgateway_sdk.resources.sandboxes import SandboxesResource


@pytest.fixture
def http() -> AsyncMock:
    """Mock HTTPClient."""
    return AsyncMock()


@pytest.fixture
def sandboxes(http: AsyncMock) -> SandboxesResource:
    """SandboxesResource with mocked HTTP."""
    return SandboxesResource(http)


def _sandbox_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "id": str(uuid4()),
        "user_id": str(uuid4()),
        "user_name": "test-user",
        "status": "running",
        "image": "mcpgateway/sandbox-runtime:latest",
        "container_id": "abc123",
        "workspace_path": "/workspace",
        "created_at": "2026-01-01T00:00:00Z",
        "last_activity_at": "2026-01-01T00:00:00Z",
        "stopped_at": None,
        "memory_mb": 512,
        "cpu_cores": 1.0,
        "disk_mb": 1024,
        "idle_timeout_seconds": 1800,
        "network_enabled": False,
        "exec_count": 0,
        "session_key": None,
    }
    data.update(overrides)
    return data


def _exec_result_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "exit_code": 0,
        "stdout": "hello world\n",
        "stderr": "",
        "duration_ms": 42,
    }
    data.update(overrides)
    return data


def _sandbox_file_data(**overrides: Any) -> dict[str, Any]:
    data: dict[str, Any] = {
        "name": "script.py",
        "path": "/workspace/script.py",
        "size": 256,
        "is_directory": False,
        "modified_at": "2026-01-01T00:00:00Z",
    }
    data.update(overrides)
    return data


# ---------------------------------------------------------------------------
# _endpoints dict
# ---------------------------------------------------------------------------


class TestEndpointsDict:
    """Tests for _endpoints class variable."""

    def test_endpoints_has_list(self) -> None:
        assert SandboxesResource._endpoints["list"] == ("GET", "/api/v1/sandboxes")

    def test_endpoints_has_create(self) -> None:
        assert SandboxesResource._endpoints["create"] == ("POST", "/api/v1/sandboxes")

    def test_endpoints_has_get(self) -> None:
        assert SandboxesResource._endpoints["get"] == (
            "GET",
            "/api/v1/sandboxes/{sandbox_id}",
        )

    def test_endpoints_has_update(self) -> None:
        assert SandboxesResource._endpoints["update"] == (
            "PATCH",
            "/api/v1/sandboxes/{sandbox_id}",
        )

    def test_endpoints_has_destroy(self) -> None:
        assert SandboxesResource._endpoints["destroy"] == (
            "DELETE",
            "/api/v1/sandboxes/{sandbox_id}",
        )

    def test_endpoints_has_exec(self) -> None:
        assert SandboxesResource._endpoints["exec"] == (
            "POST",
            "/api/v1/sandboxes/{sandbox_id}/exec",
        )

    def test_endpoints_has_stop(self) -> None:
        assert SandboxesResource._endpoints["stop"] == (
            "POST",
            "/api/v1/sandboxes/{sandbox_id}/stop",
        )

    def test_endpoints_has_resume(self) -> None:
        assert SandboxesResource._endpoints["resume"] == (
            "POST",
            "/api/v1/sandboxes/{sandbox_id}/resume",
        )

    def test_endpoints_has_activity(self) -> None:
        assert SandboxesResource._endpoints["activity"] == (
            "GET",
            "/api/v1/sandboxes/{sandbox_id}/activity",
        )

    def test_endpoints_has_list_files(self) -> None:
        assert SandboxesResource._endpoints["list_files"] == (
            "GET",
            "/api/v1/sandboxes/{sandbox_id}/files",
        )

    def test_endpoints_has_download(self) -> None:
        assert SandboxesResource._endpoints["download"] == (
            "GET",
            "/api/v1/sandboxes/{sandbox_id}/files/{file_path}",
        )

    def test_endpoints_has_upload(self) -> None:
        assert SandboxesResource._endpoints["upload"] == (
            "PUT",
            "/api/v1/sandboxes/{sandbox_id}/files/{file_path}",
        )

    def test_endpoints_has_install_skills(self) -> None:
        assert SandboxesResource._endpoints["install_skills"] == (
            "POST",
            "/api/v1/sandboxes/{sandbox_id}/skills",
        )

    def test_endpoints_has_list_images(self) -> None:
        assert SandboxesResource._endpoints["list_images"] == (
            "GET",
            "/api/v1/sandbox-images",
        )

    def test_endpoints_has_image_templates(self) -> None:
        assert SandboxesResource._endpoints["image_templates"] == (
            "GET",
            "/api/v1/sandbox-images/templates",
        )

    def test_endpoints_has_package_catalog(self) -> None:
        assert SandboxesResource._endpoints["package_catalog"] == (
            "GET",
            "/api/v1/sandbox-images/packages",
        )

    def test_endpoints_has_exactly_sixteen_entries(self) -> None:
        assert len(SandboxesResource._endpoints) == 16


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------


class TestList:
    """Tests for SandboxesResource.list."""

    async def test_sends_get_request(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await sandboxes.list()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/sandboxes")

    async def test_returns_list_of_sandboxes(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                _sandbox_data(status="running"),
                _sandbox_data(status="stopped"),
            ]
        }

        # Act
        result = await sandboxes.list()

        # Assert
        assert len(result) == 2
        assert all(isinstance(s, Sandbox) for s in result)
        assert result[0].status == "running"
        assert result[1].status == "stopped"

    async def test_empty_list_returns_empty(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        result = await sandboxes.list()

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# create
# ---------------------------------------------------------------------------


class TestCreate:
    """Tests for SandboxesResource.create."""

    async def test_sends_post_with_defaults(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.create()

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("POST", "/api/v1/sandboxes")
        body = call_kwargs.kwargs["json"]
        assert body["image"] == "mcpgateway/sandbox-runtime:latest"

    async def test_sends_post_with_custom_image(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.create(image="custom/runtime:v2")

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["image"] == "custom/runtime:v2"

    async def test_sends_post_with_all_kwargs(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.create(
            image="custom:latest",
            idle_timeout_seconds=3600,
            memory_mb=1024,
            cpu_cores=2.0,
        )

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body["image"] == "custom:latest"
        assert body["idle_timeout_seconds"] == 3600
        assert body["memory_mb"] == 1024
        assert body["cpu_cores"] == 2.0

    async def test_returns_sandbox_model(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data(status="running")

        # Act
        result = await sandboxes.create()

        # Assert
        assert isinstance(result, Sandbox)
        assert result.status == "running"


# ---------------------------------------------------------------------------
# get
# ---------------------------------------------------------------------------


class TestGet:
    """Tests for SandboxesResource.get."""

    async def test_sends_get_with_sandbox_id(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.get(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/sandboxes/{sandbox_id}")

    async def test_accepts_string_id(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        sandbox_id = str(uuid4())
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.get(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/sandboxes/{sandbox_id}")

    async def test_returns_sandbox_model(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data(status="stopped")

        # Act
        result = await sandboxes.get(uuid4())

        # Assert
        assert isinstance(result, Sandbox)
        assert result.status == "stopped"


# ---------------------------------------------------------------------------
# update
# ---------------------------------------------------------------------------


class TestUpdate:
    """Tests for SandboxesResource.update."""

    async def test_sends_patch_with_fields(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.update(sandbox_id, idle_timeout_seconds=3600)

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("PATCH", f"/api/v1/sandboxes/{sandbox_id}")
        body = call_kwargs.kwargs["json"]
        assert body == {"idle_timeout_seconds": 3600}

    async def test_sends_multiple_fields(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _sandbox_data()

        # Act
        await sandboxes.update(sandbox_id, memory_mb=2048, network_enabled=True)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body == {"memory_mb": 2048, "network_enabled": True}

    async def test_returns_sandbox_model(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data(memory_mb=2048)

        # Act
        result = await sandboxes.update(uuid4(), memory_mb=2048)

        # Assert
        assert isinstance(result, Sandbox)
        assert result.memory_mb == 2048


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


class TestDestroy:
    """Tests for SandboxesResource.destroy."""

    async def test_sends_delete_with_sandbox_id(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = None

        # Act
        await sandboxes.destroy(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("DELETE", f"/api/v1/sandboxes/{sandbox_id}")

    async def test_returns_none(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await sandboxes.destroy(uuid4())

        # Assert
        assert result is None


# ---------------------------------------------------------------------------
# exec
# ---------------------------------------------------------------------------


class TestExec:
    """Tests for SandboxesResource.exec."""

    async def test_sends_post_with_command_and_default_timeout(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _exec_result_data()

        # Act
        await sandboxes.exec(sandbox_id, "echo hello")

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == ("POST", f"/api/v1/sandboxes/{sandbox_id}/exec")
        body = call_kwargs.kwargs["json"]
        assert body == {"command": "echo hello", "timeout_seconds": 30}

    async def test_sends_custom_timeout(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _exec_result_data()

        # Act
        await sandboxes.exec(sandbox_id, "sleep 60", timeout=120)

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body == {"command": "sleep 60", "timeout_seconds": 120}

    async def test_returns_exec_result_model(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _exec_result_data(
            exit_code=0, stdout="hello world\n", duration_ms=42
        )

        # Act
        result = await sandboxes.exec(uuid4(), "echo hello")

        # Assert
        assert isinstance(result, ExecResult)
        assert result.exit_code == 0
        assert result.stdout == "hello world\n"
        assert result.duration_ms == 42

    async def test_returns_nonzero_exit_code(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _exec_result_data(exit_code=1, stderr="command not found")

        # Act
        result = await sandboxes.exec(uuid4(), "bad-command")

        # Assert
        assert result.exit_code == 1
        assert result.stderr == "command not found"


# ---------------------------------------------------------------------------
# stop
# ---------------------------------------------------------------------------


class TestStop:
    """Tests for SandboxesResource.stop."""

    async def test_sends_post_to_stop_endpoint(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = None

        # Act
        await sandboxes.stop(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("POST", f"/api/v1/sandboxes/{sandbox_id}/stop")

    async def test_returns_none(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await sandboxes.stop(uuid4())

        # Assert
        assert result is None


# ---------------------------------------------------------------------------
# resume
# ---------------------------------------------------------------------------


class TestResume:
    """Tests for SandboxesResource.resume."""

    async def test_sends_post_to_resume_endpoint(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = _sandbox_data(status="running")

        # Act
        await sandboxes.resume(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("POST", f"/api/v1/sandboxes/{sandbox_id}/resume")

    async def test_returns_sandbox_model(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = _sandbox_data(status="running")

        # Act
        result = await sandboxes.resume(uuid4())

        # Assert
        assert isinstance(result, Sandbox)
        assert result.status == "running"


# ---------------------------------------------------------------------------
# activity
# ---------------------------------------------------------------------------


class TestActivity:
    """Tests for SandboxesResource.activity."""

    async def test_sends_get_to_activity_endpoint(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = {"items": []}

        # Act
        await sandboxes.activity(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with("GET", f"/api/v1/sandboxes/{sandbox_id}/activity")

    async def test_returns_items_from_dict(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        activities = [
            {"action": "exec", "timestamp": "2026-01-01T00:00:00Z"},
            {"action": "file_upload", "timestamp": "2026-01-01T00:01:00Z"},
        ]
        http.request.return_value = {"items": activities}

        # Act
        result = await sandboxes.activity(uuid4())

        # Assert
        assert len(result) == 2
        assert result[0]["action"] == "exec"
        assert result[1]["action"] == "file_upload"

    async def test_returns_activity_key_from_dict(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        activities = [{"action": "exec"}]
        http.request.return_value = {"activity": activities}

        # Act
        result = await sandboxes.activity(uuid4())

        # Assert
        assert len(result) == 1
        assert result[0]["action"] == "exec"

    async def test_returns_raw_list(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        activities = [
            {"action": "exec", "timestamp": "2026-01-01T00:00:00Z"},
        ]
        http.request.return_value = activities

        # Act
        result = await sandboxes.activity(uuid4())

        # Assert
        assert len(result) == 1
        assert result[0]["action"] == "exec"

    async def test_empty_activity_returns_empty_list(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        result = await sandboxes.activity(uuid4())

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# list_files
# ---------------------------------------------------------------------------


class TestListFiles:
    """Tests for SandboxesResource.list_files."""

    async def test_sends_get_with_default_path(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = []

        # Act
        await sandboxes.list_files(sandbox_id)

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            f"/api/v1/sandboxes/{sandbox_id}/files",
            params={"path": "/workspace"},
        )

    async def test_sends_get_with_custom_path(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = []

        # Act
        await sandboxes.list_files(sandbox_id, path="/workspace/src")

        # Assert
        http.request.assert_awaited_once_with(
            "GET",
            f"/api/v1/sandboxes/{sandbox_id}/files",
            params={"path": "/workspace/src"},
        )

    async def test_returns_list_of_sandbox_files(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = [
            _sandbox_file_data(name="script.py"),
            _sandbox_file_data(name="data", is_directory=True),
        ]

        # Act
        result = await sandboxes.list_files(uuid4())

        # Assert
        assert len(result) == 2
        assert all(isinstance(f, SandboxFile) for f in result)
        assert result[0].name == "script.py"
        assert result[1].is_directory is True

    async def test_empty_directory_returns_empty_list(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = []

        # Act
        result = await sandboxes.list_files(uuid4())

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# download
# ---------------------------------------------------------------------------


class TestDownload:
    """Tests for SandboxesResource.download."""

    async def test_uses_request_raw(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request_raw.return_value = b"file-content"

        # Act
        await sandboxes.download(sandbox_id, "script.py")

        # Assert
        http.request_raw.assert_awaited_once_with(
            "GET", f"/api/v1/sandboxes/{sandbox_id}/files/script.py"
        )
        # Ensure regular request was NOT called
        http.request.assert_not_awaited()

    async def test_downloads_nested_file_path(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request_raw.return_value = b"nested-content"

        # Act
        await sandboxes.download(sandbox_id, "src/main.py")

        # Assert
        http.request_raw.assert_awaited_once_with(
            "GET", f"/api/v1/sandboxes/{sandbox_id}/files/src/main.py"
        )

    async def test_returns_bytes(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request_raw.return_value = b"\x89PNG\r\n"

        # Act
        result = await sandboxes.download(uuid4(), "image.png")

        # Assert
        assert isinstance(result, bytes)
        assert result == b"\x89PNG\r\n"


# ---------------------------------------------------------------------------
# upload
# ---------------------------------------------------------------------------


class TestUpload:
    """Tests for SandboxesResource.upload."""

    async def test_sends_put_with_raw_bytes(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = None
        file_content = b"print('hello world')"

        # Act
        await sandboxes.upload(sandbox_id, "script.py", file_content)

        # Assert
        http.request.assert_awaited_once_with(
            "PUT",
            f"/api/v1/sandboxes/{sandbox_id}/files/script.py",
            content=file_content,
        )

    async def test_uploads_to_nested_path(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        http.request.return_value = None
        content = b"data"

        # Act
        await sandboxes.upload(sandbox_id, "src/lib/utils.py", content)

        # Assert
        http.request.assert_awaited_once_with(
            "PUT",
            f"/api/v1/sandboxes/{sandbox_id}/files/src/lib/utils.py",
            content=content,
        )

    async def test_returns_none(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = None

        # Act
        result = await sandboxes.upload(uuid4(), "file.txt", b"data")

        # Assert
        assert result is None


# ---------------------------------------------------------------------------
# install_skills
# ---------------------------------------------------------------------------


class TestInstallSkills:
    """Tests for SandboxesResource.install_skills."""

    async def test_sends_post_with_skill_ids_as_strings(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        skill_id_1 = uuid4()
        skill_id_2 = uuid4()
        http.request.return_value = {"installed": 2}

        # Act
        await sandboxes.install_skills(sandbox_id, [skill_id_1, skill_id_2])

        # Assert
        call_kwargs = http.request.call_args
        assert call_kwargs.args == (
            "POST",
            f"/api/v1/sandboxes/{sandbox_id}/skills",
        )
        body = call_kwargs.kwargs["json"]
        assert body == {"skill_ids": [str(skill_id_1), str(skill_id_2)]}

    async def test_accepts_string_skill_ids(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        sandbox_id = uuid4()
        skill_id = str(uuid4())
        http.request.return_value = {"installed": 1}

        # Act
        await sandboxes.install_skills(sandbox_id, [skill_id])

        # Assert
        body = http.request.call_args.kwargs["json"]
        assert body == {"skill_ids": [skill_id]}

    async def test_returns_dict(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"installed": 3, "errors": []}

        # Act
        result = await sandboxes.install_skills(uuid4(), [uuid4()])

        # Assert
        assert isinstance(result, dict)
        assert result["installed"] == 3


# ---------------------------------------------------------------------------
# list_images
# ---------------------------------------------------------------------------


class TestListImages:
    """Tests for SandboxesResource.list_images."""

    async def test_sends_get_request(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"items": []}

        # Act
        await sandboxes.list_images()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/sandbox-images")

    async def test_returns_list_of_dicts_from_items(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                {"name": "sandbox-runtime", "tag": "latest"},
                {"name": "python-runtime", "tag": "3.12"},
            ]
        }

        # Act
        result = await sandboxes.list_images()

        # Assert
        assert len(result) == 2
        assert result[0]["name"] == "sandbox-runtime"
        assert result[1]["tag"] == "3.12"

    async def test_returns_raw_list_if_no_items_key(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange — API returns a plain list
        http.request.return_value = [{"name": "image-a"}]

        # Act
        result = await sandboxes.list_images()

        # Assert
        assert len(result) == 1
        assert result[0]["name"] == "image-a"


# ---------------------------------------------------------------------------
# image_templates
# ---------------------------------------------------------------------------


class TestImageTemplates:
    """Tests for SandboxesResource.image_templates."""

    async def test_sends_get_request(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"templates": []}

        # Act
        await sandboxes.image_templates()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/sandbox-images/templates")

    async def test_returns_list_from_templates_key(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "templates": [
                {"name": "python", "base_image": "python:3.12"},
                {"name": "node", "base_image": "node:20"},
            ]
        }

        # Act
        result = await sandboxes.image_templates()

        # Assert
        assert len(result) == 2
        assert result[0]["name"] == "python"

    async def test_falls_back_to_items_key(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                {"name": "python", "base_image": "python:3.12"},
            ]
        }

        # Act
        result = await sandboxes.image_templates()

        # Assert
        assert len(result) == 1
        assert result[0]["name"] == "python"

    async def test_empty_returns_empty_list(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"templates": []}

        # Act
        result = await sandboxes.image_templates()

        # Assert
        assert result == []


# ---------------------------------------------------------------------------
# package_catalog
# ---------------------------------------------------------------------------


class TestPackageCatalog:
    """Tests for SandboxesResource.package_catalog."""

    async def test_sends_get_request(self, sandboxes: SandboxesResource, http: AsyncMock) -> None:
        # Arrange
        http.request.return_value = {"categories": []}

        # Act
        await sandboxes.package_catalog()

        # Assert
        http.request.assert_awaited_once_with("GET", "/api/v1/sandbox-images/packages")

    async def test_returns_list_from_categories_key(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "categories": [
                {"name": "numpy", "version": "1.26.0"},
                {"name": "pandas", "version": "2.1.0"},
            ]
        }

        # Act
        result = await sandboxes.package_catalog()

        # Assert
        assert len(result) == 2
        assert result[0]["name"] == "numpy"
        assert result[1]["version"] == "2.1.0"

    async def test_falls_back_to_items_key(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {
            "items": [
                {"name": "numpy", "version": "1.26.0"},
            ]
        }

        # Act
        result = await sandboxes.package_catalog()

        # Assert
        assert len(result) == 1
        assert result[0]["name"] == "numpy"

    async def test_empty_returns_empty_list(
        self, sandboxes: SandboxesResource, http: AsyncMock
    ) -> None:
        # Arrange
        http.request.return_value = {"categories": []}

        # Act
        result = await sandboxes.package_catalog()

        # Assert
        assert result == []
