# 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.core.api_error import ApiError
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"

    def test_jsonrpc_forwards_trace_context_headers(self):
        """W3C trace headers should be forwarded alongside allowed x-* headers."""
        with patch.dict("os.environ", {"TERMINALUSE_BASE_URL": ""}):
            server = BaseACPServer()
            captured_headers: dict[str, str] = {}

            async def capture_headers(params: Any):
                nonlocal captured_headers
                request = params.request or {}
                headers = request.get("headers", {})
                if isinstance(headers, dict):
                    captured_headers = headers
                return {"ok": True}

            server._handlers[RPCMethod.EVENT_SEND] = capture_headers

            client = TestClient(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-sync-headers",
            }

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.RPC_SYNC_METHODS",
                [RPCMethod.EVENT_SEND],
            ):
                response = client.post(
                    "/api",
                    json=request,
                    headers={
                        "x-custom-header": "custom-value",
                        "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
                        "tracestate": "rojo=00f067aa0ba902b7",
                        "baggage": "team=platform",
                        "authorization": "Bearer secret",
                    },
                )

            assert response.status_code == 200
            assert captured_headers.get("x-custom-header") == "custom-value"
            assert captured_headers.get("traceparent") == "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
            assert captured_headers.get("tracestate") == "rojo=00f067aa0ba902b7"
            assert captured_headers.get("baggage") == "team=platform"
            assert "authorization" not in captured_headers


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 fails transiently, retry until success without crashing."""
        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"), None]
            )
            server._terminaluse_client = mock_client

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                new=AsyncMock(),
            ) as sleep_mock:
                # Should not raise
                await server._process_request("req-4", RPCMethod.EVENT_SEND, event_send_params)

            assert mock_client.tasks.handler_end.await_count == 2
            sleep_mock.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_handler_end_retries_on_retryable_429(self, event_send_params):
        """429 should be treated as transient and retried."""
        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=[ApiError(status_code=429, headers=None, body={"detail": "rate limited"}), None]
            )
            server._terminaluse_client = mock_client

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                new=AsyncMock(),
            ) as sleep_mock:
                await server._process_request("req-429", RPCMethod.EVENT_SEND, event_send_params)

            assert mock_client.tasks.handler_end.await_count == 2
            sleep_mock.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_handler_end_uses_retry_after_seconds_for_429(self, event_send_params):
        """429 Retry-After seconds should override computed backoff delay."""
        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=[
                    ApiError(status_code=429, headers={"Retry-After": "7"}, body={"detail": "rate limited"}),
                    None,
                ]
            )
            server._terminaluse_client = mock_client

            with (
                patch.object(
                    BaseACPServer,
                    "_compute_handler_end_retry_delay_seconds",
                    return_value=1.25,
                ),
                patch(
                    "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                    new=AsyncMock(),
                ) as sleep_mock,
            ):
                await server._process_request("req-429-retry-after", RPCMethod.EVENT_SEND, event_send_params)

            sleep_mock.assert_awaited_once_with(7.0)

    @pytest.mark.asyncio
    async def test_handler_end_invalid_retry_after_falls_back_to_computed_delay(self, event_send_params):
        """Invalid Retry-After should not block retry; use computed delay instead."""
        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=[
                    ApiError(status_code=429, headers={"Retry-After": "not-a-number"}, body={"detail": "rate limited"}),
                    None,
                ]
            )
            server._terminaluse_client = mock_client

            with (
                patch.object(
                    BaseACPServer,
                    "_compute_handler_end_retry_delay_seconds",
                    return_value=3.5,
                ),
                patch(
                    "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                    new=AsyncMock(),
                ) as sleep_mock,
            ):
                await server._process_request("req-429-invalid-retry-after", RPCMethod.EVENT_SEND, event_send_params)

            sleep_mock.assert_awaited_once_with(3.5)

    @pytest.mark.asyncio
    async def test_handler_end_retries_on_retryable_408(self, event_send_params):
        """408 should be treated as transient and retried."""
        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=[ApiError(status_code=408, headers=None, body={"detail": "timeout"}), None]
            )
            server._terminaluse_client = mock_client

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                new=AsyncMock(),
            ) as sleep_mock:
                await server._process_request("req-408", RPCMethod.EVENT_SEND, event_send_params)

            assert mock_client.tasks.handler_end.await_count == 2
            sleep_mock.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_handler_end_does_not_retry_non_retryable_4xx(self, event_send_params):
        """4xx other than 408/429 should be treated as terminal."""
        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=ApiError(status_code=422, headers=None, body={"detail": "bad request"})
            )
            server._terminaluse_client = mock_client

            with patch(
                "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                new=AsyncMock(),
            ) as sleep_mock:
                await server._process_request("req-422", RPCMethod.EVENT_SEND, event_send_params)

            mock_client.tasks.handler_end.assert_awaited_once()
            sleep_mock.assert_not_awaited()

    def test_handler_end_retry_delay_handles_large_attempt_without_overflow(self):
        """Very high retry counts should clamp instead of overflowing pow()."""
        with patch("terminaluse.lib.sdk.fastacp.base.base_acp_server.random.random", return_value=0.5):
            delay_seconds = BaseACPServer._compute_handler_end_retry_delay_seconds(1024)
        assert delay_seconds == pytest.approx(60.0)

    @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 client creation fails transiently, handler-end retries until client is available."""
        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()

            with patch.object(
                server,
                "_get_terminaluse_client",
                side_effect=[RuntimeError("env vars missing"), mock_client],
            ) as get_client_mock:
                with patch(
                    "terminaluse.lib.sdk.fastacp.base.base_acp_server.asyncio.sleep",
                    new=AsyncMock(),
                ) as sleep_mock:
                    # Should not raise
                    await server._process_request("req-8", RPCMethod.EVENT_SEND, event_send_params)

            assert get_client_mock.call_count == 2
            mock_client.tasks.handler_end.assert_awaited_once()
            sleep_mock.assert_awaited_once()

    @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
