//! OpenAI Responses ↔ Anthropic Messages format conversion module (used when the Codex upstream is an Anthropic gateway)
//!
//! Scenario: The Codex CLI only speaks the OpenAI Responses protocol, while the
//! upstream AI gateway only offers the native Anthropic Messages protocol
//! (`/v1/messages`). This module converts the Responses request sent by Codex
//! into an Anthropic request, then converts the Anthropic response back into a
//! Responses response.
//!
//! The direction is exactly the mirror of `transform_responses.rs`:
//! - `transform_responses.rs`: Anthropic request → Responses request, Responses response → Anthropic response
//! - this module:               Responses request → Anthropic request, Anthropic response → Responses response

use super::transform_codex_chat::{
    build_codex_tool_context_from_request, response_tool_call_item_from_chat_name,
    response_tool_call_item_id_from_chat_name, CodexToolContext,
};
use super::transform_responses::{sanitize_anthropic_tool_use_input, TOOL_RESULT_ERROR_MARKER};
use crate::proxy::error::ProxyError;
use crate::proxy::json_canonical::canonical_json_string;
use crate::proxy::sse::{strip_sse_field, take_sse_block};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use serde_json::{json, Value};
use std::collections::{BTreeMap, HashSet};

pub(crate) const ANTHROPIC_THINKING_ENCRYPTED_PREFIX: &str = "ccswitch-anthropic-thinking-v1:";
const TOOL_SEARCH_PROXY_NAME: &str = "tool_search";

/// Maps Codex's reasoning.effort to the token budget for Anthropic thinking.
///
/// Returning `None` indicates an unrecognized effort value—in that case extended
/// thinking should not be enabled (to avoid accidentally swallowing
/// temperature/top_p), keeping normal sampling.
pub(crate) fn effort_to_thinking_budget(effort: &str) -> Option<u64> {
    match effort.trim().to_ascii_lowercase().as_str() {
        "minimal" | "low" => Some(2048),
        "medium" => Some(8192),
        "high" => Some(16384),
        "xhigh" | "max" => Some(24576),
        _ => None,
    }
}

fn codex_effort_to_anthropic(effort: &str) -> Option<&'static str> {
    match effort.trim().to_ascii_lowercase().as_str() {
        "minimal" | "low" => Some("low"),
        "medium" => Some("medium"),
        "high" => Some("high"),
        "xhigh" | "max" => Some("max"),
        _ => None,
    }
}

fn reasoning_explicitly_disabled(effort: Option<&str>) -> bool {
    matches!(
        effort
            .map(str::trim)
            .map(str::to_ascii_lowercase)
            .as_deref(),
        Some("none" | "off" | "disabled")
    )
}

/// Preserve an Anthropic signed thinking/redacted-thinking block inside the opaque
/// Responses `reasoning.encrypted_content` field so Codex replays it on the next
/// tool-result request. The prefix keeps unrelated providers' ciphertext isolated.
pub(crate) fn encode_anthropic_thinking_block(block: &Value) -> Option<String> {
    match block.get("type").and_then(|value| value.as_str()) {
        Some("thinking")
            if block
                .get("signature")
                .and_then(Value::as_str)
                .is_some_and(|value| !value.is_empty()) => {}
        Some("redacted_thinking")
            if block
                .get("data")
                .and_then(Value::as_str)
                .is_some_and(|value| !value.is_empty()) => {}
        _ => return None,
    }
    let bytes = serde_json::to_vec(block).ok()?;
    Some(format!(
        "{ANTHROPIC_THINKING_ENCRYPTED_PREFIX}{}",
        URL_SAFE_NO_PAD.encode(bytes)
    ))
}

pub(crate) fn decode_anthropic_thinking_block(encrypted_content: &str) -> Option<Value> {
    let encoded = encrypted_content.strip_prefix(ANTHROPIC_THINKING_ENCRYPTED_PREFIX)?;
    let bytes = URL_SAFE_NO_PAD.decode(encoded).ok()?;
    let block: Value = serde_json::from_slice(&bytes).ok()?;
    // Reuse the encoder's validation so legacy/malformed bridge envelopes cannot
    // replay an unsigned thinking block into an Anthropic tool turn.
    encode_anthropic_thinking_block(&block).map(|_| block)
}

pub(crate) fn responses_reasoning_item_from_anthropic_block(
    item_id: &str,
    block: &Value,
) -> Option<Value> {
    let encrypted_content = encode_anthropic_thinking_block(block)?;
    let summary = block
        .get("thinking")
        .and_then(|value| value.as_str())
        .filter(|text| !text.is_empty())
        .map(|text| vec![json!({ "type": "summary_text", "text": text })])
        .unwrap_or_default();
    Some(json!({
        "id": item_id,
        "type": "reasoning",
        "summary": summary,
        "encrypted_content": encrypted_content
    }))
}

/// Anthropic's stop_reason → Responses' (status, incomplete_details.reason)
pub(crate) fn map_anthropic_stop_reason_to_status(
    stop_reason: Option<&str>,
) -> (&'static str, Option<&'static str>) {
    match stop_reason {
        Some("max_tokens") => ("incomplete", Some("max_output_tokens")),
        // Safety refusal: report as incomplete to avoid Codex treating it as a normally-completed empty reply.
        Some("refusal") => ("incomplete", Some("content_filter")),
        Some("model_context_window_exceeded") => ("incomplete", Some("max_output_tokens")),
        // pause_turn is unreachable on this path (Codex requests do not declare Anthropic server-side tools);
        // if it does occur, log a warning and treat it as completed.
        Some("pause_turn") => {
            log::warn!("[Codex] Received unexpected Anthropic stop_reason=pause_turn, treating it as completed");
            ("completed", None)
        }
        _ => ("completed", None),
    }
}

/// Builds Responses usage from Anthropic usage.
///
/// Anthropic's `input_tokens` is the cache-excluded fresh input. OpenAI Responses
/// reports total input and exposes cache reads/writes as subsets:
///   input_tokens = fresh + cache_read + cache_creation
///   input_tokens_details.cached_tokens = cache_read
///   input_tokens_details.cache_write_tokens = cache_creation
///
/// The internal billing parser subtracts both subsets before charging the normal
/// input rate, then prices cache reads and writes separately.
pub(crate) fn build_responses_usage_from_anthropic(usage: Option<&Value>) -> Value {
    let u = match usage {
        Some(v) if v.is_object() => v,
        _ => {
            return json!({
                "input_tokens": 0,
                "output_tokens": 0,
                "total_tokens": 0,
                "output_tokens_details": { "reasoning_tokens": 0 }
            })
        }
    };

    let fresh_input = u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
    let output = u.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
    let reasoning = u
        .pointer("/output_tokens_details/thinking_tokens")
        .and_then(|v| v.as_u64())
        .unwrap_or(0);
    let cache_read = u
        .get("cache_read_input_tokens")
        .and_then(|v| v.as_u64())
        .unwrap_or(0);
    let cache_creation = u
        .get("cache_creation_input_tokens")
        .and_then(|v| v.as_u64())
        .unwrap_or(0);

    let input_tokens = fresh_input
        .saturating_add(cache_read)
        .saturating_add(cache_creation);
    let total_tokens = input_tokens.saturating_add(output);

    let mut result = json!({
        "input_tokens": input_tokens,
        "output_tokens": output,
        "total_tokens": total_tokens,
        "output_tokens_details": { "reasoning_tokens": reasoning }
    });
    if cache_read > 0 || cache_creation > 0 {
        result["input_tokens_details"] = json!({
            "cached_tokens": cache_read,
            "cache_write_tokens": cache_creation
        });
    }
    // Keep the legacy top-level alias for one compatibility window. New code reads
    // the official nested cache_write_tokens field first.
    if cache_creation > 0 {
        result["cache_creation_input_tokens"] = json!(cache_creation);
    }
    result
}

/// OpenAI Responses request → Anthropic Messages request
///
/// `default_max_tokens`: injected when the Responses body has no
/// `max_output_tokens` (Anthropic's `max_tokens` is required; missing it yields a 400).
fn responses_system_text(item: &Value) -> Vec<String> {
    match item.get("content") {
        Some(Value::String(text)) if is_meaningful_text(text) => {
            vec![text.trim().to_string()]
        }
        Some(Value::Array(parts)) => parts
            .iter()
            .filter_map(|part| {
                matches!(
                    part.get("type").and_then(Value::as_str),
                    Some("input_text" | "output_text" | "text")
                )
                .then(|| part.get("text").and_then(Value::as_str))
                .flatten()
                .filter(|text| is_meaningful_text(text))
                .map(|text| text.trim().to_string())
            })
            .collect(),
        _ => Vec::new(),
    }
}

pub fn responses_request_to_anthropic(
    body: Value,
    default_max_tokens: u64,
) -> Result<Value, ProxyError> {
    let mut result = json!({});
    let tool_context = build_codex_tool_context_from_request(&body);
    let model = body
        .get("model")
        .and_then(|value| value.as_str())
        .unwrap_or("");

    // Pass model through (the upstream model has already been applied by the forwarder)
    if let Some(model) = body.get("model").and_then(|m| m.as_str()) {
        result["model"] = json!(model);
    }

    // instructions and historical system/developer messages → Anthropic system.
    // Anthropic messages only accept user/assistant roles; degrading these items to
    // user silently changes instruction precedence.
    let mut system_parts = Vec::new();
    if let Some(instructions) = body.get("instructions").and_then(|v| v.as_str()) {
        if is_meaningful_text(instructions) {
            system_parts.push(instructions.trim().to_string());
        }
    }
    if let Some(items) = body.get("input").and_then(Value::as_array) {
        for item in items {
            if matches!(
                item.get("role").and_then(Value::as_str),
                Some("system" | "developer")
            ) {
                system_parts.extend(responses_system_text(item));
            }
        }
    }
    if !system_parts.is_empty() {
        result["system"] = json!(system_parts.join("\n\n"));
    }

    // input → messages
    let mut messages = match body.get("input") {
        Some(Value::Array(items)) => convert_input_to_messages(items, &tool_context)?,
        Some(Value::String(text)) if is_meaningful_text(text) => vec![json!({
            "role": "user",
            "content": [{ "type": "text", "text": text }]
        })],
        _ => Vec::new(),
    };
    // Anthropic /v1/messages requires messages to be non-empty and the first to be user.
    // Normalize the history (compacted/resumed sessions may start with
    // assistant/function_call, or input may be entirely reasoning and thus empty after being dropped).
    // Drop incomplete tool turns first (they would otherwise 400), then guarantee a leading user.
    drop_incomplete_tool_turns(&mut messages);
    drop_empty_messages(&mut messages);
    ensure_leading_user_message(&mut messages);
    if messages.is_empty() {
        return Err(ProxyError::InvalidRequest(
            "cannot convert Codex request: empty messages".to_string(),
        ));
    }
    trim_trailing_assistant_text(&mut messages);
    drop_empty_messages(&mut messages);
    if messages.is_empty() {
        return Err(ProxyError::InvalidRequest(
            "cannot convert Codex request: empty messages".to_string(),
        ));
    }
    let thinking_history_is_valid = trailing_turn_supports_thinking(&messages);
    result["messages"] = json!(messages);

    let reasoning_effort = body
        .pointer("/reasoning/effort")
        .and_then(|value| value.as_str());
    let adaptive_model = crate::proxy::thinking_optimizer::uses_adaptive_thinking(model);
    let adaptive_by_default = crate::proxy::thinking_optimizer::adaptive_thinking_is_default(model);
    let cannot_disable_thinking =
        crate::proxy::thinking_optimizer::thinking_cannot_be_disabled(model);

    // max_output_tokens → max_tokens (required)
    let max_tokens = body
        .get("max_output_tokens")
        .and_then(|v| v.as_u64())
        .filter(|v| *v > 0)
        .unwrap_or(default_max_tokens);
    let mut thinking_enabled = false;
    let mut thinking_budget = reasoning_effort
        .and_then(effort_to_thinking_budget)
        .unwrap_or(0);
    let explicitly_disabled = reasoning_explicitly_disabled(reasoning_effort);
    let adaptive_should_think = adaptive_model
        && (adaptive_by_default
            || reasoning_effort
                .and_then(codex_effort_to_anthropic)
                .is_some());

    if !thinking_history_is_valid {
        if cannot_disable_thinking {
            return Err(ProxyError::InvalidRequest(
                "Anthropic model requires thinking, but the tool history has no signed thinking block to replay"
                    .to_string(),
            ));
        }
        if adaptive_should_think {
            result["thinking"] = json!({ "type": "disabled" });
        }
    } else if adaptive_should_think && (!explicitly_disabled || cannot_disable_thinking) {
        thinking_enabled = true;
        result["thinking"] = json!({ "type": "adaptive" });
        if let Some(effort) = reasoning_effort.and_then(codex_effort_to_anthropic) {
            result["output_config"] = json!({ "effort": effort });
        } else if explicitly_disabled && cannot_disable_thinking {
            // Fable/Mythos cannot turn thinking off. `low` is the closest safe
            // representation of Codex's explicit `none` request.
            result["output_config"] = json!({ "effort": "low" });
        }
    } else if explicitly_disabled {
        result["thinking"] = json!({ "type": "disabled" });
    } else if thinking_budget > 0 {
        thinking_enabled = true;
        // Anthropic requires max_tokens > budget_tokens and budget >= 1024. Reserve
        // headroom for the visible answer: cap the thinking budget at half of max_tokens
        // so a large derived budget (e.g. 24576 for xhigh) can't consume nearly all of a
        // modest max_tokens and leave ~1 output token (an effectively empty completion).
        // Do not raise the caller's max_tokens (it may exceed the model's output ceiling
        // and 400). If the remaining budget is below Anthropic's 1024 floor, disable
        // thinking and restore normal sampling.
        let ceiling = max_tokens / 2;
        thinking_budget = thinking_budget.min(ceiling);
        if thinking_budget < 1024 {
            thinking_enabled = false;
        }
    }
    result["max_tokens"] = json!(max_tokens);

    if thinking_enabled && !adaptive_model {
        result["thinking"] = json!({
            "type": "enabled",
            "budget_tokens": thinking_budget
        });
    }

    if !thinking_enabled {
        if let Some(v) = body.get("temperature") {
            result["temperature"] = v.clone();
        }
        if let Some(v) = body.get("top_p") {
            result["top_p"] = v.clone();
        }
    }

    if let Some(v) = body.get("stream") {
        result["stream"] = v.clone();
    }

    // Reuse the Codex tool context so function, namespace, custom, tool_search, and
    // dynamically loaded tools all receive stable flat names upstream.
    let anth_tools: Vec<Value> = tool_context
        .chat_tools()
        .iter()
        .filter_map(chat_tool_to_anthropic_tool)
        .collect();
    let has_tools = !anth_tools.is_empty();
    if has_tools {
        result["tools"] = json!(anth_tools);
    }

    // Only forward tool_choice when tools survived the filter. Anthropic 400s on a
    // tool_choice with no tools ("tool_choice may only be specified while providing
    // tools"), and that 400 is non-retryable — so a request whose only tools were
    // unsupported hosted tools (for example web_search) must drop tool_choice too.
    if has_tools {
        if let Some(tc) = body.get("tool_choice") {
            let mapped = map_tool_choice_to_anthropic(tc, &tool_context);
            let forced = matches!(
                mapped.get("type").and_then(|value| value.as_str()),
                Some("any" | "tool")
            );
            if thinking_enabled && forced {
                if cannot_disable_thinking {
                    return Err(ProxyError::InvalidRequest(
                        "Anthropic model requires adaptive thinking and cannot honor a forced tool_choice"
                            .to_string(),
                    ));
                }
                // Anthropic rejects forced tools while thinking is enabled. Preserve
                // the caller's explicit tool constraint and disable thinking for this
                // request instead of silently weakening `required`/named selection.
                result["thinking"] = json!({ "type": "disabled" });
                result.as_object_mut().unwrap().remove("output_config");
                if let Some(value) = body.get("temperature") {
                    result["temperature"] = value.clone();
                }
                if let Some(value) = body.get("top_p") {
                    result["top_p"] = value.clone();
                }
            }
            result["tool_choice"] = mapped;
        }

        if body.get("parallel_tool_calls").and_then(Value::as_bool) == Some(false) {
            if result.get("tool_choice").is_none() {
                result["tool_choice"] = json!({ "type": "auto" });
            }
            if let Some(tool_choice) = result.get_mut("tool_choice").and_then(Value::as_object_mut)
            {
                tool_choice.insert("disable_parallel_tool_use".to_string(), json!(true));
            }
        }
    }

    Ok(result)
}

fn chat_tool_to_anthropic_tool(chat_tool: &Value) -> Option<Value> {
    let function = chat_tool.get("function")?;
    let name = function
        .get("name")
        .and_then(|value| value.as_str())
        .map(str::trim)
        .filter(|value| !value.is_empty())?;
    let mut tool = json!({
        "name": name,
        "input_schema": function
            .get("parameters")
            .cloned()
            .filter(|value| value.as_object().is_some_and(|object| !object.is_empty()))
            .unwrap_or_else(|| json!({ "type": "object", "properties": {} }))
    });
    if let Some(description) = function.get("description").and_then(|value| value.as_str()) {
        tool["description"] = json!(description);
    }
    if let Some(strict) = function.get("strict").and_then(|value| value.as_bool()) {
        tool["strict"] = json!(strict);
    }
    Some(tool)
}

/// tool_choice: Responses → Anthropic (the reverse of `map_tool_choice_to_responses`)
fn map_tool_choice_to_anthropic(tool_choice: &Value, tool_context: &CodexToolContext) -> Value {
    match tool_choice {
        Value::String(s) => match s.as_str() {
            "required" => json!({ "type": "any" }),
            "auto" => json!({ "type": "auto" }),
            "none" => json!({ "type": "none" }),
            _ => json!({ "type": "auto" }),
        },
        Value::Object(obj) => match obj.get("type").and_then(|t| t.as_str()) {
            Some("function") => {
                let name = obj.get("name").and_then(|n| n.as_str()).unwrap_or("");
                let namespace = obj.get("namespace").and_then(|value| value.as_str());
                let upstream_name = tool_context.chat_name_for_response_function(name, namespace);
                json!({ "type": "tool", "name": upstream_name })
            }
            Some("custom") => json!({
                "type": "tool",
                "name": obj.get("name").and_then(|value| value.as_str()).unwrap_or("")
            }),
            Some("tool_search") => {
                json!({ "type": "tool", "name": TOOL_SEARCH_PROXY_NAME })
            }
            // Other object shapes (allowed_tools / hosted-tool selectors, etc.) are
            // not recognized by Anthropic; downgrade to auto to avoid passing OpenAI's
            // raw structure through and causing a 400.
            _ => json!({ "type": "auto" }),
        },
        _ => json!({ "type": "auto" }),
    }
}

/// Re-nests the flat Responses input[] back into Anthropic messages.
///
/// - input_text/output_text → text block of the corresponding role
/// - input_image → image block
/// - function_call → assistant's tool_use block (merged with the preceding assistant text into the same message)
/// - function_call_output → user's tool_result block (consecutive ones merged into the same user message)
/// - Anthropic-origin reasoning.encrypted_content → restored signed thinking block
fn convert_input_to_messages(
    items: &[Value],
    tool_context: &CodexToolContext,
) -> Result<Vec<Value>, ProxyError> {
    let mut messages: Vec<Value> = Vec::new();

    for item in items {
        let item_type = item.get("type").and_then(|t| t.as_str());
        if matches!(
            item_type,
            Some("function_call" | "custom_tool_call" | "tool_search_call")
        ) && item.get("status").and_then(Value::as_str) == Some("incomplete")
        {
            log::warn!(
                "[Codex/Anthropic] Dropping incomplete historical tool call: type={}, call_id={}",
                item_type.unwrap_or("unknown"),
                item.get("call_id")
                    .and_then(Value::as_str)
                    .or_else(|| item.get("id").and_then(Value::as_str))
                    .unwrap_or("")
            );
            continue;
        }

        match item_type {
            Some("function_call") => {
                let call_id = item
                    .get("call_id")
                    .and_then(|v| v.as_str())
                    .or_else(|| item.get("id").and_then(|v| v.as_str()))
                    .unwrap_or("");
                let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("");
                let namespace = item.get("namespace").and_then(|value| value.as_str());
                let upstream_name = tool_context.chat_name_for_response_function(name, namespace);
                let args_str = item.get("arguments").and_then(|v| v.as_str()).unwrap_or("");
                let input: Value = if args_str.trim().is_empty() {
                    json!({})
                } else {
                    serde_json::from_str(args_str).map_err(|error| {
                        ProxyError::InvalidRequest(format!(
                            "Invalid function_call arguments for '{name}': {error}"
                        ))
                    })?
                };
                if !input.is_object() {
                    return Err(ProxyError::InvalidRequest(format!(
                        "Function call arguments for '{name}' must be a JSON object"
                    )));
                }
                let input = sanitize_anthropic_tool_use_input(name, input);
                push_block(
                    &mut messages,
                    "assistant",
                    json!({
                        "type": "tool_use",
                        "id": call_id,
                        "name": upstream_name,
                        "input": input
                    }),
                );
            }
            Some("custom_tool_call") => {
                let call_id = item
                    .get("call_id")
                    .and_then(|value| value.as_str())
                    .or_else(|| item.get("id").and_then(|value| value.as_str()))
                    .unwrap_or("");
                let name = item
                    .get("name")
                    .and_then(|value| value.as_str())
                    .unwrap_or("");
                let input = item.get("input").cloned().unwrap_or_else(|| json!(""));
                push_block(
                    &mut messages,
                    "assistant",
                    json!({
                        "type": "tool_use",
                        "id": call_id,
                        "name": name,
                        "input": { "input": input }
                    }),
                );
            }
            Some("tool_search_call") => {
                let call_id = item
                    .get("call_id")
                    .and_then(|value| value.as_str())
                    .or_else(|| item.get("id").and_then(|value| value.as_str()))
                    .unwrap_or("");
                let input = item
                    .get("arguments")
                    .cloned()
                    .filter(Value::is_object)
                    .unwrap_or_else(|| json!({}));
                push_block(
                    &mut messages,
                    "assistant",
                    json!({
                        "type": "tool_use",
                        "id": call_id,
                        "name": TOOL_SEARCH_PROXY_NAME,
                        "input": input
                    }),
                );
            }
            Some("function_call_output" | "custom_tool_call_output" | "tool_search_output") => {
                let call_id = item.get("call_id").and_then(|v| v.as_str()).unwrap_or("");
                let output = tool_result_content_from_responses_item(item);
                let mut block = json!({
                    "type": "tool_result",
                    "tool_use_id": call_id,
                    "content": output.content
                });
                if output.is_error {
                    block["is_error"] = json!(true);
                }
                push_tool_result_block(&mut messages, block);
            }
            Some("input_text") => {
                if let Some(text) = item
                    .get("text")
                    .and_then(Value::as_str)
                    .filter(|text| is_meaningful_text(text))
                {
                    push_block(
                        &mut messages,
                        "user",
                        json!({ "type": "text", "text": text }),
                    );
                }
            }
            Some("input_image") => {
                if let Some(block) = image_block_from_input_image(item) {
                    push_block(&mut messages, "user", block);
                }
            }
            Some("reasoning") => {
                if let Some(block) = item
                    .get("encrypted_content")
                    .and_then(|value| value.as_str())
                    .and_then(decode_anthropic_thinking_block)
                {
                    push_assistant_thinking_block(&mut messages, block);
                }
            }
            // message item or an item carrying a role
            _ => {
                let role = item.get("role").and_then(|r| r.as_str()).unwrap_or("user");
                if matches!(role, "system" | "developer") {
                    continue;
                }
                let anth_role = if role == "assistant" {
                    "assistant"
                } else {
                    "user"
                };
                match item.get("content") {
                    Some(Value::String(text)) if is_meaningful_text(text) => {
                        push_block(
                            &mut messages,
                            anth_role,
                            json!({ "type": "text", "text": text }),
                        );
                    }
                    Some(Value::Array(parts)) => {
                        for part in parts {
                            let part_type = part.get("type").and_then(|t| t.as_str()).unwrap_or("");
                            match part_type {
                                "input_text" | "output_text" => {
                                    if let Some(text) = part
                                        .get("text")
                                        .and_then(|t| t.as_str())
                                        .filter(|t| is_meaningful_text(t))
                                    {
                                        push_block(
                                            &mut messages,
                                            anth_role,
                                            json!({ "type": "text", "text": text }),
                                        );
                                    }
                                }
                                "refusal" => {
                                    if let Some(text) = part
                                        .get("refusal")
                                        .and_then(|t| t.as_str())
                                        .filter(|t| is_meaningful_text(t))
                                    {
                                        push_block(
                                            &mut messages,
                                            anth_role,
                                            json!({ "type": "text", "text": text }),
                                        );
                                    }
                                }
                                "input_image" => {
                                    if let Some(block) = image_block_from_input_image(part) {
                                        push_block(&mut messages, anth_role, block);
                                    }
                                }
                                "input_file" => {
                                    if let Some(block) = document_block_from_input_file(part) {
                                        push_block(&mut messages, anth_role, block);
                                    }
                                }
                                _ => {}
                            }
                        }
                    }
                    _ => {}
                }
            }
        }
    }

    Ok(messages)
}

struct ToolResultContent {
    content: Value,
    is_error: bool,
}

fn tool_result_content_from_responses_item(item: &Value) -> ToolResultContent {
    match item.get("output") {
        Some(Value::String(text)) => ToolResultContent {
            content: json!(text),
            is_error: false,
        },
        Some(Value::Array(parts)) => {
            let mut content = Vec::new();
            let mut is_error = false;
            for part in parts {
                match part.get("type").and_then(Value::as_str) {
                    Some("input_text" | "output_text") => {
                        if let Some(text) = part.get("text").and_then(Value::as_str) {
                            if text == TOOL_RESULT_ERROR_MARKER {
                                is_error = true;
                            } else {
                                content.push(json!({"type":"text","text":text}));
                            }
                        }
                    }
                    Some("input_image") => {
                        if let Some(image) = image_block_from_input_image(part) {
                            content.push(image);
                        } else {
                            content.push(json!({
                                "type":"text",
                                "text":canonical_json_string(part)
                            }));
                        }
                    }
                    Some("input_file") => {
                        if let Some(document) = document_block_from_input_file(part) {
                            content.push(document);
                        } else {
                            content.push(json!({
                                "type":"text",
                                "text":canonical_json_string(part)
                            }));
                        }
                    }
                    _ => content.push(json!({
                        "type":"text",
                        "text":canonical_json_string(part)
                    })),
                }
            }
            ToolResultContent {
                content: Value::Array(content),
                is_error,
            }
        }
        Some(value) => ToolResultContent {
            content: json!(canonical_json_string(value)),
            is_error: false,
        },
        None => ToolResultContent {
            content: json!(canonical_json_string(item)),
            is_error: false,
        },
    }
}

/// Ensures the first message is a user: compacted/resumed sessions may start with
/// assistant or function_call, but Anthropic requires the first to be user, else 400.
/// An empty array is not handled (the caller decides whether to error).
fn ensure_leading_user_message(messages: &mut Vec<Value>) {
    let leads_with_user = messages
        .first()
        .and_then(|m| m.get("role"))
        .and_then(|r| r.as_str())
        == Some("user");
    if !messages.is_empty() && !leads_with_user {
        messages.insert(
            0,
            json!({
                "role": "user",
                "content": [{ "type": "text", "text": "(continuing the conversation)" }]
            }),
        );
    }
}

/// Removes compacted/resumed tool turns that no longer form a complete adjacent
/// assistant `tool_use` → user `tool_result` pair. Anthropic requires every tool
/// call in an assistant turn to be answered together in the immediately following
/// user turn. Dropping the whole incomplete assistant turn also avoids modifying a
/// subset of a signed thinking/tool-use response.
fn drop_incomplete_tool_turns(messages: &mut Vec<Value>) {
    let original = std::mem::take(messages);
    let mut sanitized = Vec::with_capacity(original.len());
    let mut index = 0;

    while index < original.len() {
        let message = &original[index];
        let is_assistant = message.get("role").and_then(Value::as_str) == Some("assistant");
        let tool_use_ids = if is_assistant {
            message_block_ids(message, "tool_use", "id")
        } else {
            Vec::new()
        };

        if !tool_use_ids.is_empty() {
            let paired_user = original
                .get(index + 1)
                .filter(|next| next.get("role").and_then(Value::as_str) == Some("user"));
            let tool_result_ids = paired_user
                .map(|user| message_block_ids(user, "tool_result", "tool_use_id"))
                .unwrap_or_default();
            let unique_tool_uses: HashSet<&str> = tool_use_ids.iter().copied().collect();
            let unique_tool_results: HashSet<&str> = tool_result_ids.iter().copied().collect();
            let complete = tool_use_ids.iter().all(|id| !id.is_empty())
                && tool_result_ids.iter().all(|id| !id.is_empty())
                && unique_tool_uses.len() == tool_use_ids.len()
                && unique_tool_results.len() == tool_result_ids.len()
                && unique_tool_uses == unique_tool_results;

            if complete {
                sanitized.push(message.clone());
                sanitized.push(paired_user.unwrap().clone());
            } else if let Some(user) = paired_user {
                let mut user = user.clone();
                drop_tool_result_blocks(&mut user);
                if message_has_content(&user) {
                    sanitized.push(user);
                }
            }

            index += if paired_user.is_some() { 2 } else { 1 };
            continue;
        }

        let mut message = message.clone();
        if message.get("role").and_then(Value::as_str) == Some("user") {
            // A user message not consumed as the adjacent half of a complete tool
            // pair cannot legally retain any tool_result blocks.
            drop_tool_result_blocks(&mut message);
        }
        if message_has_content(&message) {
            sanitized.push(message);
        }
        index += 1;
    }

    *messages = sanitized;
}

fn message_block_ids<'a>(message: &'a Value, block_type: &str, id_field: &str) -> Vec<&'a str> {
    message
        .get("content")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter(|block| block.get("type").and_then(Value::as_str) == Some(block_type))
        .map(|block| block.get(id_field).and_then(Value::as_str).unwrap_or(""))
        .collect()
}

fn drop_tool_result_blocks(message: &mut Value) {
    if let Some(content) = message.get_mut("content").and_then(Value::as_array_mut) {
        content.retain(|block| block.get("type").and_then(Value::as_str) != Some("tool_result"));
    }
}

fn message_has_content(message: &Value) -> bool {
    message
        .get("content")
        .and_then(Value::as_array)
        .map(|content| !content.is_empty())
        .unwrap_or(true)
}

/// A fresh user prompt can start a new thinking turn. A tool-result continuation
/// may keep thinking enabled only when the preceding assistant turn includes the
/// signed thinking/redacted-thinking block that Anthropic requires callers to replay.
fn trailing_turn_supports_thinking(messages: &[Value]) -> bool {
    let Some(last) = messages.last() else {
        return false;
    };
    if last.get("role").and_then(Value::as_str) != Some("user") {
        return false;
    }
    let mut tool_result_ids = Vec::new();
    if let Some(blocks) = last.get("content").and_then(Value::as_array) {
        for block in blocks {
            if block.get("type").and_then(Value::as_str) != Some("tool_result") {
                continue;
            }
            let Some(id) = block
                .get("tool_use_id")
                .and_then(Value::as_str)
                .filter(|id| !id.is_empty())
            else {
                return false;
            };
            tool_result_ids.push(id);
        }
    }
    if tool_result_ids.is_empty() {
        return true;
    }

    // A tool-result turn answers the immediately preceding assistant tool-use turn.
    // Looking any farther back can pick up an unrelated signed thinking block and
    // incorrectly re-enable thinking for an unsigned tool call.
    let Some(paired_assistant) = messages.get(messages.len().saturating_sub(2)) else {
        return false;
    };
    if paired_assistant.get("role").and_then(Value::as_str) != Some("assistant") {
        return false;
    }
    let Some(blocks) = paired_assistant.get("content").and_then(Value::as_array) else {
        return false;
    };
    let has_signed_thinking = blocks.iter().any(|block| {
        matches!(
            block.get("type").and_then(Value::as_str),
            Some("thinking" | "redacted_thinking")
        )
    });
    if !has_signed_thinking {
        return false;
    }

    let paired_tool_use_ids: HashSet<&str> = blocks
        .iter()
        .filter(|block| block.get("type").and_then(Value::as_str) == Some("tool_use"))
        .filter_map(|block| block.get("id").and_then(Value::as_str))
        .collect();
    tool_result_ids
        .iter()
        .all(|id| paired_tool_use_ids.contains(id))
}

/// Removes whitespace-only assistant prefills and trims trailing whitespace from a
/// real prefill. Anthropic rejects an assistant prefill whose final text ends in
/// whitespace, and Codex may replay an empty assistant text beside a tool call.
fn trim_trailing_assistant_text(messages: &mut [Value]) {
    let Some(last) = messages.last_mut() else {
        return;
    };
    if last.get("role").and_then(Value::as_str) != Some("assistant") {
        return;
    }
    let Some(blocks) = last.get_mut("content").and_then(Value::as_array_mut) else {
        return;
    };
    let Some(block) = blocks.last_mut() else {
        return;
    };
    if block.get("type").and_then(Value::as_str) != Some("text") {
        return;
    }
    let Some(text) = block.get("text").and_then(Value::as_str) else {
        return;
    };
    let trimmed = text.trim_end();
    if trimmed.is_empty() {
        blocks.pop();
    } else if trimmed.len() != text.len() {
        block["text"] = json!(trimmed);
    }
}

/// Anthropic 400s on a text content block whose text is empty or whitespace-only.
/// Such blocks arise when a prior Responses turn recorded an empty
/// input_text/output_text (e.g. an empty assistant text emitted alongside a
/// tool_use); replaying it verbatim would fail the next follow-up request.
fn is_meaningful_text(text: &str) -> bool {
    !text.trim().is_empty()
}

/// Removes messages whose content array ended up empty (e.g. a turn that carried
/// only empty text that was filtered out). Anthropic 400s on empty content.
fn drop_empty_messages(messages: &mut Vec<Value>) {
    messages.retain(|msg| {
        msg.get("content")
            .and_then(|c| c.as_array())
            .map(|arr| !arr.is_empty())
            .unwrap_or(true)
    });
}

/// Appends a content block to messages: merge if the last message has the same role, otherwise create a new message.
fn push_block(messages: &mut Vec<Value>, role: &str, block: Value) {
    if let Some(last) = messages.last_mut() {
        if last.get("role").and_then(|r| r.as_str()) == Some(role) {
            if let Some(arr) = last.get_mut("content").and_then(|c| c.as_array_mut()) {
                arr.push(block);
                return;
            }
        }
    }
    messages.push(json!({
        "role": role,
        "content": [block]
    }));
}

/// Appends a tool result to a user turn while preserving Anthropic's required
/// ordering: every tool_result block must precede any text or image blocks.
fn push_tool_result_block(messages: &mut Vec<Value>, block: Value) {
    if let Some(last) = messages.last_mut() {
        if last.get("role").and_then(Value::as_str) == Some("user") {
            if let Some(content) = last.get_mut("content").and_then(Value::as_array_mut) {
                let insert_at = content
                    .iter()
                    .position(|item| {
                        item.get("type").and_then(Value::as_str) != Some("tool_result")
                    })
                    .unwrap_or(content.len());
                content.insert(insert_at, block);
                return;
            }
        }
    }
    messages.push(json!({
        "role": "user",
        "content": [block]
    }));
}

fn push_assistant_thinking_block(messages: &mut Vec<Value>, block: Value) {
    if let Some(last) = messages.last_mut() {
        if last.get("role").and_then(Value::as_str) == Some("assistant") {
            if let Some(content) = last.get_mut("content").and_then(Value::as_array_mut) {
                let index = content
                    .iter()
                    .take_while(|item| {
                        matches!(
                            item.get("type").and_then(Value::as_str),
                            Some("thinking" | "redacted_thinking")
                        )
                    })
                    .count();
                content.insert(index, block);
                return;
            }
        }
    }
    push_block(messages, "assistant", block);
}

/// Responses' input_image → Anthropic image block.
fn image_block_from_input_image(part: &Value) -> Option<Value> {
    let url = part.get("image_url").and_then(|v| {
        v.as_str()
            .map(str::to_string)
            .or_else(|| v.get("url").and_then(|u| u.as_str()).map(str::to_string))
    })?;

    if let Some(rest) = url.strip_prefix("data:") {
        // data:<media_type>;base64,<data>
        let (meta, data) = rest.split_once(',')?;
        let media_type = meta.split(';').next().unwrap_or("image/png");
        Some(json!({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": media_type,
                "data": data
            }
        }))
    } else if url.starts_with("http://") || url.starts_with("https://") {
        Some(json!({
            "type": "image",
            "source": { "type": "url", "url": url }
        }))
    } else {
        None
    }
}

/// Responses' input_file → Anthropic document block.
fn document_block_from_input_file(part: &Value) -> Option<Value> {
    let filename = part
        .get("filename")
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty());

    let mut block = if let Some(file_url) = part
        .get("file_url")
        .and_then(Value::as_str)
        .filter(|url| url.starts_with("http://") || url.starts_with("https://"))
    {
        json!({
            "type":"document",
            "source":{"type":"url","url":file_url}
        })
    } else {
        let file_data = part.get("file_data").and_then(Value::as_str)?;
        let rest = file_data.strip_prefix("data:")?;
        let (meta, data) = rest.split_once(',')?;
        if data.is_empty() {
            return None;
        }
        let media_type = meta.split(';').next().unwrap_or("application/pdf");
        json!({
            "type":"document",
            "source":{
                "type":"base64",
                "media_type":media_type,
                "data":data
            }
        })
    };

    if let Some(filename) = filename {
        block["title"] = json!(filename);
    }
    Some(block)
}

/// Anthropic Messages response → OpenAI Responses response (non-streaming)
#[allow(dead_code)]
pub fn anthropic_response_to_responses(body: Value) -> Result<Value, ProxyError> {
    anthropic_response_to_responses_with_context(body, &CodexToolContext::default())
}

pub(crate) fn anthropic_response_to_responses_with_context(
    body: Value,
    tool_context: &CodexToolContext,
) -> Result<Value, ProxyError> {
    if body.get("type").and_then(Value::as_str) == Some("error") || body.get("error").is_some() {
        let error = body.get("error").unwrap_or(&body);
        let message = error
            .get("message")
            .and_then(Value::as_str)
            .or_else(|| error.as_str())
            .unwrap_or("Anthropic upstream returned an error envelope");
        let error_type = error.get("type").and_then(Value::as_str).unwrap_or("error");
        return Err(ProxyError::TransformError(format!(
            "Anthropic upstream {error_type}: {message}"
        )));
    }

    let id = body.get("id").and_then(|i| i.as_str()).unwrap_or("");
    let response_id = if id.is_empty() {
        "resp_ccswitch".to_string()
    } else if id.starts_with("resp_") {
        id.to_string()
    } else {
        format!("resp_{id}")
    };
    let model = body.get("model").and_then(|m| m.as_str()).unwrap_or("");

    let mut output: Vec<Value> = Vec::new();
    let mut text_parts: Vec<Value> = Vec::new();

    let flush_text = |output: &mut Vec<Value>, text_parts: &mut Vec<Value>| {
        if !text_parts.is_empty() {
            let idx = output.len();
            output.push(json!({
                "id": format!("{response_id}_msg_{idx}"),
                "type": "message",
                "status": "completed",
                "role": "assistant",
                "content": std::mem::take(text_parts)
            }));
        }
    };

    if let Some(blocks) = body.get("content").and_then(|c| c.as_array()) {
        for block in blocks {
            match block.get("type").and_then(|t| t.as_str()).unwrap_or("") {
                "text" => {
                    if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
                        text_parts.push(json!({
                            "type": "output_text",
                            "text": text,
                            "annotations": []
                        }));
                    }
                }
                "tool_use" => {
                    flush_text(&mut output, &mut text_parts);
                    let call_id = block.get("id").and_then(|v| v.as_str()).unwrap_or("");
                    let name = block.get("name").and_then(|v| v.as_str()).unwrap_or("");
                    let input = block.get("input").cloned().unwrap_or(json!({}));
                    let input = sanitize_anthropic_tool_use_input(name, input);
                    let item_id =
                        response_tool_call_item_id_from_chat_name(call_id, name, tool_context);
                    output.push(response_tool_call_item_from_chat_name(
                        &item_id,
                        "completed",
                        call_id,
                        name,
                        &canonical_json_string(&input),
                        None,
                        tool_context,
                    ));
                }
                "thinking" | "redacted_thinking" => {
                    flush_text(&mut output, &mut text_parts);
                    let idx = output.len();
                    if let Some(item) = responses_reasoning_item_from_anthropic_block(
                        &format!("rs_{response_id}_{idx}"),
                        block,
                    ) {
                        output.push(item);
                    }
                }
                _ => {}
            }
        }
    }
    flush_text(&mut output, &mut text_parts);

    let (status, incomplete_reason) =
        map_anthropic_stop_reason_to_status(body.get("stop_reason").and_then(|s| s.as_str()));
    let usage = build_responses_usage_from_anthropic(body.get("usage"));

    let mut result = json!({
        "id": response_id,
        "object": "response",
        "created_at": 0,
        "status": status,
        "model": model,
        "output": output,
        "usage": usage
    });
    if let Some(reason) = incomplete_reason {
        result["incomplete_details"] = json!({ "reason": reason });
    }

    Ok(result)
}

/// Aggregates an Anthropic Messages **SSE stream** (with no Content-Type marker)
/// back into a single Anthropic non-streaming message JSON.
///
/// Used as a fallback: the upstream returned an SSE body for a `stream:false`
/// request but without the `text/event-stream` header (symmetric to the
/// `body_looks_like_sse` fallback on the chat / claude side, see #2234). The
/// aggregated message can be handed directly to [`anthropic_response_to_responses`].
///
/// It also tolerates the last event missing a trailing blank line (truncated
/// stream): after looping over complete event blocks, it processes the residual
/// buffer as the last event.
pub fn anthropic_sse_to_message_value(body: &str) -> Result<Value, ProxyError> {
    let mut message: Option<Value> = None;
    // Collect blocks by content index along with the partial_json accumulator for their tool_use.
    let mut blocks: BTreeMap<u64, Value> = BTreeMap::new();
    let mut json_accum: BTreeMap<u64, String> = BTreeMap::new();
    let mut stop_reason: Option<String> = None;
    let mut delta_output_tokens: Option<u64> = None;
    let mut saw_message_stop = false;

    let mut buffer = body.to_string();
    let process_block = |block: &str,
                         message: &mut Option<Value>,
                         blocks: &mut BTreeMap<u64, Value>,
                         json_accum: &mut BTreeMap<u64, String>,
                         stop_reason: &mut Option<String>,
                         delta_output_tokens: &mut Option<u64>,
                         saw_message_stop: &mut bool|
     -> Result<(), ProxyError> {
        let mut data = String::new();
        for line in block.lines() {
            if let Some(chunk) = strip_sse_field(line, "data") {
                if !data.is_empty() {
                    data.push('\n');
                }
                data.push_str(chunk);
            }
        }
        if data.trim().is_empty() || data.trim() == "[DONE]" {
            return Ok(());
        }
        let value: Value = match serde_json::from_str(data.trim()) {
            Ok(v) => v,
            Err(_) => return Ok(()), // Skip events that cannot be parsed (ping, etc.)
        };
        match value.get("type").and_then(|t| t.as_str()).unwrap_or("") {
            "message_start" => {
                if let Some(msg) = value.get("message") {
                    *message = Some(msg.clone());
                }
            }
            "content_block_start" => {
                if let Some(index) = value.get("index").and_then(|v| v.as_u64()) {
                    let block = value.get("content_block").cloned().unwrap_or(json!({}));
                    blocks.insert(index, block);
                    json_accum.entry(index).or_default();
                }
            }
            "content_block_delta" => {
                if let Some(index) = value.get("index").and_then(|v| v.as_u64()) {
                    let delta = value.get("delta").cloned().unwrap_or(json!({}));
                    match delta.get("type").and_then(|t| t.as_str()).unwrap_or("") {
                        "text_delta" => {
                            if let Some(text) = delta.get("text").and_then(|t| t.as_str()) {
                                append_str_field(
                                    blocks.entry(index).or_insert(json!({})),
                                    "text",
                                    text,
                                );
                            }
                        }
                        "thinking_delta" => {
                            if let Some(text) = delta.get("thinking").and_then(|t| t.as_str()) {
                                append_str_field(
                                    blocks.entry(index).or_insert(json!({})),
                                    "thinking",
                                    text,
                                );
                            }
                        }
                        "signature_delta" => {
                            if let Some(sig) = delta.get("signature").and_then(|t| t.as_str()) {
                                blocks.entry(index).or_insert(json!({}))["signature"] = json!(sig);
                            }
                        }
                        "input_json_delta" => {
                            if let Some(partial) =
                                delta.get("partial_json").and_then(|t| t.as_str())
                            {
                                json_accum.entry(index).or_default().push_str(partial);
                            }
                        }
                        _ => {}
                    }
                }
            }
            "content_block_stop" => {
                if let Some(index) = value.get("index").and_then(|v| v.as_u64()) {
                    if let Some(accum) = json_accum.get(&index) {
                        if !accum.trim().is_empty() {
                            let parsed: Value =
                                serde_json::from_str(accum).unwrap_or_else(|_| json!({}));
                            if let Some(block) = blocks.get_mut(&index) {
                                block["input"] = parsed;
                            }
                        }
                    }
                }
            }
            "message_delta" => {
                if let Some(reason) = value.pointer("/delta/stop_reason").and_then(|v| v.as_str()) {
                    *stop_reason = Some(reason.to_string());
                }
                if let Some(output) = value
                    .pointer("/usage/output_tokens")
                    .and_then(|v| v.as_u64())
                {
                    *delta_output_tokens = Some(output);
                }
            }
            "message_stop" => *saw_message_stop = true,
            "error" => {
                let msg = value
                    .pointer("/error/message")
                    .and_then(|v| v.as_str())
                    .unwrap_or("upstream anthropic SSE error");
                return Err(ProxyError::TransformError(format!(
                    "anthropic SSE error event: {msg}"
                )));
            }
            _ => {}
        }
        Ok(())
    };

    while let Some(block) = take_sse_block(&mut buffer) {
        process_block(
            &block,
            &mut message,
            &mut blocks,
            &mut json_accum,
            &mut stop_reason,
            &mut delta_output_tokens,
            &mut saw_message_stop,
        )?;
    }
    // Tolerate the last event missing a trailing blank line (truncated stream).
    if !buffer.trim().is_empty() {
        process_block(
            &buffer.clone(),
            &mut message,
            &mut blocks,
            &mut json_accum,
            &mut stop_reason,
            &mut delta_output_tokens,
            &mut saw_message_stop,
        )?;
    }

    let mut message = message.ok_or_else(|| {
        ProxyError::TransformError(
            "anthropic SSE aggregation: missing message_start event".to_string(),
        )
    })?;

    if !saw_message_stop && stop_reason.is_none() {
        if blocks.is_empty() {
            return Err(ProxyError::TransformError(
                "anthropic SSE aggregation: stream ended before message_stop".to_string(),
            ));
        }
        // Preserve partial content but make the truncation visible to Codex instead
        // of returning a normal completed response.
        stop_reason = Some("max_tokens".to_string());
    }

    // Merge in the content blocks (ordered by index), stop_reason, and the cumulative output_tokens.
    let content: Vec<Value> = blocks.into_values().collect();
    message["content"] = json!(content);
    if let Some(reason) = stop_reason {
        message["stop_reason"] = json!(reason);
    }
    if let Some(output) = delta_output_tokens {
        // message_delta's usage.output_tokens is a cumulative value, overriding the 0 from message_start.
        if let Some(usage) = message.get_mut("usage").and_then(|u| u.as_object_mut()) {
            usage.insert("output_tokens".to_string(), json!(output));
        }
    }

    Ok(message)
}

/// Appends content to a string field of a JSON object (creating it if absent).
fn append_str_field(block: &mut Value, field: &str, text: &str) {
    let existing = block
        .get(field)
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    block[field] = json!(format!("{existing}{text}"));
}

#[cfg(test)]
mod tests {
    use super::*;

    // ==================== Request: Responses → Anthropic ====================

    #[test]
    fn test_request_simple_text() {
        let input = json!({
            "model": "claude-3-5-sonnet",
            "max_output_tokens": 1024,
            "input": [
                { "role": "user", "content": [{ "type": "input_text", "text": "Hello" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["model"], "claude-3-5-sonnet");
        assert_eq!(result["max_tokens"], 1024);
        assert_eq!(result["messages"][0]["role"], "user");
        assert_eq!(result["messages"][0]["content"][0]["type"], "text");
        assert_eq!(result["messages"][0]["content"][0]["text"], "Hello");
    }

    #[test]
    fn test_request_missing_max_output_tokens_injects_default() {
        let input = json!({
            "model": "claude",
            "input": [{ "role": "user", "content": "Hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["max_tokens"], 4096);
    }

    #[test]
    fn test_request_instructions_to_system() {
        let input = json!({
            "model": "claude",
            "max_output_tokens": 100,
            "instructions": "You are helpful.",
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["system"], "You are helpful.");
    }

    #[test]
    fn test_request_system_and_developer_history_are_hoisted() {
        let input = json!({
            "model":"claude",
            "instructions":"base",
            "input":[
                {"role":"system","content":"system history"},
                {"role":"developer","content":[{"type":"input_text","text":"developer history"}]},
                {"role":"user","content":"hi"}
            ]
        });

        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(
            result["system"],
            "base\n\nsystem history\n\ndeveloper history"
        );
        assert_eq!(result["messages"].as_array().unwrap().len(), 1);
        assert_eq!(result["messages"][0]["role"], "user");
    }

    #[test]
    fn test_request_no_instructions_no_system() {
        let input = json!({
            "model": "claude",
            "max_output_tokens": 100,
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert!(result.get("system").is_none());
    }

    #[test]
    fn test_request_tools_and_filtering() {
        let input = json!({
            "model": "claude",
            "max_output_tokens": 100,
            "input": [{ "role": "user", "content": "hi" }],
            "tools": [
                { "type": "function", "name": "get_weather", "description": "d", "parameters": {"type": "object"} },
                { "type": "web_search" },
                { "type": "custom", "name": "apply_patch" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let tools = result["tools"].as_array().unwrap();
        assert_eq!(tools.len(), 2);
        assert_eq!(tools[0]["name"], "get_weather");
        assert_eq!(tools[0]["input_schema"]["type"], "object");
        assert!(tools[0].get("parameters").is_none());
        assert_eq!(tools[1]["name"], "apply_patch");
    }

    #[test]
    fn test_request_tool_choice_mapping() {
        // A function tool must be present, else tool_choice is (correctly) dropped.
        let base = |tc: Value| {
            json!({
                "model": "c", "max_output_tokens": 100,
                "input": [{ "role": "user", "content": "hi" }],
                "tools": [{ "type": "function", "name": "x", "parameters": {"type": "object"} }],
                "tool_choice": tc
            })
        };
        assert_eq!(
            responses_request_to_anthropic(base(json!("required")), 4096).unwrap()["tool_choice"],
            json!({"type": "any"})
        );
        assert_eq!(
            responses_request_to_anthropic(base(json!("auto")), 4096).unwrap()["tool_choice"],
            json!({"type": "auto"})
        );
        assert_eq!(
            responses_request_to_anthropic(base(json!({"type": "function", "name": "x"})), 4096)
                .unwrap()["tool_choice"],
            json!({"type": "tool", "name": "x"})
        );
    }

    #[test]
    fn test_request_function_call_renests_into_assistant_tool_use() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "assistant", "content": [{ "type": "output_text", "text": "Let me check" }] },
                { "type": "function_call", "call_id": "call_1", "name": "get_weather", "arguments": "{\"city\":\"Tokyo\"}" },
                { "type": "function_call_output", "call_id": "call_1", "output": "sunny" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        // The assistant-first history is normalized: a synthetic user message is
        // prepended, and the complete assistant tool turn remains intact.
        assert_eq!(messages.len(), 3);
        assert_eq!(messages[0]["role"], "user");
        assert_eq!(messages[1]["role"], "assistant");
        assert_eq!(messages[1]["content"][0]["type"], "text");
        assert_eq!(messages[1]["content"][1]["type"], "tool_use");
        assert_eq!(messages[1]["content"][1]["id"], "call_1");
        assert_eq!(messages[1]["content"][1]["input"]["city"], "Tokyo");
    }

    #[test]
    fn test_request_function_call_outputs_merge_into_one_user_message() {
        // Consecutive function_call_output items (each paired with a preceding
        // function_call) merge into a single user message of tool_result blocks.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call", "call_id": "c2", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "A" },
                { "type": "function_call_output", "call_id": "c2", "output": "B" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        // Leading assistant history is normalized with a synthetic leading user.
        let last = messages.last().unwrap();
        assert_eq!(last["role"], "user");
        let content = last["content"].as_array().unwrap();
        assert_eq!(content.len(), 2);
        assert_eq!(content[0]["type"], "tool_result");
        assert_eq!(content[0]["tool_use_id"], "c1");
        assert_eq!(content[0]["content"], "A");
        assert_eq!(content[1]["tool_use_id"], "c2");
    }

    #[test]
    fn test_request_unanswered_trailing_tool_use_is_dropped() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "user", "content": "run it" },
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
        assert_eq!(messages[0]["content"][0]["text"], "run it");
    }

    #[test]
    fn test_request_partial_parallel_tool_turn_is_dropped_as_a_unit() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "user", "content": "run both" },
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call", "call_id": "c2", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "one" },
                { "role": "user", "content": [{ "type": "input_text", "text": "continue" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert!(messages.iter().all(|message| {
            message["content"].as_array().unwrap().iter().all(|block| {
                !matches!(
                    block.get("type").and_then(Value::as_str),
                    Some("tool_use" | "tool_result")
                )
            })
        }));
        assert_eq!(messages.last().unwrap()["content"][0]["text"], "continue");
    }

    #[test]
    fn test_request_tool_results_precede_user_text() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "user", "content": "run it" },
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "role": "user", "content": [{ "type": "input_text", "text": "then explain" }] },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let content = result["messages"].as_array().unwrap().last().unwrap()["content"]
            .as_array()
            .unwrap();
        assert_eq!(content[0]["type"], "tool_result");
        assert_eq!(content[0]["tool_use_id"], "c1");
        assert_eq!(content[1]["type"], "text");
        assert_eq!(content[1]["text"], "then explain");
    }

    #[test]
    fn test_request_orphan_tool_result_dropped() {
        // A function_call_output whose matching function_call was dropped (e.g. by
        // compaction) becomes an orphan tool_result; it must be removed so Anthropic
        // does not 400, while the rest of the turn is preserved.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "function_call_output", "call_id": "ghost", "output": "X" },
                { "role": "user", "content": [{ "type": "input_text", "text": "hello" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
        let content = messages[0]["content"].as_array().unwrap();
        // Only the text survives; the orphan tool_result is gone.
        assert_eq!(content.len(), 1);
        assert_eq!(content[0]["type"], "text");
        assert_eq!(content[0]["text"], "hello");
    }

    #[test]
    fn test_request_empty_text_blocks_dropped() {
        // An empty/whitespace-only assistant text emitted alongside a tool_use must be
        // filtered out (Anthropic 400s on empty text blocks), keeping the tool_use, and
        // a user turn made up solely of empty text must not leave an empty message.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "user", "content": [{ "type": "input_text", "text": "hi" }] },
                { "role": "assistant", "content": [{ "type": "output_text", "text": "" }] },
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" },
                { "role": "user", "content": [{ "type": "input_text", "text": "   " }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        // Empty assistant text is gone; no message carries an empty text block.
        for msg in messages {
            for block in msg["content"].as_array().unwrap() {
                if block["type"] == "text" {
                    assert!(!block["text"].as_str().unwrap().trim().is_empty());
                }
            }
            assert!(!msg["content"].as_array().unwrap().is_empty());
        }
        // The whitespace-only trailing user turn collapsed into the tool_result user message.
        let last = messages.last().unwrap();
        assert_eq!(last["role"], "user");
        assert_eq!(last["content"][0]["type"], "tool_result");
    }

    #[test]
    fn test_request_all_orphan_tool_results_error() {
        // If the entire input is orphan tool_results, dropping them empties the
        // message list and conversion errors (nothing valid to send to Anthropic).
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "function_call_output", "call_id": "c1", "output": "A" },
                { "type": "function_call_output", "call_id": "c2", "output": "B" }
            ]
        });
        assert!(responses_request_to_anthropic(input, 4096).is_err());
    }

    #[test]
    fn test_request_paired_tool_result_kept() {
        // A tool_result whose function_call is present survives the orphan guard.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        let last = messages.last().unwrap();
        assert_eq!(last["role"], "user");
        assert_eq!(last["content"][0]["type"], "tool_result");
        assert_eq!(last["content"][0]["tool_use_id"], "c1");
    }

    #[test]
    fn test_request_empty_arguments_parses_to_empty_object() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "" },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        // function_call at the head → a synthetic user is prepended, tool_use is in the second assistant message.
        assert_eq!(result["messages"][1]["content"][0]["input"], json!({}));
    }

    #[test]
    fn test_request_invalid_or_non_object_arguments_error() {
        for arguments in ["{broken", "[1,2]"] {
            let input = json!({
                "model":"c",
                "input":[
                    {"type":"function_call","call_id":"c1","name":"t","arguments":arguments},
                    {"type":"function_call_output","call_id":"c1","output":"ok"}
                ]
            });
            assert!(matches!(
                responses_request_to_anthropic(input, 4096),
                Err(ProxyError::InvalidRequest(_))
            ));
        }
    }

    #[test]
    fn test_request_drops_incomplete_tool_call_and_orphaned_output() {
        let input = json!({
            "model":"c",
            "input":[
                {"role":"user","content":[{"type":"input_text","text":"run it"}]},
                {
                    "type":"function_call",
                    "call_id":"c1",
                    "name":"exec",
                    "arguments":"{\"cmd\":",
                    "status":"incomplete"
                },
                {"type":"function_call_output","call_id":"c1","output":"never ran"}
            ]
        });

        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let serialized = result["messages"].to_string();
        assert!(!serialized.contains("tool_use"));
        assert!(!serialized.contains("tool_result"));
        assert!(serialized.contains("run it"));
    }

    #[test]
    fn test_request_image_data_url() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [{
                "role": "user",
                "content": [
                    { "type": "input_text", "text": "what?" },
                    { "type": "input_image", "image_url": "data:image/png;base64,abc123" }
                ]
            }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let content = result["messages"][0]["content"].as_array().unwrap();
        assert_eq!(content[1]["type"], "image");
        assert_eq!(content[1]["source"]["type"], "base64");
        assert_eq!(content[1]["source"]["media_type"], "image/png");
        assert_eq!(content[1]["source"]["data"], "abc123");
    }

    #[test]
    fn test_request_top_level_content_items() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "input_text", "text": "what?" },
                { "type": "input_image", "image_url": "data:image/png;base64,abc123" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
        let content = messages[0]["content"].as_array().unwrap();
        assert_eq!(content[0]["type"], "text");
        assert_eq!(content[0]["text"], "what?");
        assert_eq!(content[1]["type"], "image");
        assert_eq!(content[1]["source"]["type"], "base64");
        assert_eq!(content[1]["source"]["data"], "abc123");
    }

    #[test]
    fn test_request_image_http_url() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [{
                "role": "user",
                "content": [{ "type": "input_image", "image_url": "https://x/y.png" }]
            }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let block = &result["messages"][0]["content"][0];
        assert_eq!(block["type"], "image");
        assert_eq!(block["source"]["type"], "url");
        assert_eq!(block["source"]["url"], "https://x/y.png");
    }

    #[test]
    fn test_request_effort_to_thinking_and_drops_temperature() {
        let input = json!({
            "model": "c",
            // Well above 2× the high-effort budget so the output-headroom cap
            // (max_tokens/2) does not clamp it — this test covers effort mapping.
            "max_output_tokens": 40000,
            "temperature": 0.7,
            "top_p": 0.9,
            "reasoning": { "effort": "high" },
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["thinking"]["type"], "enabled");
        assert_eq!(result["thinking"]["budget_tokens"], 16384);
        assert!(result.get("temperature").is_none());
        assert!(result.get("top_p").is_none());
    }

    #[test]
    fn test_adaptive_thinking_respects_model_defaults() {
        let request = |model: &str| {
            json!({
                "model": model,
                "max_output_tokens": 4096,
                "input": [{"role": "user", "content": "hi"}]
            })
        };
        let sonnet = responses_request_to_anthropic(request("claude-sonnet-5"), 4096).unwrap();
        assert_eq!(sonnet["thinking"]["type"], "adaptive");

        let opus = responses_request_to_anthropic(request("claude-opus-4.8"), 4096).unwrap();
        assert!(opus.get("thinking").is_none());
    }

    #[test]
    fn test_request_unknown_effort_keeps_sampling_params() {
        // An unrecognized effort should not enable thinking, nor swallow temperature/top_p.
        let input = json!({
            "model": "c",
            "max_output_tokens": 20000,
            "temperature": 0.7,
            "top_p": 0.9,
            "reasoning": { "effort": "turbo" },
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert!(result.get("thinking").is_none());
        assert_eq!(result["temperature"], 0.7);
        assert_eq!(result["top_p"], 0.9);
    }

    #[test]
    fn test_request_tool_history_disables_thinking() {
        // When tool history is present (function_call_output), do not inject thinking
        // even if effort is set, and fall back to passing temperature/top_p through,
        // to avoid the Anthropic 400 caused by a missing thinking block.
        let input = json!({
            "model": "c",
            "max_output_tokens": 20000,
            "temperature": 0.5,
            "reasoning": { "effort": "high" },
            "input": [
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert!(result.get("thinking").is_none());
        assert_eq!(result["temperature"], 0.5);
    }

    #[test]
    fn test_older_signed_turn_does_not_enable_thinking_for_unsigned_tool_call() {
        let encrypted = encode_anthropic_thinking_block(&json!({
            "type": "thinking",
            "thinking": "old reasoning",
            "signature": "sig_old"
        }))
        .unwrap();
        let input = json!({
            "model": "claude-sonnet-5",
            "max_output_tokens": 4096,
            "reasoning": { "effort": "high" },
            "input": [
                { "role": "user", "content": "first question" },
                { "type": "reasoning", "encrypted_content": encrypted },
                { "role": "assistant", "content": [{ "type": "output_text", "text": "first answer" }] },
                { "role": "user", "content": "call the tool" },
                { "type": "function_call", "call_id": "c2", "name": "Read", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c2", "output": "ok" }
            ]
        });

        let result = responses_request_to_anthropic(input, 4096).unwrap();

        assert_eq!(result["thinking"]["type"], "disabled");
        let messages = result["messages"].as_array().unwrap();
        let paired_assistant = &messages[messages.len() - 2];
        assert_eq!(paired_assistant["role"], "assistant");
        assert_eq!(paired_assistant["content"][0]["type"], "tool_use");
    }

    #[test]
    fn test_thinking_requires_all_parallel_tool_results_to_match_paired_turn() {
        let paired = json!([
            {"role": "assistant", "content": [
                {"type": "thinking", "thinking": "reason", "signature": "sig"},
                {"type": "tool_use", "id": "c1", "name": "a", "input": {}},
                {"type": "tool_use", "id": "c2", "name": "b", "input": {}}
            ]},
            {"role": "user", "content": [
                {"type": "tool_result", "tool_use_id": "c1", "content": "one"},
                {"type": "tool_result", "tool_use_id": "c2", "content": "two"}
            ]}
        ]);
        assert!(trailing_turn_supports_thinking(paired.as_array().unwrap()));

        let mismatched = json!([
            paired[0].clone(),
            {"role": "user", "content": [
                {"type": "tool_result", "tool_use_id": "c1", "content": "one"},
                {"type": "tool_result", "tool_use_id": "c3", "content": "three"}
            ]}
        ]);
        assert!(!trailing_turn_supports_thinking(
            mismatched.as_array().unwrap()
        ));
    }

    #[test]
    fn test_request_completed_tool_round_reenables_thinking() {
        // A *completed* tool round (assistant answered after the tool_result) followed
        // by a fresh user question: the trailing turn is text-only, so thinking must be
        // re-enabled — unlike a whole-history scan, which would stay off forever.
        let input = json!({
            "model": "c",
            "max_output_tokens": 20000,
            "reasoning": { "effort": "high" },
            "input": [
                { "role": "user", "content": [{ "type": "input_text", "text": "hi" }] },
                { "type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}" },
                { "type": "function_call_output", "call_id": "c1", "output": "ok" },
                { "role": "assistant", "content": [{ "type": "output_text", "text": "done" }] },
                { "role": "user", "content": [{ "type": "input_text", "text": "next question" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        // The last message is a text-only user turn → not mid tool-cycle → thinking on.
        let messages = result["messages"].as_array().unwrap();
        let last = messages.last().unwrap();
        assert_eq!(last["role"], "user");
        assert_eq!(last["content"][0]["type"], "text");
        assert_eq!(result["thinking"]["type"], "enabled");
    }

    #[test]
    fn test_request_custom_tool_survives_with_required_choice() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [{ "role": "user", "content": "hi" }],
            "tools": [
                { "type": "web_search" },
                { "type": "custom", "name": "apply_patch" }
            ],
            "tool_choice": "required"
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["tools"].as_array().unwrap().len(), 1);
        assert_eq!(result["tools"][0]["name"], "apply_patch");
        assert_eq!(result["tool_choice"], json!({ "type": "any" }));
    }

    #[test]
    fn test_request_forced_tool_choice_disables_thinking_instead_of_downgrading() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 20000,
            "reasoning": { "effort": "high" },
            "tools": [{ "type": "function", "name": "x", "parameters": {"type": "object"} }],
            "tool_choice": "required",
            "input": [
                { "role": "user", "content": [{ "type": "input_text", "text": "hi" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["thinking"]["type"], "disabled");
        assert_eq!(result["tool_choice"], json!({ "type": "any" }));
    }

    #[test]
    fn test_request_forced_tool_choice_kept_without_thinking() {
        // With no effort (thinking off), a forced tool_choice is kept as-is.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "tools": [{ "type": "function", "name": "x", "parameters": {"type": "object"} }],
            "tool_choice": "required",
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["tool_choice"], json!({ "type": "any" }));
    }

    #[test]
    fn test_request_small_max_tokens_disables_thinking() {
        // The chosen effort budget is clamped below max_tokens; after clamping it is
        // < 1024 → disable thinking, fall back to sampling, and do not raise the
        // caller's max_tokens (to avoid exceeding the model's output ceiling and 400).
        let input = json!({
            "model": "c",
            "max_output_tokens": 1000,
            "temperature": 0.7,
            "reasoning": { "effort": "high" },
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert!(result.get("thinking").is_none());
        assert_eq!(result["max_tokens"], 1000);
        assert_eq!(result["temperature"], 0.7);
    }

    #[test]
    fn test_request_thinking_budget_clamped_below_max_tokens() {
        // The chosen effort budget exceeds half of max_tokens, so it is capped at
        // max_tokens/2 (reserving the other half for the visible answer) while staying
        // >= 1024, so thinking stays enabled.
        let input = json!({
            "model": "c",
            "max_output_tokens": 5000,
            "reasoning": { "effort": "high" }, // budget 16384
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["thinking"]["type"], "enabled");
        assert_eq!(result["thinking"]["budget_tokens"], 2500);
        assert_eq!(result["max_tokens"], 5000);
    }

    #[test]
    fn test_request_default_max_tokens_leaves_output_headroom() {
        // Regression: on the no-max_output_tokens fallback path, a large derived thinking
        // budget must not consume nearly all of the default max_tokens. With default 8192
        // and high effort (16384), the budget is capped at 8192/2 = 4096, leaving 4096 for
        // the visible answer (previously it clamped to 8191, leaving ~1 output token).
        let input = json!({
            "model": "c",
            "reasoning": { "effort": "high" },
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 8192).unwrap();
        assert_eq!(result["thinking"]["type"], "enabled");
        assert_eq!(result["thinking"]["budget_tokens"], 4096);
        assert_eq!(result["max_tokens"], 8192);
        assert!(
            result["max_tokens"].as_u64().unwrap()
                - result["thinking"]["budget_tokens"].as_u64().unwrap()
                >= 4096,
            "at least half of max_tokens must remain for the visible answer"
        );
    }

    #[test]
    fn test_request_unpaired_tool_turn_is_dropped_before_thinking_gate() {
        // A resumed/compacted history ending in an unpaired assistant tool_use is
        // removed, leaving the original user prompt as a fresh thinking turn.
        let input = json!({
            "model": "c",
            "max_output_tokens": 40000,
            "reasoning": { "effort": "high" },
            "input": [
                { "role": "user", "content": [{ "type": "input_text", "text": "run it" }] },
                { "type": "function_call", "call_id": "c1", "name": "sh", "arguments": "{}" }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
        assert_eq!(result["thinking"]["type"], "enabled");
    }

    #[test]
    fn test_request_reasoning_item_dropped() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "type": "reasoning", "id": "rs_1", "encrypted_content": "xxx" },
                { "role": "user", "content": [{ "type": "input_text", "text": "hi" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
    }

    #[test]
    fn test_request_drops_openai_only_fields() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "store": false,
            "include": ["reasoning.encrypted_content"],
            "service_tier": "priority",
            "parallel_tool_calls": true,
            "input": [{ "role": "user", "content": "hi" }]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert!(result.get("store").is_none());
        assert!(result.get("include").is_none());
        assert!(result.get("service_tier").is_none());
        assert!(result.get("parallel_tool_calls").is_none());
    }

    // ==================== Response: Anthropic → Responses ====================

    #[test]
    fn test_response_text_end_turn() {
        let input = json!({
            "id": "msg_1",
            "type": "message",
            "role": "assistant",
            "model": "claude",
            "content": [{ "type": "text", "text": "Hello!" }],
            "stop_reason": "end_turn",
            "usage": { "input_tokens": 10, "output_tokens": 5 }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["id"], "resp_msg_1");
        assert_eq!(result["status"], "completed");
        assert_eq!(result["output"][0]["type"], "message");
        assert_eq!(result["output"][0]["content"][0]["type"], "output_text");
        assert_eq!(result["output"][0]["content"][0]["text"], "Hello!");
        assert_eq!(result["usage"]["input_tokens"], 10);
        assert_eq!(result["usage"]["output_tokens"], 5);
        assert_eq!(result["usage"]["total_tokens"], 15);
    }

    #[test]
    fn test_response_tool_use() {
        let input = json!({
            "id": "msg_1",
            "content": [{
                "type": "tool_use",
                "id": "call_1",
                "name": "get_weather",
                "input": { "city": "Tokyo" }
            }],
            "stop_reason": "tool_use",
            "usage": { "input_tokens": 10, "output_tokens": 15 }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["status"], "completed");
        assert_eq!(result["output"][0]["type"], "function_call");
        assert_eq!(result["output"][0]["call_id"], "call_1");
        assert_eq!(result["output"][0]["name"], "get_weather");
        assert_eq!(result["output"][0]["arguments"], "{\"city\":\"Tokyo\"}");
    }

    #[test]
    fn test_response_thinking_becomes_reasoning() {
        let input = json!({
            "id": "msg_1",
            "content": [
                { "type": "thinking", "thinking": "Let me think", "signature": "sig" },
                { "type": "text", "text": "answer" }
            ],
            "stop_reason": "end_turn",
            "usage": { "input_tokens": 1, "output_tokens": 2 }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["output"][0]["type"], "reasoning");
        assert_eq!(result["output"][0]["summary"][0]["text"], "Let me think");
        assert_eq!(result["output"][1]["type"], "message");
    }

    #[test]
    fn test_unsigned_thinking_is_not_replayed_as_encrypted_reasoning() {
        let input = json!({
            "id":"msg_unsigned",
            "content":[
                {"type":"thinking","thinking":"unsigned"},
                {"type":"text","text":"answer"}
            ],
            "stop_reason":"end_turn"
        });

        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["output"].as_array().unwrap().len(), 1);
        assert_eq!(result["output"][0]["type"], "message");
    }

    #[test]
    fn test_response_max_tokens_incomplete() {
        let input = json!({
            "id": "msg_1",
            "content": [{ "type": "text", "text": "partial" }],
            "stop_reason": "max_tokens",
            "usage": { "input_tokens": 1, "output_tokens": 2 }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["status"], "incomplete");
        assert_eq!(result["incomplete_details"]["reason"], "max_output_tokens");
    }

    #[test]
    fn test_response_usage_cache_no_double_count() {
        let input = json!({
            "id": "msg_1",
            "content": [{ "type": "text", "text": "x" }],
            "stop_reason": "end_turn",
            "usage": {
                "input_tokens": 20,
                "output_tokens": 5,
                "output_tokens_details": {"thinking_tokens": 3},
                "cache_read_input_tokens": 60,
                "cache_creation_input_tokens": 20
            }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        // Responses input_tokens is the inclusive total: fresh + read + write.
        assert_eq!(result["usage"]["input_tokens"], 100);
        assert_eq!(result["usage"]["output_tokens"], 5);
        assert_eq!(
            result["usage"]["output_tokens_details"]["reasoning_tokens"],
            3
        );
        // total includes input total + output exactly once.
        assert_eq!(result["usage"]["total_tokens"], 105);
        assert_eq!(result["usage"]["input_tokens_details"]["cached_tokens"], 60);
        assert_eq!(
            result["usage"]["input_tokens_details"]["cache_write_tokens"],
            20
        );
        // Aggregate cache creation is exposed for downstream billing attribution (counted only once).
        assert_eq!(result["usage"]["cache_creation_input_tokens"], 20);
    }

    #[test]
    fn test_response_read_tool_drops_empty_pages() {
        let input = json!({
            "id": "msg_1",
            "content": [{
                "type": "tool_use",
                "id": "call_1",
                "name": "Read",
                "input": { "file_path": "/tmp/x", "pages": "" }
            }],
            "stop_reason": "tool_use"
        });
        let result = anthropic_response_to_responses(input).unwrap();
        let args = result["output"][0]["arguments"].as_str().unwrap();
        assert!(args.contains("/tmp/x"));
        assert!(!args.contains("pages"));
    }

    #[test]
    fn test_response_refusal_is_incomplete_content_filter() {
        let input = json!({
            "id": "msg_1",
            "content": [{ "type": "text", "text": "" }],
            "stop_reason": "refusal",
            "usage": { "input_tokens": 1, "output_tokens": 0 }
        });
        let result = anthropic_response_to_responses(input).unwrap();
        assert_eq!(result["status"], "incomplete");
        assert_eq!(result["incomplete_details"]["reason"], "content_filter");
    }

    #[test]
    fn test_signed_thinking_round_trips_through_responses_tool_loop() {
        let converted = anthropic_response_to_responses(json!({
            "id": "msg_signed",
            "model": "claude-sonnet-5",
            "stop_reason": "tool_use",
            "content": [
                {"type": "thinking", "thinking": "check", "signature": "sig_123"},
                {"type": "tool_use", "id": "call_1", "name": "Read", "input": {"path": "/tmp/a"}}
            ]
        }))
        .unwrap();
        let reasoning = converted["output"][0].clone();
        assert!(reasoning["encrypted_content"]
            .as_str()
            .unwrap()
            .starts_with(ANTHROPIC_THINKING_ENCRYPTED_PREFIX));

        let replay = responses_request_to_anthropic(
            json!({
                "model": "claude-sonnet-5",
                "max_output_tokens": 4096,
                "reasoning": {"effort": "high"},
                "input": [
                    reasoning,
                    converted["output"][1].clone(),
                    {"type": "function_call_output", "call_id": "call_1", "output": "ok"}
                ]
            }),
            4096,
        )
        .unwrap();
        assert_eq!(replay["thinking"]["type"], "adaptive");
        assert_eq!(replay["messages"][1]["content"][0]["type"], "thinking");
        assert_eq!(replay["messages"][1]["content"][0]["signature"], "sig_123");
    }

    #[test]
    fn test_redacted_thinking_is_preserved_without_visible_summary() {
        let response = anthropic_response_to_responses(json!({
            "id": "msg_redacted",
            "content": [{"type": "redacted_thinking", "data": "opaque"}]
        }))
        .unwrap();
        assert_eq!(response["output"][0]["summary"], json!([]));
        assert!(response["output"][0]["encrypted_content"].is_string());
    }

    #[test]
    fn test_namespace_tool_response_restores_namespace() {
        let context = build_codex_tool_context_from_request(&json!({
            "tools": [{
                "type": "namespace",
                "name": "mcp_files",
                "tools": [{"type": "function", "name": "read", "parameters": {"type": "object"}}]
            }]
        }));
        let response = anthropic_response_to_responses_with_context(
            json!({
                "id": "msg_ns",
                "content": [{"type": "tool_use", "id": "call_1", "name": "mcp_files__read", "input": {}}]
            }),
            &context,
        )
        .unwrap();
        assert_eq!(response["output"][0]["type"], "function_call");
        assert_eq!(response["output"][0]["name"], "read");
        assert_eq!(response["output"][0]["namespace"], "mcp_files");
    }

    #[test]
    fn test_success_status_error_envelope_is_not_completed() {
        let error = anthropic_response_to_responses(json!({
            "type": "error",
            "error": {"type": "overloaded_error", "message": "busy"}
        }))
        .unwrap_err();
        assert!(error.to_string().contains("overloaded_error"));
    }

    #[test]
    fn test_context_window_stop_reason_is_incomplete() {
        let response = anthropic_response_to_responses(json!({
            "id": "msg_ctx",
            "stop_reason": "model_context_window_exceeded",
            "content": [{"type": "text", "text": "partial"}]
        }))
        .unwrap();
        assert_eq!(response["status"], "incomplete");
        assert_eq!(
            response["incomplete_details"]["reason"],
            "max_output_tokens"
        );
    }

    #[test]
    fn test_request_parallel_tool_calls_false_disables_parallel_use() {
        let response = responses_request_to_anthropic(
            json!({
                "model": "c",
                "input": [{"role": "user", "content": "hi"}],
                "tools": [{"type": "function", "name": "x", "parameters": {"type": "object"}}],
                "parallel_tool_calls": false
            }),
            4096,
        )
        .unwrap();
        assert_eq!(response["tool_choice"]["type"], "auto");
        assert_eq!(response["tool_choice"]["disable_parallel_tool_use"], true);
    }

    #[test]
    fn test_request_trims_trailing_assistant_whitespace() {
        let response = responses_request_to_anthropic(
            json!({
                "model": "c",
                "input": [
                    {"role": "user", "content": "continue"},
                    {"role": "assistant", "content": [{"type": "output_text", "text": "prefix   \n"}]}
                ]
            }),
            4096,
        )
        .unwrap();
        assert_eq!(response["messages"][1]["content"][0]["text"], "prefix");
    }

    #[test]
    fn test_structured_tool_output_preserves_text_and_image_blocks() {
        let response = responses_request_to_anthropic(
            json!({
                "model": "c",
                "input": [
                    {"type": "function_call", "call_id": "c1", "name": "inspect", "arguments": "{}"},
                    {"type": "function_call_output", "call_id": "c1", "output": [
                        {"type": "output_text", "text": "result"},
                        {"type": "input_image", "image_url": "data:image/png;base64,abc"}
                    ]}
                ]
            }),
            4096,
        )
        .unwrap();
        let content = &response["messages"][2]["content"][0]["content"];
        assert_eq!(content[0]["type"], "text");
        assert_eq!(content[1]["type"], "image");
    }

    #[test]
    fn test_structured_tool_output_restores_error_file_and_unknown_parts() {
        let response = responses_request_to_anthropic(
            json!({
                "model":"c",
                "input":[
                    {"type":"function_call","call_id":"c1","name":"inspect","arguments":"{}"},
                    {"type":"function_call_output","call_id":"c1","output":[
                        {"type":"input_text","text":TOOL_RESULT_ERROR_MARKER},
                        {"type":"input_text","text":"failed"},
                        {"type":"input_file","file_url":"https://example.com/log.pdf","filename":"log.pdf"},
                        {"type":"future_part","payload":{"x":1}}
                    ]}
                ]
            }),
            4096,
        )
        .unwrap();

        let tool_result = &response["messages"][2]["content"][0];
        assert_eq!(tool_result["is_error"], true);
        assert_eq!(
            tool_result["content"][0],
            json!({"type":"text","text":"failed"})
        );
        assert_eq!(tool_result["content"][1]["type"], "document");
        assert_eq!(tool_result["content"][1]["source"]["type"], "url");
        assert_eq!(tool_result["content"][2]["type"], "text");
        assert!(tool_result["content"][2]["text"]
            .as_str()
            .unwrap()
            .contains("future_part"));
    }

    // ==================== Request normalization: non-empty & first is user ====================

    #[test]
    fn test_request_empty_messages_errors() {
        // input is entirely reasoning (dropped) → messages is empty → error explicitly rather than leaving it to the upstream 400.
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "instructions": "sys",
            "input": [
                { "type": "reasoning", "id": "rs_1", "encrypted_content": "xxx" }
            ]
        });
        assert!(responses_request_to_anthropic(input, 4096).is_err());
    }

    #[test]
    fn test_request_assistant_first_gets_leading_user() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [
                { "role": "assistant", "content": [{ "type": "output_text", "text": "hi" }] }
            ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let messages = result["messages"].as_array().unwrap();
        assert_eq!(messages[0]["role"], "user");
        assert_eq!(messages[1]["role"], "assistant");
    }

    // ==================== tools / tool_choice edge cases ====================

    #[test]
    fn test_request_tool_without_description_or_params() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [{ "role": "user", "content": "hi" }],
            "tools": [ { "type": "function", "name": "noop" } ]
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        let tool = &result["tools"][0];
        // Do not emit an explicit null description; input_schema falls back to a valid object schema.
        assert!(tool.get("description").is_none());
        assert_eq!(tool["input_schema"]["type"], "object");
    }

    #[test]
    fn test_request_unknown_object_tool_choice_degrades_to_auto() {
        let input = json!({
            "model": "c",
            "max_output_tokens": 100,
            "input": [{ "role": "user", "content": "hi" }],
            "tools": [{ "type": "function", "name": "x", "parameters": {"type": "object"} }],
            "tool_choice": { "type": "allowed_tools", "tools": [] }
        });
        let result = responses_request_to_anthropic(input, 4096).unwrap();
        assert_eq!(result["tool_choice"], json!({ "type": "auto" }));
    }

    // ==================== SSE aggregation fallback ====================

    #[test]
    fn test_anthropic_sse_aggregation_text_and_usage() {
        let sse = "event: message_start\n\
data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude\",\"content\":[],\"stop_reason\":null,\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}\n\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" world\"}}\n\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\n\
event: message_delta\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":7}}\n\n\
event: message_stop\n\
data: {\"type\":\"message_stop\"}\n\n";
        let msg = anthropic_sse_to_message_value(sse).unwrap();
        assert_eq!(msg["content"][0]["type"], "text");
        assert_eq!(msg["content"][0]["text"], "Hello world");
        assert_eq!(msg["stop_reason"], "end_turn");
        assert_eq!(msg["usage"]["input_tokens"], 10);
        assert_eq!(msg["usage"]["output_tokens"], 7);

        // The aggregated result can be converted directly into Responses.
        let resp = anthropic_response_to_responses(msg).unwrap();
        assert_eq!(resp["status"], "completed");
        assert_eq!(resp["output"][0]["content"][0]["text"], "Hello world");
    }

    #[test]
    fn test_anthropic_sse_aggregation_tool_use_partial_json() {
        let sse = "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"c\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"get_weather\",\"input\":{}}}\n\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\"}}\n\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"Tokyo\\\"}\"}}\n\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":3}}\n\n";
        let msg = anthropic_sse_to_message_value(sse).unwrap();
        assert_eq!(msg["content"][0]["type"], "tool_use");
        assert_eq!(msg["content"][0]["name"], "get_weather");
        assert_eq!(msg["content"][0]["input"]["city"], "Tokyo");
        assert_eq!(msg["stop_reason"], "tool_use");
    }

    #[test]
    fn test_anthropic_sse_aggregation_tool_use_input_only_in_start() {
        // Parity guard with the live streaming emitter's
        // `test_tool_use_input_only_in_start_event`: a gateway that carries the full tool
        // `input` on content_block_start and emits NO input_json_delta must still resolve
        // the same arguments (the empty-accum fallback keeps the start-carried input).
        let sse = "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"c\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Tokyo\"}}}\n\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":3}}\n\n";
        let msg = anthropic_sse_to_message_value(sse).unwrap();
        assert_eq!(msg["content"][0]["type"], "tool_use");
        assert_eq!(msg["content"][0]["name"], "get_weather");
        // Identical to the deltas-only case above — neither path may drop start input.
        assert_eq!(msg["content"][0]["input"]["city"], "Tokyo");
        assert_eq!(msg["stop_reason"], "tool_use");
    }

    #[test]
    fn test_anthropic_sse_aggregation_missing_message_start_errors() {
        let sse = "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n";
        assert!(anthropic_sse_to_message_value(sse).is_err());
    }

    #[test]
    fn test_anthropic_sse_aggregation_error_event_errors() {
        let sse = "data: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"overloaded\"}}\n\n";
        assert!(anthropic_sse_to_message_value(sse).is_err());
    }

    #[test]
    fn test_anthropic_sse_aggregation_tolerates_missing_trailing_blank_line() {
        // The last event missing a trailing blank line (truncated stream) should still be processed.
        let sse = "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"c\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"hi\"}}\n\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":2}}";
        let msg = anthropic_sse_to_message_value(sse).unwrap();
        assert_eq!(msg["stop_reason"], "end_turn");
        assert_eq!(msg["usage"]["output_tokens"], 2);
    }

    #[test]
    fn test_anthropic_sse_aggregation_truncated_output_is_incomplete() {
        let sse = "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"content\":[]}}\n\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"partial\"}}\n\n";
        let message = anthropic_sse_to_message_value(sse).unwrap();
        let response = anthropic_response_to_responses(message).unwrap();
        assert_eq!(response["status"], "incomplete");
        assert_eq!(
            response["incomplete_details"]["reason"],
            "max_output_tokens"
        );
    }

    #[test]
    fn test_anthropic_sse_aggregation_truncated_without_output_errors() {
        let sse =
            "data: {\"type\":\"message_start\",\"message\":{\"id\":\"m\",\"content\":[]}}\n\n";
        assert!(anthropic_sse_to_message_value(sse).is_err());
    }
}
