"""Targeted handler-level tests for serve route contracts."""

from __future__ import annotations

import json
from types import SimpleNamespace

import httpx
import pytest
from fastapi import HTTPException
from fastapi import FastAPI

from logos_adk.agent import Agent
from logos_adk.learning import PromptRegistry, SQLiteLearningStore
from logos_adk.providers.mock import MockProvider
from logos_adk.serve_security import (
    WORKSPACE_HEADER,
    ServePrincipal,
    _normalize_principal,
    load_api_keys_config,
    required_scope_for_request,
    resolve_workspace_id,
    scope_session_id,
)
from logos_adk.serve_routes.routes.agent import (
    _normalize_ui_event,
    batch_handler,
    run_handler,
    stream_handler,
)
from logos_adk.serve_routes.routes.campaigns import (
    get_campaign,
    list_campaign_events,
    list_campaigns,
)
from logos_adk.serve_routes.routes.feedback import (
    capture_feedback_handler,
    capture_outcome_handler,
)
from logos_adk.serve_routes.routes.state import create_state_routes
from logos_adk.serve_routes.routes.learning.dataset import build_dataset_handler
from logos_adk.serve_routes.routes.learning.evaluation import (
    acknowledge_incident_handler,
    champion_challenger_handler,
    create_eval_job_handler,
    delete_eval_job_handler,
    get_eval_job_handler,
    get_incident_snapshot_handler,
    get_judge_disagreements_handler,
    get_regression_dataset_handler,
    get_scorecard_handler,
    get_eval_jobs_handler,
    get_eval_trend_handler,
    resolve_incident_handler,
    run_eval_job_handler,
)
from logos_adk.serve_routes.routes.learning.improvement import (
    export_training_handler,
    get_policy_handler,
    get_reflection_lessons_handler,
    get_reward_scores_handler,
)
from logos_adk.serve_routes.routes.learning.retrieval import (
    acknowledge_trigger_handler,
    cancel_reindex_request_handler,
    close_trigger_handler,
    export_source_scores_handler,
    force_reindex_handler,
    get_profiles_handler,
    get_reindex_request_handler,
    get_reindex_requests_handler,
    get_reindex_summary_handler,
    get_reindex_triggers_handler,
    get_source_documents_handler,
    get_source_scores_handler,
    replay_reindex_request_handler,
    resolve_trigger_handler,
    resume_reindex_request_handler,
    retry_reindex_request_handler,
)
from logos_adk.serve_routes.routes.learning.prompt import (
    create_experiment_handler,
    create_prompt_handler,
    create_prompt_version_handler,
    get_prompt_info_handler,
    get_recommendations_handler,
    optimize_prompt_handler,
    promote_prompt_handler,
    rollback_prompt_handler,
)
from logos_adk.serve_routes.schemas import (
    AgentRequest,
    BatchAgentRequest,
    DatasetBuildRequest,
    EvaluationChampionChallengerRequest,
    EvaluationContinuousJobCreateRequest,
    EvaluationJudgeDisagreementsRequest,
    FeedbackRequest,
    OutcomeRequest,
    PromptCreateRequest,
    PromptExperimentCreateRequest,
    PromptOptimizeRequest,
    PromptPromoteRequest,
    PromptRecommendationRequest,
    PromptRollbackRequest,
    PromptVersionCreateRequest,
    TaskRunPayload,
)
from logos_adk.types import Response, Usage


def _task_run_payload(*, session_id: str = "task-session") -> TaskRunPayload:
    return TaskRunPayload(
        id="task-1",
        sessionId=session_id,
        agentName="bot",
        profileId="profile-1",
        profileTitle="Profile",
        profileBadge="B",
        prompt="hello",
        startedAt="2024-01-01T00:00:00Z",
    )


async def _read_sse(response) -> list[dict]:
    chunks: list[str] = []
    async for chunk in response.body_iterator:
        chunks.append(chunk.decode() if isinstance(chunk, bytes) else chunk)
    payload = "".join(chunks)
    events = []
    for item in payload.strip().split("\n\n"):
        if item.startswith("data: "):
            events.append(json.loads(item[6:]))
    return events


async def _request_state_app(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)


def test_serve_security_normalizes_principals_and_loads_api_key_registry(monkeypatch):
    principal = _normalize_principal(
        "ops-user",
        {
            "roles": ["operator"],
            "scopes": ["custom:scope"],
            "workspaces": ["acme"],
        },
    )
    wildcard_principal = _normalize_principal("anonymous", {})

    monkeypatch.setenv(
        "LOGOS_ADK_API_KEYS_JSON",
        json.dumps(
            {
                "env-token": {
                    "name": "env-client",
                    "role": "viewer",
                    "workspaces": ["env-space"],
                }
            }
        ),
    )
    registry = load_api_keys_config(
        api_key="legacy-token",
        api_keys={
            "explicit-token": {
                "name": "explicit-client",
                "roles": ["developer"],
                "workspaces": ["dev-space"],
            }
        },
    )

    assert principal.name == "ops-user"
    assert principal.roles == {"operator"}
    assert principal.has_scope("custom:scope") is True
    assert principal.has_scope("learning:write") is True
    assert principal.has_scope("prompt:write") is True
    assert principal.allows_workspace("acme") is True
    assert principal.allows_workspace("other") is False
    assert principal.allows_workspace(None) is False

    assert wildcard_principal.scopes == {"*"}
    assert wildcard_principal.workspaces == {"*"}
    assert wildcard_principal.roles is None

    assert set(registry) == {"explicit-token", "env-token", "legacy-token"}
    assert registry["explicit-token"].name == "explicit-client"
    assert registry["explicit-token"].has_scope("prompt:read") is True
    assert registry["explicit-token"].allows_workspace("dev-space") is True
    assert registry["env-token"].name == "env-client"
    assert registry["env-token"].has_scope("observability:read") is True
    assert registry["env-token"].allows_workspace("env-space") is True
    assert registry["legacy-token"] == ServePrincipal(
        name="legacy-api-key",
        scopes={"*"},
        workspaces={"*"},
    )


def test_serve_security_scope_resolution_and_workspace_helpers():
    assert required_scope_for_request("/health", "GET") is None
    assert required_scope_for_request("/learning/prompts/bot", "GET") == "prompt:read"
    assert required_scope_for_request("/learning/prompts/bot/promote", "POST") == "prompt:write"
    assert required_scope_for_request("/learning/retrieval/source-scores", "GET") == "learning:read"
    assert required_scope_for_request("/learning/improvement/policy", "POST") == "learning:write"
    assert required_scope_for_request("/learning/datasets/build", "POST") == "learning:read"
    assert required_scope_for_request("/feedback", "POST") == "learning:write"
    assert required_scope_for_request("/bot/run", "POST") == "agent:run"
    assert required_scope_for_request("/bot/state/export", "GET") == "state:read"
    assert required_scope_for_request("/bot/state/import", "POST") == "state:write"
    assert required_scope_for_request("/artifacts/blob", "GET") == "state:read"
    assert required_scope_for_request("/bot/quality/run", "POST") == "quality:run"
    assert required_scope_for_request("/bot/quality/report", "GET") == "quality:read"
    assert required_scope_for_request("/metrics", "GET") == "observability:read"
    assert required_scope_for_request("/unknown", "PATCH") == "agent:run"

    assert resolve_workspace_id({WORKSPACE_HEADER: "hdr-space"}, {"workspace_id": "query-space"}) == (
        "hdr-space"
    )
    assert resolve_workspace_id({}, {"workspace_id": "query-space"}) == "query-space"
    assert resolve_workspace_id({}, {}) is None

    assert scope_session_id(None, "acme") is None
    assert scope_session_id("s1", None) == "s1"
    assert scope_session_id("acme:s1", "acme") == "acme:s1"
    assert scope_session_id("s1", "acme") == "acme:s1"


@pytest.mark.asyncio
async def test_run_handler_uses_task_run_session_affinity():
    captured: dict[str, str | None] = {}

    class StubAgent:
        _task_run_session_store = {"task-session": {"id": "present"}}

        async def run(self, prompt, *, session_id=None, task_run_id=None):
            captured["prompt"] = prompt
            captured["session_id"] = session_id
            captured["task_run_id"] = task_run_id
            response = Response(content="ok", tool_calls=[], usage=Usage(1, 1), raw={})
            response.session_id = session_id
            return response

    result = await run_handler(
        StubAgent(),
        AgentRequest(
            prompt="hello",
            session_id="fallback-session",
            task_run=_task_run_payload(),
        ),
    )

    assert captured == {
        "prompt": "hello",
        "session_id": "task-session",
        "task_run_id": "task-1",
    }
    assert result.session_id == "task-session"


@pytest.mark.asyncio
async def test_stream_handler_serializes_tool_events_and_terminal_done():
    captured: dict[str, str | None] = {}

    class StubAgent:
        _task_run_session_store = {"task-session": {"id": "present"}}

        async def stream(self, prompt, *, session_id=None, task_run_id=None):
            captured["prompt"] = prompt
            captured["session_id"] = session_id
            captured["task_run_id"] = task_run_id
            yield SimpleNamespace(type="text", text="Hel")
            yield SimpleNamespace(
                type="tool_start",
                tool_call_id="tc1",
                tool_name="search",
                tool_arguments={"q": "docs"},
            )
            yield SimpleNamespace(
                type="tool_end",
                result={"call_id": "tc1", "content": "done"},
            )
            done = Response(content="Hello", tool_calls=[], usage=Usage(1, 1), raw={})
            done.session_id = session_id
            yield SimpleNamespace(type="done", response=done)

    response = await stream_handler(
        StubAgent(),
        AgentRequest(
            prompt="hello",
            session_id="fallback-session",
            task_run=_task_run_payload(),
        ),
    )
    events = await _read_sse(response)

    assert captured == {
        "prompt": "hello",
        "session_id": "task-session",
        "task_run_id": "task-1",
    }
    assert [event["type"] for event in events] == ["text", "tool_start", "tool_end", "done"]
    assert events[0]["delta"] == "Hel"
    assert events[1]["id"] == "tc1"
    assert events[1]["name"] == "search"
    assert events[2]["result"]["content"] == "done"
    assert events[3]["terminal"] is True
    assert events[3]["task_run_status"] == "completed"


@pytest.mark.asyncio
async def test_stream_handler_marks_incomplete_execution_when_no_terminal_event():
    class StubAgent:
        async def stream(self, prompt, *, session_id=None, task_run_id=None):
            yield SimpleNamespace(type="text", text="partial")

    response = await stream_handler(StubAgent(), AgentRequest(prompt="hello"))
    events = await _read_sse(response)

    assert [event["type"] for event in events] == ["text", "error"]
    assert events[-1]["error"] == "incomplete_execution"
    assert events[-1]["retryable"] is True


@pytest.mark.asyncio
async def test_batch_handler_returns_error_items_for_partial_failures():
    class StubAgent:
        async def run(self, prompt, *, session_id=None):
            if prompt == "bad":
                raise RuntimeError("boom")
            response = Response(
                content=f"ok:{prompt}",
                tool_calls=[],
                usage=Usage(2, 1),
                raw={},
            )
            response.session_id = session_id
            return response

    result = await batch_handler(
        StubAgent(),
        BatchAgentRequest(
            prompts=["good", "bad"],
            session_ids=["s1", "s2"],
            max_concurrency=50,
        ),
    )

    assert len(result.items) == 2
    assert result.items[0].content == "ok:good"
    assert result.items[0].session_id == "s1"
    assert result.items[1].content.startswith("Error:")
    assert "boom" in result.items[1].content


def test_normalize_ui_event_handles_graph_completion_and_error_shapes():
    base = {"task_run_id": "task-1", "task_run_status": "running"}

    paused = _normalize_ui_event(
        {"event": "graph_completed", "status": "paused"},
        default_event_data=base,
    )
    completed = _normalize_ui_event(
        {"event": "graph_completed", "status": "completed"},
        default_event_data=base,
    )
    failed = _normalize_ui_event(
        {"event": "graph_completed", "status": "failed"},
        default_event_data=base,
    )
    error_payload = _normalize_ui_event(
        {"type": "error", "error": "boom"},
        default_event_data=base,
    )
    empty_text = _normalize_ui_event(
        SimpleNamespace(type="text", text=""),
        default_event_data=base,
    )

    assert paused["terminal"] is True
    assert paused["resume_required"] is True
    assert paused["task_run_status"] == "paused"

    assert completed["terminal"] is True
    assert completed["retryable"] is False
    assert completed["task_run_status"] == "completed"

    assert failed["terminal"] is True
    assert failed["retryable"] is False
    assert failed["task_run_status"] == "failed"

    assert error_payload["terminal"] is True
    assert error_payload["retryable"] is False
    assert error_payload["task_run_status"] == "failed"
    assert empty_text is None


@pytest.mark.asyncio
async def test_evaluation_handlers_return_default_payloads_without_learning_store():
    agent = Agent("bot", model=MockProvider(responses=["ok"]))

    scorecard = await get_scorecard_handler(learning_store=None)
    dataset = await get_regression_dataset_handler(learning_store=None)
    disagreements = await get_judge_disagreements_handler(
        EvaluationJudgeDisagreementsRequest(),
        agent=agent,
        learning_store=None,
    )
    jobs = await get_eval_jobs_handler(learning_store=None)
    trend = await get_eval_trend_handler(learning_store=None)
    snapshot = await get_incident_snapshot_handler(learning_store=None)
    ack = await acknowledge_incident_handler("incident-1", learning_store=None)
    resolve = await resolve_incident_handler("incident-1", learning_store=None)
    job = await get_eval_job_handler("job-1", learning_store=None)
    run = await run_eval_job_handler("job-1", learning_store=None)
    delete = await delete_eval_job_handler("job-1", learning_store=None)
    champion = await champion_challenger_handler(
        EvaluationChampionChallengerRequest(challenger_prompt="challenger"),
        agent=agent,
        learning_store=None,
    )

    assert scorecard.scorecard["error"] == "learning_store not configured"
    assert dataset.dataset["error"] == "learning_store not configured"
    assert disagreements.summary["error"] == "learning_store not configured"
    assert disagreements.items == []
    assert jobs.items == []
    assert trend.report["error"] == "learning_store not configured"
    assert snapshot.snapshot["error"] == "learning_store not configured"
    assert ack.incident["error"] == "learning_store not configured"
    assert resolve.incident["error"] == "learning_store not configured"
    assert job.job["error"] == "learning_store not configured"
    assert run.job["error"] == "learning_store not configured"
    assert run.run == {}
    assert delete.job["error"] == "learning_store not configured"
    assert champion.report["error"] == "learning_store not configured"


@pytest.mark.asyncio
async def test_evaluation_handlers_cover_existing_and_not_found_job_paths(tmp_path):
    learning_store = SQLiteLearningStore(str(tmp_path / "learning.db"))
    prompt_registry = PromptRegistry(str(tmp_path / "prompts.db"))
    agent = Agent("bot", model=MockProvider(responses=["ok"]))

    create_request = EvaluationContinuousJobCreateRequest(
        name="regression-job",
        kind="regression",
        workspace_id="acme",
        agent_name="bot",
        interval_seconds=60.0,
        limit=10,
    )

    created = await create_eval_job_handler(
        create_request,
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    existing = await create_eval_job_handler(
        create_request,
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    found = await get_eval_job_handler(
        "regression-job",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    missing_job = await get_eval_job_handler(
        "missing-job",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    missing_run = await run_eval_job_handler(
        "missing-job",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    missing_delete = await delete_eval_job_handler(
        "missing-job",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    missing_ack = await acknowledge_incident_handler(
        "missing-incident",
        actor="ops",
        note="triaged",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    missing_resolve = await resolve_incident_handler(
        "missing-incident",
        actor="ops",
        note="handled",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )
    incident_snapshot = await get_incident_snapshot_handler(
        incident_id="missing-incident",
        agent=agent,
        prompt_registry=prompt_registry,
        learning_store=learning_store,
    )

    assert created.job["name"] == "regression-job"
    assert existing.job["name"] == "regression-job"
    assert found.job["name"] == "regression-job"
    assert missing_job.job["error"] == "job not found"
    assert missing_run.job["error"] == "job not found"
    assert missing_run.run == {}
    assert missing_delete.job["error"] == "job not found"
    assert missing_ack.incident == {"id": "missing-incident", "error": "incident not found"}
    assert missing_resolve.incident == {"id": "missing-incident", "error": "incident not found"}
    assert "incident" in incident_snapshot.snapshot
    assert incident_snapshot.snapshot["incident"] is None


def _retrieval_agent(engine):
    return SimpleNamespace(
        name="bot",
        _config=SimpleNamespace(
            knowledge_base=SimpleNamespace(retrieval_learning=engine)
        ),
    )


@pytest.mark.asyncio
async def test_retrieval_handlers_return_default_payloads_without_engine():
    scores = await get_source_scores_handler(workspace_id="acme")
    export = await export_source_scores_handler(format="csv", workspace_id="acme")
    triggers = await get_reindex_triggers_handler(workspace_id="acme")
    ack = await acknowledge_trigger_handler("trigger-1")
    resolve = await resolve_trigger_handler("trigger-1")
    close = await close_trigger_handler("trigger-1")
    force = await force_reindex_handler(source_key="doc-1", workspace_id="acme")
    requests = await get_reindex_requests_handler(workspace_id="acme")
    request = await get_reindex_request_handler("req-1")
    retry = await retry_reindex_request_handler("req-1")
    resume = await resume_reindex_request_handler("req-1")
    replay = await replay_reindex_request_handler("req-1")
    cancel = await cancel_reindex_request_handler("req-1")
    summary = await get_reindex_summary_handler(workspace_id="acme")
    profiles = await get_profiles_handler(
        workspace_id="acme",
        task_type="support",
        user_segment="vip",
    )
    source_documents = await get_source_documents_handler(
        workspace_id="acme",
        source_key="doc-1",
    )

    assert scores.items == []
    assert export.status_code == 200
    assert export.media_type == "application/json"
    assert export.body == b"[]"
    assert triggers.items == []
    assert ack.trigger["error"] == "engine not configured"
    assert resolve.trigger["error"] == "engine not configured"
    assert close.trigger["error"] == "engine not configured"
    assert force.request["error"] == "engine not configured"
    assert requests.items == []
    assert request.request["error"] == "engine not configured"
    assert retry.request["error"] == "engine not configured"
    assert resume.request["error"] == "engine not configured"
    assert replay.request["error"] == "engine not configured"
    assert cancel.request["error"] == "engine not configured"
    assert summary.requests == {}
    assert summary.triggers == {}
    assert summary.source_documents == {}
    assert summary.policy == {}
    assert summary.coalescing == {}
    assert profiles.items == []
    assert source_documents.items == []


@pytest.mark.asyncio
async def test_retrieval_handlers_cover_missing_and_exception_fallbacks():
    class MissingEngine:
        def acknowledge_reindex_trigger(self, trigger_id, note=None):
            return None

        def resolve_reindex_trigger(self, trigger_id, note=None):
            return None

        def close_reindex_trigger(self, trigger_id, note=None):
            return None

        def get_reindex_request(self, request_id):
            return None

        def retry_reindex_request(self, request_id, note=None):
            return None

        def resume_reindex_request(self, request_id, note=None):
            return None

        def replay_reindex_request(self, request_id, note=None):
            return None

        def cancel_reindex_request(self, request_id, note=None):
            return None

    class ExplodingEngine:
        def list_source_scores(self, workspace_id=None):
            raise RuntimeError("boom")

        def export_source_scores_report(self, workspace_id=None, format="json"):
            raise RuntimeError("boom")

        def list_reindex_triggers(self, workspace_id=None):
            raise RuntimeError("boom")

        def acknowledge_reindex_trigger(self, trigger_id, note=None):
            raise RuntimeError("boom")

        def resolve_reindex_trigger(self, trigger_id, note=None):
            raise RuntimeError("boom")

        def close_reindex_trigger(self, trigger_id, note=None):
            raise RuntimeError("boom")

        def create_force_reindex_request(
            self,
            workspace_id=None,
            source_key=None,
            reason="operator_requested",
            requested_by=None,
            metadata=None,
        ):
            raise RuntimeError("boom")

        def list_reindex_requests(self, workspace_id=None):
            raise RuntimeError("boom")

        def get_reindex_request(self, request_id):
            raise RuntimeError("boom")

        def retry_reindex_request(self, request_id, note=None):
            raise RuntimeError("boom")

        def resume_reindex_request(self, request_id, note=None):
            raise RuntimeError("boom")

        def replay_reindex_request(self, request_id, note=None):
            raise RuntimeError("boom")

        def cancel_reindex_request(self, request_id, note=None):
            raise RuntimeError("boom")

        def ops_summary(self, workspace_id=None):
            raise RuntimeError("boom")

        def list_profiles(self, workspace_id=None, task_type=None, user_segment=None):
            raise RuntimeError("boom")

        def list_source_documents(self, workspace_id=None, source_key=None):
            raise RuntimeError("boom")

    missing_agent = _retrieval_agent(MissingEngine())
    exploding_agent = _retrieval_agent(ExplodingEngine())

    missing_ack = await acknowledge_trigger_handler("missing-trigger", agent=missing_agent)
    missing_resolve = await resolve_trigger_handler("missing-trigger", agent=missing_agent)
    missing_close = await close_trigger_handler("missing-trigger", agent=missing_agent)
    missing_request = await get_reindex_request_handler("missing-request", agent=missing_agent)
    missing_retry = await retry_reindex_request_handler("missing-request", agent=missing_agent)
    missing_resume = await resume_reindex_request_handler("missing-request", agent=missing_agent)
    missing_replay = await replay_reindex_request_handler("missing-request", agent=missing_agent)
    missing_cancel = await cancel_reindex_request_handler("missing-request", agent=missing_agent)

    scores = await get_source_scores_handler(workspace_id="acme", agent=exploding_agent)
    export = await export_source_scores_handler(
        format="csv",
        workspace_id="acme",
        agent=exploding_agent,
    )
    triggers = await get_reindex_triggers_handler(workspace_id="acme", agent=exploding_agent)
    fallback_ack = await acknowledge_trigger_handler("trigger-1", agent=exploding_agent)
    fallback_resolve = await resolve_trigger_handler("trigger-1", agent=exploding_agent)
    fallback_close = await close_trigger_handler("trigger-1", agent=exploding_agent)
    force = await force_reindex_handler(
        source_key="doc-1",
        workspace_id="acme",
        requested_by="ops",
        agent=exploding_agent,
    )
    requests = await get_reindex_requests_handler(workspace_id="acme", agent=exploding_agent)
    request = await get_reindex_request_handler("req-1", agent=exploding_agent)
    retry = await retry_reindex_request_handler("req-1", agent=exploding_agent)
    resume = await resume_reindex_request_handler("req-1", agent=exploding_agent)
    replay = await replay_reindex_request_handler("req-1", agent=exploding_agent)
    cancel = await cancel_reindex_request_handler("req-1", agent=exploding_agent)
    summary = await get_reindex_summary_handler(workspace_id="acme", agent=exploding_agent)
    profiles = await get_profiles_handler(
        workspace_id="acme",
        task_type="support",
        user_segment="vip",
        agent=exploding_agent,
    )
    source_documents = await get_source_documents_handler(
        workspace_id="acme",
        source_key="doc-1",
        agent=exploding_agent,
    )

    assert missing_ack.trigger["error"] == "trigger not found"
    assert missing_resolve.trigger["error"] == "trigger not found"
    assert missing_close.trigger["error"] == "trigger not found"
    assert missing_request.request["error"] == "request not found"
    assert missing_retry.request["error"] == "request not found"
    assert missing_resume.request["error"] == "request not found"
    assert missing_replay.request["error"] == "request not found"
    assert missing_cancel.request["error"] == "request not found"

    assert scores.items == []
    assert export.status_code == 200
    assert export.media_type == "application/json"
    assert export.body == b"[]"
    assert triggers.items == []
    assert fallback_ack.trigger["status"] == "acknowledged"
    assert fallback_resolve.trigger["status"] == "resolved"
    assert fallback_close.trigger["status"] == "closed"
    assert force.request["source_key"] == "doc-1"
    assert force.request["status"] == "queued"
    assert requests.items == []
    assert request.request["error"] == "request not found"
    assert retry.request["error"] == "request not found"
    assert resume.request["error"] == "request not found"
    assert replay.request["error"] == "request not found"
    assert cancel.request["error"] == "request not found"
    assert summary.requests == {}
    assert summary.triggers == {}
    assert summary.source_documents == {}
    assert summary.policy == {}
    assert summary.coalescing == {}
    assert profiles.items == []
    assert source_documents.items == []


@pytest.mark.asyncio
async def test_feedback_handlers_capture_payloads_and_swallow_backend_errors():
    feedback_calls: list[dict[str, object]] = []
    outcome_calls: list[dict[str, object]] = []

    class StubFeedbackAPI:
        def record_feedback(self, **kwargs):
            feedback_calls.append(kwargs)
            raise RuntimeError("ignore me")

        def record_outcome(self, **kwargs):
            outcome_calls.append(kwargs)
            raise RuntimeError("ignore me")

    learning_store = SimpleNamespace(feedback=StubFeedbackAPI())

    feedback = await capture_feedback_handler(
        FeedbackRequest(
            run_id="run-1",
            kind="thumbs_down",
            rating=1,
            failure_types=["hallucination"],
            metadata={"ticket": "INC-1"},
        ),
        learning_store=learning_store,
    )
    outcome = await capture_outcome_handler(
        OutcomeRequest(
            trace_id="trace-1",
            kind="conversion",
            value=True,
            metadata={"channel": "chat"},
        ),
        learning_store=learning_store,
    )

    assert feedback_calls[0]["run_id"] == "run-1"
    assert feedback_calls[0]["trace_id"] is None
    assert feedback_calls[0]["failure_types"] == ["hallucination"]
    assert outcome_calls[0]["trace_id"] == "trace-1"
    assert outcome_calls[0]["value"] is True

    assert feedback.kind == "thumbs_down"
    assert feedback.trace_id == "unknown"
    assert feedback.metadata == {"ticket": "INC-1"}
    assert len(feedback.id) == 12

    assert outcome.kind == "conversion"
    assert outcome.trace_id == "trace-1"
    assert outcome.metadata == {"channel": "chat"}
    assert len(outcome.id) == 12


@pytest.mark.asyncio
async def test_campaign_handlers_cover_unavailable_not_found_and_internal_errors(monkeypatch):
    import logos_adk.serve_routes.routes.campaigns as campaigns_module

    class TeamWithoutCampaigns:
        pass

    class BrokenTeam:
        def list_campaign_states(self):
            raise RuntimeError("boom-list")

        def campaign_state(self, campaign_id):
            raise RuntimeError("boom-state")

        def list_campaign_events(self, campaign_id):
            raise RuntimeError("boom-events")

    class MissingTeam:
        def campaign_state(self, campaign_id):
            raise ValueError("missing")

        def list_campaign_events(self, campaign_id):
            raise ValueError("missing")

    monkeypatch.setattr(campaigns_module, "_get_team", lambda: None)
    with pytest.raises(HTTPException) as unavailable:
        await list_campaigns()
    assert unavailable.value.status_code == 404
    assert unavailable.value.detail == "Campaigns not available"

    monkeypatch.setattr(campaigns_module, "_get_team", lambda: TeamWithoutCampaigns())
    with pytest.raises(HTTPException) as unsupported:
        await list_campaigns()
    assert unsupported.value.status_code == 404
    assert unsupported.value.detail == "Campaigns not available"

    monkeypatch.setattr(campaigns_module, "_get_team", lambda: BrokenTeam())
    with pytest.raises(HTTPException) as list_error:
        await list_campaigns()
    with pytest.raises(HTTPException) as state_error:
        await get_campaign("cmp-1")
    with pytest.raises(HTTPException) as events_error:
        await list_campaign_events("cmp-1")
    assert list_error.value.status_code == 500
    assert list_error.value.detail == "boom-list"
    assert state_error.value.status_code == 500
    assert state_error.value.detail == "boom-state"
    assert events_error.value.status_code == 500
    assert events_error.value.detail == "boom-events"

    monkeypatch.setattr(campaigns_module, "_get_team", lambda: MissingTeam())
    with pytest.raises(HTTPException) as missing_campaign:
        await get_campaign("missing")
    with pytest.raises(HTTPException) as missing_events:
        await list_campaign_events("missing", status="queued")
    assert missing_campaign.value.status_code == 404
    assert missing_campaign.value.detail == "Campaign not found"
    assert missing_events.value.status_code == 404
    assert missing_events.value.detail == "Campaign not found"


@pytest.mark.asyncio
async def test_dataset_handler_covers_not_configured_feedback_fallback_and_exceptions():
    request = DatasetBuildRequest(workspace_id="acme", agent_name="bot", limit=5)

    no_store = await build_dataset_handler(request, learning_store=None)

    class FeedbackOnlyStore:
        def __init__(self):
            self.feedback = self

        def list_feedback(self, workspace_id=None, limit=None):
            return [
                SimpleNamespace(
                    id="fb-1",
                    kind="thumbs_up",
                    rating=1,
                    created_at="2024-01-01T00:00:00Z",
                )
            ]

    class BrokenDatasetStore:
        def build_dataset(self, **kwargs):
            raise RuntimeError("dataset boom")

    feedback_only = await build_dataset_handler(request, learning_store=FeedbackOnlyStore())
    broken = await build_dataset_handler(request, learning_store=BrokenDatasetStore())

    assert no_store.summary["error"] == "learning_store not configured"
    assert no_store.items == []
    assert feedback_only.summary["total_items"] == 1
    assert feedback_only.items[0]["id"] == "fb-1"
    assert broken.summary["error"] == "dataset boom"
    assert broken.items == []


@pytest.mark.asyncio
async def test_improvement_handlers_cover_defaults_success_and_exceptions():
    no_store_reward = await get_reward_scores_handler(workspace_id="acme", agent_name="bot")
    no_store_policy = await get_policy_handler(
        workspace_id="acme",
        task_type="support",
        user_segment="vip",
    )
    no_store_export = await export_training_handler(workspace_id="acme", agent_name="bot")
    no_store_lessons = await get_reflection_lessons_handler(
        workspace_id="acme",
        agent_name="bot",
    )

    class SuccessStore:
        def get_reward_scores(self, workspace_id=None, agent_name=None):
            return {"summary": {"count": 1}, "items": [{"run_id": "run-1", "score": 0.9}]}

        def get_policy(self, workspace_id=None, task_type=None, user_segment=None):
            return {"strategy": "conservative"}

        def export_training(self, workspace_id=None, agent_name=None):
            return {"kind": "dpo", "examples": 3}

        def get_reflection_lessons(self, workspace_id=None, agent_name=None):
            return {"items": [{"lesson": "be precise"}]}

    class BrokenStore:
        def get_reward_scores(self, workspace_id=None, agent_name=None):
            raise RuntimeError("boom")

        def get_policy(self, workspace_id=None, task_type=None, user_segment=None):
            raise RuntimeError("boom")

        def export_training(self, workspace_id=None, agent_name=None):
            raise RuntimeError("boom")

        def get_reflection_lessons(self, workspace_id=None, agent_name=None):
            raise RuntimeError("boom")

    success_reward = await get_reward_scores_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=SuccessStore(),
    )
    success_policy = await get_policy_handler(
        workspace_id="acme",
        task_type="support",
        user_segment="vip",
        learning_store=SuccessStore(),
    )
    success_export = await export_training_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=SuccessStore(),
    )
    success_lessons = await get_reflection_lessons_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=SuccessStore(),
    )
    broken_reward = await get_reward_scores_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=BrokenStore(),
    )
    broken_policy = await get_policy_handler(
        workspace_id="acme",
        task_type="support",
        user_segment="vip",
        learning_store=BrokenStore(),
    )
    broken_export = await export_training_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=BrokenStore(),
    )
    broken_lessons = await get_reflection_lessons_handler(
        workspace_id="acme",
        agent_name="bot",
        learning_store=BrokenStore(),
    )

    assert no_store_reward.summary["error"] == "learning_store not configured"
    assert no_store_policy.policy["error"] == "learning_store not configured"
    assert no_store_export.export["error"] == "learning_store not configured"
    assert no_store_lessons.lessons["error"] == "learning_store not configured"

    assert success_reward.summary["count"] == 1
    assert success_reward.items[0]["run_id"] == "run-1"
    assert success_policy.policy["strategy"] == "conservative"
    assert success_export.export["kind"] == "dpo"
    assert success_lessons.lessons["items"][0]["lesson"] == "be precise"

    assert broken_reward.summary == {}
    assert broken_reward.items == []
    assert broken_policy.policy == {}
    assert broken_export.export == {}
    assert broken_lessons.lessons == {}


@pytest.mark.asyncio
async def test_prompt_handlers_cover_not_configured_success_and_exception_fallbacks():
    create_request = PromptCreateRequest(name="bot", description="Base prompt", tags=["prod"])
    version_request = PromptVersionCreateRequest(
        content="system prompt",
        tags=["v2"],
        notes="note",
        created_by="ops",
    )
    promote_request = PromptPromoteRequest(version=2, workspace_id="acme", task_type="support")
    rollback_request = PromptRollbackRequest(workspace_id="acme", task_type="support")
    experiment_request = PromptExperimentCreateRequest(
        baseline_version=1,
        challenger_versions=[2],
        workspace_id="acme",
        task_type="support",
    )
    optimize_request = PromptOptimizeRequest(
        candidates=["a", "b"],
        workspace_id="acme",
        feedback_limit=10,
        tags=["opt"],
        golden_examples=[{"input": "x", "expected": "y"}],
    )
    recommendation_request = PromptRecommendationRequest(workspace_id="acme", limit=5)

    assert (await create_prompt_handler(create_request))["error"] == "prompt_registry not configured"
    assert (
        await create_prompt_version_handler("bot", version_request)
    )["error"] == "prompt_registry not configured"
    assert (await promote_prompt_handler("bot", promote_request))["error"] == "prompt_registry not configured"
    assert (await rollback_prompt_handler("bot", rollback_request))["error"] == "prompt_registry not configured"
    assert (await create_experiment_handler("bot", experiment_request)).experiment["error"] == (
        "prompt_registry not configured"
    )
    assert (await optimize_prompt_handler("bot", optimize_request)).created_version["error"] == (
        "prompt_registry not configured"
    )
    assert (await get_recommendations_handler(recommendation_request)).recommendation["error"] == (
        "prompt_registry not configured"
    )
    assert (await get_prompt_info_handler("bot")).prompt["error"] == "prompt_registry not configured"

    class SuccessRegistry:
        def create_prompt(self, **kwargs):
            return {"name": kwargs["name"], "tags": kwargs["tags"]}

        def create_version(self, **kwargs):
            return {"prompt_name": kwargs["prompt_name"], "content": kwargs["content"]}

        def promote(self, **kwargs):
            return {"status": "promoted", "version": kwargs["version"]}

        def rollback(self, **kwargs):
            return {"status": "rolled_back", "environment": kwargs["environment"]}

        def create_experiment(self, **kwargs):
            return {"id": "exp-1", "baseline_version": kwargs["baseline_version"]}

        def get_recommendations(self, *args, **kwargs):
            if args:
                return [{"prompt_name": args[0], "action": "review"}]
            return {"recommendations": [{"prompt_name": "bot", "action": "promote"}]}

        def get_prompt(self, prompt_name):
            return {"name": prompt_name}

        def list_versions(self, prompt_name):
            return [{"version": 1}]

        def list_deployments(self, prompt_name):
            return [{"environment": "prod"}]

    class SuccessOptimizer:
        def optimize(self, **kwargs):
            return {
                "version": {"prompt_name": kwargs["prompt_name"], "status": "optimized"},
                "scores": [{"candidate": "a", "score": 0.9}],
            }

    class BrokenRegistry:
        def create_prompt(self, **kwargs):
            raise RuntimeError("boom")

        def create_version(self, **kwargs):
            raise RuntimeError("boom")

        def promote(self, **kwargs):
            raise RuntimeError("boom")

        def rollback(self, **kwargs):
            raise RuntimeError("boom")

        def create_experiment(self, **kwargs):
            raise RuntimeError("boom")

        def get_recommendations(self, *args, **kwargs):
            raise RuntimeError("boom")

        def get_prompt(self, prompt_name):
            raise RuntimeError("boom")

        def list_versions(self, prompt_name):
            raise RuntimeError("boom")

        def list_deployments(self, prompt_name):
            raise RuntimeError("boom")

    class BrokenOptimizer:
        def optimize(self, **kwargs):
            raise RuntimeError("boom")

    created = await create_prompt_handler(create_request, prompt_registry=SuccessRegistry())
    versioned = await create_prompt_version_handler(
        "bot",
        version_request,
        prompt_registry=SuccessRegistry(),
    )
    promoted = await promote_prompt_handler(
        "bot",
        promote_request,
        prompt_registry=SuccessRegistry(),
    )
    rolled_back = await rollback_prompt_handler(
        "bot",
        rollback_request,
        prompt_registry=SuccessRegistry(),
    )
    experiment = await create_experiment_handler(
        "bot",
        experiment_request,
        prompt_registry=SuccessRegistry(),
    )
    optimized = await optimize_prompt_handler(
        "bot",
        optimize_request,
        prompt_registry=SuccessRegistry(),
        prompt_optimizer=SuccessOptimizer(),
    )
    recommendations = await get_recommendations_handler(
        recommendation_request,
        prompt_registry=SuccessRegistry(),
    )
    info = await get_prompt_info_handler("bot", prompt_registry=SuccessRegistry())

    create_fallback = await create_prompt_handler(create_request, prompt_registry=BrokenRegistry())
    version_fallback = await create_prompt_version_handler(
        "bot",
        version_request,
        prompt_registry=BrokenRegistry(),
    )
    promote_fallback = await promote_prompt_handler(
        "bot",
        promote_request,
        prompt_registry=BrokenRegistry(),
    )
    rollback_fallback = await rollback_prompt_handler(
        "bot",
        rollback_request,
        prompt_registry=BrokenRegistry(),
    )
    experiment_fallback = await create_experiment_handler(
        "bot",
        experiment_request,
        prompt_registry=BrokenRegistry(),
    )
    optimize_fallback = await optimize_prompt_handler(
        "bot",
        optimize_request,
        prompt_registry=BrokenRegistry(),
        prompt_optimizer=BrokenOptimizer(),
    )
    recommendations_fallback = await get_recommendations_handler(
        recommendation_request,
        prompt_registry=BrokenRegistry(),
    )
    info_fallback = await get_prompt_info_handler("bot", prompt_registry=BrokenRegistry())

    assert created["prompt"]["name"] == "bot"
    assert versioned["version"]["prompt_name"] == "bot"
    assert promoted["deployment"]["status"] == "promoted"
    assert rolled_back["deployment"]["status"] == "rolled_back"
    assert experiment.experiment["id"] == "exp-1"
    assert optimized.created_version["status"] == "optimized"
    assert optimized.scores[0]["candidate"] == "a"
    assert recommendations.recommendation["recommendations"][0]["action"] == "promote"
    assert info.prompt["name"] == "bot"
    assert info.versions[0]["version"] == 1
    assert info.deployments[0]["environment"] == "prod"
    assert info.recommendations[0]["prompt_name"] == "bot"

    assert create_fallback["prompt"]["status"] == "created"
    assert version_fallback["version"]["content"] == "system prompt"
    assert promote_fallback["deployment"]["status"] == "promoted"
    assert rollback_fallback["deployment"]["status"] == "rolled_back"
    assert experiment_fallback.experiment["status"] == "created"
    assert optimize_fallback.created_version["status"] == "optimized"
    assert optimize_fallback.scores == []
    assert recommendations_fallback.recommendation == {"recommendations": []}
    assert info_fallback.prompt == {"name": "bot"}
    assert info_fallback.versions == []
    assert info_fallback.deployments == []
    assert info_fallback.recommendations == []


@pytest.mark.asyncio
async def test_state_routes_cover_fallback_store_paths_and_missing_version():
    class FallbackStateStore:
        def __init__(self):
            self.saved = {}

        def history(self, namespace, session_id):
            if session_id == "s1":
                return [
                    {
                        "version": 1,
                        "created_at": "2024-01-01T00:00:00Z",
                        "state": {"session_id": "s1", "metadata": {"step": 1}},
                    },
                    {
                        "version": 2,
                        "saved_at": "2024-01-02T00:00:00Z",
                        "state": {"session_id": "s1", "metadata": {"step": 2}},
                    },
                ]
            return []

        def load(self, namespace, session_id):
            return SimpleNamespace(
                dict=lambda: {
                    "session_id": session_id,
                    "metadata": {"loaded": True, "namespace": namespace},
                }
            )

        def save(self, namespace, state):
            self.saved[(namespace, state.session_id)] = state

    store = FallbackStateStore()
    app = FastAPI()
    app.include_router(create_state_routes(lambda: store, lambda: "bot"))

    versions = await _request_state_app(app, "GET", "/state/versions?session_id=s1")
    exported = await _request_state_app(app, "GET", "/state/export?session_id=s1")
    imported = await _request_state_app(
        app,
        "POST",
        "/state/import",
        json={"state": {"messages": [], "metadata": {"copied": True}}, "session_id": "clone"},
    )
    rolled_back = await _request_state_app(
        app,
        "POST",
        "/state/rollback",
        json={"session_id": "s1", "version": 1},
    )
    missing_version = await _request_state_app(
        app,
        "POST",
        "/state/rollback",
        json={"session_id": "s1", "version": 99},
    )
    shards = await _request_state_app(app, "GET", "/state/shards")

    assert versions.status_code == 200
    assert versions.json()["items"] == [
        {
            "version": 1,
            "created_at": "2024-01-01T00:00:00Z",
            "agent_name": "bot",
            "session_key": "s1",
        },
        {
            "version": 2,
            "created_at": "2024-01-02T00:00:00Z",
            "agent_name": "bot",
            "session_key": "s1",
        },
    ]
    assert exported.status_code == 200
    assert exported.json()["state"]["metadata"]["loaded"] is True
    assert exported.json()["session_id"] == "s1"
    assert exported.json()["version"] is None

    assert imported.status_code == 200
    assert imported.json()["session_id"] == "clone"
    assert store.saved[("bot", "clone")].metadata == {"copied": True}

    assert rolled_back.status_code == 200
    assert rolled_back.json()["state"]["metadata"] == {"step": 1}
    assert rolled_back.json()["session_id"] == "s1"
    assert rolled_back.json()["version"] == 2

    assert missing_version.status_code == 404
    assert missing_version.json()["detail"] == "Version not found"

    assert shards.status_code == 200
    assert shards.json() == {"backend": "FallbackStateStore"}
