# ruff: noqa: ARG001
import asyncio
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, Mock, patch

import pytest
from fastapi.testclient import TestClient

from terminaluse.lib.sandbox.runner import SandboxResult
from terminaluse.lib.sdk.fastacp.base.base_acp_server import (
    BaseACPServer,
    SandboxedHandlerError,
    UnsandboxableHandlerError,
)
from terminaluse.lib.types.acp import RPCMethod
from terminaluse.lib.types.task_context import TaskContext
from terminaluse.types.event import Event


async def _sandboxable_event_handler(ctx: TaskContext, event: Event):
    return {"ok": True}


class TestBaseACPServerInitialization:
    """Test BaseACPServer initialization and setup"""

    def test_base_acp_server_init(self):
        """Test BaseACPServer initialization sets up routes correctly"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            # Check that FastAPI routes are set up
            routes = [route.path for route in server.routes]  # type: ignore[attr-defined]
            assert "/healthz" in routes
            assert "/api" in routes

            # Check that handlers dict is initialized
            assert hasattr(server, "_handlers")
            assert isinstance(server._handlers, dict)

    def test_base_acp_server_create_classmethod(self):
        """Test BaseACPServer.create() class method"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer.create()

            assert isinstance(server, BaseACPServer)
            assert hasattr(server, "_handlers")

    def test_lifespan_function_setup(self):
        """Test that lifespan function is properly configured"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            # Check that lifespan is configured
            assert server.router.lifespan_context is not None

    def test_require_node_raises_when_node_missing(self):
        """TERMINALUSE_REQUIRE_NODE should fail fast if node is not sandbox-visible."""
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_REQUIRE_NODE": "true"},
            clear=False,
        ):
            server = BaseACPServer()
            with patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.shutil.which", return_value=None):
                with pytest.raises(RuntimeError, match="TERMINALUSE_REQUIRE_NODE=true"):
                    server._enforce_required_runtime_binaries()

    def test_require_node_passes_when_node_present(self):
        """TERMINALUSE_REQUIRE_NODE should pass when node exists in sandbox path."""
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_REQUIRE_NODE": "true"},
            clear=False,
        ):
            server = BaseACPServer()
            with patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.shutil.which", return_value="/usr/bin/node"):
                server._enforce_required_runtime_binaries()


class TestHealthCheckEndpoint:
    """Test health check endpoint functionality"""

    def test_health_check_endpoint(self, base_acp_server):
        """Test GET /healthz endpoint returns correct response"""
        client = TestClient(base_acp_server)

        response = client.get("/healthz")

        assert response.status_code == 200
        assert response.json() == {"status": "healthy"}

    def test_health_check_content_type(self, base_acp_server):
        """Test health check returns JSON content type"""
        client = TestClient(base_acp_server)

        response = client.get("/healthz")

        assert response.headers["content-type"] == "application/json"


class TestJSONRPCEndpointCore:
    """Test core JSON-RPC endpoint functionality"""

    def test_jsonrpc_endpoint_exists(self, base_acp_server):
        """Test POST /api endpoint exists"""
        client = TestClient(base_acp_server)

        # Send a basic request to check endpoint exists
        response = client.post("/api", json={})

        # Should not return 404 (endpoint exists)
        assert response.status_code != 404

    def test_jsonrpc_malformed_request(self, base_acp_server):
        """Test JSON-RPC endpoint handles malformed requests"""
        client = TestClient(base_acp_server)

        # Send malformed JSON
        response = client.post("/api", json={"invalid": "request"})

        assert response.status_code == 200
        data = response.json()
        assert "error" in data
        assert data["jsonrpc"] == "2.0"

    def test_jsonrpc_method_not_found(self, base_acp_server):
        """Test JSON-RPC method not found error"""
        client = TestClient(base_acp_server)

        request = {
            "jsonrpc": "2.0",
            "method": "nonexistent/method",
            "params": {},
            "id": "test-1",
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert "error" in data
        assert data["error"]["code"] == -32601  # Method not found
        assert data["id"] == "test-1"

    def test_jsonrpc_valid_request_structure(self, base_acp_server):
        """Test JSON-RPC request parsing with valid structure"""
        client = TestClient(base_acp_server)

        # Add a mock handler for testing
        async def mock_handler(params):
            return {"status": "success"}

        base_acp_server._handlers[RPCMethod.EVENT_SEND] = mock_handler

        request = {
            "jsonrpc": "2.0",
            "method": "event/send",
            "params": {
                "agent": {
                    "id": "test-agent",
                    "name": "test-agent",
                    "namespace_id": "ns-123",
                    "description": "test agent",
                    "created_at": "2023-01-01T00:00:00Z",
                    "updated_at": "2023-01-01T00:00:00Z",
                },
                "task": {
                    "id": "test-task",
                    "namespace_id": "ns-123",
                    "filesystem_id": "fs-123",
                    "status": "RUNNING",
                },
                "event": {
                    "id": "event-123",
                    "agent_id": "test-agent",
                    "task_id": "test-task",
                    "sequence_id": 1,
                    "created_at": "2023-01-01T00:00:00Z",
                    "content": {
                        "type": "text",
                        "text": "test message",
                    },
                },
            },
            "id": "test-1",
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert data["jsonrpc"] == "2.0"
        assert data["id"] == "test-1"
        # Should return immediate acknowledgment
        assert data["result"]["status"] == "processing"


class TestHandlerRegistration:
    """Test handler registration and management"""

    def test_on_event_decorator(self):
        """Test on_event decorator registration"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            @server.on_event
            async def test_handler(ctx: TaskContext, event: Event):
                return {"test": "response"}

            # Check handler is registered
            assert RPCMethod.EVENT_SEND in server._handlers
            assert server._handlers[RPCMethod.EVENT_SEND] is not None

    def test_on_cancel_decorator(self):
        """Test on_cancel decorator registration"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            @server.on_cancel
            async def test_handler(ctx: TaskContext):
                return {"test": "response"}

            # Check handler is registered
            assert RPCMethod.TASK_CANCEL in server._handlers
            assert server._handlers[RPCMethod.TASK_CANCEL] is not None

    def test_on_create_decorator(self):
        """Test on_create decorator registration"""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            @server.on_create
            async def test_handler(ctx: TaskContext, params: dict[str, Any]):
                return {"test": "response"}

            # Check handler is registered
            assert RPCMethod.TASK_CREATE in server._handlers
            assert server._handlers[RPCMethod.TASK_CREATE] is not None

    def test_on_create_registers_with_post_sync_policy(self):
        """task/create handlers must always register with post-sync enabled."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            with patch.object(server, "_register_handler") as register_handler:

                @server.on_create
                async def test_handler(ctx: TaskContext, params: dict[str, Any]):
                    return {"test": "response"}

                assert test_handler is not None

            assert register_handler.call_count == 1
            assert register_handler.call_args.kwargs["sync_up_after"] is True

    def test_on_event_registers_with_post_sync_policy(self):
        """event/send handlers must always register with post-sync enabled."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            with patch.object(server, "_register_handler") as register_handler:

                @server.on_event
                async def test_handler(ctx: TaskContext, event: Event):
                    return {"test": "response"}

                assert test_handler is not None

            assert register_handler.call_count == 1
            assert register_handler.call_args.kwargs["sync_up_after"] is True

    @pytest.mark.asyncio
    async def test_on_create_performs_sync_down_and_sync_up(self, sample_create_task_params):
        """Test on_create wrapper runs both pre-sync and post-sync operations."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            local_fs_path = Path("/tmp/test-fs-123")

            filesystem_module = Mock()
            filesystem_module.get_filesystem_path = Mock(return_value=local_fs_path)
            filesystem_module.sync_down = AsyncMock(return_value=None)
            filesystem_module.sync_up = AsyncMock(return_value=None)
            task_module = Mock()
            task_module.sync_down_system_folder = AsyncMock(return_value=None)
            task_module.sync_up_system_folder = AsyncMock(return_value=None)

            server._filesystem_module = filesystem_module
            server._task_module = task_module

            handler_called = False

            @server.on_create
            async def test_handler(ctx: TaskContext, params: dict[str, Any]):
                nonlocal handler_called
                handler_called = True
                return {"ok": True}

            with (
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.load_config", return_value={}),
                patch(
                    "terminaluse.lib.sdk.fastacp.base.base_acp_server.assemble_sandbox_mounts",
                    return_value=[],
                ) as assemble_sandbox_mounts_mock,
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.write_sandbox_mounts_file"),
            ):
                await server._handlers[RPCMethod.TASK_CREATE](sample_create_task_params)

            filesystem_module.get_filesystem_path.assert_called_once_with(sample_create_task_params.task.filesystem_id)
            filesystem_module.sync_down.assert_awaited_once_with(
                sample_create_task_params.task.filesystem_id,
                local_path=local_fs_path,
            )
            task_module.sync_down_system_folder.assert_awaited_once_with(sample_create_task_params.task.id)
            filesystem_module.sync_up.assert_awaited_once_with(
                sample_create_task_params.task.filesystem_id,
                local_path=local_fs_path,
            )
            task_module.sync_up_system_folder.assert_awaited_once_with(sample_create_task_params.task.id)
            assemble_sandbox_mounts_mock.assert_called_once()
            call_args, call_kwargs = assemble_sandbox_mounts_mock.call_args
            assert call_args[0] == sample_create_task_params.task.id
            assert call_args[1] == sample_create_task_params.task.filesystem_id
            assert call_args[2] == {}
            assert isinstance(call_args[3], str)
            assert call_kwargs["filesystem_host_path"] == str(local_fs_path)
            assert call_kwargs["workspace_read_only"] is False
            assert handler_called is True

    @pytest.mark.asyncio
    async def test_on_create_uses_read_only_workspace_when_task_metadata_requests_it(self, sample_create_task_params):
        """filesystem_access_mode=ro should mount /workspace read-only."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            local_fs_path = Path("/tmp/test-fs-123")

            filesystem_module = Mock()
            filesystem_module.get_filesystem_path = Mock(return_value=local_fs_path)
            filesystem_module.sync_down = AsyncMock(return_value=None)
            filesystem_module.sync_up = AsyncMock(return_value=None)
            task_module = Mock()
            task_module.sync_down_system_folder = AsyncMock(return_value=None)
            task_module.sync_up_system_folder = AsyncMock(return_value=None)
            server._filesystem_module = filesystem_module
            server._task_module = task_module

            @server.on_create
            async def test_handler(ctx: TaskContext, params: dict[str, Any]):
                return {"ok": True}

            ro_task = sample_create_task_params.task.model_copy(
                update={"task_metadata": {"filesystem_access_mode": "ro"}}
            )
            ro_params = sample_create_task_params.model_copy(update={"task": ro_task})

            with (
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.load_config", return_value={}),
                patch(
                    "terminaluse.lib.sdk.fastacp.base.base_acp_server.assemble_sandbox_mounts",
                    return_value=[],
                ) as assemble_sandbox_mounts_mock,
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.write_sandbox_mounts_file"),
            ):
                await server._handlers[RPCMethod.TASK_CREATE](ro_params)

            call_args, call_kwargs = assemble_sandbox_mounts_mock.call_args
            assert call_args[0] == ro_params.task.id
            assert call_args[1] == ro_params.task.filesystem_id
            assert call_kwargs["workspace_read_only"] is True

    @pytest.mark.asyncio
    async def test_on_create_syncs_up_even_when_handler_fails(self, sample_create_task_params):
        """Test on_create still performs post-sync when handler raises."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            local_fs_path = Path("/tmp/test-fs-123")

            filesystem_module = Mock()
            filesystem_module.get_filesystem_path = Mock(return_value=local_fs_path)
            filesystem_module.sync_down = AsyncMock(return_value=None)
            filesystem_module.sync_up = AsyncMock(return_value=None)
            task_module = Mock()
            task_module.sync_down_system_folder = AsyncMock(return_value=None)
            task_module.sync_up_system_folder = AsyncMock(return_value=None)

            server._filesystem_module = filesystem_module
            server._task_module = task_module

            @server.on_create
            async def test_handler(ctx: TaskContext, params: dict[str, Any]):
                raise RuntimeError("handler failure")

            with (
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.load_config", return_value={}),
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.assemble_sandbox_mounts", return_value=[]),
                patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.write_sandbox_mounts_file"),
                pytest.raises(RuntimeError, match="handler failure"),
            ):
                await server._handlers[RPCMethod.TASK_CREATE](sample_create_task_params)

            filesystem_module.get_filesystem_path.assert_called_once_with(sample_create_task_params.task.filesystem_id)
            filesystem_module.sync_down.assert_awaited_once_with(
                sample_create_task_params.task.filesystem_id,
                local_path=local_fs_path,
            )
            task_module.sync_down_system_folder.assert_awaited_once_with(sample_create_task_params.task.id)
            filesystem_module.sync_up.assert_awaited_once_with(
                sample_create_task_params.task.filesystem_id,
                local_path=local_fs_path,
            )
            task_module.sync_up_system_folder.assert_awaited_once_with(sample_create_task_params.task.id)


class TestSandboxRegistrationPolicy:
    """Test registration behavior for sandbox compatibility guarantees."""

    def test_nested_handler_fails_closed_by_default(self):
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_SANDBOX_FAIL_OPEN": "false"},
            clear=False,
        ):
            server = BaseACPServer()

            async def local_handler(ctx: TaskContext, event: Event):
                return {"ok": True}

            with pytest.raises(UnsandboxableHandlerError, match="module-level"):
                server.on_event(local_handler)

    def test_nested_handler_falls_back_when_fail_open_enabled(self):
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_SANDBOX_FAIL_OPEN": "true"},
            clear=False,
        ):
            server = BaseACPServer()

            async def local_handler(ctx: TaskContext, event: Event):
                return {"ok": True}

            server.on_event(local_handler)
            assert RPCMethod.EVENT_SEND in server._handlers
            assert RPCMethod.EVENT_SEND not in server._handler_refs

    def test_nested_handler_can_explicitly_allow_in_process(self):
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_SANDBOX_FAIL_OPEN": "false"},
            clear=False,
        ):
            server = BaseACPServer()

            @server.on_event(allow_in_process=True)
            async def local_handler(ctx: TaskContext, event: Event):
                return {"ok": True}

            assert local_handler is not None
            assert RPCMethod.EVENT_SEND in server._handlers
            assert RPCMethod.EVENT_SEND not in server._handler_refs

    def test_top_level_handler_registers_for_sandbox(self):
        with patch.dict(
            "os.environ",
            {"TERMINALUSE_BASE_URL": "", "TERMINALUSE_SANDBOX_FAIL_OPEN": "false"},
            clear=False,
        ):
            server = BaseACPServer()
            server.on_event(_sandboxable_event_handler)

            handler_ref = server._handler_refs[RPCMethod.EVENT_SEND]
            assert handler_ref.module == _sandboxable_event_handler.__module__
            assert handler_ref.function == _sandboxable_event_handler.__name__


class TestSandboxFailureHandling:
    """Test sandbox execution failure propagation and request logging behavior."""

    @pytest.mark.asyncio
    async def test_sandbox_failure_raises_sandboxed_handler_error(self):
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            server.on_event(_sandboxable_event_handler)

            params = SimpleNamespace(task=SimpleNamespace(id=None, filesystem_id=None))
            failed_result = SandboxResult(
                success=False,
                exit_code=1,
                stdout='{"status":"error","error":"boom"}',
                stderr="trace",
                timed_out=False,
            )

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.run_handler_sandboxed",
                new=AsyncMock(return_value=failed_result),
            ):
                with pytest.raises(SandboxedHandlerError, match="boom") as exc_info:
                    await server._handlers[RPCMethod.EVENT_SEND](params)

            assert exc_info.value.method == RPCMethod.EVENT_SEND
            assert exc_info.value.exit_code == 1
            assert exc_info.value.timed_out is False

    @pytest.mark.asyncio
    async def test_process_request_logs_error_not_success_on_sandbox_failure(self, caplog):
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            server.on_event(_sandboxable_event_handler)

            params = SimpleNamespace(task=SimpleNamespace(id=None, filesystem_id=None))
            failed_result = SandboxResult(
                success=False,
                exit_code=1,
                stdout='{"status":"error","error":"boom"}',
                stderr="trace",
                timed_out=False,
            )

            caplog.set_level("INFO")
            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.run_handler_sandboxed",
                new=AsyncMock(return_value=failed_result),
            ):
                await server._process_request("req-1", RPCMethod.EVENT_SEND, params)

            messages = [record.getMessage() for record in caplog.records]
            assert any("Error processing request req-1" in message for message in messages)
            assert not any("Successfully processed request req-1" in message for message in messages)


class TestBackgroundProcessing:
    """Test background processing functionality"""

    @pytest.mark.asyncio
    async def test_notification_processing(self, async_base_acp_server):
        """Test notification processing (requests with no ID)"""
        # Add a mock handler
        handler_called = False
        received_params = None

        async def mock_handler(params):
            nonlocal handler_called, received_params
            handler_called = True
            received_params = params
            return {"status": "processed"}

        async_base_acp_server._handlers[RPCMethod.EVENT_SEND] = mock_handler

        client = TestClient(async_base_acp_server)

        request = {
            "jsonrpc": "2.0",
            "method": "event/send",
            "params": {
                "agent": {
                    "id": "test-agent",
                    "name": "test-agent",
                    "namespace_id": "ns-123",
                    "description": "test agent",
                    "created_at": "2023-01-01T00:00:00Z",
                    "updated_at": "2023-01-01T00:00:00Z",
                },
                "task": {
                    "id": "test-task",
                    "namespace_id": "ns-123",
                    "filesystem_id": "fs-123",
                    "status": "RUNNING",
                },
                "event": {
                    "id": "event-123",
                    "agent_id": "test-agent",
                    "task_id": "test-task",
                    "sequence_id": 1,
                    "created_at": "2023-01-01T00:00:00Z",
                    "content": {
                        "type": "text",
                        "text": "test message",
                    },
                },
            },
            # No ID = notification
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert data["id"] is None  # Notification response

        # Give background task time to execute
        await asyncio.sleep(0.1)

        # Handler should have been called
        assert handler_called is True
        assert received_params is not None

    @pytest.mark.asyncio
    async def test_request_processing_with_id(self, async_base_acp_server):
        """Test request processing with ID returns immediate acknowledgment"""

        # Add a mock handler
        async def mock_handler(params):
            return {"status": "processed"}

        async_base_acp_server._handlers[RPCMethod.TASK_CANCEL] = mock_handler

        client = TestClient(async_base_acp_server)

        request = {
            "jsonrpc": "2.0",
            "method": "task/cancel",
            "params": {
                "agent": {
                    "id": "test-agent",
                    "name": "test-agent",
                    "namespace_id": "ns-123",
                    "description": "test agent",
                    "created_at": "2023-01-01T00:00:00Z",
                    "updated_at": "2023-01-01T00:00:00Z",
                },
                "task": {
                    "id": "test-task-123",
                    "namespace_id": "ns-123",
                    "filesystem_id": "fs-123",
                    "status": "RUNNING",
                },
            },
            "id": "test-request-1",
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert data["jsonrpc"] == "2.0"
        assert data["id"] == "test-request-1"
        assert data["result"]["status"] == "processing"  # Immediate acknowledgment


class TestAsyncRPCMethods:
    """Test RPC methods that return processing status (async behavior)"""

    def test_create_task_async_response(self, base_acp_server):
        """Test that TASK_CREATE method returns processing status (async behavior)"""
        client = TestClient(base_acp_server)

        # Add a mock handler for init task
        async def mock_init_handler(params):
            return {
                "task_id": params.task.id,
                "status": "initialized",
            }

        base_acp_server._handlers[RPCMethod.TASK_CREATE] = mock_init_handler

        request = {
            "jsonrpc": "2.0",
            "method": "task/create",
            "params": {
                "agent": {
                    "id": "test-agent",
                    "name": "test-agent",
                    "namespace_id": "ns-123",
                    "description": "test agent",
                    "created_at": "2023-01-01T00:00:00Z",
                    "updated_at": "2023-01-01T00:00:00Z",
                },
                "task": {
                    "id": "test-task-456",
                    "namespace_id": "ns-123",
                    "filesystem_id": "fs-123",
                    "status": "RUNNING",
                },
            },
            "id": "test-init-1",
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()

        # Verify JSON-RPC structure
        assert data["jsonrpc"] == "2.0"
        assert data["id"] == "test-init-1"
        assert "result" in data
        assert data.get("error") is None

        # Verify it returns async "processing" status (not the handler's result)
        result = data["result"]
        assert result["status"] == "processing"

        # Verify it's NOT the handler's actual result
        assert result.get("status") != "initialized"


class TestErrorHandling:
    """Test error handling scenarios"""

    def test_invalid_json_request(self, base_acp_server):
        """Test handling of invalid JSON in request body"""
        client = TestClient(base_acp_server)

        # Send invalid JSON
        response = client.post("/api", content="invalid json", headers={"Content-Type": "application/json"})

        assert response.status_code == 200
        data = response.json()
        assert "error" in data
        assert data["jsonrpc"] == "2.0"

    def test_missing_required_fields(self, base_acp_server):
        """Test handling of requests missing required JSON-RPC fields"""
        client = TestClient(base_acp_server)

        # Missing method field
        request = {"jsonrpc": "2.0", "params": {}, "id": "test-1"}

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert "error" in data

    def test_invalid_method_enum(self, base_acp_server):
        """Test handling of invalid method names"""
        client = TestClient(base_acp_server)

        request = {
            "jsonrpc": "2.0",
            "method": "invalid/method/name",
            "params": {},
            "id": "test-1",
        }

        response = client.post("/api", json=request)

        assert response.status_code == 200
        data = response.json()
        assert "error" in data
        assert data["error"]["code"] == -32601  # Method not found

    @pytest.mark.asyncio
    async def test_handler_exception_handling(self, async_base_acp_server):
        """Test that handler exceptions are properly handled"""

        # Add a handler that raises an exception
        async def failing_handler(params):
            raise ValueError("Test exception")

        async_base_acp_server._handlers[RPCMethod.EVENT_SEND] = failing_handler

        client = TestClient(async_base_acp_server)

        request = {
            "jsonrpc": "2.0",
            "method": "event/send",
            "params": {
                "agent": {
                    "id": "test-agent",
                    "name": "test-agent",
                    "namespace_id": "ns-123",
                    "description": "test agent",
                    "created_at": "2023-01-01T00:00:00Z",
                    "updated_at": "2023-01-01T00:00:00Z",
                },
                "task": {
                    "id": "test-task",
                    "namespace_id": "ns-123",
                    "filesystem_id": "fs-123",
                    "status": "RUNNING",
                },
                "event": {
                    "id": "event-123",
                    "agent_id": "test-agent",
                    "task_id": "test-task",
                    "sequence_id": 1,
                    "created_at": "2023-01-01T00:00:00Z",
                    "content": {
                        "type": "text",
                        "text": "test message",
                    },
                },
            },
            "id": "test-1",
        }

        response = client.post("/api", json=request)

        # Should still return immediate acknowledgment
        assert response.status_code == 200
        data = response.json()
        assert data["result"]["status"] == "processing"

        # Give background task time to fail
        await asyncio.sleep(0.1)
        # Exception should be logged but not crash the server


class TestHandlerEndSignaling:
    """Tests for handler-end signaling after event handler completes.

    After an EVENT_SEND handler finishes (success or failure), the agent server
    must call handler_end() to transition the task from RUNNING back to IDLE.
    Without this, tasks get permanently stuck in RUNNING.
    """

    @pytest.fixture
    def event_send_params(self):
        """Parsed SendEventParams-like object with task.id for handler-end tests."""
        return SimpleNamespace(
            task=SimpleNamespace(id="task-abc-123", filesystem_id="fs-123"),
            agent=SimpleNamespace(id="agent-1", name="test-agent", namespace_id="ns-1"),
            event=SimpleNamespace(id="evt-1", agent_id="agent-1", task_id="task-abc-123"),
        )

    @pytest.fixture
    def cancel_params(self):
        """Parsed CancelTaskParams-like object for non-event methods."""
        return SimpleNamespace(
            task=SimpleNamespace(id="task-abc-123", filesystem_id="fs-123"),
            agent=SimpleNamespace(id="agent-1", name="test-agent", namespace_id="ns-1"),
        )

    @pytest.mark.asyncio
    async def test_process_request_calls_handler_end_on_success(self, event_send_params):
        """After a successful EVENT_SEND handler, handler_end is called with success=True."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_request("req-1", RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args
            assert call_kwargs.kwargs["task_id"] == "task-abc-123"
            assert call_kwargs.kwargs["event_id"] == "evt-1"
            assert call_kwargs.kwargs["success"] is True
            assert call_kwargs.kwargs["error"] is None
            assert isinstance(call_kwargs.kwargs["duration_ms"], int)

    @pytest.mark.asyncio
    async def test_process_request_calls_handler_end_on_failure(self, event_send_params):
        """After a failed EVENT_SEND handler, handler_end is called with success=False."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def failing_handler(params):
                raise RuntimeError("handler exploded")

            server._handlers[RPCMethod.EVENT_SEND] = failing_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_request("req-2", RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args
            assert call_kwargs.kwargs["task_id"] == "task-abc-123"
            assert call_kwargs.kwargs["event_id"] == "evt-1"
            assert call_kwargs.kwargs["success"] is False
            assert "handler exploded" in call_kwargs.kwargs["error"]

    @pytest.mark.asyncio
    async def test_process_notification_calls_handler_end_on_success(self, event_send_params):
        """Notifications (no request ID) also call handler_end after EVENT_SEND."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_notification(RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args
            assert call_kwargs.kwargs["task_id"] == "task-abc-123"
            assert call_kwargs.kwargs["event_id"] == "evt-1"
            assert call_kwargs.kwargs["success"] is True

    @pytest.mark.asyncio
    async def test_process_notification_calls_handler_end_on_failure(self, event_send_params):
        """Notifications call handler_end with success=False when handler fails."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def failing_handler(params):
                raise ValueError("bad event")

            server._handlers[RPCMethod.EVENT_SEND] = failing_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_notification(RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args
            assert call_kwargs.kwargs["event_id"] == "evt-1"
            assert call_kwargs.kwargs["success"] is False
            assert "bad event" in call_kwargs.kwargs["error"]

    @pytest.mark.asyncio
    async def test_handler_end_omits_event_id_when_event_missing(self):
        """When params.event is missing, handler_end receives event_id=None."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            params_no_event = SimpleNamespace(
                task=SimpleNamespace(id="task-abc-123", filesystem_id="fs-123"),
                agent=SimpleNamespace(id="agent-1", name="test-agent", namespace_id="ns-1"),
                event=None,
            )

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_request("req-no-event", RPCMethod.EVENT_SEND, params_no_event)

            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args
            assert call_kwargs.kwargs["task_id"] == "task-abc-123"
            assert call_kwargs.kwargs["event_id"] is None

    @pytest.mark.asyncio
    async def test_no_handler_end_for_non_event_methods(self, cancel_params):
        """handler_end should NOT be called for TASK_CANCEL or other non-event methods."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.TASK_CANCEL] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_request("req-3", RPCMethod.TASK_CANCEL, cancel_params)

            mock_client.tasks.handler_end.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_handler_end_failure_does_not_crash_server(self, event_send_params):
        """If handler_end itself fails, the error is logged but doesn't propagate."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock(side_effect=Exception("network error calling handler-end"))
            server._terminaluse_client = mock_client

            # Should not raise
            await server._process_request("req-4", RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_handler_end_includes_duration_ms(self, event_send_params):
        """handler_end should report handler duration in milliseconds."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def slow_handler(params):
                await asyncio.sleep(0.05)  # 50ms
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = slow_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            await server._process_request("req-5", RPCMethod.EVENT_SEND, event_send_params)

            call_kwargs = mock_client.tasks.handler_end.call_args
            duration_ms = call_kwargs.kwargs["duration_ms"]
            assert duration_ms >= 40  # At least ~40ms (allowing for timing variance)

    @pytest.mark.asyncio
    async def test_handler_end_not_called_when_no_task_id(self):
        """If params has no task.id, handler_end should be skipped gracefully."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            params_no_task = SimpleNamespace(task=SimpleNamespace(id=None, filesystem_id=None))
            await server._process_request("req-6", RPCMethod.EVENT_SEND, params_no_task)

            mock_client.tasks.handler_end.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_no_handler_end_for_task_create(self):
        """handler_end should NOT be called for TASK_CREATE."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.TASK_CREATE] = mock_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            create_params = SimpleNamespace(
                task=SimpleNamespace(id="task-abc-123", filesystem_id="fs-123"),
                agent=SimpleNamespace(id="agent-1", name="test-agent", namespace_id="ns-1"),
            )
            await server._process_request("req-7", RPCMethod.TASK_CREATE, create_params)

            mock_client.tasks.handler_end.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_client_creation_failure_does_not_crash(self, event_send_params):
        """If _get_terminaluse_client() throws, the error is swallowed."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def mock_handler(params):
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = mock_handler

            # Force client creation to fail — no pre-set _terminaluse_client
            server._terminaluse_client = None
            with patch.object(
                server,
                "_get_terminaluse_client",
                side_effect=RuntimeError("env vars missing"),
            ):
                # Should not raise
                await server._process_request("req-8", RPCMethod.EVENT_SEND, event_send_params)

    @pytest.mark.asyncio
    async def test_handler_crash_triggers_handler_end_with_error_details(self, event_send_params):
        """Full round-trip: handler crash → handler_end called with failure info → server survives.

        Proves that a crashed handler doesn't leave the task stuck in RUNNING:
        the SDK signals handler_end(success=False) so Nucleus can transition back to IDLE.
        """
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()

            async def crashing_handler(params):
                raise RuntimeError("segfault in user code")

            server._handlers[RPCMethod.EVENT_SEND] = crashing_handler

            mock_client = AsyncMock()
            mock_client.tasks.handler_end = AsyncMock()
            server._terminaluse_client = mock_client

            # Process via notification (fire-and-forget path, like real deployments)
            await server._process_notification(RPCMethod.EVENT_SEND, event_send_params)

            # handler_end was called exactly once
            mock_client.tasks.handler_end.assert_awaited_once()
            call_kwargs = mock_client.tasks.handler_end.call_args.kwargs
            assert call_kwargs["task_id"] == "task-abc-123"
            assert call_kwargs["success"] is False
            assert "segfault in user code" in call_kwargs["error"]
            assert isinstance(call_kwargs["duration_ms"], int)
            assert call_kwargs["duration_ms"] >= 0
