#![allow(dead_code, unused_imports)]

pub(crate) use async_trait::async_trait;
pub(crate) use chrono::Utc;
pub(crate) use std::path::PathBuf;
pub(crate) use std::sync::Arc;
pub(crate) use tempfile::{tempdir, TempDir};
pub(crate) use tokio::runtime::Runtime;
pub(crate) use tokio::sync::Mutex;

pub(crate) use crate::{
    config::AppConfig,
    context::ContextConfig,
    host::RuntimeHost,
    prompt::{render_section, PromptSection, PromptStability},
    provider::{
        provider_turn_error, AgentProvider, ConversationMessage, ModelBlock,
        ProviderAttemptOutcome, ProviderAttemptRecord, ProviderAttemptTimeline,
        ProviderHttpTraceDiagnostics, ProviderTransportDiagnostics, ProviderTurnRequest,
        ProviderTurnResponse, ReqwestTransportDiagnostics, StubProvider,
    },
    storage::AppStorage,
    system::{ExecutionProfile, ExecutionSnapshot, WorkspaceAccessMode, WorkspaceProjectionKind},
    types::{
        ActiveWorkspaceEntry, AgentIdentityView, AgentKind, AgentOwnership, AgentProfilePreset,
        AgentRegistryStatus, AgentState, AgentStatus, AgentVisibility, AuthorityClass, BriefKind,
        BriefRecord, CallbackDeliveryMode, ClosureDecision, ClosureOutcome, ContinuationClass,
        ContinuationTriggerKind, DeliverySummaryRecord, LoadedAgentsMd, MessageBody,
        MessageDeliverySurface, MessageKind, MessageOrigin, PendingWakeHint, Priority,
        QueueEntryStatus, TaskKind, TaskOutputRetrievalStatus, TaskRecord, TaskRecoverySpec,
        TaskStatus, TimerRecord, TimerStatus, TodoItem, TodoItemState, TokenUsage, TrustLevel,
        TurnTerminalKind, TurnTerminalRecord, WaitingIntentStatus, WaitingReason, WorkItemRecord,
        WorkItemState, WorkReactivationMode, WorkspaceEntry,
    },
};

use super::super::*;

pub(crate) fn context_config() -> ContextConfig {
    ContextConfig {
        recent_messages: 8,
        recent_briefs: 8,
        compaction_trigger_messages: 10,
        compaction_keep_recent_messages: 4,
        ..ContextConfig::default()
    }
}

pub(crate) async fn host_backed_test_runtime() -> (TempDir, RuntimeHost, RuntimeHandle) {
    let home = tempdir().unwrap();
    std::fs::write(
        home.path().join("config.json"),
        r#"{"model":{"default":"openai/gpt-5.4"}}"#,
    )
    .unwrap();
    let config = crate::config::AppConfig::load_with_home(Some(home.path().to_path_buf())).unwrap();
    let host = RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("done"))).unwrap();
    let runtime = host.default_runtime().await.unwrap();
    (home, host, runtime)
}

pub(crate) fn private_child_identity(agent_id: &str) -> AgentIdentityView {
    AgentIdentityView {
        agent_id: agent_id.into(),
        kind: AgentKind::Child,
        visibility: AgentVisibility::Private,
        ownership: AgentOwnership::ParentSupervised,
        profile_preset: AgentProfilePreset::PrivateChild,
        status: AgentRegistryStatus::Active,
        is_default_agent: false,
        parent_agent_id: Some("default".into()),
        lineage_parent_agent_id: Some("default".into()),
        delegated_from_task_id: Some("task-1".into()),
    }
}

pub(crate) fn test_effective_prompt() -> EffectivePrompt {
    EffectivePrompt {
        identity: AgentIdentityView {
            agent_id: "default".into(),
            kind: AgentKind::Default,
            visibility: AgentVisibility::Public,
            ownership: AgentOwnership::SelfOwned,
            profile_preset: AgentProfilePreset::PublicNamed,
            status: AgentRegistryStatus::Active,
            is_default_agent: true,
            parent_agent_id: None,
            lineage_parent_agent_id: None,
            delegated_from_task_id: None,
        },
        agent_home: PathBuf::from("/tmp/agent-home"),
        execution: ExecutionSnapshot {
            profile: ExecutionProfile::default(),
            policy: ExecutionProfile::default().policy_snapshot(),
            attached_workspaces: vec![],
            workspace_id: None,
            workspace_anchor: PathBuf::from("/tmp/agent-home"),
            execution_root: PathBuf::from("/tmp/agent-home"),
            cwd: PathBuf::from("/tmp/agent-home"),
            execution_root_id: None,
            projection_kind: None,
            access_mode: None,
            worktree_root: None,
        },
        loaded_agents_md: LoadedAgentsMd::default(),
        cache_identity: crate::prompt::PromptCacheIdentity {
            agent_id: "default".into(),
            prompt_cache_key: "default".into(),
            context_fingerprint: "fingerprint-default".into(),
            working_memory_revision: 1,
            compression_epoch: 0,
        },
        system_sections: vec![],
        context_sections: vec![],
        rendered_system_prompt: "system".into(),
        rendered_context_attachment: "context".into(),
    }
}

pub(crate) fn closure_decision(
    outcome: ClosureOutcome,
    waiting_reason: Option<WaitingReason>,
) -> ClosureDecision {
    ClosureDecision {
        outcome,
        waiting_reason,
        work_signal: None,
        runtime_posture: RuntimePosture::Awake,
        evidence: Vec::new(),
    }
}

pub(crate) async fn bind_turn_to_work_item(runtime: &RuntimeHandle, work_item_id: &str) {
    let mut guard = runtime.inner.agent.lock().await;
    guard.state.turn_index = 1;
    guard.state.current_turn_work_item_id = Some(work_item_id.to_string());
    guard.state.last_turn_terminal = Some(TurnTerminalRecord {
        turn_index: 1,
        kind: TurnTerminalKind::Completed,
        reason: None,
        last_assistant_message: Some("done".into()),
        checkpoint: None,
        completed_at: Utc::now(),
        duration_ms: 10,
    });
    runtime.inner.storage.write_agent(&guard.state).unwrap();
}

pub(crate) async fn seed_bound_work_item(
    runtime: &RuntimeHandle,
    state: WorkItemState,
    summary: Option<&str>,
    blocked_by: Option<&str>,
) -> String {
    let mut record = runtime
        .create_work_item(
            summary.unwrap_or("finish the bound objective").to_string(),
            None,
            None,
            Vec::new(),
        )
        .await
        .unwrap();
    if let Some(blocked_by) = blocked_by {
        record = runtime
            .update_work_item_fields(
                record.id.clone(),
                None,
                None,
                None,
                None,
                Some(Some(blocked_by.to_string())),
            )
            .await
            .unwrap();
    }
    if state == WorkItemState::Completed {
        record = runtime
            .complete_work_item(record.id.clone(), Vec::new())
            .await
            .unwrap();
        if let Some(summary) = summary {
            record = runtime
                .promote_work_item_completion_report(
                    record.id.clone(),
                    summary.to_string(),
                    None,
                    None,
                    Vec::new(),
                )
                .await
                .unwrap();
        }
    }
    bind_turn_to_work_item(runtime, &record.id).await;
    record.id
}

pub(crate) async fn mark_blocking_task(runtime: &RuntimeHandle, task_id: &str) {
    runtime
        .inner
        .storage
        .append_task(&TaskRecord {
            id: task_id.into(),
            agent_id: "default".into(),
            kind: TaskKind::CommandTask,
            status: TaskStatus::Running,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            parent_message_id: None,
            work_item_id: None,
            summary: Some("blocking command".into()),
            detail: Some(serde_json::json!({
                "wait_policy": "blocking"
            })),
            recovery: None,
        })
        .unwrap();
}

pub(crate) struct TruncatingProvider {
    pub(crate) calls: Mutex<usize>,
}

impl TruncatingProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

pub(crate) struct TimelineProvider;

pub(crate) struct OneToolThenTextProvider {
    pub(crate) calls: Mutex<usize>,
}

impl OneToolThenTextProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

#[async_trait]
impl AgentProvider for OneToolThenTextProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        let blocks = if *calls == 1 {
            vec![ModelBlock::ToolUse {
                id: "verify".into(),
                name: "ExecCommand".into(),
                input: serde_json::json!({
                    "cmd": "printf 'ok'",
                    "shell": "sh",
                }),
            }]
        } else {
            vec![ModelBlock::Text {
                text: "done".into(),
            }]
        };
        Ok(ProviderTurnResponse {
            blocks,
            stop_reason: None,
            input_tokens: 10,
            output_tokens: 10,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

pub(crate) struct FailingTimelineProvider;

pub(crate) struct ToolCaptureProvider {
    pub(crate) requests: Mutex<Vec<Vec<String>>>,
}

impl ToolCaptureProvider {
    pub(crate) async fn request_history(&self) -> tokio::sync::MutexGuard<'_, Vec<Vec<String>>> {
        self.requests.lock().await
    }
}

pub(crate) struct TurnLocalCompactionProbeProvider {
    pub(crate) calls: Mutex<usize>,
    pub(crate) requests: Mutex<Vec<ProviderTurnRequest>>,
}

impl TurnLocalCompactionProbeProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
    pub(crate) async fn request_history(
        &self,
    ) -> tokio::sync::MutexGuard<'_, Vec<ProviderTurnRequest>> {
        self.requests.lock().await
    }
}

pub(crate) struct BaselineOverBudgetProbeProvider {
    pub(crate) calls: Mutex<usize>,
}

impl BaselineOverBudgetProbeProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

pub(crate) struct ContextLengthExceededProvider;

pub(crate) struct DeferredFallbackProvider;

pub(crate) struct TextThenFailingFallbackProvider {
    pub(crate) calls: Mutex<usize>,
}

pub(crate) struct SleepOnlyToolProvider {
    pub(crate) calls: Mutex<usize>,
}

impl SleepOnlyToolProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

pub(crate) struct DisallowedToolThenTextProvider {
    pub(crate) calls: Mutex<usize>,
}

impl DisallowedToolThenTextProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

pub(crate) struct MaxOutputMutationToolProvider {
    pub(crate) calls: Mutex<usize>,
}

impl MaxOutputMutationToolProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

#[async_trait]
impl AgentProvider for TruncatingProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        if *calls == 1 {
            return Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::Text {
                    text: "Partial report heading:".into(),
                }],
                stop_reason: Some("max_tokens".into()),
                input_tokens: 100,
                output_tokens: 50,
                cache_usage: None,
                request_diagnostics: None,
            });
        }

        assert!(request.conversation.iter().any(|message| match message {
            ConversationMessage::UserText(text) => {
                text.contains("Output token limit hit")
                    && text.contains("Continue exactly where you left off")
                    && text.contains("Do not restart from the top, repeat analysis")
                    && text.contains("re-read context already provided")
            }
            _ => false,
        }));

        Ok(ProviderTurnResponse {
            blocks: vec![ModelBlock::Text {
                text: "\n\n- final grounded recommendation".into(),
            }],
            stop_reason: None,
            input_tokens: 50,
            output_tokens: 25,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for TimelineProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        Ok(ProviderTurnResponse {
            blocks: vec![ModelBlock::Text {
                text: "done with fallback history".into(),
            }],
            stop_reason: None,
            input_tokens: 12,
            output_tokens: 6,
            cache_usage: None,
            request_diagnostics: None,
        })
    }

    async fn complete_turn_with_diagnostics(
        &self,
        request: ProviderTurnRequest,
    ) -> Result<(ProviderTurnResponse, Option<ProviderAttemptTimeline>)> {
        let response = self.complete_turn(request).await?;
        Ok((
            response,
            Some(ProviderAttemptTimeline {
                attempts: vec![
                    ProviderAttemptRecord {
                        provider: "openai".into(),
                        model_ref: "openai/gpt-5.4".into(),
                        attempt: 1,
                        max_attempts: 3,
                        started_at: None,
                        completed_at: None,
                        duration_ms: Some(125),
                        failure_kind: Some("server_error".into()),
                        disposition: Some("retryable".into()),
                        outcome: ProviderAttemptOutcome::Retrying,
                        advanced_to_fallback: false,
                        backoff_ms: Some(200),
                        token_usage: None,
                        transport_diagnostics: None,
                    },
                    ProviderAttemptRecord {
                        provider: "anthropic".into(),
                        model_ref: "anthropic/claude-sonnet-4-6".into(),
                        attempt: 1,
                        max_attempts: 3,
                        started_at: None,
                        completed_at: None,
                        duration_ms: Some(250),
                        failure_kind: None,
                        disposition: None,
                        outcome: ProviderAttemptOutcome::Succeeded,
                        advanced_to_fallback: false,
                        backoff_ms: None,
                        token_usage: Some(TokenUsage::new(12, 6)),
                        transport_diagnostics: None,
                    },
                ],
                aggregated_token_usage: Some(TokenUsage::new(12, 6)),
                requested_model_ref: "openai/gpt-5.4".into(),
                active_model_ref: Some("anthropic/claude-sonnet-4-6".into()),
                winning_model_ref: Some("anthropic/claude-sonnet-4-6".into()),
                pending_fallback_model_ref: None,
            }),
        ))
    }
}

#[async_trait]
impl AgentProvider for ToolCaptureProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        self.requests.lock().await.push(
            request
                .tools
                .iter()
                .map(|tool| tool.name.clone())
                .collect::<Vec<_>>(),
        );
        Ok(ProviderTurnResponse {
            blocks: vec![ModelBlock::Text {
                text: "captured tool set".into(),
            }],
            stop_reason: None,
            input_tokens: 8,
            output_tokens: 4,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for TurnLocalCompactionProbeProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        self.requests.lock().await.push(request);
        let mut calls = self.calls.lock().await;
        *calls += 1;
        let response = match *calls {
            1 => ProviderTurnResponse {
                blocks: vec![
                    ModelBlock::Text {
                        text: format!("Round 1 planning {}", "very detailed preamble ".repeat(150)),
                    },
                    ModelBlock::ToolUse {
                        id: "exec-round-1".into(),
                        name: "ExecCommand".into(),
                        input: serde_json::json!({
                            "cmd": "printf 'first-round-output-should-not-stay-exact'",
                        }),
                    },
                ],
                stop_reason: Some("tool_use".into()),
                input_tokens: 0,
                output_tokens: 0,
                cache_usage: None,
                request_diagnostics: None,
            },
            2 => ProviderTurnResponse {
                blocks: vec![
                    ModelBlock::Text {
                        text: "Round 2 planning keep recent exact.".into(),
                    },
                    ModelBlock::ToolUse {
                        id: "exec-round-2".into(),
                        name: "ExecCommand".into(),
                        input: serde_json::json!({
                            "cmd": "printf 'second-round-output-should-remain-exact'",
                        }),
                    },
                ],
                stop_reason: Some("tool_use".into()),
                input_tokens: 0,
                output_tokens: 0,
                cache_usage: None,
                request_diagnostics: None,
            },
            3 => ProviderTurnResponse {
                blocks: vec![
                    ModelBlock::Text {
                        text: "Round 3 planning keep recent exact too.".into(),
                    },
                    ModelBlock::ToolUse {
                        id: "exec-round-3".into(),
                        name: "ExecCommand".into(),
                        input: serde_json::json!({
                            "cmd": "printf 'third-round-output-should-remain-exact'",
                        }),
                    },
                ],
                stop_reason: Some("tool_use".into()),
                input_tokens: 0,
                output_tokens: 0,
                cache_usage: None,
                request_diagnostics: None,
            },
            _ => ProviderTurnResponse {
                blocks: vec![ModelBlock::Text {
                    text: "Finished after compacted continuation.".into(),
                }],
                stop_reason: Some("end_turn".into()),
                input_tokens: 0,
                output_tokens: 0,
                cache_usage: None,
                request_diagnostics: None,
            },
        };
        Ok(response)
    }
}

#[async_trait]
impl AgentProvider for BaselineOverBudgetProbeProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        match *calls {
            1 => Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::ToolUse {
                    id: "exec-baseline-over-budget".into(),
                    name: "ExecCommand".into(),
                    input: serde_json::json!({
                        "cmd": "printf 'baseline-over-budget'",
                    }),
                }],
                stop_reason: Some("tool_use".into()),
                input_tokens: 0,
                output_tokens: 0,
                cache_usage: None,
                request_diagnostics: None,
            }),
            _ => panic!("continuation request should not be sent after baseline-over-budget"),
        }
    }
}

#[async_trait]
impl AgentProvider for SleepOnlyToolProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        if *calls > 1 {
            anyhow::bail!("sleep-only round should not force another provider turn");
        }
        assert!(
            request.tools.iter().any(|tool| tool.name == "Sleep"),
            "Sleep must be visible in the provider tool surface"
        );

        Ok(ProviderTurnResponse {
            blocks: vec![ModelBlock::ToolUse {
                id: "sleep-1".into(),
                name: "Sleep".into(),
                input: serde_json::json!({
                    "reason": "waiting for review",
                    "duration_ms": 250,
                }),
            }],
            stop_reason: None,
            input_tokens: 10,
            output_tokens: 5,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for DisallowedToolThenTextProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        match *calls {
            1 => Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::ToolUse {
                    id: "legacy-task".into(),
                    name: "CreateTask".into(),
                    input: serde_json::json!({
                        "prompt": "removed public task surface",
                    }),
                }],
                stop_reason: None,
                input_tokens: 10,
                output_tokens: 5,
                cache_usage: None,
                request_diagnostics: None,
            }),
            2 => {
                assert!(
                    request.conversation.iter().any(|message| matches!(
                        message,
                        ConversationMessage::UserToolResults(results)
                            if results.iter().any(|result|
                                result.tool_use_id == "legacy-task"
                                    && result.is_error
                                    && result
                                        .error
                                        .as_ref()
                                        .is_some_and(|error| error.kind == "tool_not_exposed_for_round")
                            )
                    )),
                    "continuation should receive a structured error for the unavailable tool"
                );
                Ok(ProviderTurnResponse {
                    blocks: vec![ModelBlock::Text {
                        text: "Recovered after unavailable tool.".into(),
                    }],
                    stop_reason: None,
                    input_tokens: 10,
                    output_tokens: 5,
                    cache_usage: None,
                    request_diagnostics: None,
                })
            }
            _ => anyhow::bail!("unexpected provider call after recovery text"),
        }
    }
}

#[async_trait]
impl AgentProvider for MaxOutputMutationToolProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        match *calls {
            1 => Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::ToolUse {
                    id: "truncated-patch".into(),
                    name: "ApplyPatch".into(),
                    input: serde_json::json!({
                        "patch": "--- /dev/null\n+++ b/app.txt\n@@ -0,0 +1 @@\n+should-not-be-written\n",
                    }),
                }],
                stop_reason: Some("max_tokens".into()),
                input_tokens: 20,
                output_tokens: 10,
                cache_usage: None,
                request_diagnostics: None,
            }),
            2 => {
                assert!(
                    request.conversation.iter().any(|message| matches!(
                        message,
                        ConversationMessage::UserToolResults(results)
                            if results.iter().any(|result|
                                result.tool_use_id == "truncated-patch"
                                    && result.is_error
                                    && result
                                        .error
                                        .as_ref()
                                        .is_some_and(|error| error.kind == "truncated_mutation_tool_call")
                            )
                    )),
                    "continuation should receive a structured truncation error"
                );
                Ok(ProviderTurnResponse {
                    blocks: vec![ModelBlock::Text {
                        text: "Recovered after rejected truncated mutation.".into(),
                    }],
                    stop_reason: None,
                    input_tokens: 15,
                    output_tokens: 8,
                    cache_usage: None,
                    request_diagnostics: None,
                })
            }
            _ => panic!("provider should stop after recovery"),
        }
    }
}

#[async_trait]
impl AgentProvider for FailingTimelineProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        Err(provider_turn_error(
            "all configured providers failed for this turn: openai/gpt-5.4: fail_fast (contract_error): bad request",
            ProviderAttemptTimeline {
                attempts: vec![ProviderAttemptRecord {
                    provider: "openai".into(),
                    model_ref: "openai/gpt-5.4".into(),
                    attempt: 1,
                    max_attempts: 3,
                    started_at: None,
                    completed_at: None,
                    duration_ms: Some(125),
                    failure_kind: Some("contract_error".into()),
                    disposition: Some("fail_fast".into()),
                    outcome: ProviderAttemptOutcome::FailFastAborted,
                    advanced_to_fallback: false,
                    backoff_ms: None,
                    token_usage: None,
                    transport_diagnostics: Some(ProviderTransportDiagnostics {
                        stage: "request_send".into(),
                        provider: Some("openai".into()),
                        model_ref: Some("openai/gpt-5.4".into()),
                        url: Some(
                            "https://user:secret@example.com/v1/responses?api_key=token#frag"
                                .into(),
                        ),
                        status: None,
                        reqwest: Some(ReqwestTransportDiagnostics {
                            is_timeout: false,
                            is_connect: false,
                            is_request: false,
                            is_body: true,
                            is_decode: false,
                            is_redirect: false,
                            status: None,
                        }),
                        http_trace: Some(ProviderHttpTraceDiagnostics {
                            mode: "failure_only".into(),
                            path: "/Users/example/.holon/http-trace/default/trace-1-1.jsonl"
                                .into(),
                            status: None,
                        }),
                        source_chain: vec!["connection reset by peer".into()],
                    }),
                }],
                aggregated_token_usage: None,
                requested_model_ref: "openai/gpt-5.4".into(),
                active_model_ref: None,
                winning_model_ref: None,
                pending_fallback_model_ref: None,
            },
            anyhow!("bad request"),
        ))
    }
}

#[async_trait]
impl AgentProvider for ContextLengthExceededProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        Err(provider_turn_error(
            "all configured providers failed for this turn: openai-codex/gpt-5.3-codex-spark: fail_fast (contract_error): context_length_exceeded",
            ProviderAttemptTimeline {
                attempts: vec![ProviderAttemptRecord {
                    provider: "openai-codex".into(),
                    model_ref: "openai-codex/gpt-5.3-codex-spark".into(),
                    attempt: 1,
                    max_attempts: 3,
                    started_at: None,
                    completed_at: None,
                    duration_ms: Some(125),
                    failure_kind: Some("contract_error".into()),
                    disposition: Some("fail_fast".into()),
                    outcome: ProviderAttemptOutcome::FailFastAborted,
                    advanced_to_fallback: false,
                    backoff_ms: None,
                    token_usage: Some(TokenUsage::new(125_166, 0)),
                    transport_diagnostics: None,
                }],
                aggregated_token_usage: Some(TokenUsage::new(125_166, 0)),
                requested_model_ref: "openai-codex/gpt-5.3-codex-spark".into(),
                active_model_ref: None,
                winning_model_ref: None,
                pending_fallback_model_ref: None,
            },
            anyhow!("context_length_exceeded: input too long"),
        ))
    }
}

#[async_trait]
impl AgentProvider for DeferredFallbackProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        Err(provider_turn_error(
            "all configured providers failed for this turn: openai/gpt-5.4: retryable exhausted",
            ProviderAttemptTimeline {
                attempts: vec![ProviderAttemptRecord {
                    provider: "openai".into(),
                    model_ref: "openai/gpt-5.4".into(),
                    attempt: 3,
                    max_attempts: 3,
                    started_at: None,
                    completed_at: None,
                    duration_ms: Some(125),
                    failure_kind: Some("server_error".into()),
                    disposition: Some("retryable".into()),
                    outcome: ProviderAttemptOutcome::RetriesExhausted,
                    advanced_to_fallback: true,
                    backoff_ms: None,
                    token_usage: None,
                    transport_diagnostics: None,
                }],
                aggregated_token_usage: None,
                requested_model_ref: "openai/gpt-5.4".into(),
                active_model_ref: None,
                winning_model_ref: None,
                pending_fallback_model_ref: Some("anthropic/claude-sonnet-4-6".into()),
            },
            anyhow!("server unavailable"),
        ))
    }
}

#[async_trait]
impl AgentProvider for TextThenFailingFallbackProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        if *calls == 1 {
            return Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::Text {
                    text: "Partial report heading".into(),
                }],
                stop_reason: Some("max_output_tokens".into()),
                input_tokens: 20,
                output_tokens: 10,
                cache_usage: None,
                request_diagnostics: None,
            });
        }

        Err(provider_turn_error(
            "all configured providers failed for this turn: openai/gpt-5.4: retryable exhausted",
            ProviderAttemptTimeline {
                attempts: vec![ProviderAttemptRecord {
                    provider: "openai".into(),
                    model_ref: "openai/gpt-5.4".into(),
                    attempt: 3,
                    max_attempts: 3,
                    started_at: None,
                    completed_at: None,
                    duration_ms: Some(125),
                    failure_kind: Some("server_error".into()),
                    disposition: Some("retryable".into()),
                    outcome: ProviderAttemptOutcome::RetriesExhausted,
                    advanced_to_fallback: true,
                    backoff_ms: None,
                    token_usage: None,
                    transport_diagnostics: None,
                }],
                aggregated_token_usage: None,
                requested_model_ref: "openai/gpt-5.4".into(),
                active_model_ref: None,
                winning_model_ref: None,
                pending_fallback_model_ref: Some("anthropic/claude-sonnet-4-6".into()),
            },
            anyhow!("server unavailable"),
        ))
    }
}

pub(crate) struct StagnatingAfterVerificationProvider {
    pub(crate) calls: Mutex<usize>,
}

pub(crate) struct SkillReadProvider {
    pub(crate) calls: Mutex<usize>,
}

pub(crate) struct SkillActivationCommandProvider {
    pub(crate) calls: Mutex<usize>,
    pub(crate) tool_name: &'static str,
    pub(crate) input: serde_json::Value,
}

pub(crate) struct CountingProvider {
    pub(crate) calls: Mutex<usize>,
    pub(crate) reply: &'static str,
}

impl StagnatingAfterVerificationProvider {
    pub(crate) fn new() -> Self {
        Self {
            calls: Mutex::new(0),
        }
    }
}

impl SkillReadProvider {
    pub(crate) fn new() -> Self {
        Self {
            calls: Mutex::new(0),
        }
    }
}

impl SkillActivationCommandProvider {
    pub(crate) fn new(tool_name: &'static str, input: serde_json::Value) -> Self {
        Self {
            calls: Mutex::new(0),
            tool_name,
            input,
        }
    }
}

impl CountingProvider {
    pub(crate) async fn call_count(&self) -> usize {
        *self.calls.lock().await
    }
}

#[async_trait]
impl AgentProvider for StagnatingAfterVerificationProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        if request.tools.is_empty() {
            return Ok(ProviderTurnResponse {
                blocks: vec![ModelBlock::Text {
                    text: "What changed: app.txt\nWhy: to address the requested task.\nVerification: successful verification command completed.".into(),
                }],
                stop_reason: None,
                input_tokens: 25,
                output_tokens: 25,
                cache_usage: None,
                request_diagnostics: None,
            });
        }

        let mut calls = self.calls.lock().await;
        *calls += 1;

        let blocks = match *calls {
            1 => vec![
                ModelBlock::ToolUse {
                    id: "patch".into(),
                    name: "ApplyPatch".into(),
                    input: serde_json::json!({
                        "patch": "--- a/app.txt\n+++ b/app.txt\n@@ -1,1 +1,1 @@\n-before\n+after\n",
                    }),
                },
                ModelBlock::ToolUse {
                    id: "verify".into(),
                    name: "ExecCommand".into(),
                    input: serde_json::json!({
                        "cmd": "printf 'tests passed'",
                        "shell": "sh",
                    }),
                },
            ],
            2 => vec![ModelBlock::ToolUse {
                id: "read".into(),
                name: "ExecCommand".into(),
                input: serde_json::json!({
                    "cmd": "cat app.txt",
                    "workdir": ".",
                }),
            }],
            _ => vec![ModelBlock::ToolUse {
                id: "agent".into(),
                name: "AgentGet".into(),
                input: serde_json::json!({}),
            }],
        };

        Ok(ProviderTurnResponse {
            blocks,
            stop_reason: None,
            input_tokens: 25,
            output_tokens: 25,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for SkillReadProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;

        let blocks = match *calls {
            1 => vec![ModelBlock::ToolUse {
                id: "read-skill".into(),
                name: "ExecCommand".into(),
                input: serde_json::json!({
                    "cmd": "cat .agents/skills/demo/SKILL.md",
                    "workdir": ".",
                }),
            }],
            _ => vec![ModelBlock::Text {
                text: "Skill loaded and applied.".into(),
            }],
        };

        Ok(ProviderTurnResponse {
            blocks,
            stop_reason: None,
            input_tokens: 20,
            output_tokens: 20,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for SkillActivationCommandProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;

        let blocks = match *calls {
            1 => vec![ModelBlock::ToolUse {
                id: "activate-skill".into(),
                name: self.tool_name.into(),
                input: self.input.clone(),
            }],
            _ => vec![ModelBlock::Text {
                text: "Skill activation observed.".into(),
            }],
        };

        Ok(ProviderTurnResponse {
            blocks,
            stop_reason: None,
            input_tokens: 20,
            output_tokens: 20,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}

#[async_trait]
impl AgentProvider for CountingProvider {
    async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut calls = self.calls.lock().await;
        *calls += 1;
        Ok(ProviderTurnResponse {
            blocks: vec![ModelBlock::Text {
                text: self.reply.into(),
            }],
            stop_reason: None,
            input_tokens: 10,
            output_tokens: 5,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}
