"""
Server-side tests for async execution path (JSON-RPC execute_async and POST /api/async).

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

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

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from mcp_proxy_adapter.api.core.app_factory_routes import setup_routes
from mcp_proxy_adapter.api.handlers import handle_json_rpc
from mcp_proxy_adapter.core.errors import MethodNotFoundError


def _make_app() -> FastAPI:
    """Minimal app with routes and builtin commands for async tests."""
    from mcp_proxy_adapter.commands.builtin_commands import register_builtin_commands

    app = FastAPI()
    register_builtin_commands()
    setup_routes(app)
    return app


class TestExecuteAsyncJsonrpc:
    """JSON-RPC method execute_async (POST /api/jsonrpc)."""

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_returns_accepted_and_deliver_id(
        self,
    ) -> None:
        """POST /api/jsonrpc with method execute_async returns accepted and deliver_id."""
        app = _make_app()
        client = TestClient(app)
        response = client.post(
            "/api/jsonrpc",
            json={
                "jsonrpc": "2.0",
                "method": "execute_async",
                "params": {"command": "echo", "params": {"message": "hello"}},
                "id": 1,
            },
        )
        assert response.status_code == 200
        data = response.json()
        assert "result" in data
        assert data["result"]["accepted"] is True
        assert data["result"]["deliver_id"]
        assert len(data["result"]["deliver_id"]) > 0

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_result_delivered_via_ws(self) -> None:
        """Server calls notify_job_state_changed with completed when command finishes."""
        from mcp_proxy_adapter.api.handlers_async import execute_command_async
        from mcp_proxy_adapter.commands.builtin_commands import (
            register_builtin_commands,
        )

        register_builtin_commands()
        received_calls = []

        async def capture_notify(
            job_id: str,
            status: str,
            result: object,
            error: object,
            progress: int,
            description: str,
        ) -> None:
            received_calls.append(
                {"job_id": job_id, "status": status, "result": result}
            )

        with patch(
            "mcp_proxy_adapter.api.handlers_async.notify_job_state_changed",
            new_callable=AsyncMock,
            side_effect=capture_notify,
        ):
            result = await execute_command_async(
                "echo",
                {"message": "ws-test"},
                None,
                request_id=None,
                request=None,
            )
            deliver_id = result["deliver_id"]
            await asyncio.sleep(0.25)
            completed = [c for c in received_calls if c["status"] == "completed"]
            assert len(completed) >= 1
            ev = completed[0]
            assert ev["job_id"] == deliver_id
            assert isinstance(ev["result"], dict)
            assert ev["result"].get("message") == "ws-test" or "message" in str(
                ev["result"]
            )

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_missing_command(self) -> None:
        """Params without command return JSON-RPC error."""
        result = await handle_json_rpc(
            {
                "jsonrpc": "2.0",
                "method": "execute_async",
                "params": {"params": {}},
                "id": 1,
            }
        )
        assert "error" in result
        assert result["error"]["code"] == -32600

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_unknown_command(self) -> None:
        """Unknown command returns JSON-RPC error (MethodNotFoundError)."""
        with patch(
            "mcp_proxy_adapter.api.handlers.execute_command_async",
            new_callable=AsyncMock,
        ) as mock_async:
            mock_async.side_effect = MethodNotFoundError(
                "Method not found: nonexistent_command"
            )
            with pytest.raises(MethodNotFoundError):
                await handle_json_rpc(
                    {
                        "jsonrpc": "2.0",
                        "method": "execute_async",
                        "params": {
                            "command": "nonexistent_command",
                            "params": {},
                        },
                        "id": 1,
                    }
                )

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_failed_command_delivers_job_failed(
        self,
    ) -> None:
        """When command fails, notifier receives job_failed."""
        received_events = []

        def capture_listener(
            job_id: str,
            status: str,
            result: object,
            error: object,
            progress: int,
            description: str,
        ) -> None:
            received_events.append({"job_id": job_id, "status": status, "error": error})

        from mcp_proxy_adapter.core.job_push.notifier import (
            register_notifier_listener,
            clear_notifier_listeners,
        )

        register_notifier_listener(capture_listener)
        try:
            failing_command_class = MagicMock()
            failing_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 = failing_command_class
                app = _make_app()
                client = TestClient(app)
                response = client.post(
                    "/api/jsonrpc",
                    json={
                        "jsonrpc": "2.0",
                        "method": "execute_async",
                        "params": {
                            "command": "failing_cmd",
                            "params": {},
                        },
                        "id": 1,
                    },
                )
                assert response.status_code == 200
                assert response.json()["result"]["accepted"] is True
                await asyncio.sleep(0.3)
                failed = [e for e in received_events if e["status"] == "failed"]
                assert len(failed) >= 1
        finally:
            clear_notifier_listeners()


class TestPostApiAsync:
    """POST /api/async dedicated route (Step 02)."""

    @pytest.mark.asyncio
    async def test_post_api_async_returns_accepted(self) -> None:
        """POST /api/async with command returns 200, accepted=True, deliver_id."""
        app = _make_app()
        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"]

    @pytest.mark.asyncio
    async def test_post_api_async_missing_command_returns_400(self) -> None:
        """POST /api/async with empty body or no command returns 400."""
        app = _make_app()
        client = TestClient(app)
        response = client.post("/api/async", json={})
        assert response.status_code in (400, 422)
        if response.status_code == 400:
            assert "error" in response.json()
