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

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

import asyncio
from typing import Any, Dict, List
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
    from mcp_proxy_adapter.core.delivery.terminal_replay_store import (
        TerminalReplayStore,
    )
    from mcp_proxy_adapter.core.delivery.ws_delivery_manager import (
        WsDeliveryManager,
    )
    from mcp_proxy_adapter.core.job_push.subscriptions import SubscriberRegistry

    app = FastAPI()
    register_builtin_commands()
    setup_routes(app)
    # Initialize ws_delivery_manager for tests
    registry = SubscriberRegistry()
    replay = TerminalReplayStore()
    manager = WsDeliveryManager(registry, replay)
    app.state.ws_delivery_manager = manager
    return app


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

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_returns_accepted_and_ws_id(
        self,
    ) -> None:
        """POST /api/jsonrpc with method execute_async returns accepted and ws_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"]["ws_id"]
        assert len(data["result"]["ws_id"]) > 0
        assert data["result"]["job_id"] is None
        assert data["result"]["delivery"]["mode"] == "ws"
        assert "deliver" + "_id" not in data["result"]

    @pytest.mark.asyncio
    async def test_execute_async_jsonrpc_result_delivered_via_ws(self) -> None:
        """Server calls notify_terminal_delivery with completed when command finishes."""
        from fastapi import Request
        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(ws_id: str, payload: dict) -> None:
            received_calls.append({"ws_id": ws_id, "payload": payload})

        app = _make_app()
        # Patch the manager's notify_terminal_delivery method
        manager = app.state.ws_delivery_manager
        original_notify = manager.notify_terminal_delivery
        manager.notify_terminal_delivery = AsyncMock(side_effect=capture_notify)

        # Create a mock request with app
        mock_request = MagicMock(spec=Request)
        mock_request.app = app
        mock_request.state = MagicMock()

        try:
            result = await execute_command_async(
                "echo",
                {"message": "ws-test"},
                None,
                request_id=None,
                request=mock_request,
            )
            ws_id = result["ws_id"]
            await asyncio.sleep(0.25)
            assert len(received_calls) >= 1
            ev = received_calls[0]
            assert ev["ws_id"] == ws_id
            assert isinstance(ev["payload"], dict)
            assert ev["payload"].get("event") in (
                "job_completed",
                "delivery_completed",
            )
            result_data = ev["payload"].get("result", {})
            assert result_data.get("message") == "ws-test" or "message" in str(
                result_data
            )
        finally:
            manager.notify_terminal_delivery = original_notify

    @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, delivery manager receives failed notification."""
        received_calls: List[Dict[str, Any]] = []

        async def capture_notify(ws_id: str, payload: Dict[str, Any]) -> None:
            received_calls.append({"ws_id": ws_id, "payload": payload})

        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()
            # Patch the manager's notify_terminal_delivery method
            manager = app.state.ws_delivery_manager
            original_notify = manager.notify_terminal_delivery
            manager.notify_terminal_delivery = AsyncMock(side_effect=capture_notify)

            client = TestClient(app)
            try:
                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 = [
                    c
                    for c in received_calls
                    if isinstance(c, dict)
                    and isinstance(c.get("payload"), dict)
                    and c["payload"].get("event") in ("job_failed", "delivery_failed")
                ]
                assert len(failed) >= 1
                ev = failed[0]
                assert isinstance(ev, dict)
                payload = ev.get("payload", {})
                assert isinstance(payload, dict)
                assert "error" in payload
                assert "command failed" in payload["error"]
            finally:
                manager.notify_terminal_delivery = original_notify


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, ws_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["ws_id"]
        assert data["job_id"] is None
        assert "deliver" + "_id" not in data
        assert "delivery" in data
        assert data["delivery"]["mode"] == "ws"

    @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()
