"""Tests for web server."""

import asyncio
import json
import os

import httpx
import pytest

from logos_adk.event_bus import InMemoryEventBus
from logos_adk.serve_routes.schemas import (
    AgentRequest,
    AgentResponse,
    ToolCallResponse,
    UsageResponse,
)


class TestPydanticModels:
    def test_agent_request_defaults(self):
        req = AgentRequest(prompt="hello")
        assert req.session_id is None

    def test_agent_request_with_session(self):
        req = AgentRequest(prompt="hello", session_id="s1")
        assert req.session_id == "s1"

    def test_agent_response(self):
        resp = AgentResponse(
            content="hi",
            tool_calls=[ToolCallResponse(id="1", name="search", arguments={"q": "x"})],
            usage=UsageResponse(input_tokens=10, output_tokens=5),
        )
        assert resp.content == "hi"
        assert len(resp.tool_calls) == 1

    def test_agent_response_serialization(self):
        resp = AgentResponse(
            content="hello",
            tool_calls=[],
            usage=UsageResponse(input_tokens=0, output_tokens=0),
        )
        data = resp.model_dump()
        assert data["content"] == "hello"
        assert data["tool_calls"] == []


from logos_adk.agent import Agent
from logos_adk.observe import reset_observability_store
from logos_adk.providers.mock import MockProvider
from logos_adk.quality_history import reset_quality_execution_manager
from logos_adk.scaling import DistributedRuntime, LoadBalancer, SQLiteTaskQueue
from logos_adk.state_store import SQLiteStateStore
from logos_adk.spend_ledger import InMemorySpendLedger, SpendRecord
from logos_adk.team import Team
from logos_adk.workflow import Graph, SQLiteGraphCheckpointStore
from logos_adk.workflow.steps import ApprovalStep

FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")


async def _request(app, method: str, path: str, **kwargs):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
        return await client.request(method, path, **kwargs)


class TestCreateApp:
    def setup_method(self):
        reset_observability_store().reset()
        reset_quality_execution_manager()

    def test_creates_fastapi_app(self):
        from logos_adk.serve import create_app
        from fastapi import FastAPI

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])
        assert isinstance(app, FastAPI)

    async def test_health_endpoint(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(app, "GET", "/health")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "bot" in data["agents"]

        live = await _request(app, "GET", "/health/live")
        ready = await _request(app, "GET", "/health/ready")
        assert live.status_code == 200
        assert ready.status_code == 200

    async def test_run_endpoint(self):
        from logos_adk.serve import create_app

        provider = MockProvider(responses=["hello from bot"])
        agent = Agent("bot", model=provider)
        app = create_app([agent])

        resp = await _request(app, "POST", "/run", json={"prompt": "hi"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["content"] == "hello from bot"
        assert data["usage"]["input_tokens"] == 0

    async def test_batch_endpoint(self):
        from logos_adk.serve import create_app

        provider = MockProvider(responses=["first", "second"])
        agent = Agent("bot", model=provider)
        app = create_app([agent])

        resp = await _request(
            app,
            "POST",
            "/batch",
            json={"prompts": ["hi", "there"], "max_concurrency": 2},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert [item["content"] for item in data["items"]] == ["first", "second"]

    async def test_create_app_from_config_paths_supports_department_supervisor(self, tmp_path):
        from logos_adk.serve import create_app_from_config_paths

        config_path = tmp_path / "department_supervisor_serve.yaml"
        config_path.write_text(
            "name: ceo\n"
            "type: department_supervisor\n"
            "departments:\n"
            "  - name: seo_swarm\n"
            "    kind: swarm\n"
            "    request_topic: need_seo\n"
            "    response_topic: seo_done\n"
            "    subscriptions:\n"
            "      - topic: need_seo\n"
            "        handler: tests.test_dynamic_orchestration_config:declarative_swarm_handler\n"
            "        publish_to: seo_done\n"
        )
        app = create_app_from_config_paths([str(config_path)])

        resp = await _request(app, "POST", "/run", json={"prompt": "optimize seo title"})

        assert resp.status_code == 200
        data = resp.json()
        assert data["content"] == "swarm-config::OPTIMIZE SEO TITLE"

    async def test_rate_limit_middleware_returns_429(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["first", "second"]))
        app = create_app([agent], rate_limit_per_minute=1, rate_limit_burst=0)

        first = await _request(app, "POST", "/run", json={"prompt": "hi"})
        second = await _request(app, "POST", "/run", json={"prompt": "hi"})
        assert first.status_code == 200
        assert second.status_code == 429
        assert second.json()["error"] == "rate_limited"

    async def test_stream_endpoint(self):
        from logos_adk.serve import create_app

        provider = MockProvider(responses=["streamed response"])
        agent = Agent("bot", model=provider)
        app = create_app([agent])

        resp = await _request(app, "POST", "/stream", json={"prompt": "hi"})
        assert resp.status_code == 200
        assert "text/event-stream" in resp.headers["content-type"]

        lines = resp.text.strip().split("\n\n")
        events = []
        for line in lines:
            if line.startswith("data: "):
                events.append(json.loads(line[6:]))

        types = [e["type"] for e in events]
        assert "done" in types

    async def test_stream_ui_continue_uses_agent_resume_state(self):
        from logos_adk.serve import create_app

        async def draft(ctx):
            ctx.blackboard["draft"] = "saved-draft"
            return "draft"

        async def publish(ctx):
            return f"{ctx['draft']}::{ctx.blackboard['draft']}::{ctx['review']}"

        graph = (
            Graph("workflow")
            .node(draft, name="draft")
            .node(ApprovalStep("review", "Need approval"), name="review")
            .node(publish, name="publish")
            .edge("draft", "review")
            .edge("review", "publish")
            .entrypoint("draft")
            .terminal("publish")
        )
        app = create_app([graph])

        paused_resp = await _request(
            app,
            "POST",
            "/stream-ui",
            json={
                "prompt": "go",
                "session_id": "sess-ui",
                "correlation_id": "corr-ui",
            },
        )
        assert paused_resp.status_code == 200
        paused_events = []
        for line in paused_resp.text.strip().split("\n\n"):
            if line.startswith("data: "):
                paused_events.append(json.loads(line[6:]))
        assert paused_events[-1]["event"] in ("graph_completed", "done")
        assert paused_events[-1]["status"] == "paused"

        resumed_resp = await _request(
            app,
            "POST",
            "/stream-ui/continue",
            json={
                "prompt": "ignored",
                "user_input": "approved",
                "session_id": "sess-ui",
                "continue_correlation_id": "corr-ui",
                "continue_from_node": "review",
            },
        )
        assert resumed_resp.status_code == 200
        resumed_events = []
        for line in resumed_resp.text.strip().split("\n\n"):
            if line.startswith("data: "):
                resumed_events.append(json.loads(line[6:]))
        assert resumed_events[-1]["event"] in ("graph_completed", "done")
        assert resumed_events[-1]["status"] == "completed"
        assert resumed_events[-1]["output"] == "draft::saved-draft::approved"

    async def test_resume_endpoint_alias_maps_node_name_to_resume(self):
        from logos_adk.serve import create_app

        async def draft(ctx):
            ctx.blackboard["draft"] = "saved-draft"
            return "draft"

        async def publish(ctx):
            return f"{ctx['draft']}::{ctx.blackboard['draft']}::{ctx['review']}"

        graph = (
            Graph("workflow")
            .node(draft, name="draft")
            .node(ApprovalStep("review", "Need approval"), name="review")
            .node(publish, name="publish")
            .edge("draft", "review")
            .edge("review", "publish")
            .entrypoint("draft")
            .terminal("publish")
        )
        app = create_app([graph])

        paused_resp = await _request(
            app,
            "POST",
            "/stream-ui",
            json={
                "prompt": "go",
                "session_id": "sess-resume",
                "correlation_id": "corr-resume",
            },
        )
        assert paused_resp.status_code == 200

        resumed_resp = await _request(
            app,
            "POST",
            "/resume",
            json={
                "prompt": "ignored",
                "user_input": "approved",
                "session_id": "sess-resume",
                "correlation_id": "corr-resume",
                "node_name": "review",
            },
        )

        assert resumed_resp.status_code == 200
        resumed_events = []
        for line in resumed_resp.text.strip().split("\n\n"):
            if line.startswith("data: "):
                resumed_events.append(json.loads(line[6:]))
        assert resumed_events[-1]["event"] in ("graph_completed", "done")
        assert resumed_events[-1]["status"] == "completed"
        assert resumed_events[-1]["output"] == "draft::saved-draft::approved"

    async def test_stream_ui_continue_requires_session_id(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        resp = await _request(
            app,
            "POST",
            "/stream-ui/continue",
            json={"prompt": "resume me", "user_input": "approved"},
        )

        assert resp.status_code == 400
        assert resp.json()["detail"]["error"] == "session_id required"

    async def test_resume_endpoint_reports_resume_not_supported_for_plain_agent(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        resp = await _request(
            app,
            "POST",
            "/resume",
            json={
                "prompt": "resume me",
                "session_id": "sess-plain",
                "correlation_id": "corr-plain",
            },
        )

        assert resp.status_code == 200
        events = []
        for line in resp.text.strip().split("\n\n"):
            if line.startswith("data: "):
                events.append(json.loads(line[6:]))
        assert events[-1]["error"] == "resume_not_supported"
        assert events[-1]["terminal"] is True

    async def test_stream_ui_continue_survives_restart_with_sqlite_checkpoint_store(self, tmp_path):
        from logos_adk.serve import create_app

        db_path = tmp_path / "graph_checkpoints.db"
        before_calls = {"draft": 0, "publish": 0}

        async def before_draft(ctx):
            before_calls["draft"] += 1
            ctx.blackboard["draft"] = "saved-draft"
            return "draft"

        async def before_publish(ctx):
            before_calls["publish"] += 1
            return f"{ctx['draft']}::{ctx.blackboard['draft']}::{ctx['review']}"

        app_before = create_app(
            [
                (
                    Graph("workflow", checkpoint_store=SQLiteGraphCheckpointStore(str(db_path)))
                    .node(before_draft, name="draft")
                    .node(ApprovalStep("review", "Need approval"), name="review")
                    .node(before_publish, name="publish")
                    .edge("draft", "review")
                    .edge("review", "publish")
                    .entrypoint("draft")
                    .terminal("publish")
                )
            ]
        )

        paused_resp = await _request(
            app_before,
            "POST",
            "/stream-ui",
            json={
                "prompt": "go",
                "session_id": "sess-ui",
                "correlation_id": "corr-ui-restart",
            },
        )
        assert paused_resp.status_code == 200

        after_calls = {"draft": 0, "publish": 0}

        async def after_draft(ctx):
            after_calls["draft"] += 1
            ctx.blackboard["draft"] = "should-not-rerun"
            return "draft-rerun"

        async def after_publish(ctx):
            after_calls["publish"] += 1
            return f"{ctx['draft']}::{ctx.blackboard['draft']}::{ctx['review']}"

        app_after = create_app(
            [
                (
                    Graph("workflow", checkpoint_store=SQLiteGraphCheckpointStore(str(db_path)))
                    .node(after_draft, name="draft")
                    .node(ApprovalStep("review", "Need approval"), name="review")
                    .node(after_publish, name="publish")
                    .edge("draft", "review")
                    .edge("review", "publish")
                    .entrypoint("draft")
                    .terminal("publish")
                )
            ]
        )

        resumed_resp = await _request(
            app_after,
            "POST",
            "/stream-ui/continue",
            json={
                "prompt": "ignored",
                "user_input": "approved",
                "session_id": "sess-ui",
                "continue_correlation_id": "corr-ui-restart",
                "continue_from_node": "review",
            },
        )
        assert resumed_resp.status_code == 200
        resumed_events = []
        for line in resumed_resp.text.strip().split("\n\n"):
            if line.startswith("data: "):
                resumed_events.append(json.loads(line[6:]))
        assert resumed_events[-1]["event"] in ("graph_completed", "done")
        assert resumed_events[-1]["status"] == "completed"
        assert resumed_events[-1]["output"] == "draft::saved-draft::approved"
        assert before_calls["draft"] == 1
        assert before_calls["publish"] == 0
        assert after_calls["draft"] == 0
        assert after_calls["publish"] == 1

    async def test_lifespan_autostarts_and_stops_chronos_for_team(self):
        from logos_adk.serve import create_app

        processed: list[str] = []

        async def handle_seo(event):
            processed.append(event.payload["prompt"])
            return {"done": event.payload["prompt"]}

        seo = Agent("seo", model=MockProvider(responses=["unused"]))
        team = Team(
            "campaign-team",
            agents=[seo],
            event_bus=InMemoryEventBus(),
            topic_policy={"seo": ["need_seo", "seo_done"]},
        )
        team.subscribe("seo", "need_seo", handler=handle_seo, publish_to="seo_done")
        app = create_app([team], chronos_poll_interval_seconds=0.01)

        assert len(app.state.chronos_runners) == 1
        runner = app.state.chronos_runners[0]
        assert runner.running is False

        async with app.router.lifespan_context(app):
            assert runner.running is True
            team.schedule_event(
                "seo",
                "need_seo",
                {"prompt": "summer sale"},
                campaign_id="cmp-lifecycle",
                correlation_id="corr-lifecycle",
                run_id="run-lifecycle",
                delay_seconds=0.01,
            )
            for _ in range(10):
                if processed:
                    break
                await asyncio.sleep(0.02)
            assert processed == ["summer sale"]
            response_events = team.event_bus().list_events(
                topic="seo_done",
                campaign_id="cmp-lifecycle",
            )
            assert len(response_events) == 1

        assert runner.running is False

    async def test_campaign_operator_surface(self):
        from logos_adk.serve import create_app

        async def handle_seo(event):
            return {"done": event.payload["prompt"]}

        seo = Agent("seo", model=MockProvider(responses=["unused"]))
        team = Team(
            "campaign-team",
            agents=[seo],
            event_bus=InMemoryEventBus(),
            topic_policy={"seo": ["need_seo", "seo_done"]},
        )
        team.subscribe("seo", "need_seo", handler=handle_seo, publish_to="seo_done")
        team.publish_event(
            "seo",
            "need_seo",
            {"prompt": "launch ad set"},
            campaign_id="cmp-active",
            correlation_id="corr-active",
            run_id="run-active",
        )
        team.schedule_event(
            "seo",
            "need_seo",
            {"prompt": "check ctr in 3 days"},
            campaign_id="cmp-scheduled",
            correlation_id="corr-scheduled",
            run_id="run-scheduled",
            delay_seconds=3600,
        )
        await team.run_rounds()
        app = create_app([team])

        campaigns = await _request(app, "GET", "/campaigns")
        active = await _request(app, "GET", "/campaigns/cmp-active")
        active_events = await _request(app, "GET", "/campaigns/cmp-active/events")
        scheduled_events = await _request(
            app,
            "GET",
            "/campaigns/cmp-scheduled/events?status=queued",
        )

        assert campaigns.status_code == 200
        campaign_ids = {item["campaign_id"] for item in campaigns.json()["items"]}
        assert {"cmp-active", "cmp-scheduled"} <= campaign_ids

        assert active.status_code == 200
        active_payload = active.json()
        assert active_payload["campaign_id"] == "cmp-active"
        assert active_payload["completed_events"] == 1
        assert active_payload["total_events"] == 2

        assert active_events.status_code == 200
        active_event_payload = active_events.json()
        assert len(active_event_payload["items"]) == 2
        assert {item["topic"] for item in active_event_payload["items"]} == {"need_seo", "seo_done"}

        assert scheduled_events.status_code == 200
        scheduled_items = scheduled_events.json()["items"]
        assert len(scheduled_items) == 1
        assert scheduled_items[0]["campaign_id"] == "cmp-scheduled"
        assert scheduled_items[0]["status"] == "queued"

    async def test_multi_agent_routing(self):
        from logos_adk.serve import create_app

        agent1 = Agent("analyst", model=MockProvider(responses=["analysis"]))
        agent2 = Agent("writer", model=MockProvider(responses=["written"]))
        app = create_app([agent1, agent2])

        r1 = await _request(app, "POST", "/analyst/run", json={"prompt": "hi"})
        assert r1.status_code == 200
        assert r1.json()["content"] == "analysis"

        r2 = await _request(app, "POST", "/writer/run", json={"prompt": "hi"})
        assert r2.status_code == 200
        assert r2.json()["content"] == "written"

        health = await _request(app, "GET", "/health")
        assert set(health.json()["agents"]) == {"analyst", "writer"}

    async def test_cors_enabled_by_default(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent], cors=True)

        resp = await _request(
            app,
            "OPTIONS",
            "/run",
            headers={"Origin": "http://example.com", "Access-Control-Request-Method": "POST"},
        )
        assert "access-control-allow-origin" in resp.headers

    async def test_api_key_auth(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["secret"]))
        app = create_app([agent], api_key="test-key")

        # No auth → 401
        resp = await _request(app, "POST", "/run", json={"prompt": "hi"})
        assert resp.status_code == 401

        # Wrong key → 401
        resp = await _request(
            app,
            "POST",
            "/run",
            json={"prompt": "hi"},
            headers={"Authorization": "Bearer wrong-key"},
        )
        assert resp.status_code == 401

        # Correct key → 200
        resp = await _request(
            app,
            "POST",
            "/run",
            json={"prompt": "hi"},
            headers={"Authorization": "Bearer test-key"},
        )
        assert resp.status_code == 200

        # Health always works
        resp = await _request(app, "GET", "/health")
        assert resp.status_code == 200

    async def test_api_key_query_token_and_response_headers(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["secret"]))
        app = create_app(
            [agent],
            api_keys={
                "query-key": {
                    "name": "query",
                    "scopes": ["agent:run"],
                    "workspaces": ["acme"],
                }
            },
            require_workspace=True,
        )

        resp = await _request(
            app,
            "POST",
            "/run?token=query-key",
            json={"prompt": "hi"},
            headers={
                "X-Workspace-Id": "acme",
                "X-Trace-Id": "trace-query",
                "X-Request-Id": "req-query",
            },
        )

        assert resp.status_code == 200
        assert resp.headers["X-Trace-Id"] == "trace-query"
        assert resp.headers["X-Request-Id"] == "req-query"
        assert resp.headers["X-Workspace-Id"] == "acme"

    async def test_payload_too_large_returns_413(self):
        from logos_adk.serve import create_app

        app = create_app(
            [Agent("bot", model=MockProvider(responses=["ok"]))],
            max_body_size=16,
        )

        resp = await _request(
            app,
            "POST",
            "/run",
            json={"prompt": "x" * 100},
        )

        assert resp.status_code == 413
        assert resp.json()["error"] == "payload_too_large"

    async def test_metrics_endpoint_tracks_request_counts(self):
        from logos_adk.serve import create_app, get_metrics

        agent = Agent("bot", model=MockProvider(responses=["first", "second"]))
        app = create_app([agent])
        before = get_metrics().get_stats()["total_requests"]

        first = await _request(app, "POST", "/run", json={"prompt": "hi"})
        second = await _request(app, "POST", "/run", json={"prompt": "there"})
        metrics = await _request(app, "GET", "/metrics")

        assert first.status_code == 200
        assert second.status_code == 200
        assert metrics.status_code == 200
        payload = metrics.json()
        assert payload["total_requests"] >= before + 2
        assert payload["by_agent"]["bot"]["requests"] >= 2

    async def test_health_ready_reports_degraded_when_provider_unhealthy(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["ok"]))

        def unhealthy():
            return {"providers": [{"name": "mock", "healthy": False}]}

        agent.health = unhealthy  # type: ignore[method-assign]
        app = create_app([agent])

        ready = await _request(app, "GET", "/health/ready")

        assert ready.status_code == 503
        assert ready.json()["status"] == "degraded"
        assert ready.json()["providers"][0]["healthy"] is False

    async def test_refresh_agents_registers_dynamic_agents(self):
        from logos_adk.serve import create_app
        from logos_adk.tools.factory import dynamic_agents

        dynamic_agents.clear()
        dynamic_agents["writer"] = Agent("writer", model=MockProvider(responses=["written"]))
        try:
            app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

            refresh = await _request(app, "POST", "/refresh_agents")
            routed = await _request(app, "POST", "/writer/run", json={"prompt": "hi"})

            assert refresh.status_code == 200
            assert refresh.json()["added"] == 1
            assert routed.status_code == 200
            assert routed.json()["content"] == "written"
        finally:
            dynamic_agents.clear()

    async def test_guardrail_error_returns_422(self):
        from logos_adk.serve import create_app
        from logos_adk.guardrails import ContentFilter

        agent = Agent("bot", model=MockProvider(responses=["ok"]))
        agent.input_guardrail(ContentFilter(blocked=["evil"]))
        app = create_app([agent])

        resp = await _request(app, "POST", "/run", json={"prompt": "evil request"})
        assert resp.status_code == 422
        assert resp.json()["error"] == "guardrail"

    async def test_timeout_returns_504(self):
        from logos_adk.serve import create_app

        import asyncio

        class SlowProvider(MockProvider):
            async def generate(self, messages, tools=None, output_schema=None, **kwargs):
                await asyncio.sleep(10)
                return await super().generate(messages, tools, output_schema, **kwargs)

        agent = Agent("bot", model=SlowProvider(responses=["slow"]))
        app = create_app([agent], timeout=0.01)

        resp = await _request(app, "POST", "/run", json={"prompt": "hi"})
        assert resp.status_code == 504
        assert resp.json()["error"] == "timeout"

    async def test_internal_error_returns_500(self):
        from logos_adk.serve import create_app

        class BrokenProvider(MockProvider):
            async def generate(self, messages, tools=None, output_schema=None, **kwargs):
                raise RuntimeError("provider broke")

        agent = Agent("bot", model=BrokenProvider())
        app = create_app([agent])

        resp = await _request(app, "POST", "/run", json={"prompt": "hi"})
        assert resp.status_code == 500
        assert resp.json()["error"] == "internal"

    async def test_distributed_runtime_routes_requests(self, tmp_path):
        from logos_adk.serve import create_app

        runtime = DistributedRuntime(
            load_balancer=LoadBalancer("round_robin"),
            task_queue=SQLiteTaskQueue(path=str(tmp_path / "tasks.db")),
        )
        runtime.register("bot", Agent("bot-a", model=MockProvider(responses=["first"])))
        runtime.register("bot", Agent("bot-b", model=MockProvider(responses=["second"])))

        app = create_app([], runtime=runtime)

        resp1 = await _request(app, "POST", "/run", json={"prompt": "hi"})
        resp2 = await _request(app, "POST", "/run", json={"prompt": "hi"})
        health = await _request(app, "GET", "/health")

        assert resp1.status_code == 200
        assert resp2.status_code == 200
        assert {resp1.json()["content"], resp2.json()["content"]} == {"first", "second"}
        assert health.json()["runtime"]["agents"]["bot"]["replicas"] == 2

    async def test_observability_endpoints_expose_summary_and_prometheus(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["ok"]))
        app = create_app([agent])

        await _request(app, "POST", "/run", json={"prompt": "hi"})
        summary = await _request(app, "GET", "/observability/summary")
        logs = await _request(app, "GET", "/observability/logs")
        prometheus = await _request(app, "GET", "/metrics/prometheus")
        grafana = await _request(app, "GET", "/observability/grafana/dashboard")

        assert summary.status_code == 200
        assert summary.json()["totals"]["spans"] >= 1
        assert logs.status_code == 200
        assert len(logs.json()["items"]) >= 1
        assert prometheus.status_code == 200
        assert "logos_adk_requests_total" in prometheus.text
        assert grafana.status_code == 200
        assert grafana.json()["title"] == "Logos ADK Observability"

    async def test_observability_filter_endpoints_accept_query_params(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["ok"]))
        app = create_app([agent])

        await _request(
            app, "POST", "/run", json={"prompt": "hi"}, headers={"x-trace-id": "trace-xyz"}
        )
        spans = await _request(app, "GET", "/observability/spans?trace_id=trace-xyz&status=ok")
        logs = await _request(app, "GET", "/observability/logs?level=info&trace_id=trace-xyz")
        alerts = await _request(app, "GET", "/observability/alerts?severity=warning")
        metrics = await _request(app, "GET", "/observability/metrics?name=adk_request_latency_ms")

        assert spans.status_code == 200
        assert isinstance(spans.json()["items"], list)
        assert logs.status_code == 200
        assert isinstance(logs.json()["items"], list)
        assert alerts.status_code == 200
        assert isinstance(alerts.json()["items"], list)
        assert metrics.status_code == 200
        assert isinstance(metrics.json()["items"], list)

    async def test_observability_stream_endpoint_emits_snapshot(self, tmp_path):
        from logos_adk.serve import create_app

        store = SQLiteStateStore(str(tmp_path / "state.db"))
        agent = Agent("bot", model=MockProvider(responses=["ok"])).state_backend(store)
        app = create_app([agent])

        await _request(app, "POST", "/run", json={"prompt": "hello", "session_id": "s1"})
        stream_resp = await _request(
            app,
            "GET",
            "/observability/stream?selected_agent=bot&session_id=s1&once=true",
        )

        assert stream_resp.status_code == 200
        assert "text/event-stream" in stream_resp.headers["content-type"]
        assert "event: snapshot" in stream_resp.text
        assert '"state_versions"' in stream_resp.text
        assert '"summary"' in stream_resp.text

    async def test_observability_stream_handles_unknown_selected_agent(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        stream_resp = await _request(
            app,
            "GET",
            "/observability/stream?selected_agent=missing&once=true",
        )

        assert stream_resp.status_code == 200
        assert '"state_backend":null' in stream_resp.text
        assert '"state_export":null' in stream_resp.text
        assert '"state_versions":[]' in stream_resp.text

    async def test_state_management_endpoints(self, tmp_path):
        from logos_adk.serve import create_app

        store = SQLiteStateStore(str(tmp_path / "state.db"))
        provider = MockProvider(responses=["first", "second"])
        agent = Agent("bot", model=provider).state_backend(store)
        app = create_app([agent])

        run_resp = await _request(app, "POST", "/run", json={"prompt": "hello", "session_id": "s1"})
        assert run_resp.status_code == 200

        versions = await _request(app, "GET", "/state/versions?session_id=s1")
        exported = await _request(app, "GET", "/state/export?session_id=s1")
        assert versions.status_code == 200
        assert versions.json()["items"][0]["version"] == 1
        assert versions.json()["items"][0]["session_key"] == "s1"
        assert versions.json()["items"][0]["agent_name"] == "bot"
        assert exported.status_code == 200
        assert "state" in exported.json()
        assert exported.json()["session_id"] == "s1"
        assert exported.json()["version"] == 1

        imported = await _request(
            app,
            "POST",
            "/state/import",
            json={"state": exported.json()["state"], "session_id": "clone"},
        )
        assert imported.status_code == 200
        assert imported.json()["session_id"] == "clone"
        cloned_export = await _request(app, "GET", "/state/export?session_id=clone")
        assert cloned_export.status_code == 200
        assert cloned_export.json()["session_id"] == "clone"
        assert cloned_export.json()["state"]["session_id"] == "clone"

        await _request(app, "POST", "/run", json={"prompt": "again", "session_id": "s1"})
        rollback = await _request(
            app,
            "POST",
            "/state/rollback",
            json={"session_id": "s1", "version": 1},
        )
        assert rollback.status_code == 200
        assert rollback.json()["version"] == 3

        shards = await _request(app, "GET", "/state/shards")
        assert shards.status_code == 200
        assert "backend" in shards.json()

    async def test_state_routes_require_configured_backend(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        versions = await _request(app, "GET", "/state/versions?session_id=s1")
        export = await _request(app, "GET", "/state/export?session_id=s1")
        shards = await _request(app, "GET", "/state/shards")

        assert versions.status_code == 404
        assert export.status_code == 404
        assert shards.status_code == 404
        assert versions.json()["error"] == "State store not configured"

    async def test_state_rollback_returns_404_for_missing_version(self, tmp_path):
        from logos_adk.serve import create_app

        store = SQLiteStateStore(str(tmp_path / "state.db"))
        agent = Agent("bot", model=MockProvider(responses=["first"])).state_backend(store)
        app = create_app([agent])

        await _request(app, "POST", "/run", json={"prompt": "hello", "session_id": "s1"})
        rollback = await _request(
            app,
            "POST",
            "/state/rollback",
            json={"session_id": "s1", "version": 99},
        )

        assert rollback.status_code == 404
        assert rollback.json()["error"] == "Version not found"

    async def test_quality_endpoints_expose_template_and_run_suite(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["hello", "Alex"]))
        app = create_app([agent])

        template = await _request(app, "GET", "/quality/template/integration?format=json")
        run = await _request(
            app,
            "POST",
            "/quality/run",
            json={
                "kind": "integration",
                "suite": {
                    "scenarios": [
                        {
                            "name": "context",
                            "steps": [
                                {"prompt": "My name is Alex", "session_id": "s1"},
                                {
                                    "prompt": "What is my name?",
                                    "session_id": "s1",
                                    "expect_contains": ["Alex"],
                                },
                            ],
                        }
                    ]
                },
            },
        )

        assert template.status_code == 200
        assert template.json()["kind"] == "integration"
        assert run.status_code == 200
        assert run.json()["summary"]["passed"] == 1
        assert "html" in run.json()["reports"]
        run_id = run.json()["run_id"]

        history = await _request(app, "GET", "/quality/runs?kind=integration")
        assert history.status_code == 200
        assert any(item["id"] == run_id for item in history.json()["items"])

        detail = await _request(app, "GET", f"/quality/runs/{run_id}")
        assert detail.status_code == 200
        assert detail.json()["status"] == "completed"

    async def test_quality_compare_endpoint_returns_summary_diff(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["alpha", "bravo", "alpha", "alpha"]))
        app = create_app([agent])

        suite_a = {
            "examples": [
                {"name": "exact-pass", "prompt": "one", "expected": "alpha", "mode": "exact"},
                {"name": "exact-fail", "prompt": "two", "expected": "alpha", "mode": "exact"},
            ]
        }
        suite_b = {
            "examples": [
                {"name": "exact-pass", "prompt": "three", "expected": "alpha", "mode": "exact"},
                {"name": "exact-pass-2", "prompt": "four", "expected": "alpha", "mode": "exact"},
            ]
        }

        run_a = await _request(
            app, "POST", "/quality/run", json={"kind": "regression", "suite": suite_a}
        )
        run_b = await _request(
            app, "POST", "/quality/run", json={"kind": "regression", "suite": suite_b}
        )
        assert run_a.status_code == 200
        assert run_b.status_code == 200

        compare = await _request(
            app,
            "GET",
            f"/quality/compare?run_id_a={run_a.json()['run_id']}&run_id_b={run_b.json()['run_id']}",
        )
        assert compare.status_code == 200
        payload = compare.json()
        assert payload["summary_diff"]["passed"] == {"a": 1, "b": 2}
        assert payload["row_count_diff"] == {"a": 2, "b": 2}

    async def test_quality_background_runs_are_persisted_and_queryable(self):
        from logos_adk.serve import create_app

        class SlowProvider(MockProvider):
            async def generate(self, messages, tools=None, output_schema=None, **kwargs):
                await asyncio.sleep(0.05)
                return await super().generate(messages, tools, output_schema, **kwargs)

        agent = Agent(
            "bot", model=SlowProvider(responses=["load-ok", '{"score": 0.9, "rationale": "good"}'])
        )
        app = create_app([agent])

        run = await _request(
            app,
            "POST",
            "/quality/run",
            json={
                "kind": "load-test",
                "suite": {"prompts": ["hello"], "concurrency": 1, "iterations": 1},
                "background": True,
            },
        )
        assert run.status_code == 202
        run_id = run.json()["run_id"]

        detail = None
        for _ in range(20):
            detail = await _request(app, "GET", f"/quality/runs/{run_id}")
            if detail.json()["status"] == "completed":
                break
            await asyncio.sleep(0.05)

        assert detail is not None
        assert detail.status_code == 200
        assert detail.json()["status"] == "completed"
        assert detail.json()["summary"]["requests"] == 1
        assert detail.json()["kind"] == "load-test"

    async def test_quality_runs_endpoint_filters_by_kind(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        integration = await _request(
            app,
            "POST",
            "/quality/run",
            json={"kind": "integration", "suite": {"scenarios": [{"name": "basic", "steps": []}]}},
        )
        regression = await _request(
            app,
            "POST",
            "/quality/run",
            json={"kind": "regression", "suite": {"examples": [{"name": "case", "prompt": "x"}]}},
        )

        assert integration.status_code == 200
        assert regression.status_code == 200

        filtered = await _request(app, "GET", "/quality/runs?kind=integration")

        assert filtered.status_code == 200
        items = filtered.json()["items"]
        assert items
        assert all(item["kind"] == "integration" for item in items)

    async def test_quality_compare_returns_404_for_unknown_run(self):
        from logos_adk.serve import create_app

        app = create_app([Agent("bot", model=MockProvider(responses=["ok"]))])

        known = await _request(
            app,
            "POST",
            "/quality/run",
            json={"kind": "integration", "suite": {"scenarios": [{"name": "basic", "steps": []}]}},
        )
        assert known.status_code == 200

        compare = await _request(
            app,
            "GET",
            f"/quality/compare?run_id_a={known.json()['run_id']}&run_id_b=missing-run",
        )

        assert compare.status_code == 404
        assert compare.json()["error"] == "Run not found"

    async def test_tools_routes_list_status_and_toggle(self):
        from logos_adk.serve import create_app
        from logos_adk.tools import tool

        @tool(name="search_docs", description="Search docs")
        async def search_docs(query: str) -> str:
            return f"found:{query}"

        agent = Agent("bot", model=MockProvider(responses=["ok"])).tools([search_docs])
        app = create_app([agent])

        tools_resp = await _request(app, "GET", "/tools")
        status_before = await _request(app, "GET", "/tools/status")
        toggle_off = await _request(app, "POST", "/tools/search_docs/toggle?enabled=false")
        status_after = await _request(app, "GET", "/tools/status")
        toggle_on = await _request(app, "POST", "/tools/search_docs/toggle?enabled=true")

        assert tools_resp.status_code == 200
        assert tools_resp.json()[0]["name"] == "search_docs"
        assert tools_resp.json()[0]["parameters"]["name"] == "search_docs"

        assert status_before.status_code == 200
        assert status_before.json()[0]["enabled"] is True

        assert toggle_off.status_code == 200
        assert toggle_off.json() == {"status": "ok", "tool": "search_docs", "enabled": False}

        assert status_after.status_code == 200
        assert status_after.json()[0]["enabled"] is False

        assert toggle_on.status_code == 200
        assert toggle_on.json()["enabled"] is True

    async def test_graph_route_uses_runtime_and_returns_404_when_missing(self):
        from logos_adk.serve import create_app

        class GraphRuntime:
            def get_graph(self, name):
                if name == "bot":
                    return {
                        "nodes": [{"id": "draft", "type": "step"}],
                        "edges": [{"from": "draft", "to": "done"}],
                        "metadata": {"source": "runtime"},
                    }
                return None

        bot = Agent("bot", model=MockProvider(responses=["ok"]))
        app = create_app([bot], runtime=GraphRuntime())

        graph_resp = await _request(app, "GET", "/graph")
        assert graph_resp.status_code == 200
        assert graph_resp.json()["metadata"]["source"] == "runtime"

        missing_graph = await _request(
            create_app([Agent("plain", model=MockProvider(responses=["ok"]))]),
            "GET",
            "/graph",
        )
        assert missing_graph.status_code == 404
        assert missing_graph.json()["error"] == "not_found"

    async def test_cache_routes_expose_stats_and_clear_backend(self):
        from logos_adk.cache import InMemoryCacheBackend
        from logos_adk.serve import create_app

        backend = InMemoryCacheBackend()
        agent = Agent("bot", model=MockProvider(responses=["cached"])).cache(backend)
        app = create_app([agent])

        await _request(app, "POST", "/run", json={"prompt": "hello"})
        await _request(app, "POST", "/run", json={"prompt": "hello", "session_id": "s2"})

        stats = await _request(app, "GET", "/cache/stats")
        cleared = await _request(app, "DELETE", "/cache")
        stats_after_clear = await _request(app, "GET", "/cache/stats")
        no_cache = await _request(
            create_app([Agent("plain", model=MockProvider(responses=["ok"]))]),
            "GET",
            "/cache/stats",
        )

        assert stats.status_code == 200
        assert stats.json()["backend"] == "InMemory"
        assert stats.json()["total_entries"] == 1
        assert stats.json()["total_hits"] >= 1

        assert cleared.status_code == 200
        assert cleared.json()["status"] == "ok"

        assert stats_after_clear.status_code == 200
        assert stats_after_clear.json()["total_entries"] == 0
        assert no_cache.status_code == 404
        assert no_cache.json()["error"] == "cache_not_enabled"

    async def test_billing_stats_aggregates_spend_records(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider(responses=["ok"]))
        ledger = InMemorySpendLedger()
        ledger.record(
            SpendRecord(
                agent_name="bot",
                model="gpt-4o-mini",
                amount_usd=0.12,
                session_id="s1",
            )
        )
        ledger.record(
            SpendRecord(
                agent_name="bot",
                model="gpt-4o-mini",
                amount_usd=0.08,
                session_id="s2",
            )
        )
        agent._spend_ledger = ledger
        app = create_app([agent])

        resp = await _request(app, "GET", "/stats/billing")

        assert resp.status_code == 200
        payload = resp.json()
        assert payload["total_spent_usd"] == pytest.approx(0.2)
        assert payload["by_agent"]["bot"] == pytest.approx(0.2)
        assert len(payload["recent_records"]) == 2
        assert {item["session_id"] for item in payload["recent_records"]} == {"s1", "s2"}


class TestAgentServeMethod:
    def test_serve_method_exists(self):
        agent = Agent("bot", model=MockProvider())
        assert hasattr(agent, "serve")


class TestChatSessionsEndpoints:
    """Tests for the chat sessions CRUD endpoints."""

    def setup_method(self):
        from logos_adk.serve_routes import _get_global_sessions_store

        _get_global_sessions_store().clear()

    async def test_list_sessions_empty(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(app, "GET", "/sessions")
        assert resp.status_code == 200
        data = resp.json()
        assert "sessions" in data
        assert "total" in data
        assert data["sessions"] == []
        assert data["total"] == 0

    async def test_create_session(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(
            app, "POST", "/sessions", json={"name": "Test Chat", "agent_name": "bot"}
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "id" in data
        assert data["name"] == "Test Chat"
        assert data["agent_name"] == "bot"
        assert data["messages"] == []
        assert data["tokens_by_agent"] == {}
        assert "created_at" in data
        return data["id"]

    async def test_get_session(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "Get Test"})
        session_id = create_resp.json()["id"]

        get_resp = await _request(app, "GET", f"/sessions/{session_id}")
        assert get_resp.status_code == 200
        data = get_resp.json()
        assert data["id"] == session_id
        assert data["name"] == "Get Test"

    async def test_get_session_not_found(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(app, "GET", "/sessions/nonexistent-id")
        assert resp.status_code == 404
        assert "Session not found" in resp.json()["error"]

    async def test_update_session(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "Original Name"})
        session_id = create_resp.json()["id"]

        update_resp = await _request(
            app,
            "PUT",
            f"/sessions/{session_id}",
            json={
                "name": "Updated Name",
                "messages": [
                    {
                        "id": "1",
                        "role": "user",
                        "content": "Hello",
                        "timestamp": "2024-01-01T00:00:00Z",
                    }
                ],
            },
        )
        assert update_resp.status_code == 200
        data = update_resp.json()
        assert data["name"] == "Updated Name"
        assert len(data["messages"]) == 1

    async def test_update_session_not_found(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(app, "PUT", "/sessions/nonexistent-id", json={"name": "New Name"})
        assert resp.status_code == 404

    async def test_delete_session(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "To Delete"})
        session_id = create_resp.json()["id"]

        delete_resp = await _request(app, "DELETE", f"/sessions/{session_id}")
        assert delete_resp.status_code == 200
        assert delete_resp.json()["status"] == "deleted"

        get_resp = await _request(app, "GET", f"/sessions/{session_id}")
        assert get_resp.status_code == 404

    async def test_delete_session_not_found(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        resp = await _request(app, "DELETE", "/sessions/nonexistent-id")
        assert resp.status_code == 404

    async def test_list_sessions_multiple(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        await _request(app, "POST", "/sessions", json={"name": "Chat 1", "agent_name": "bot"})
        await _request(app, "POST", "/sessions", json={"name": "Chat 2", "agent_name": "bot"})
        await _request(app, "POST", "/sessions", json={"name": "Chat 3", "agent_name": "other"})

        resp = await _request(app, "GET", "/sessions")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 3
        assert len(data["sessions"]) == 3

    async def test_list_sessions_filter_by_agent(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        await _request(app, "POST", "/sessions", json={"name": "Bot Chat 1", "agent_name": "bot"})
        await _request(app, "POST", "/sessions", json={"name": "Bot Chat 2", "agent_name": "bot"})
        await _request(app, "POST", "/sessions", json={"name": "Other Chat", "agent_name": "other"})

        resp = await _request(app, "GET", "/sessions", params={"agent_name_filter": "bot"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 2
        assert all(s["agent_name"] == "bot" for s in data["sessions"])

    async def test_list_sessions_with_limit(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        for i in range(5):
            await _request(app, "POST", "/sessions", json={"name": f"Chat {i}"})

        resp = await _request(app, "GET", "/sessions", params={"limit": 2})
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["sessions"]) == 2
        assert data["total"] == 5

    async def test_session_persists_across_requests(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "Persistent Chat"})
        session_id = create_resp.json()["id"]

        update_resp = await _request(
            app,
            "PUT",
            f"/sessions/{session_id}",
            json={
                "messages": [
                    {
                        "id": "1",
                        "role": "user",
                        "content": "Hello",
                        "timestamp": "2024-01-01T00:00:00Z",
                    }
                ]
            },
        )
        assert update_resp.status_code == 200

        get_resp = await _request(app, "GET", f"/sessions/{session_id}")
        assert get_resp.status_code == 200
        assert len(get_resp.json()["messages"]) == 1

    async def test_namespaced_session_routes_scope_agent_name(self):
        from logos_adk.serve import create_app

        bot = Agent("bot", model=MockProvider())
        writer = Agent("writer", model=MockProvider())
        app = create_app([bot, writer])

        create_resp = await _request(
            app,
            "POST",
            "/bot/sessions",
            json={"name": "Scoped Chat", "agent_name": "writer"},
        )
        assert create_resp.status_code == 200
        session_id = create_resp.json()["id"]
        assert create_resp.json()["agent_name"] == "bot"

        bot_get = await _request(app, "GET", f"/bot/sessions/{session_id}")
        writer_get = await _request(app, "GET", f"/writer/sessions/{session_id}")

        assert bot_get.status_code == 200
        assert writer_get.status_code == 404
        assert writer_get.json()["error"] == "session_not_found"

    async def test_cancel_task_run_updates_running_task_to_paused(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "Cancelable"})
        session_id = create_resp.json()["id"]

        update_resp = await _request(
            app,
            "PUT",
            f"/sessions/{session_id}",
            json={
                "task_runs": [
                    {
                        "id": "task-1",
                        "session_id": session_id,
                        "agent_name": "bot",
                        "profile_id": "profile-1",
                        "profile_title": "Profile",
                        "profile_badge": "B",
                        "prompt": "hello",
                        "status": "running",
                        "started_at": "2024-01-01T00:00:00Z",
                    }
                ]
            },
        )
        assert update_resp.status_code == 200

        cancel_resp = await _request(
            app,
            "POST",
            f"/sessions/{session_id}/task-runs/task-1/cancel",
        )

        assert cancel_resp.status_code == 200
        assert cancel_resp.json()["status"] == "cancel_requested"
        assert cancel_resp.json()["task_run_status"] == "paused"

        session_resp = await _request(app, "GET", f"/sessions/{session_id}")
        assert session_resp.status_code == 200
        assert session_resp.json()["task_runs"][0]["status"] == "paused"

    async def test_cancel_task_run_reports_already_terminal_for_completed_task(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        create_resp = await _request(app, "POST", "/sessions", json={"name": "Terminal"})
        session_id = create_resp.json()["id"]

        await _request(
            app,
            "PUT",
            f"/sessions/{session_id}",
            json={
                "task_runs": [
                    {
                        "id": "task-1",
                        "session_id": session_id,
                        "agent_name": "bot",
                        "profile_id": "profile-1",
                        "profile_title": "Profile",
                        "profile_badge": "B",
                        "prompt": "hello",
                        "status": "completed",
                        "started_at": "2024-01-01T00:00:00Z",
                    }
                ]
            },
        )

        cancel_resp = await _request(
            app,
            "POST",
            f"/sessions/{session_id}/task-runs/task-1/cancel",
        )

        assert cancel_resp.status_code == 200
        assert cancel_resp.json()["status"] == "already_terminal"
        assert cancel_resp.json()["task_run_status"] == "completed"

    async def test_cancel_task_run_returns_404_for_missing_session_or_task(self):
        from logos_adk.serve import create_app

        agent = Agent("bot", model=MockProvider())
        app = create_app([agent])

        missing_session = await _request(
            app,
            "POST",
            "/sessions/missing/task-runs/task-1/cancel",
        )
        assert missing_session.status_code == 404
        assert missing_session.json()["error"] == "session_not_found"

        create_resp = await _request(app, "POST", "/sessions", json={"name": "No Task"})
        session_id = create_resp.json()["id"]
        missing_task = await _request(
            app,
            "POST",
            f"/sessions/{session_id}/task-runs/task-404/cancel",
        )

        assert missing_task.status_code == 404
        assert missing_task.json()["error"] == "Task run not found"
