"""
Unit tests for async execution handler (execute_command_async, execute_async JSON-RPC).

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

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

import pytest

from mcp_proxy_adapter.api.handlers import handle_json_rpc
from mcp_proxy_adapter.api.handlers_async import execute_command_async
from mcp_proxy_adapter.core.errors import MethodNotFoundError


class TestExecuteCommandAsync:
    """Tests for execute_command_async (server async path)."""

    @pytest.mark.asyncio
    async def test_returns_accepted_and_deliver_id(self) -> None:
        """execute_command_async returns accepted=True and non-empty deliver_id."""
        mock_command_class = MagicMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {"success": True, "data": {"message": "hi"}}
        mock_command_class.run = AsyncMock(return_value=mock_result)

        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.return_value = mock_command_class

            result = await execute_command_async(
                "echo", {"message": "hi"}, None, None, None
            )

        assert result["accepted"] is True
        assert result["deliver_id"]
        assert isinstance(result["deliver_id"], str)
        assert len(result["deliver_id"]) > 0
        assert "message" in result
        assert "WebSocket" in result["message"]

    @pytest.mark.asyncio
    async def test_notify_called_on_completion(self) -> None:
        """Background task calls notify_job_state_changed with completed and result."""
        mock_command_class = MagicMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {"success": True, "data": {"message": "hi"}}
        mock_command_class.run = AsyncMock(return_value=mock_result)

        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.return_value = mock_command_class
            with patch(
                "mcp_proxy_adapter.api.handlers_async.notify_job_state_changed",
                new_callable=AsyncMock,
            ) as mock_notify:
                out = await execute_command_async(
                    "echo", {"message": "hi"}, None, None, None
                )
                deliver_id = out["deliver_id"]
                # Allow background task to run
                await asyncio.sleep(0.1)

                mock_notify.assert_called_once()
                call_args = mock_notify.call_args[0]
                assert call_args[0] == deliver_id
                assert call_args[1] == "completed"
                assert call_args[2] == {"success": True, "data": {"message": "hi"}}
                assert call_args[3] is None
                assert call_args[4] == 100

    @pytest.mark.asyncio
    async def test_notify_called_on_failure(self) -> None:
        """Background task calls notify_job_state_changed with failed on exception."""
        mock_command_class = MagicMock()
        mock_command_class.run = AsyncMock(side_effect=RuntimeError("command failed"))

        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.return_value = mock_command_class
            with patch(
                "mcp_proxy_adapter.api.handlers_async.notify_job_state_changed",
                new_callable=AsyncMock,
            ) as mock_notify:
                out = await execute_command_async("failing", {}, None, None, None)
                deliver_id = out["deliver_id"]
                await asyncio.sleep(0.1)

                mock_notify.assert_called_once()
                call_args = mock_notify.call_args[0]
                assert call_args[0] == deliver_id
                assert call_args[1] == "failed"
                assert call_args[2] is None
                assert call_args[3] == "command failed"

    @pytest.mark.asyncio
    async def test_method_not_found_raises(self) -> None:
        """execute_command_async raises MethodNotFoundError for unknown command."""
        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.side_effect = KeyError("not found")

            with pytest.raises(MethodNotFoundError, match="not found"):
                await execute_command_async("nonexistent", {}, None, None, None)

    @pytest.mark.asyncio
    async def test_uses_provided_deliver_id(self) -> None:
        """When deliver_id is provided it is used and returned."""
        mock_command_class = MagicMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {}
        mock_command_class.run = AsyncMock(return_value=mock_result)

        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.return_value = mock_command_class

            result = await execute_command_async(
                "echo", {}, "my-deliver-id-123", None, None
            )

        assert result["deliver_id"] == "my-deliver-id-123"

    @pytest.mark.asyncio
    async def test_strips_deliver_id_from_params(self) -> None:
        """deliver_id is not passed to command.run (transport-level)."""
        mock_command_class = MagicMock()
        mock_result = MagicMock()
        mock_result.to_dict.return_value = {}
        mock_command_class.run = AsyncMock(return_value=mock_result)

        with patch("mcp_proxy_adapter.api.handlers_async.registry") as mock_registry:
            mock_registry.get_command.return_value = mock_command_class

            await execute_command_async(
                "echo",
                {"message": "x", "deliver_id": "should-be-removed"},
                None,
                None,
                None,
            )
            await asyncio.sleep(0.05)

            run_kwargs = mock_command_class.run.call_args[1]
            assert "deliver_id" not in run_kwargs
            assert run_kwargs.get("message") == "x"
            assert "context" in run_kwargs


class TestHandleJsonRpcExecuteAsync:
    """Tests for handle_json_rpc with method execute_async."""

    @pytest.mark.asyncio
    async def test_execute_async_success(self) -> None:
        """POST execute_async with command and params returns result with accepted and deliver_id."""
        with patch(
            "mcp_proxy_adapter.api.handlers.execute_command_async",
            new_callable=AsyncMock,
        ) as mock_async:
            mock_async.return_value = {
                "accepted": True,
                "deliver_id": "test-uuid-123",
                "message": "Command accepted; result will be delivered via WebSocket",
            }

            result = await handle_json_rpc(
                {
                    "jsonrpc": "2.0",
                    "method": "execute_async",
                    "params": {
                        "command": "echo",
                        "params": {"message": "hello"},
                    },
                    "id": 1,
                }
            )

            assert result["jsonrpc"] == "2.0"
            assert result["id"] == 1
            assert "result" in result
            assert result["result"]["accepted"] is True
            assert result["result"]["deliver_id"] == "test-uuid-123"
            mock_async.assert_called_once()
            call_args = mock_async.call_args[0]
            call_kw = mock_async.call_args[1]
            assert call_args[0] == "echo"
            assert call_args[1] == {"message": "hello"}
            assert call_kw.get("request_id") is None
            assert call_kw.get("request") is None

    @pytest.mark.asyncio
    async def test_execute_async_missing_command_returns_error(self) -> None:
        """execute_async without command in params returns JSON-RPC Invalid Request error."""
        result = await handle_json_rpc(
            {
                "jsonrpc": "2.0",
                "method": "execute_async",
                "params": {"params": {}},
                "id": 2,
            }
        )

        assert result["jsonrpc"] == "2.0"
        assert result["id"] == 2
        assert "error" in result
        assert result["error"]["code"] == -32600  # Invalid Request

    @pytest.mark.asyncio
    async def test_execute_async_empty_command_returns_error(self) -> None:
        """execute_async with empty string command returns error."""
        result = await handle_json_rpc(
            {
                "jsonrpc": "2.0",
                "method": "execute_async",
                "params": {"command": "  "},
                "id": 3,
            }
        )

        assert "error" in result
        assert result["error"]["code"] == -32600

    @pytest.mark.asyncio
    async def test_execute_async_invalid_command_propagates(self) -> None:
        """execute_async with invalid command name: MethodNotFoundError propagates from handler."""
        with patch(
            "mcp_proxy_adapter.api.handlers.execute_command_async",
            new_callable=AsyncMock,
        ) as mock_async:
            mock_async.side_effect = MethodNotFoundError("Method not found: bad_cmd")

            with pytest.raises(MethodNotFoundError):
                await handle_json_rpc(
                    {
                        "jsonrpc": "2.0",
                        "method": "execute_async",
                        "params": {"command": "bad_cmd", "params": {}},
                        "id": 4,
                    }
                )


class TestPostApiAsyncRoute:
    """Tests for POST /api/async dedicated route (step 02)."""

    @pytest.mark.asyncio
    async def test_post_api_async_success(self) -> None:
        """POST /api/async with command and params returns 200 with accepted and deliver_id."""
        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        from mcp_proxy_adapter.api.core.app_factory_routes import setup_routes

        app = FastAPI()
        setup_routes(app)

        with patch(
            "mcp_proxy_adapter.api.core.app_factory_routes.execute_command_async",
            new_callable=AsyncMock,
        ) as mock_async:
            mock_async.return_value = {
                "accepted": True,
                "deliver_id": "route-test-uuid",
                "message": "Command accepted; result will be delivered via WebSocket",
            }

            client = TestClient(app)
            response = client.post(
                "/api/async",
                json={"command": "echo", "params": {"message": "hi"}},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["accepted"] is True
        assert data["deliver_id"] == "route-test-uuid"
        mock_async.assert_called_once()
        call_args = mock_async.call_args[0]
        assert call_args[0] == "echo"
        assert call_args[1] == {"message": "hi"}

    @pytest.mark.asyncio
    async def test_post_api_async_missing_command_400(self) -> None:
        """POST /api/async without command returns 400."""
        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        from mcp_proxy_adapter.api.core.app_factory_routes import setup_routes

        app = FastAPI()
        setup_routes(app)
        client = TestClient(app)
        # Schema allows optional command; route validates and returns 400
        response = client.post(
            "/api/async",
            json={"params": {}},
        )
        # FastAPI may return 422 if command is required in schema
        assert response.status_code in (400, 422)
        if response.status_code == 400:
            assert "error" in response.json()

    @pytest.mark.asyncio
    async def test_post_api_async_unknown_command_404(self) -> None:
        """POST /api/async with unknown command returns 404."""
        from fastapi import FastAPI
        from fastapi.testclient import TestClient

        from mcp_proxy_adapter.api.core.app_factory_routes import setup_routes

        app = FastAPI()
        setup_routes(app)

        with patch(
            "mcp_proxy_adapter.api.core.app_factory_routes.execute_command_async",
            new_callable=AsyncMock,
        ) as mock_async:
            mock_async.side_effect = MethodNotFoundError(
                "Method not found: no_such_cmd"
            )

            client = TestClient(app)
            response = client.post(
                "/api/async",
                json={"command": "no_such_cmd", "params": {}},
            )

        assert response.status_code == 404
        assert "error" in response.json()
