"""
Unit tests for client execute_async (plan step 03).

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mcp_proxy_adapter.client.jsonrpc_client.client import JsonRpcClient


class TestClientExecuteAsync:
    """Tests for JsonRpcClient.execute_async."""

    @pytest.mark.asyncio
    async def test_execute_async_returns_accepted_and_deliver_id(self) -> None:
        """execute_async returns server response with accepted and deliver_id."""
        client = JsonRpcClient(host="127.0.0.1", port=8080)
        callback = MagicMock()

        async def fake_rpc(method: str, params: dict) -> dict:
            return {
                "jsonrpc": "2.0",
                "result": {
                    "accepted": True,
                    "deliver_id": params["deliver_id"],
                    "message": "Command accepted; result will be delivered via WebSocket",
                },
                "id": 1,
            }

        with patch.object(
            client, "jsonrpc_call", new_callable=AsyncMock, side_effect=fake_rpc
        ):
            with patch(
                "mcp_proxy_adapter.client.jsonrpc_client.command_api.wait_for_job_via_websocket",
                new_callable=AsyncMock,
            ) as mock_wait:
                mock_wait.return_value = {"event": "job_completed", "result": {}}

                result = await client.execute_async(
                    "echo", {"message": "hi"}, on_result=callback
                )
                assert result["accepted"] is True
                assert result["deliver_id"]
                assert len(result["deliver_id"]) > 0
                await asyncio.sleep(0.15)
                callback.assert_called_once()
                assert callback.call_args[0][0]["event"] == "job_completed"

    @pytest.mark.asyncio
    async def test_execute_async_invokes_callback_with_payload(self) -> None:
        """When WS delivers job_completed, on_result is invoked with event payload."""
        client = JsonRpcClient(host="127.0.0.1", port=8080)
        received: list = []

        def on_result(payload: dict) -> None:
            received.append(payload)

        with patch.object(client, "jsonrpc_call", new_callable=AsyncMock) as mock_rpc:
            deliver_id = "deliver-abc"
            mock_rpc.return_value = {
                "jsonrpc": "2.0",
                "result": {
                    "accepted": True,
                    "deliver_id": deliver_id,
                    "message": "OK",
                },
                "id": 1,
            }
            with patch(
                "mcp_proxy_adapter.client.jsonrpc_client.command_api.wait_for_job_via_websocket",
                new_callable=AsyncMock,
            ) as mock_wait:
                mock_wait.return_value = {
                    "event": "job_completed",
                    "job_id": deliver_id,
                    "result": {"success": True, "data": {"message": "hi"}},
                }
                with patch(
                    "mcp_proxy_adapter.client.jsonrpc_client.command_api.uuid.uuid4"
                ) as mock_uuid:
                    mock_uuid.return_value = MagicMock(__str__=lambda self: deliver_id)

                    await client.execute_async(
                        "echo", {"message": "hi"}, on_result=on_result
                    )
                    await asyncio.sleep(0.2)
                    assert len(received) == 1
                    assert received[0]["event"] == "job_completed"
                    assert received[0]["result"]["data"]["message"] == "hi"

    @pytest.mark.asyncio
    async def test_execute_async_server_error_cancels_wait_and_raises(self) -> None:
        """When RPC returns error, wait task is cancelled and client raises."""
        client = JsonRpcClient(host="127.0.0.1", port=8080)
        callback = MagicMock()

        with patch.object(client, "jsonrpc_call", new_callable=AsyncMock) as mock_rpc:
            mock_rpc.side_effect = RuntimeError(
                "JSON-RPC error: Method not found (code: -32601)"
            )
            with patch(
                "mcp_proxy_adapter.client.jsonrpc_client.command_api.wait_for_job_via_websocket",
                new_callable=AsyncMock,
            ) as mock_wait:
                mock_wait.return_value = {"event": "job_completed"}

                with pytest.raises(RuntimeError, match="Method not found"):
                    await client.execute_async("nonexistent", {}, on_result=callback)

        await asyncio.sleep(0.1)
        # Callback should not be invoked (wait task was cancelled before any push)
        callback.assert_not_called()

    @pytest.mark.asyncio
    async def test_execute_async_async_callback_awaited(self) -> None:
        """When on_result is async, it is awaited in the background task."""
        client = JsonRpcClient(host="127.0.0.1", port=8080)
        callback = AsyncMock()

        with patch.object(client, "jsonrpc_call", new_callable=AsyncMock) as mock_rpc:
            mock_rpc.return_value = {
                "jsonrpc": "2.0",
                "result": {"accepted": True, "deliver_id": "d1", "message": "OK"},
                "id": 1,
            }
            with patch(
                "mcp_proxy_adapter.client.jsonrpc_client.command_api.wait_for_job_via_websocket",
                new_callable=AsyncMock,
            ) as mock_wait:
                mock_wait.return_value = {"event": "job_completed", "result": {}}

                await client.execute_async("echo", {}, on_result=callback)
                await asyncio.sleep(0.2)
                callback.assert_called_once()
                assert callback.await_count == 1
