//! Event bridging — converts runtime events to NATS response types.

use nenjo::manifest::Manifest;
use nenjo_events::{
    AsyncOperationTranscriptEvent, ExecutionEventPayload, ExecutionTaskCompletedEvent,
    ExecutionTraceRoutineStep, ExecutionWorkflowStepEvent, Response, StepAgent, StreamEvent,
};
use serde::Serialize;
use tracing::{debug, trace};
use uuid::Uuid;

use nenjo_harness::preview::{PREVIEW_MAX_CHARS, summarize_preview, truncate_preview};

fn event_text_preview(value: &str) -> String {
    truncate_preview(value, PREVIEW_MAX_CHARS)
}

fn bounded_json_value(value: &serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::String(text) => serde_json::Value::String(event_text_preview(text)),
        serde_json::Value::Array(items) => {
            serde_json::Value::Array(items.iter().map(bounded_json_value).collect())
        }
        serde_json::Value::Object(object) => serde_json::Value::Object(
            object
                .iter()
                .map(|(key, value)| (key.clone(), bounded_json_value(value)))
                .collect(),
        ),
        _ => value.clone(),
    }
}

fn parsed_tool_output(output: &str) -> Option<serde_json::Value> {
    let value = serde_json::from_str::<serde_json::Value>(output).ok()?;
    let serialized = serde_json::to_vec(&value).ok()?;
    (serialized.len() <= PREVIEW_MAX_CHARS).then_some(value)
}

fn with_parsed_tool_output(mut payload: serde_json::Value, output: &str) -> serde_json::Value {
    let Some(parsed) = parsed_tool_output(output) else {
        return payload;
    };
    let Some(payload_object) = payload.as_object_mut() else {
        return payload;
    };
    payload_object.insert("output".to_string(), parsed);
    payload
}

fn merge_tool_payload(
    mut payload: serde_json::Value,
    metadata: Option<&serde_json::Value>,
) -> serde_json::Value {
    let Some(metadata) = metadata.and_then(serde_json::Value::as_object) else {
        return payload;
    };
    let Some(payload_object) = payload.as_object_mut() else {
        return payload;
    };

    for (key, value) in metadata {
        payload_object.insert(key.clone(), bounded_json_value(value));
    }

    payload
}

// ---------------------------------------------------------------------------
// Typed step-event data payloads
// ---------------------------------------------------------------------------

#[derive(Serialize)]
pub struct StepCompletedData {
    pub step_slug: String,
    pub step_run_id: Uuid,
    pub passed: bool,
    pub input_tokens: u64,
    pub output_tokens: u64,
}

#[derive(Serialize)]
pub struct StepFailedData {
    pub step_slug: String,
    pub step_run_id: Uuid,
    pub error: &'static str,
}

#[derive(Serialize)]
pub struct CronCycleStartedData {
    pub cycle: u32,
}

#[derive(Serialize)]
pub struct CronCycleCompletedData {
    pub cycle: u32,
    pub passed: bool,
}

/// Convert a TurnEvent to StreamEvents for the frontend.
pub fn turn_event_to_stream_events(
    event: &nenjo::TurnEvent,
    agent_name: &str,
    run_id: &str,
    session_id: Uuid,
) -> Vec<StreamEvent> {
    let stream_events = match event {
        nenjo::TurnEvent::ModelRequestStarted {
            request_id,
            parent_call_id,
            provider,
            model,
        } => vec![StreamEvent::ModelRequestStarted {
            run_id: run_id.to_string(),
            request_id: request_id.clone(),
            parent_call_id: parent_call_id.clone(),
            provider: provider.clone(),
            model: Some(model.clone()),
        }],
        nenjo::TurnEvent::AssistantTextDelta { request_id, delta } => {
            vec![StreamEvent::AssistantTextDelta {
                run_id: run_id.to_string(),
                request_id: request_id.clone(),
                payload: Some(serde_json::json!({
                    "delta": delta,
                })),
                encrypted_payload: None,
            }]
        }
        nenjo::TurnEvent::AssistantResponse { message, status } => {
            vec![StreamEvent::AssistantResponse {
                run_id: run_id.to_string(),
                payload: Some(serde_json::json!({
                    "message": message,
                    "status": status,
                })),
                encrypted_payload: None,
            }]
        }
        nenjo::TurnEvent::ModelRequestCompleted {
            request_id,
            parent_call_id,
        } => {
            vec![StreamEvent::ModelRequestCompleted {
                run_id: run_id.to_string(),
                request_id: request_id.clone(),
                parent_call_id: parent_call_id.clone(),
            }]
        }
        nenjo::TurnEvent::AbilityStarted {
            call_id,
            ability_tool_name,
            ability_name,
            task_input,
            ..
        } => vec![StreamEvent::AsyncOperationEvent {
            operation_id: call_id.clone(),
            kind: "ability".to_string(),
            label: ability_name.clone(),
            status: "running".to_string(),
            signal: "started".to_string(),
            model_visible: true,
            parent_operation_id: None,
            parent_tool_name: Some(ability_tool_name.clone()),
            summary: Some(event_text_preview(task_input)),
            payload: Some(serde_json::json!({
                "ability": ability_name,
                "tool_name": ability_tool_name,
                "task_preview": event_text_preview(task_input),
            })),
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::ToolCallStart {
            batch_id,
            parent_tool_name,
            calls,
        } => calls
            .iter()
            .map(|call| StreamEvent::ToolCallStarted {
                run_id: run_id.to_string(),
                batch_id: batch_id.clone(),
                call_id: call
                    .tool_call_id
                    .clone()
                    .unwrap_or_else(|| format!("{}:{}", call.tool_name, call.tool_args.len())),
                parent_call_id: parent_tool_name.clone(),
                tool_name: call.tool_name.clone(),
                payload: Some(merge_tool_payload(
                    serde_json::json!({
                        "tool_name": call.tool_name,
                        "tool_args": event_text_preview(&call.tool_args),
                        "text_preview": call.text_preview.as_deref().map(event_text_preview),
                    }),
                    call.metadata.as_ref(),
                )),
                encrypted_payload: None,
            })
            .collect(),
        nenjo::TurnEvent::ToolCallEnd {
            batch_id,
            parent_tool_name,
            tool_call_id,
            tool_name,
            tool_args,
            result,
            metadata,
        } => vec![StreamEvent::ToolCallCompleted {
            run_id: run_id.to_string(),
            batch_id: batch_id.clone(),
            call_id: tool_call_id
                .clone()
                .unwrap_or_else(|| format!("{}:{}", tool_name, tool_args.len())),
            parent_call_id: parent_tool_name.clone(),
            success: result.success,
            payload: Some(merge_tool_payload(
                with_parsed_tool_output(
                    serde_json::json!({
                        "tool_name": tool_name,
                        "tool_args": event_text_preview(tool_args),
                        "output_preview": truncate_preview(&result.output, PREVIEW_MAX_CHARS),
                        "error_preview": result.error.as_deref().and_then(summarize_preview),
                    }),
                    &result.output,
                ),
                metadata.as_ref(),
            )),
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::HookStarted {
            hook,
            hook_event,
            hook_type,
            source,
        } => vec![StreamEvent::HookStarted {
            agent: agent_name.to_string(),
            hook: hook.clone(),
            hook_event: hook_event.clone(),
            hook_type: hook_type.clone(),
            source: source.clone(),
            payload: None,
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::HookActivated { .. } => Vec::new(),
        nenjo::TurnEvent::HookCompleted {
            hook,
            hook_event,
            hook_type,
            source,
            success,
            blocked,
            exit_code,
            output,
            error,
            reason,
        } => vec![StreamEvent::HookCompleted {
            agent: agent_name.to_string(),
            hook: hook.clone(),
            hook_event: hook_event.clone(),
            hook_type: hook_type.clone(),
            source: source.clone(),
            success: *success,
            blocked: *blocked,
            payload: Some(serde_json::json!({
                "exit_code": exit_code,
                "output_preview": summarize_preview(output),
                "error_preview": error.as_deref().and_then(summarize_preview),
                "reason": reason,
            })),
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::AbilityCompleted {
            call_id,
            ability_tool_name,
            ability_name,
            success,
            final_output,
        } => vec![StreamEvent::AsyncOperationEvent {
            operation_id: call_id.clone(),
            kind: "ability".to_string(),
            label: ability_name.clone(),
            status: if *success {
                "completed".to_string()
            } else {
                "failed".to_string()
            },
            signal: if *success {
                "completed".to_string()
            } else {
                "failed".to_string()
            },
            model_visible: true,
            parent_operation_id: None,
            parent_tool_name: Some(ability_tool_name.clone()),
            summary: Some(event_text_preview(final_output)),
            payload: Some(serde_json::json!({
                "ability": ability_name,
                "tool_name": ability_tool_name,
                "result_preview": event_text_preview(final_output),
                "output_preview": event_text_preview(final_output),
            })),
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::SubAgentEvent { .. } => Vec::new(),
        nenjo::TurnEvent::SubAgentTranscript { .. } => Vec::new(),
        nenjo::TurnEvent::AsyncOperationEvent {
            operation_id,
            kind,
            label,
            parent_operation_id,
            parent_tool_name,
            status,
            signal,
            summary,
            payload,
            model_visible,
        } => vec![StreamEvent::AsyncOperationEvent {
            operation_id: operation_id.clone(),
            kind: kind.clone(),
            label: label.clone(),
            status: status.clone(),
            signal: signal.clone(),
            model_visible: *model_visible,
            parent_operation_id: parent_operation_id.clone(),
            parent_tool_name: parent_tool_name.clone(),
            summary: summary.clone(),
            payload: payload.clone(),
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::AsyncOperationTranscript {
            operation_id,
            kind,
            label,
            event,
        } => vec![StreamEvent::AsyncOperationTranscript {
            operation_id: operation_id.clone(),
            kind: kind.clone(),
            label: label.clone(),
            event: AsyncOperationTranscriptEvent {
                kind: event.kind().to_string(),
                summary: event.summary().to_string(),
                tool: event.tool_name().map(ToOwned::to_owned),
                success: event.success(),
            },
            payload: None,
            encrypted_payload: None,
        }],
        nenjo::TurnEvent::MessageCompacted {
            messages_before,
            messages_after,
        } => vec![StreamEvent::MessageCompacted {
            messages_before: *messages_before,
            messages_after: *messages_after,
        }],
        nenjo::TurnEvent::TranscriptMessage { .. } => Vec::new(),
        nenjo::TurnEvent::Paused => vec![StreamEvent::Paused],
        nenjo::TurnEvent::Resumed => vec![StreamEvent::Resumed],
        nenjo::TurnEvent::Done { output } => vec![
            StreamEvent::RunCompleted {
                run_id: run_id.to_string(),
                session_id: session_id.to_string(),
            },
            StreamEvent::Done {
                payload: Some(serde_json::Value::String(event_text_preview(&output.text))),
                encrypted_payload: None,
                total_input_tokens: output.input_tokens,
                total_output_tokens: output.output_tokens,
                project: None,
                agent: None,
                session_id: None,
            },
        ],
    };

    for stream_event in &stream_events {
        trace!(
            turn_event = %summarize_turn_event(event),
            stream_event = %summarize_stream_event(stream_event),
            agent = agent_name,
            "Bridged turn event to pre-codec stream event"
        );
    }
    if stream_events.is_empty() {
        debug!(
            turn_event = %summarize_turn_event(event),
            agent = agent_name,
            "Turn event did not produce a stream event"
        );
    }

    stream_events
}

pub fn summarize_turn_event(event: &nenjo::TurnEvent) -> String {
    match event {
        nenjo::TurnEvent::ModelRequestStarted {
            request_id,
            parent_call_id,
            model,
            ..
        } => format!(
            "model_request_started(request={request_id}, parent={}, model={model})",
            parent_call_id.as_deref().unwrap_or("-")
        ),
        nenjo::TurnEvent::AssistantTextDelta { request_id, delta } => {
            format!(
                "assistant_text_delta(request={request_id}, len={})",
                delta.len()
            )
        }
        nenjo::TurnEvent::AssistantResponse { message, status } => {
            format!("assistant_response(status={status}, len={})", message.len())
        }
        nenjo::TurnEvent::ModelRequestCompleted {
            request_id,
            parent_call_id,
        } => format!(
            "model_request_completed(request={request_id}, parent={})",
            parent_call_id.as_deref().unwrap_or("-")
        ),
        nenjo::TurnEvent::AbilityStarted {
            call_id,
            ability_tool_name,
            ability_name,
            task_input,
            caller_history,
        } => format!(
            "ability_started(call={call_id}, tool={ability_tool_name}, ability={ability_name}, task_len={}, caller_messages={})",
            task_input.len(),
            caller_history.len()
        ),
        nenjo::TurnEvent::ToolCallStart {
            batch_id,
            parent_tool_name,
            calls,
        } => format!(
            "tool_call_start(batch={batch_id}, parent={}, tools=[{}], count={})",
            parent_tool_name.as_deref().unwrap_or("-"),
            calls
                .iter()
                .map(|call| call.tool_name.as_str())
                .collect::<Vec<_>>()
                .join(", "),
            calls.len()
        ),
        nenjo::TurnEvent::ToolCallEnd {
            batch_id,
            parent_tool_name,
            tool_name,
            tool_args,
            result,
            ..
        } => format!(
            "tool_call_end(batch={batch_id}, parent={}, tool={tool_name}, args_len={}, success={}, output_len={}, error={})",
            parent_tool_name.as_deref().unwrap_or("-"),
            tool_args.len(),
            result.success,
            result.output.len(),
            result
                .error
                .as_deref()
                .map(|err| truncate_preview(err, 80))
                .unwrap_or_else(|| "-".to_string())
        ),
        nenjo::TurnEvent::AbilityCompleted {
            call_id,
            ability_tool_name,
            ability_name,
            success,
            final_output,
        } => format!(
            "ability_completed(call={call_id}, tool={ability_tool_name}, ability={ability_name}, success={success}, output_len={})",
            final_output.len()
        ),
        nenjo::TurnEvent::HookStarted {
            hook,
            hook_event,
            hook_type,
            source,
        } => format!(
            "hook_started(hook={hook}, event={hook_event}, type={hook_type}, source={source})"
        ),
        nenjo::TurnEvent::HookActivated {
            hook,
            hook_event,
            hook_type,
            source,
        } => format!(
            "hook_signal(hook={hook}, event={hook_event}, type={hook_type}, source={source})"
        ),
        nenjo::TurnEvent::HookCompleted {
            hook,
            hook_event,
            hook_type,
            source,
            success,
            blocked,
            exit_code,
            ..
        } => format!(
            "hook_completed(hook={hook}, event={hook_event}, type={hook_type}, source={source}, success={success}, blocked={blocked}, exit_code={})",
            exit_code
                .map(|code| code.to_string())
                .unwrap_or_else(|| "-".to_string())
        ),
        nenjo::TurnEvent::SubAgentEvent {
            slug,
            agent_name,
            kind,
            summary,
            ..
        } => format!(
            "sub_agent_event(slug={slug}, agent={agent_name}, kind={kind}, summary_len={})",
            summary.len()
        ),
        nenjo::TurnEvent::SubAgentTranscript {
            slug,
            agent_name,
            event,
        } => format!(
            "sub_agent_transcript(slug={slug}, agent={agent_name}, kind={}, summary_len={})",
            event.kind(),
            event.summary().len()
        ),
        nenjo::TurnEvent::AsyncOperationEvent {
            operation_id,
            kind,
            status,
            signal,
            ..
        } => format!(
            "async_operation_event(id={operation_id}, kind={kind}, signal={signal}, status={status})"
        ),
        nenjo::TurnEvent::AsyncOperationTranscript {
            operation_id,
            kind,
            event,
            ..
        } => format!(
            "async_operation_transcript(id={operation_id}, kind={kind}, event={}, summary_len={})",
            event.kind(),
            event.summary().len()
        ),
        nenjo::TurnEvent::MessageCompacted {
            messages_before,
            messages_after,
        } => format!("message_compacted({messages_before}->{messages_after})"),
        nenjo::TurnEvent::TranscriptMessage { message } => format!(
            "transcript_message(role={}, content_len={})",
            message.role,
            message.content.len()
        ),
        nenjo::TurnEvent::Paused => "paused".to_string(),
        nenjo::TurnEvent::Resumed => "resumed".to_string(),
        nenjo::TurnEvent::Done { output } => format!(
            "done(text_len={}, input_tokens={}, output_tokens={}, tool_calls={}, messages={})",
            output.text.len(),
            output.input_tokens,
            output.output_tokens,
            output.tool_calls,
            output.messages.len()
        ),
    }
}

pub fn summarize_stream_event(event: &StreamEvent) -> String {
    match event {
        StreamEvent::RunStarted {
            run_id, session_id, ..
        } => format!("run_started(run={run_id}, session={session_id})"),
        StreamEvent::RunCompleted { run_id, .. } => format!("run_completed(run={run_id})"),
        StreamEvent::RunFailed { run_id, .. } => format!("run_failed(run={run_id})"),
        StreamEvent::RunCancelled { run_id, .. } => format!("run_cancelled(run={run_id})"),
        StreamEvent::ModelRequestStarted {
            run_id,
            request_id,
            model,
            ..
        } => format!(
            "model_request_started(run={run_id}, request={request_id}, model={})",
            model.as_deref().unwrap_or("-")
        ),
        StreamEvent::AssistantTextDelta {
            run_id,
            request_id,
            payload,
            encrypted_payload,
        } => format!(
            "assistant_text_delta(run={run_id}, request={request_id}, payload={}, encrypted={})",
            payload.is_some(),
            encrypted_payload.is_some()
        ),
        StreamEvent::AssistantResponse {
            run_id,
            payload,
            encrypted_payload,
        } => format!(
            "assistant_response(run={run_id}, payload={}, encrypted={})",
            payload.is_some(),
            encrypted_payload.is_some()
        ),
        StreamEvent::ModelRequestCompleted {
            run_id, request_id, ..
        } => {
            format!("model_request_completed(run={run_id}, request={request_id})")
        }
        StreamEvent::ToolCallStarted {
            run_id,
            batch_id,
            call_id,
            tool_name,
            ..
        } => format!(
            "tool_call_started(run={run_id}, batch={batch_id}, call={call_id}, tool={tool_name})"
        ),
        StreamEvent::ToolOutputDelta {
            run_id,
            call_id,
            stream,
            payload,
            encrypted_payload,
        } => format!(
            "tool_output_delta(run={run_id}, call={call_id}, stream={stream}, payload={}, encrypted={})",
            payload.is_some(),
            encrypted_payload.is_some()
        ),
        StreamEvent::ToolCallCompleted {
            run_id,
            batch_id,
            call_id,
            parent_call_id,
            success,
            ..
        } => format!(
            "tool_call_completed(run={run_id}, batch={batch_id}, call={call_id}, parent={}, success={success})",
            parent_call_id.as_deref().unwrap_or("-")
        ),
        StreamEvent::HookStarted {
            agent,
            hook,
            hook_event,
            hook_type,
            source,
            ..
        } => format!(
            "hook_started(agent={agent}, hook={hook}, event={hook_event}, type={hook_type}, source={source})"
        ),
        StreamEvent::HookCompleted {
            agent,
            hook,
            hook_event,
            hook_type,
            source,
            success,
            blocked,
            ..
        } => format!(
            "hook_completed(agent={agent}, hook={hook}, event={hook_event}, type={hook_type}, source={source}, success={success}, blocked={blocked})"
        ),
        StreamEvent::AsyncOperationEvent {
            operation_id,
            kind,
            signal,
            status,
            ..
        } => format!(
            "async_operation_event(id={operation_id}, kind={kind}, signal={signal}, status={status})"
        ),
        StreamEvent::AsyncOperationTranscript {
            operation_id,
            kind,
            event,
            ..
        } => format!(
            "async_operation_transcript(id={operation_id}, kind={kind}, event={})",
            event.kind
        ),
        StreamEvent::Error { message, .. } => {
            format!(
                "error(message={:?}, len={})",
                truncate_preview(message, 80),
                message.len()
            )
        }
        StreamEvent::Done {
            payload,
            encrypted_payload,
            total_input_tokens,
            total_output_tokens,
            project,
            agent,
            session_id,
        } => format!(
            "done(payload={}, encrypted={}, input_tokens={}, output_tokens={}, project={}, agent={}, session_id={})",
            if payload.is_some() { "yes" } else { "no" },
            encrypted_payload.is_some(),
            total_input_tokens,
            total_output_tokens,
            project.as_deref().unwrap_or("-"),
            agent.as_deref().unwrap_or("-"),
            session_id
                .map(|id| id.to_string())
                .unwrap_or_else(|| "-".to_string())
        ),
        StreamEvent::DomainEntered {
            session_id,
            domain_name,
        } => format!("domain_entered(name={domain_name}, session={session_id})"),
        StreamEvent::DomainExited {
            session_id,
            artifact_id,
            document_id,
        } => format!(
            "domain_exited(session={}, artifact_id={}, document_id={})",
            session_id,
            artifact_id
                .map(|id| id.to_string())
                .unwrap_or_else(|| "-".to_string()),
            document_id
                .map(|id| id.to_string())
                .unwrap_or_else(|| "-".to_string())
        ),
        StreamEvent::MessageCompacted {
            messages_before,
            messages_after,
        } => format!("message_compacted({messages_before}->{messages_after})"),
        StreamEvent::Paused => "paused".to_string(),
        StreamEvent::Resumed => "resumed".to_string(),
    }
}

/// Resolve `StepAgent` from an optional agent ID using the manifest.
fn resolve_agent(manifest: &Manifest, agent_id: Option<Uuid>) -> Option<StepAgent> {
    agent_id.map(|aid| {
        let a = manifest
            .agents
            .iter()
            .find(|a| crate::resource_resolver::stable_resource_id("agent", &a.slug) == aid);
        StepAgent {
            agent: a
                .map(|a| a.slug.to_string())
                .unwrap_or_else(|| aid.to_string()),
            agent_name: a.map(|a| a.name.clone()),
            agent_color: a.and_then(|a| a.color.clone()),
        }
    })
}

#[derive(Debug, Clone)]
pub struct RoutineStepRef {
    pub step_slug: String,
    pub step_run_id: Uuid,
}

#[derive(Debug, Clone)]
pub struct TaskTurnEventContext {
    pub execution_run_id: Uuid,
    pub task_id: Option<Uuid>,
    pub agent: Option<StepAgent>,
    pub routine_step: Option<RoutineStepRef>,
    pub agent_duration_ms: Option<u64>,
    pub emit_done: bool,
    pub summarize_outputs: bool,
}

#[derive(Debug, Clone)]
pub struct ExecutionAgentTraceContext {
    pub execution_run_id: Uuid,
    pub task_id: Option<Uuid>,
    pub agent_name: String,
    pub trace_run_id: String,
    pub trace_session_id: Uuid,
    pub routine_step: Option<ExecutionTraceRoutineStep>,
}

#[derive(Debug, Clone)]
pub struct ExecutionWorkflowStepEventContext {
    pub execution_run_id: Uuid,
    pub task_id: Option<Uuid>,
    pub agent: Option<StepAgent>,
}

pub fn execution_workflow_step_response(
    context: &ExecutionWorkflowStepEventContext,
    event_type: impl Into<String>,
    step_name: impl Into<String>,
    step_type: impl Into<String>,
    duration_ms: Option<u64>,
    data: serde_json::Value,
    payload: Option<serde_json::Value>,
) -> Response {
    Response::ExecutionEvent {
        execution_run_id: context.execution_run_id.to_string(),
        task_id: context.task_id.map(|id| id.to_string()),
        event: ExecutionEventPayload::WorkflowStep(ExecutionWorkflowStepEvent {
            event_type: event_type.into(),
            step_name: step_name.into(),
            step_type: step_type.into(),
            duration_ms,
            data,
            payload,
            encrypted_payload: None,
            agent: context.agent.clone(),
        }),
    }
}

pub fn execution_task_completed_response(
    execution_run_id: Uuid,
    task_id: Option<Uuid>,
    success: bool,
    error: Option<String>,
    merge_error: Option<String>,
    total_input_tokens: u64,
    total_output_tokens: u64,
) -> Response {
    Response::ExecutionEvent {
        execution_run_id: execution_run_id.to_string(),
        task_id: task_id.map(|id| id.to_string()),
        event: ExecutionEventPayload::TaskCompleted(ExecutionTaskCompletedEvent {
            success,
            error,
            merge_error,
            total_input_tokens,
            total_output_tokens,
        }),
    }
}

fn task_data(
    routine_step: Option<RoutineStepRef>,
    mut data: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
    if let Some(routine_step) = routine_step {
        data.insert(
            "step_slug".to_string(),
            serde_json::Value::String(routine_step.step_slug),
        );
        data.insert(
            "step_run_id".to_string(),
            serde_json::Value::String(routine_step.step_run_id.to_string()),
        );
    }
    serde_json::Value::Object(data)
}

fn is_agent_trace_stream_event(event: &StreamEvent) -> bool {
    !matches!(event, StreamEvent::Done { .. })
}

/// Convert a TurnEvent into canonical execution trace responses.
pub fn turn_event_to_agent_trace_responses(
    event: &nenjo::TurnEvent,
    context: &ExecutionAgentTraceContext,
) -> Vec<Response> {
    turn_event_to_stream_events(
        event,
        &context.agent_name,
        &context.trace_run_id,
        context.trace_session_id,
    )
    .into_iter()
    .filter(is_agent_trace_stream_event)
    .map(|payload| Response::ExecutionEvent {
        execution_run_id: context.execution_run_id.to_string(),
        task_id: context.task_id.map(|id| id.to_string()),
        event: ExecutionEventPayload::AgentTrace {
            routine_step: context.routine_step.clone(),
            payload,
        },
    })
    .collect()
}

fn task_output_preview(output: &str, summarize: bool) -> String {
    let preview = if summarize {
        summarize_preview(output).unwrap_or_else(|| output.to_string())
    } else {
        output.to_string()
    };
    event_text_preview(&preview)
}

fn scoped_child_event_type(
    context: &TaskTurnEventContext,
    child_type: &str,
    direct_step_event_type: &str,
) -> String {
    if context.routine_step.is_some() {
        format!(
            "{child_type}_{}",
            direct_step_event_type.trim_start_matches("step_")
        )
    } else {
        direct_step_event_type.to_string()
    }
}

pub fn turn_event_to_workflow_step_response(
    event: &nenjo::TurnEvent,
    context: &TaskTurnEventContext,
) -> Option<Response> {
    let workflow_context = ExecutionWorkflowStepEventContext {
        execution_run_id: context.execution_run_id,
        task_id: context.task_id,
        agent: context.agent.clone(),
    };

    match event {
        nenjo::TurnEvent::ToolCallStart {
            batch_id,
            parent_tool_name,
            calls,
        } => Some(execution_workflow_step_response(
            &workflow_context,
            scoped_child_event_type(context, "tool", "step_started"),
            calls
                .first()
                .map(|c| c.tool_name.clone())
                .unwrap_or_else(|| "tool_call".to_string()),
            "tool",
            None,
            task_data(
                context.routine_step.clone(),
                serde_json::Map::from_iter([
                    (
                        "batch_id".to_string(),
                        serde_json::Value::String(batch_id.clone()),
                    ),
                    (
                        "parent_tool_name".to_string(),
                        serde_json::to_value(parent_tool_name).unwrap_or_default(),
                    ),
                    (
                        "parent_call_id".to_string(),
                        serde_json::to_value(parent_tool_name).unwrap_or_default(),
                    ),
                    (
                        "tool_call_ids".to_string(),
                        serde_json::to_value(
                            calls
                                .iter()
                                .map(|c| c.tool_call_id.clone())
                                .collect::<Vec<_>>(),
                        )
                        .unwrap_or_default(),
                    ),
                    (
                        "tool_names".to_string(),
                        serde_json::to_value(
                            calls
                                .iter()
                                .map(|c| c.tool_name.clone())
                                .collect::<Vec<_>>(),
                        )
                        .unwrap_or_default(),
                    ),
                    (
                        "tool_args".to_string(),
                        serde_json::to_value(
                            calls
                                .iter()
                                .map(|c| event_text_preview(&c.tool_args))
                                .collect::<Vec<_>>(),
                        )
                        .unwrap_or_default(),
                    ),
                    (
                        "tool_metadata".to_string(),
                        serde_json::Value::Array(
                            calls
                                .iter()
                                .map(|c| {
                                    c.metadata
                                        .as_ref()
                                        .map(bounded_json_value)
                                        .unwrap_or(serde_json::Value::Null)
                                })
                                .collect(),
                        ),
                    ),
                ]),
            ),
            calls.first().and_then(|c| {
                let payload = c
                    .text_preview
                    .as_ref()
                    .map(|preview| serde_json::json!({ "text_preview": preview }))
                    .unwrap_or_else(|| serde_json::json!({}));
                let merged = merge_tool_payload(payload, c.metadata.as_ref());
                (!merged.as_object().is_some_and(serde_json::Map::is_empty)).then_some(merged)
            }),
        )),
        nenjo::TurnEvent::ToolCallEnd {
            batch_id,
            parent_tool_name,
            tool_call_id,
            tool_name,
            tool_args,
            result,
            metadata,
        } => Some(execution_workflow_step_response(
            &workflow_context,
            if result.success {
                scoped_child_event_type(context, "tool", "step_completed")
            } else {
                scoped_child_event_type(context, "tool", "step_failed")
            },
            tool_name.clone(),
            "tool",
            None,
            task_data(
                context.routine_step.clone(),
                serde_json::Map::from_iter([
                    (
                        "batch_id".to_string(),
                        serde_json::Value::String(batch_id.clone()),
                    ),
                    (
                        "parent_tool_name".to_string(),
                        serde_json::to_value(parent_tool_name).unwrap_or_default(),
                    ),
                    (
                        "parent_call_id".to_string(),
                        serde_json::to_value(parent_tool_name).unwrap_or_default(),
                    ),
                    (
                        "tool_call_id".to_string(),
                        serde_json::to_value(tool_call_id).unwrap_or_default(),
                    ),
                    (
                        "tool_args".to_string(),
                        serde_json::Value::String(event_text_preview(tool_args)),
                    ),
                    (
                        "success".to_string(),
                        serde_json::Value::Bool(result.success),
                    ),
                    (
                        "error".to_string(),
                        if result.error.is_some() {
                            serde_json::Value::String("Tool execution failed".to_string())
                        } else {
                            serde_json::Value::Null
                        },
                    ),
                    (
                        "tool_metadata".to_string(),
                        metadata
                            .as_ref()
                            .map(bounded_json_value)
                            .unwrap_or(serde_json::Value::Null),
                    ),
                ]),
            ),
            Some(merge_tool_payload(
                with_parsed_tool_output(
                    serde_json::json!({
                        "output_preview": task_output_preview(&result.output, context.summarize_outputs),
                        "error": result.error.as_deref().map(event_text_preview),
                    }),
                    &result.output,
                ),
                metadata.as_ref(),
            )),
        )),
        nenjo::TurnEvent::AbilityStarted {
            call_id,
            ability_name,
            task_input,
            ..
        } => Some(execution_workflow_step_response(
            &workflow_context,
            scoped_child_event_type(context, "ability", "step_started"),
            ability_name.clone(),
            "ability",
            None,
            task_data(
                context.routine_step.clone(),
                serde_json::Map::from_iter([(
                    "call_id".to_string(),
                    serde_json::Value::String(call_id.clone()),
                )]),
            ),
            Some(serde_json::json!({
                "task_preview": event_text_preview(task_input),
            })),
        )),
        nenjo::TurnEvent::AbilityCompleted {
            call_id,
            ability_name,
            success,
            final_output,
            ..
        } => Some(execution_workflow_step_response(
            &workflow_context,
            if *success {
                scoped_child_event_type(context, "ability", "step_completed")
            } else {
                scoped_child_event_type(context, "ability", "step_failed")
            },
            ability_name.clone(),
            "ability",
            None,
            task_data(
                context.routine_step.clone(),
                serde_json::Map::from_iter([
                    ("success".to_string(), serde_json::Value::Bool(*success)),
                    (
                        "call_id".to_string(),
                        serde_json::Value::String(call_id.clone()),
                    ),
                ]),
            ),
            Some(serde_json::json!({
                "output_preview": event_text_preview(final_output),
            })),
        )),
        nenjo::TurnEvent::SubAgentEvent { .. } => None,
        nenjo::TurnEvent::AsyncOperationEvent {
            operation_id,
            kind,
            label,
            status,
            signal,
            summary,
            ..
        } => Some(execution_workflow_step_response(
            &workflow_context,
            format!("async_operation_{signal}"),
            label.clone(),
            kind.clone(),
            None,
            task_data(
                context.routine_step.clone(),
                serde_json::Map::from_iter([
                    (
                        "operation_id".to_string(),
                        serde_json::Value::String(operation_id.clone()),
                    ),
                    (
                        "status".to_string(),
                        serde_json::Value::String(status.clone()),
                    ),
                    (
                        "summary".to_string(),
                        serde_json::to_value(summary).unwrap_or_default(),
                    ),
                ]),
            ),
            None,
        )),
        nenjo::TurnEvent::HookActivated { .. }
        | nenjo::TurnEvent::HookStarted { .. }
        | nenjo::TurnEvent::HookCompleted { .. }
        | nenjo::TurnEvent::ModelRequestStarted { .. }
        | nenjo::TurnEvent::AssistantTextDelta { .. }
        | nenjo::TurnEvent::AssistantResponse { .. }
        | nenjo::TurnEvent::ModelRequestCompleted { .. } => None,
        nenjo::TurnEvent::Done { output } if context.emit_done => {
            Some(execution_workflow_step_response(
                &workflow_context,
                "step_completed",
                "agent_response",
                "agent",
                context.agent_duration_ms,
                serde_json::json!({
                "input_tokens": output.input_tokens,
                "output_tokens": output.output_tokens,
                }),
                Some(serde_json::json!({
                "output_preview": event_text_preview(&output.text),
                })),
            ))
        }
        nenjo::TurnEvent::Done { .. } => None,
        nenjo::TurnEvent::SubAgentTranscript { .. } => None,
        nenjo::TurnEvent::AsyncOperationTranscript { .. } => None,
        nenjo::TurnEvent::TranscriptMessage { .. } => None,
        nenjo::TurnEvent::MessageCompacted { .. } => None,
        nenjo::TurnEvent::Paused | nenjo::TurnEvent::Resumed => None,
    }
}

/// Convert a RoutineEvent to NATS responses.
pub fn routine_event_to_responses(
    event: &nenjo::RoutineEvent,
    execution_run_id: Uuid,
    task_id: Option<Uuid>,
    current_agent_id: Option<Uuid>,
    manifest: &Manifest,
) -> Vec<Response> {
    let workflow_context = ExecutionWorkflowStepEventContext {
        execution_run_id,
        task_id,
        agent: None,
    };

    match event {
        nenjo::RoutineEvent::StepStarted {
            step_slug,
            step_run_id,
            step_name,
            step_type,
            ..
        } => vec![execution_workflow_step_response(
            &workflow_context,
            "step_started",
            step_name.clone(),
            step_type.clone(),
            None,
            serde_json::json!({ "step_slug": step_slug, "step_run_id": step_run_id }),
            None,
        )],
        nenjo::RoutineEvent::StepCompleted {
            step_slug,
            step_run_id,
            result,
            duration_ms,
            ..
        } => {
            let data = StepCompletedData {
                step_slug: step_slug.to_string(),
                step_run_id: *step_run_id,
                passed: result.passed,
                input_tokens: result.input_tokens,
                output_tokens: result.output_tokens,
            };

            vec![execution_workflow_step_response(
                &ExecutionWorkflowStepEventContext {
                    agent: resolve_agent(manifest, current_agent_id),
                    ..workflow_context.clone()
                },
                "step_completed",
                result.step_name.clone(),
                String::new(),
                Some(*duration_ms),
                serde_json::to_value(data).unwrap_or_default(),
                Some(serde_json::json!({
                    "output_preview": event_text_preview(&result.output),
                    "verdict": result
                        .data
                        .get("verdict")
                        .and_then(|v| v.as_str())
                        .map(event_text_preview),
                    "reasoning": result
                        .data
                        .get("reasoning")
                        .and_then(|v| v.as_str())
                        .map(event_text_preview),
                })),
            )]
        }
        nenjo::RoutineEvent::StepFailed {
            step_slug,
            step_run_id,
            step_name,
            step_type,
            error,
            duration_ms,
            ..
        } => vec![execution_workflow_step_response(
            &workflow_context,
            "step_failed",
            step_name.clone(),
            step_type.clone(),
            Some(*duration_ms),
            serde_json::to_value(StepFailedData {
                step_slug: step_slug.to_string(),
                step_run_id: *step_run_id,
                error: "Step failed",
            })
            .unwrap_or_default(),
            Some(serde_json::json!({ "error": event_text_preview(error) })),
        )],
        nenjo::RoutineEvent::AgentEvent {
            step_slug,
            step_run_id,
            event,
        } => routine_agent_event_to_responses(
            event,
            execution_run_id,
            task_id,
            step_slug.to_string(),
            *step_run_id,
            current_agent_id,
            manifest,
        ),
        nenjo::RoutineEvent::Done { .. } => Vec::new(),
        nenjo::RoutineEvent::CronCycleStarted { cycle } => vec![execution_workflow_step_response(
            &workflow_context,
            "cron_cycle_started",
            format!("cycle-{cycle}"),
            "cron",
            None,
            serde_json::to_value(CronCycleStartedData { cycle: *cycle }).unwrap_or_default(),
            None,
        )],
        nenjo::RoutineEvent::CronCycleCompleted { cycle, result, .. } => {
            vec![execution_workflow_step_response(
                &workflow_context,
                "cron_cycle_completed",
                format!("cycle-{cycle}"),
                "cron",
                None,
                serde_json::to_value(CronCycleCompletedData {
                    cycle: *cycle,
                    passed: result.passed,
                })
                .unwrap_or_default(),
                None,
            )]
        }
    }
}

fn routine_step_trace_context(
    manifest: &Manifest,
    step_slug: &str,
    step_run_id: Uuid,
) -> ExecutionTraceRoutineStep {
    let manifest_step = manifest
        .routines
        .iter()
        .flat_map(|routine| routine.steps.iter())
        .find(|step| step.slug.as_str() == step_slug);

    ExecutionTraceRoutineStep {
        step_slug: step_slug.to_string(),
        step_run_id,
        step_name: manifest_step.map(|step| step.name.clone()),
        step_type: manifest_step.map(|step| step.step_type.to_string()),
    }
}

fn routine_step_agent_name(
    manifest: &Manifest,
    step_slug: &str,
    current_agent_id: Option<Uuid>,
) -> String {
    let agent_slug = manifest
        .routines
        .iter()
        .flat_map(|routine| routine.steps.iter())
        .find(|step| step.slug.as_str() == step_slug)
        .and_then(|step| step.agent.as_ref());

    if let Some(agent_slug) = agent_slug
        && let Some(agent) = manifest
            .agents
            .iter()
            .find(|agent| agent.slug == *agent_slug)
    {
        return agent.name.clone();
    }

    current_agent_id
        .map(|agent_id| agent_name(manifest, agent_id))
        .unwrap_or_else(|| "agent".to_string())
}

fn routine_agent_event_to_responses(
    event: &nenjo::TurnEvent,
    execution_run_id: Uuid,
    task_id: Option<Uuid>,
    routine_step_slug: String,
    routine_step_run_id: Uuid,
    current_agent_id: Option<Uuid>,
    manifest: &Manifest,
) -> Vec<Response> {
    turn_event_to_agent_trace_responses(
        event,
        &ExecutionAgentTraceContext {
            execution_run_id,
            task_id,
            agent_name: routine_step_agent_name(manifest, &routine_step_slug, current_agent_id),
            trace_run_id: routine_step_run_id.to_string(),
            trace_session_id: routine_step_run_id,
            routine_step: Some(routine_step_trace_context(
                manifest,
                &routine_step_slug,
                routine_step_run_id,
            )),
        },
    )
}

/// Get project slug from manifest, falling back to UUID string.
pub fn project_slug(manifest: &Manifest, project_id: Uuid) -> String {
    if project_id.is_nil() {
        return String::new();
    }
    match manifest
        .projects
        .iter()
        .find(|p| crate::resource_resolver::stable_resource_id("project", &p.slug) == project_id)
    {
        Some(p) => p.slug.to_string(),
        None => project_id.to_string(),
    }
}

/// Get agent name from manifest, falling back to UUID string.
pub fn agent_name(manifest: &Manifest, agent_id: Uuid) -> String {
    manifest
        .agents
        .iter()
        .find(|a| crate::resource_resolver::stable_resource_id("agent", &a.slug) == agent_id)
        .map(|a| a.name.clone())
        .unwrap_or_else(|| agent_id.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use nenjo::Slug;
    use nenjo::manifest::{
        AgentManifest, Manifest, PromptConfig, PromptTemplates, RoutineManifest, RoutineMetadata,
        RoutineStepManifest, RoutineStepType, RoutineTrigger,
    };

    fn expect_workflow_step(
        response: Response,
    ) -> (String, Option<String>, ExecutionWorkflowStepEvent) {
        match response {
            Response::ExecutionEvent {
                execution_run_id,
                task_id,
                event: ExecutionEventPayload::WorkflowStep(step),
            } => (execution_run_id, task_id, step),
            other => panic!("expected execution workflow step event, got {other:?}"),
        }
    }

    fn expect_agent_trace(
        response: Response,
    ) -> (
        String,
        Option<String>,
        Option<ExecutionTraceRoutineStep>,
        StreamEvent,
    ) {
        match response {
            Response::ExecutionEvent {
                execution_run_id,
                task_id,
                event:
                    ExecutionEventPayload::AgentTrace {
                        routine_step,
                        payload,
                    },
            } => (execution_run_id, task_id, routine_step, payload),
            other => panic!("expected execution agent trace event, got {other:?}"),
        }
    }

    #[test]
    fn sub_agent_lifecycle_is_not_sent_as_stream_event() {
        let event = nenjo::TurnEvent::SubAgentEvent {
            slug: "review".to_string(),
            agent_name: "specialist".to_string(),
            kind: "completed".to_string(),
            summary: "done".to_string(),
            model_visible: false,
        };

        assert!(turn_event_to_stream_events(&event, "leader", "run-1", Uuid::new_v4()).is_empty());
    }

    #[test]
    fn sub_agent_lifecycle_is_not_sent_as_workflow_step_response() {
        let event = nenjo::TurnEvent::SubAgentEvent {
            slug: "review".to_string(),
            agent_name: "specialist".to_string(),
            kind: "completed".to_string(),
            summary: "done".to_string(),
            model_visible: false,
        };

        let response = turn_event_to_workflow_step_response(
            &event,
            &TaskTurnEventContext {
                execution_run_id: Uuid::new_v4(),
                task_id: Some(Uuid::new_v4()),
                agent: None,
                routine_step: None,
                agent_duration_ms: None,
                emit_done: true,
                summarize_outputs: false,
            },
        );

        assert!(response.is_none());
    }

    #[test]
    fn stream_tool_output_preview_keeps_json_body() {
        let output = serde_json::json!({
            "models": [
                { "id": "model-a", "name": "Model A" },
                { "id": "model-b", "name": "Model B" }
            ]
        })
        .to_string();

        let events = turn_event_to_stream_events(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "list_models".to_string(),
                tool_args: "{}".to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: output.clone(),
                    error: None,
                },
                metadata: None,
            },
            "agent",
            "run-1",
            Uuid::new_v4(),
        );

        match events.as_slice() {
            [
                StreamEvent::ToolCallCompleted {
                    batch_id, payload, ..
                },
            ] => {
                assert_eq!(batch_id, "batch-1");
                assert_eq!(
                    payload
                        .as_ref()
                        .and_then(|payload| payload.get("output_preview"))
                        .and_then(serde_json::Value::as_str),
                    Some(output.as_str()),
                );
                assert_eq!(
                    payload
                        .as_ref()
                        .and_then(|payload| payload.get("output"))
                        .and_then(|output| output.get("models"))
                        .and_then(serde_json::Value::as_array)
                        .map(Vec::len),
                    Some(2),
                );
            }
            other => panic!("unexpected stream events: {other:?}"),
        }
    }

    #[test]
    fn provider_tool_metadata_is_included_in_stream_tool_events() {
        let metadata = serde_json::json!({
            "tool_origin": "provider",
            "provider_native": true,
            "provider": "xai",
        });

        let started = turn_event_to_stream_events(
            &nenjo::TurnEvent::ToolCallStart {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                calls: vec![nenjo::agents::ToolCall {
                    tool_call_id: Some("call-1".to_string()),
                    tool_name: "web_search".to_string(),
                    tool_args: "{}".to_string(),
                    text_preview: Some("provider web search".to_string()),
                    metadata: Some(metadata.clone()),
                }],
            },
            "agent",
            "run-1",
            Uuid::new_v4(),
        );

        match started.as_slice() {
            [StreamEvent::ToolCallStarted { payload, .. }] => {
                let payload = payload.as_ref().expect("payload");
                assert_eq!(payload["tool_origin"], "provider");
                assert_eq!(payload["provider_native"], true);
                assert_eq!(payload["provider"], "xai");
                assert_eq!(payload["tool_name"], "web_search");
            }
            other => panic!("unexpected stream events: {other:?}"),
        }

        let completed = turn_event_to_stream_events(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "web_search".to_string(),
                tool_args: "{}".to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: "{\"status\":\"completed\"}".to_string(),
                    error: None,
                },
                metadata: Some(metadata),
            },
            "agent",
            "run-1",
            Uuid::new_v4(),
        );

        match completed.as_slice() {
            [StreamEvent::ToolCallCompleted { payload, .. }] => {
                let payload = payload.as_ref().expect("payload");
                assert_eq!(payload["tool_origin"], "provider");
                assert_eq!(payload["provider_native"], true);
                assert_eq!(payload["provider"], "xai");
                assert_eq!(payload["tool_name"], "web_search");
            }
            other => panic!("unexpected stream events: {other:?}"),
        }
    }

    #[test]
    fn provider_tool_metadata_is_included_in_workflow_step_events() {
        let metadata = serde_json::json!({
            "tool_origin": "provider",
            "provider_native": true,
            "provider": "xai",
        });
        let context = TaskTurnEventContext {
            execution_run_id: Uuid::new_v4(),
            task_id: Some(Uuid::new_v4()),
            agent: None,
            routine_step: None,
            agent_duration_ms: None,
            emit_done: true,
            summarize_outputs: false,
        };

        let started = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallStart {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                calls: vec![nenjo::agents::ToolCall {
                    tool_call_id: Some("call-1".to_string()),
                    tool_name: "web_search".to_string(),
                    tool_args: "{}".to_string(),
                    text_preview: Some("provider web search".to_string()),
                    metadata: Some(metadata.clone()),
                }],
            },
            &context,
        )
        .expect("tool start should bridge");

        let (_, _, step) = expect_workflow_step(started);
        assert_eq!(step.data["tool_metadata"][0]["tool_origin"], "provider");
        let payload = step.payload.expect("payload");
        assert_eq!(payload["provider_native"], true);
        assert_eq!(payload["provider"], "xai");

        let completed = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "web_search".to_string(),
                tool_args: "{}".to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: "{\"status\":\"completed\"}".to_string(),
                    error: None,
                },
                metadata: Some(metadata),
            },
            &context,
        )
        .expect("tool completion should bridge");

        let (_, _, step) = expect_workflow_step(completed);
        assert_eq!(step.data["tool_metadata"]["tool_origin"], "provider");
        let payload = step.payload.expect("payload");
        assert_eq!(payload["provider_native"], true);
        assert_eq!(payload["provider"], "xai");
    }

    #[test]
    fn direct_turn_events_are_wrapped_as_agent_trace_events() {
        let execution_run_id = Uuid::new_v4();
        let task_id = Uuid::new_v4();
        let trace_session_id = Uuid::new_v4();

        let responses = turn_event_to_agent_trace_responses(
            &nenjo::TurnEvent::ToolCallStart {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                calls: vec![nenjo::agents::ToolCall {
                    tool_call_id: Some("call-1".to_string()),
                    tool_name: "file_write".to_string(),
                    tool_args: "{\"path\":\"src/app.tsx\"}".to_string(),
                    text_preview: Some("Writing src/app.tsx".to_string()),
                    metadata: None,
                }],
            },
            &ExecutionAgentTraceContext {
                execution_run_id,
                task_id: Some(task_id),
                agent_name: "Code Implementer".to_string(),
                trace_run_id: "trace-run-1".to_string(),
                trace_session_id,
                routine_step: None,
            },
        );

        assert_eq!(responses.len(), 1);
        let (bridged_execution_run_id, bridged_task_id, routine_step, payload) =
            expect_agent_trace(responses.into_iter().next().unwrap());
        match payload {
            StreamEvent::ToolCallStarted {
                run_id,
                batch_id,
                call_id,
                tool_name,
                payload,
                ..
            } => {
                assert_eq!(bridged_execution_run_id, execution_run_id.to_string());
                assert_eq!(
                    bridged_task_id.as_deref(),
                    Some(task_id.to_string().as_str())
                );
                assert!(routine_step.is_none());
                assert_eq!(run_id, "trace-run-1");
                assert_eq!(batch_id, "batch-1");
                assert_eq!(call_id, "call-1");
                assert_eq!(tool_name, "file_write");
                assert_eq!(
                    payload
                        .as_ref()
                        .and_then(|payload| payload.get("text_preview"))
                        .and_then(serde_json::Value::as_str),
                    Some("Writing src/app.tsx"),
                );
            }
            other => panic!("unexpected responses: {other:?}"),
        }
    }

    #[test]
    fn routine_agent_events_preserve_routine_step_metadata_in_agent_trace_events() {
        let execution_run_id = Uuid::new_v4();
        let task_id = Uuid::new_v4();
        let step_run_id = Uuid::new_v4();
        let routine_slug = Slug::parse("code_generation").unwrap();
        let step_slug = Slug::parse("implement").unwrap();
        let agent_slug = Slug::parse("code_implementer").unwrap();
        let manifest = Manifest {
            routines: vec![RoutineManifest {
                name: "Code Generation".to_string(),
                slug: routine_slug.clone(),
                description: None,
                trigger: RoutineTrigger::Task,
                metadata: RoutineMetadata::default(),
                steps: vec![RoutineStepManifest {
                    slug: step_slug.clone(),
                    routine: routine_slug,
                    name: "Implement".to_string(),
                    step_type: RoutineStepType::Agent,
                    council: None,
                    agent: Some(agent_slug.clone()),
                    config: serde_json::json!({}),
                    order_index: 0,
                }],
                edges: Vec::new(),
            }],
            models: Vec::new(),
            agents: vec![AgentManifest {
                name: "Code Implementer".to_string(),
                slug: agent_slug,
                description: None,
                prompt_config: PromptConfig {
                    templates: PromptTemplates {
                        task_execution: String::new(),
                        chat_task: String::new(),
                        gate_eval: String::new(),
                        heartbeat_task: String::new(),
                    },
                    ..Default::default()
                },
                color: None,
                model: None,
                domains: Vec::new(),
                platform_scopes: Vec::new(),
                mcp_servers: Vec::new(),
                script_tools: Vec::new(),
                media: Vec::new(),
                abilities: Vec::new(),
                prompt_locked: false,
                heartbeat: None,
            }],
            councils: Vec::new(),
            domains: Vec::new(),
            projects: Vec::new(),
            mcp_servers: Vec::new(),
            abilities: Vec::new(),
            context_blocks: Vec::new(),
            skills: Vec::new(),
            commands: Vec::new(),
            hooks: Vec::new(),
            script_tools: Vec::new(),
            knowledge_packs: Vec::new(),
        };

        let responses = routine_event_to_responses(
            &nenjo::RoutineEvent::AgentEvent {
                step_slug,
                step_run_id,
                event: nenjo::TurnEvent::ToolCallStart {
                    batch_id: "batch-1".to_string(),
                    parent_tool_name: None,
                    calls: vec![nenjo::agents::ToolCall {
                        tool_call_id: Some("call-1".to_string()),
                        tool_name: "file_write".to_string(),
                        tool_args: "{}".to_string(),
                        text_preview: None,
                        metadata: None,
                    }],
                },
            },
            execution_run_id,
            Some(task_id),
            None,
            &manifest,
        );

        assert_eq!(responses.len(), 1);
        let (bridged_execution_run_id, bridged_task_id, routine_step, payload) =
            expect_agent_trace(responses.into_iter().next().unwrap());
        match (routine_step, payload) {
            (Some(routine_step), StreamEvent::ToolCallStarted { run_id, .. }) => {
                assert_eq!(bridged_execution_run_id, execution_run_id.to_string());
                assert_eq!(
                    bridged_task_id.as_deref(),
                    Some(task_id.to_string().as_str())
                );
                assert_eq!(routine_step.step_slug, "implement");
                assert_eq!(routine_step.step_run_id, step_run_id);
                assert_eq!(routine_step.step_name.as_deref(), Some("Implement"));
                assert_eq!(routine_step.step_type.as_deref(), Some("agent"));
                assert_eq!(run_id, step_run_id.to_string());
            }
            other => panic!("unexpected responses: {other:?}"),
        }
    }

    #[test]
    fn tool_event_stream_payloads_are_bounded() {
        let long = "x".repeat(PREVIEW_MAX_CHARS + 128);
        let events = turn_event_to_stream_events(
            &nenjo::TurnEvent::ToolCallStart {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                calls: vec![nenjo::agents::ToolCall {
                    tool_call_id: Some("call-1".to_string()),
                    tool_name: "file_write".to_string(),
                    tool_args: long.clone(),
                    text_preview: Some(long.clone()),
                    metadata: Some(serde_json::json!({
                        "nested": {
                            "large": long,
                        },
                    })),
                }],
            },
            "agent",
            "run-1",
            Uuid::new_v4(),
        );

        match events.as_slice() {
            [StreamEvent::ToolCallStarted { payload, .. }] => {
                let payload = payload.as_ref().expect("payload");
                for key in ["tool_args", "text_preview"] {
                    let value = payload[key].as_str().expect(key);
                    assert!(value.ends_with("..."));
                    assert!(value.len() < PREVIEW_MAX_CHARS + 128);
                }
                let metadata_value = payload["nested"]["large"].as_str().expect("metadata");
                assert!(metadata_value.ends_with("..."));
                assert!(metadata_value.len() < PREVIEW_MAX_CHARS + 128);
            }
            other => panic!("unexpected stream events: {other:?}"),
        }
    }

    #[test]
    fn workflow_step_tool_payloads_are_bounded() {
        let long = "x".repeat(PREVIEW_MAX_CHARS + 128);
        let response = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "file_write".to_string(),
                tool_args: long.clone(),
                result: nenjo::ToolResult {
                    success: false,
                    output: long.clone(),
                    error: Some(long.clone()),
                },
                metadata: Some(serde_json::json!({
                    "nested": {
                        "large": long,
                    },
                })),
            },
            &TaskTurnEventContext {
                execution_run_id: Uuid::new_v4(),
                task_id: Some(Uuid::new_v4()),
                agent: None,
                routine_step: None,
                agent_duration_ms: None,
                emit_done: true,
                summarize_outputs: false,
            },
        )
        .expect("tool completion should bridge");

        let (_, _, step) = expect_workflow_step(response);
        let tool_args = step.data["tool_args"].as_str().expect("tool_args");
        assert!(tool_args.ends_with("..."));
        assert!(tool_args.len() < PREVIEW_MAX_CHARS + 128);
        let metadata_value = step.data["tool_metadata"]["nested"]["large"]
            .as_str()
            .expect("metadata");
        assert!(metadata_value.ends_with("..."));
        assert!(metadata_value.len() < PREVIEW_MAX_CHARS + 128);

        let payload = step.payload.expect("payload");
        for key in ["output_preview", "error"] {
            let value = payload[key].as_str().expect(key);
            assert!(value.ends_with("..."));
            assert!(value.len() < PREVIEW_MAX_CHARS + 128);
        }
        let metadata_value = payload["nested"]["large"].as_str().expect("metadata");
        assert!(metadata_value.ends_with("..."));
        assert!(metadata_value.len() < PREVIEW_MAX_CHARS + 128);
    }

    #[test]
    fn routine_tool_events_use_tool_event_types() {
        let execution_run_id = Uuid::new_v4();
        let task_id = Uuid::new_v4();
        let step_run_id = Uuid::new_v4();
        let context = TaskTurnEventContext {
            execution_run_id,
            task_id: Some(task_id),
            agent: None,
            routine_step: Some(RoutineStepRef {
                step_slug: "implement".to_string(),
                step_run_id,
            }),
            agent_duration_ms: None,
            emit_done: false,
            summarize_outputs: true,
        };

        let started = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallStart {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                calls: vec![nenjo::agents::ToolCall {
                    tool_call_id: Some("call-1".to_string()),
                    tool_name: "file_write".to_string(),
                    tool_args: "{}".to_string(),
                    text_preview: None,
                    metadata: None,
                }],
            },
            &context,
        )
        .expect("routine tool start should bridge");

        let (_, _, step) = expect_workflow_step(started);
        assert_eq!(step.event_type, "tool_started");
        assert_eq!(step.data["step_slug"], "implement");
        assert_eq!(step.data["step_run_id"], step_run_id.to_string());

        let completed = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "file_write".to_string(),
                tool_args: "{}".to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: "written".to_string(),
                    error: None,
                },
                metadata: None,
            },
            &context,
        )
        .expect("routine tool completion should bridge");

        let (_, _, step) = expect_workflow_step(completed);
        assert_eq!(step.event_type, "tool_completed");
    }

    #[test]
    fn direct_task_turn_done_emits_agent_response_step() {
        let execution_run_id = Uuid::new_v4();
        let task_id = Uuid::new_v4();
        let response = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::Done {
                output: nenjo::TurnOutput {
                    task_id: Some(task_id),
                    text: "final answer".to_string(),
                    input_tokens: 10,
                    output_tokens: 20,
                    tool_calls: 0,
                    messages: Vec::new(),
                },
            },
            &TaskTurnEventContext {
                execution_run_id,
                task_id: Some(task_id),
                agent: Some(StepAgent {
                    agent: "agent".to_string(),
                    agent_name: Some("agent".to_string()),
                    agent_color: None,
                }),
                routine_step: None,
                agent_duration_ms: Some(123),
                emit_done: true,
                summarize_outputs: false,
            },
        )
        .expect("direct task done should emit a workflow step");

        let (bridged_execution_run_id, bridged_task_id, step) = expect_workflow_step(response);
        assert_eq!(bridged_execution_run_id, execution_run_id.to_string());
        assert_eq!(bridged_task_id, Some(task_id.to_string()));
        assert_eq!(step.event_type, "step_completed");
        assert_eq!(step.step_name, "agent_response");
        assert_eq!(step.step_type, "agent");
        assert_eq!(step.duration_ms, Some(123));
        assert_eq!(step.data["input_tokens"], 10);
        assert_eq!(step.data["output_tokens"], 20);
        assert_eq!(step.payload.unwrap()["output_preview"], "final answer");
        assert_eq!(step.agent.unwrap().agent, "agent");
    }

    #[test]
    fn routine_turn_done_is_suppressed() {
        let response = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::Done {
                output: nenjo::TurnOutput {
                    task_id: None,
                    text: "done".to_string(),
                    input_tokens: 1,
                    output_tokens: 2,
                    tool_calls: 0,
                    messages: Vec::new(),
                },
            },
            &TaskTurnEventContext {
                execution_run_id: Uuid::new_v4(),
                task_id: Some(Uuid::new_v4()),
                agent: None,
                routine_step: Some(RoutineStepRef {
                    step_slug: "review".to_string(),
                    step_run_id: Uuid::new_v4(),
                }),
                agent_duration_ms: None,
                emit_done: false,
                summarize_outputs: true,
            },
        );

        assert!(response.is_none());
    }

    #[test]
    fn routine_step_ids_are_only_added_for_routine_turn_events() {
        let execution_run_id = Uuid::new_v4();
        let task_id = Uuid::new_v4();
        let step_slug = "review".to_string();
        let step_run_id = Uuid::new_v4();
        let event = nenjo::TurnEvent::AbilityStarted {
            call_id: "ability-call-1".to_string(),
            ability_tool_name: "ability.review".to_string(),
            ability_name: "Review".to_string(),
            task_input: "inspect".to_string(),
            caller_history: Vec::new(),
        };

        let routine = turn_event_to_workflow_step_response(
            &event,
            &TaskTurnEventContext {
                execution_run_id,
                task_id: Some(task_id),
                agent: None,
                routine_step: Some(RoutineStepRef {
                    step_slug: step_slug.clone(),
                    step_run_id,
                }),
                agent_duration_ms: None,
                emit_done: false,
                summarize_outputs: true,
            },
        )
        .expect("routine ability start should bridge");

        let direct = turn_event_to_workflow_step_response(
            &event,
            &TaskTurnEventContext {
                execution_run_id,
                task_id: Some(task_id),
                agent: None,
                routine_step: None,
                agent_duration_ms: None,
                emit_done: true,
                summarize_outputs: false,
            },
        )
        .expect("direct ability start should bridge");

        let (_, _, step) = expect_workflow_step(routine);
        assert_eq!(step.data["step_slug"], step_slug);
        assert_eq!(step.data["step_run_id"], step_run_id.to_string());
        let (_, _, step) = expect_workflow_step(direct);
        assert_eq!(step.data["call_id"], "ability-call-1");
        assert!(step.data.get("step_slug").is_none());
        assert!(step.data.get("step_run_id").is_none());
    }

    #[test]
    fn direct_task_tool_output_preview_is_not_summarized() {
        let output = "x".repeat(600);
        let response = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "tool".to_string(),
                tool_args: "{}".to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: output.clone(),
                    error: None,
                },
                metadata: None,
            },
            &TaskTurnEventContext {
                execution_run_id: Uuid::new_v4(),
                task_id: Some(Uuid::new_v4()),
                agent: None,
                routine_step: None,
                agent_duration_ms: None,
                emit_done: true,
                summarize_outputs: false,
            },
        )
        .expect("tool completion should bridge");

        let (_, _, step) = expect_workflow_step(response);
        assert_eq!(step.payload.unwrap()["output_preview"], output);
    }

    #[test]
    fn workflow_tool_output_includes_structured_json_when_available() {
        let output = serde_json::json!([
            {
                "document": {
                    "selector": "resources.agents",
                    "title": "Agents"
                },
                "matched": ["title"],
                "score": 100
            }
        ])
        .to_string();
        let response = turn_event_to_workflow_step_response(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "search_knowledge".to_string(),
                tool_args: r#"{"pack":"pkg:core","query":"agents"}"#.to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output: output.clone(),
                    error: None,
                },
                metadata: None,
            },
            &TaskTurnEventContext {
                execution_run_id: Uuid::new_v4(),
                task_id: Some(Uuid::new_v4()),
                agent: None,
                routine_step: None,
                agent_duration_ms: None,
                emit_done: true,
                summarize_outputs: false,
            },
        )
        .expect("tool completion should bridge");

        let (_, _, step) = expect_workflow_step(response);
        let payload = step.payload.expect("payload");
        assert_eq!(payload["output_preview"], output);
        assert_eq!(
            payload["output"][0]["document"]["selector"],
            "resources.agents"
        );
    }

    #[test]
    fn oversized_structured_tool_output_stays_as_string_preview() {
        let output = serde_json::Value::Array(
            (0..8)
                .map(|index| {
                    serde_json::json!({
                        "document": {
                            "selector": format!("doc.{index}"),
                            "summary": "x".repeat(PREVIEW_MAX_CHARS + 100),
                            "related": (0..8)
                                .map(|related| serde_json::json!({ "target": format!("doc.{related}") }))
                                .collect::<Vec<_>>()
                        },
                        "matched": ["content"],
                        "score": index
                    })
                })
                .collect(),
        )
        .to_string();

        let events = turn_event_to_stream_events(
            &nenjo::TurnEvent::ToolCallEnd {
                batch_id: "batch-1".to_string(),
                parent_tool_name: None,
                tool_call_id: Some("call-1".to_string()),
                tool_name: "search_knowledge".to_string(),
                tool_args: r#"{"pack":"pkg:core","query":"workflow"}"#.to_string(),
                result: nenjo::ToolResult {
                    success: true,
                    output,
                    error: None,
                },
                metadata: None,
            },
            "agent",
            "run-1",
            Uuid::new_v4(),
        );

        let [StreamEvent::ToolCallCompleted { payload, .. }] = events.as_slice() else {
            panic!("unexpected stream events: {events:?}");
        };
        assert!(
            payload
                .as_ref()
                .and_then(|payload| payload.get("output"))
                .is_none()
        );
        let output_preview = payload
            .as_ref()
            .and_then(|payload| payload.get("output_preview"))
            .and_then(serde_json::Value::as_str)
            .expect("output preview");
        assert!(
            output_preview.ends_with("..."),
            "oversized JSON should remain a truncated string preview"
        );
    }
}
