from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agent.server import (
    SANDBOX_BACKENDS,
    SANDBOX_CREATING,
    SANDBOX_CREATION_TIMEOUT,
    ensure_sandbox_for_thread,
)
from agent.utils.sandbox_state import get_or_create_sandbox_backend_proxy


@pytest.mark.asyncio
async def test_stale_sandbox_creating_without_cached_backend_resets_and_creates() -> None:
    thread_id = "thread-stale-creating"
    SANDBOX_BACKENDS.clear()
    sandbox_backend = MagicMock()
    sandbox_backend.id = "sandbox-new"

    stale_at = 0.0  # epoch 0 → far older than the timeout
    thread = {"metadata": {"sandbox_id": SANDBOX_CREATING, "sandbox_creating_at": stale_at}}

    with (
        patch(
            "agent.server.get_sandbox_id_from_metadata",
            new_callable=AsyncMock,
            return_value=SANDBOX_CREATING,
        ),
        patch("agent.server.client.threads.get", new_callable=AsyncMock, return_value=thread),
        patch("agent.server._create_sandbox_with_proxy", new_callable=AsyncMock) as create_sandbox,
        patch("agent.server._configure_git_identity", new_callable=AsyncMock),
        patch("agent.server.client.threads.update", new_callable=AsyncMock) as update_thread,
    ):
        create_sandbox.return_value = sandbox_backend

        result = await ensure_sandbox_for_thread(thread_id)

    assert result.id == "sandbox-new"
    create_sandbox.assert_awaited_once()
    # Stale sentinel reset (id + timestamp cleared), then fresh creation claims it.
    assert update_thread.await_args_list[0].kwargs == {
        "thread_id": thread_id,
        "metadata": {"sandbox_id": None, "sandbox_creating_at": None},
    }
    assert update_thread.await_args_list[1].kwargs["metadata"]["sandbox_id"] == SANDBOX_CREATING
    assert update_thread.await_args_list[2].kwargs == {
        "thread_id": thread_id,
        "metadata": {"sandbox_id": "sandbox-new"},
    }

    SANDBOX_BACKENDS.clear()


@pytest.mark.asyncio
async def test_fresh_sandbox_creating_waits_for_other_worker() -> None:
    """A recent sentinel from another worker must be waited on, not overwritten."""
    thread_id = "thread-concurrent-creating"
    SANDBOX_BACKENDS.clear()
    existing_backend = MagicMock()
    existing_backend.id = "sandbox-existing"

    import time

    fresh_at = time.time()  # well within the timeout
    threads = [
        {"metadata": {"sandbox_id": SANDBOX_CREATING, "sandbox_creating_at": fresh_at}},
        {"metadata": {"sandbox_id": "sandbox-existing", "sandbox_creating_at": fresh_at}},
    ]

    async def passthrough(
        sb,
        _thread_id,
        _github_proxy_token=None,
        _github_proxy_repositories=None,
        _repo=None,
    ):
        return sb

    with (
        patch(
            "agent.server.get_sandbox_id_from_metadata",
            new_callable=AsyncMock,
            return_value=SANDBOX_CREATING,
        ),
        patch("agent.server.client.threads.get", new_callable=AsyncMock, side_effect=threads),
        patch("agent.server.asyncio.sleep", new_callable=AsyncMock),
        patch(
            "agent.server.create_sandbox",
            new_callable=AsyncMock,
            return_value=existing_backend,
        ) as connect_sandbox,
        patch("agent.server._create_sandbox_with_proxy", new_callable=AsyncMock) as create_sandbox,
        patch("agent.server.check_or_recreate_sandbox", side_effect=passthrough),
        patch("agent.server._refresh_github_proxy_or_recreate", side_effect=passthrough),
        patch("agent.server._configure_git_identity", new_callable=AsyncMock),
        patch("agent.server.client.threads.update", new_callable=AsyncMock) as update_thread,
    ):
        result = await ensure_sandbox_for_thread(thread_id)

    assert result.id == "sandbox-existing"
    # Connected to the worker's sandbox; no second sandbox was created.
    connect_sandbox.assert_called_once_with("sandbox-existing")
    create_sandbox.assert_not_awaited()
    # The fresh sentinel was never reset.
    for call in update_thread.await_args_list:
        assert call.kwargs["metadata"] != {"sandbox_id": None, "sandbox_creating_at": None}

    assert SANDBOX_CREATION_TIMEOUT > 0
    SANDBOX_BACKENDS.clear()


@pytest.mark.asyncio
async def test_ensure_sandbox_resolves_unresolved_backend_proxy() -> None:
    thread_id = "thread-unresolved-proxy"
    SANDBOX_BACKENDS.clear()
    proxy = get_or_create_sandbox_backend_proxy(thread_id)
    existing_backend = MagicMock()
    existing_backend.id = "sandbox-existing"

    async def passthrough(
        sandbox_backend,
        _thread_id,
        _github_proxy_token=None,
        _github_proxy_repositories=None,
        _repo=None,
    ):
        return sandbox_backend

    with (
        patch(
            "agent.server.get_sandbox_id_from_metadata",
            new_callable=AsyncMock,
            return_value="sandbox-existing",
        ),
        patch(
            "agent.server.create_sandbox",
            new_callable=AsyncMock,
            return_value=existing_backend,
        ) as connect_sandbox,
        patch(
            "agent.server.check_or_recreate_sandbox",
            new_callable=AsyncMock,
            side_effect=passthrough,
        ),
        patch(
            "agent.server._refresh_github_proxy_or_recreate",
            new_callable=AsyncMock,
            side_effect=passthrough,
        ) as refresh_proxy,
        patch("agent.server._configure_git_identity", new_callable=AsyncMock),
        patch("agent.server.client.threads.update", new_callable=AsyncMock) as update_thread,
    ):
        result = await ensure_sandbox_for_thread(thread_id)

    assert result is proxy
    assert proxy.current is existing_backend
    connect_sandbox.assert_awaited_once_with("sandbox-existing")
    assert refresh_proxy.await_count == 1
    update_thread.assert_not_awaited()
    SANDBOX_BACKENDS.clear()
