use std::borrow::Cow;

use serde_json::{Value, json};

use crate::error::{LiterLlmError, Result};
use crate::provider::Provider;
use crate::types::{ChatCompletionChunk, FinishReason, StreamChoice, StreamDelta, StreamFunctionCall, StreamToolCall};

/// Anthropic's stable API version. This is the only GA version as of 2025;
/// Anthropic gates new features via beta headers (e.g. `anthropic-beta`),
/// not by bumping the version date.
static ANTHROPIC_EXTRA_HEADERS: &[(&str, &str)] = &[("anthropic-version", "2023-06-01")];

/// Default max_tokens for Anthropic requests when none is specified.
/// Anthropic requires this field; OpenAI makes it optional.
const DEFAULT_MAX_TOKENS: u64 = 4096;

/// Known Anthropic hosted tool type names that require beta headers.
const HOSTED_TOOL_TYPES: &[&str] = &[
    "computer_20241022",
    "computer_use_20250124",
    "web_search_20250305",
    "code_execution_20250522",
];

/// Anthropic beta header values for hosted tool features.
const BETA_COMPUTER_USE: &str = "computer-use-2025-01-24";
const BETA_WEB_SEARCH: &str = "web-search-2025-03-05";
const BETA_CODE_EXECUTION: &str = "code-execution-2025-05-22";
const BETA_THINKING: &str = "thinking-2025-04-14";
const BETA_PROMPT_CACHING: &str = "prompt-caching-2024-07-31";
const BETA_PDFS: &str = "pdfs-2024-09-25";

/// Anthropic provider (Claude model family).
///
/// Differences from the OpenAI-compatible baseline:
/// - Auth uses `x-api-key` instead of `Authorization: Bearer`.
/// - Requires a mandatory `anthropic-version` header on every request.
/// - Model names start with `claude-` or are routed via the `anthropic/` prefix.
/// - Chat endpoint is `/messages`, not `/chat/completions`.
/// - Request and response JSON formats differ from OpenAI.
pub struct AnthropicProvider;

impl Provider for AnthropicProvider {
    fn name(&self) -> &str {
        "anthropic"
    }

    fn base_url(&self) -> &str {
        "https://api.anthropic.com/v1"
    }

    fn auth_header<'a>(&'a self, api_key: &'a str) -> Option<(Cow<'static, str>, Cow<'a, str>)> {
        // Anthropic uses x-api-key, not Authorization: Bearer.
        Some((Cow::Borrowed("x-api-key"), Cow::Borrowed(api_key)))
    }

    fn extra_headers(&self) -> &'static [(&'static str, &'static str)] {
        ANTHROPIC_EXTRA_HEADERS
    }

    /// Compute request-dependent beta headers based on the request body.
    ///
    /// Inspects the body for features that require Anthropic beta headers:
    /// - `thinking` field present -> `anthropic-beta: thinking-2025-04-14`
    /// - Hosted tools (computer_use, web_search, code_execution) -> appropriate betas
    ///
    /// Multiple betas are combined with comma separator.
    fn dynamic_headers(&self, body: &serde_json::Value) -> Vec<(String, String)> {
        let mut betas: Vec<&str> = Vec::new();

        // Check for extended thinking.
        if body.get("thinking").is_some() {
            betas.push(BETA_THINKING);
        }

        // Check for hosted tools in the tools array.
        if let Some(tools) = body.get("tools").and_then(|t| t.as_array()) {
            for tool in tools {
                let tool_type = tool.get("type").and_then(|t| t.as_str()).unwrap_or("");
                match tool_type {
                    "computer_20241022" | "computer_use_20250124" if !betas.contains(&BETA_COMPUTER_USE) => {
                        betas.push(BETA_COMPUTER_USE);
                    }
                    "web_search_20250305" if !betas.contains(&BETA_WEB_SEARCH) => {
                        betas.push(BETA_WEB_SEARCH);
                    }
                    "code_execution_20250522" if !betas.contains(&BETA_CODE_EXECUTION) => {
                        betas.push(BETA_CODE_EXECUTION);
                    }
                    _ => {}
                }
            }
        }

        // Check for prompt caching: any `cache_control` field anywhere in the body.
        if body_contains_cache_control(body) && !betas.contains(&BETA_PROMPT_CACHING) {
            betas.push(BETA_PROMPT_CACHING);
        }

        // Check for PDF/document content blocks.
        if body_contains_document_block(body) && !betas.contains(&BETA_PDFS) {
            betas.push(BETA_PDFS);
        }

        if betas.is_empty() {
            vec![]
        } else {
            vec![("anthropic-beta".to_owned(), betas.join(","))]
        }
    }

    fn matches_model(&self, model: &str) -> bool {
        model.starts_with("claude-") || model.starts_with("anthropic/")
    }

    fn strip_model_prefix<'m>(&self, model: &'m str) -> &'m str {
        model.strip_prefix("anthropic/").unwrap_or(model)
    }

    /// Anthropic uses `/messages` instead of `/chat/completions`.
    fn chat_completions_path(&self) -> &str {
        "/messages"
    }

    /// Transform an OpenAI-format request body into Anthropic Messages API format.
    ///
    /// Key differences handled here:
    /// - System messages extracted to top-level `system` field as content blocks.
    /// - User/assistant messages converted to Anthropic content block arrays.
    /// - Tool messages (role=tool) become user messages with `tool_result` blocks.
    /// - Consecutive same-role messages are merged (Anthropic requires alternating roles).
    /// - `max_tokens` defaults to 4096 if not set (Anthropic requires it).
    /// - `stop` renamed to `stop_sequences` and normalised to an array.
    /// - `tool_choice` mapped from OpenAI semantics to Anthropic semantics.
    /// - Tools converted from OpenAI `function` wrappers to Anthropic `input_schema` format.
    /// - Unsupported parameters removed: `n`, `presence_penalty`, `frequency_penalty`,
    ///   `logit_bias`, `stream` (the client handles stream separately).
    fn transform_request(&self, body: &mut Value) -> Result<()> {
        // ── 0. Validate messages array is non-empty ────────────────────────────
        // Take ownership of the messages array to avoid cloning.
        let messages = body
            .as_object_mut()
            .and_then(|o| o.remove("messages"))
            .and_then(|v| match v {
                Value::Array(arr) => Some(arr),
                _ => None,
            })
            .unwrap_or_default();

        if messages.is_empty() {
            return Err(LiterLlmError::BadRequest {
                message: "messages array must not be empty".to_owned(),
                status: 400,
            });
        }

        // ── 1. Extract system messages ────────────────────────────────────────
        let mut system_blocks: Vec<Value> = Vec::new();
        let mut non_system_messages: Vec<Value> = Vec::new();

        for msg in messages {
            let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");
            match role {
                "system" | "developer" => {
                    // Both system and developer roles map to Anthropic system content.
                    // Content may be a string or a content-block array.
                    match msg.get("content") {
                        Some(Value::String(s)) if !s.is_empty() => {
                            let mut block = json!({"type": "text", "text": s});
                            // Propagate cache_control if present.
                            if let Some(cc) = msg.get("cache_control") {
                                block["cache_control"] = cc.clone();
                            }
                            system_blocks.push(block);
                        }
                        Some(Value::Array(parts)) => {
                            for part in parts {
                                system_blocks.push(part.clone());
                            }
                        }
                        _ => {}
                    }
                }
                _ => non_system_messages.push(msg),
            }
        }

        if !system_blocks.is_empty() {
            body["system"] = json!(system_blocks);
        }

        // ── 2. Convert non-system messages to Anthropic format ────────────────
        let converted_messages: Vec<Value> = non_system_messages
            .into_iter()
            .map(convert_message_to_anthropic)
            .collect();

        // ── 3. Merge consecutive same-role messages ───────────────────────────
        // Anthropic requires strictly alternating user/assistant roles.
        let merged_messages = merge_consecutive_same_role(converted_messages);

        body["messages"] = json!(merged_messages);

        // ── 4. Ensure max_tokens is present (required by Anthropic) ───────────
        // Also handle the OpenAI alias max_completion_tokens.
        if body.get("max_tokens").is_none() {
            if let Some(mct) = body.get("max_completion_tokens").cloned() {
                body["max_tokens"] = mct;
            } else {
                body["max_tokens"] = json!(DEFAULT_MAX_TOKENS);
            }
        }
        body.as_object_mut().map(|o| o.remove("max_completion_tokens"));

        // ── 5. Convert stop → stop_sequences (must be array) ─────────────────
        if let Some(stop) = body.as_object_mut().and_then(|o| o.remove("stop")) {
            let stop_sequences = match stop {
                Value::String(s) => json!([s]),
                arr @ Value::Array(_) => arr,
                _ => json!([]),
            };
            body["stop_sequences"] = stop_sequences;
        }

        // ── 6. Convert tool_choice ─────────────────────────────────────────────
        if let Some(tool_choice) = body.as_object_mut().and_then(|o| o.remove("tool_choice")) {
            let anthropic_tool_choice = convert_tool_choice(&tool_choice);
            match anthropic_tool_choice {
                Some(tc) => {
                    body["tool_choice"] = tc;
                }
                None => {
                    // tool_choice: "none" → remove tools entirely
                    body.as_object_mut().map(|o| o.remove("tools"));
                }
            }
        }

        // ── 7. Convert tools from OpenAI format to Anthropic format ───────────
        // Hosted tools (computer_use, web_search, code_execution) are passed through
        // as-is in Anthropic's native format; only function-type tools are converted.
        if let Some(tools) = body.as_object_mut().and_then(|o| o.remove("tools"))
            && let Some(tools_array) = tools.as_array()
        {
            let anthropic_tools: Vec<Value> = tools_array
                .iter()
                .map(|tool| {
                    let tool_type = tool.get("type").and_then(|t| t.as_str()).unwrap_or("");
                    if is_hosted_tool_type(tool_type) {
                        // Pass through hosted tool definitions as-is.
                        tool.clone()
                    } else {
                        convert_tool_to_anthropic(tool)
                    }
                })
                .collect();
            body["tools"] = json!(anthropic_tools);
        }

        // ── 7a. Extended thinking / reasoning effort ──────────────────────────
        // Map reasoning_effort (from body or extra_body) to Anthropic's thinking block.
        let reasoning_effort = body
            .as_object_mut()
            .and_then(|o| o.remove("reasoning_effort"))
            .and_then(|v| v.as_str().map(String::from))
            .or_else(|| {
                body.pointer("/extra_body/reasoning_effort")
                    .and_then(|v| v.as_str().map(String::from))
            });

        if let Some(effort) = reasoning_effort {
            let budget_tokens: u64 = match effort.as_str() {
                "low" => 1024,
                "medium" => 4096,
                "high" => 16384,
                _ => 4096, // default to medium for unknown values
            };
            body["thinking"] = json!({
                "type": "enabled",
                "budget_tokens": budget_tokens
            });

            // Anthropic requires max_tokens >= budget_tokens + 1 when thinking
            // is enabled.  Auto-increase if the current value is too low.
            let min_max_tokens = budget_tokens + 1;
            let current_max = body.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
            if current_max < min_max_tokens {
                body["max_tokens"] = json!(min_max_tokens);
            }
        }

        // ── 7b. Response format → JSON mode ───────────────────────────────────
        // Anthropic doesn't have a native JSON mode, so we prepend system instructions.
        if let Some(response_format) = body.as_object_mut().and_then(|o| o.remove("response_format")) {
            let rf_type = response_format.get("type").and_then(|t| t.as_str()).unwrap_or("");
            match rf_type {
                "json_object" => {
                    // Prepend a system instruction to respond with valid JSON.
                    let instruction = json!({"type": "text", "text": "Respond with valid JSON only. Do not include any text outside the JSON object."});
                    if let Some(system) = body.get_mut("system").and_then(|s| s.as_array_mut()) {
                        system.insert(0, instruction);
                    } else {
                        body["system"] = json!([instruction]);
                    }
                }
                "json_schema" => {
                    // Prepend the schema as a system instruction.
                    if let Some(schema_def) = response_format.get("json_schema") {
                        let schema_name = schema_def.get("name").and_then(|n| n.as_str()).unwrap_or("output");
                        let schema = schema_def.get("schema").cloned().unwrap_or(json!({}));
                        let schema_str = serde_json::to_string_pretty(&schema).unwrap_or_default();
                        let instruction_text = format!(
                            "Respond with valid JSON matching the following schema named '{schema_name}':\n```json\n{schema_str}\n```\nDo not include any text outside the JSON object."
                        );
                        let instruction = json!({"type": "text", "text": instruction_text});
                        if let Some(system) = body.get_mut("system").and_then(|s| s.as_array_mut()) {
                            system.insert(0, instruction);
                        } else {
                            body["system"] = json!([instruction]);
                        }
                    }
                }
                _ => {} // "text" or unknown — no-op
            }
        }

        // ── 8. Remove unsupported parameters ──────────────────────────────────
        // NOTE: `stream` is intentionally kept — Anthropic requires it in the
        // request body to enable streaming responses.
        if let Some(obj) = body.as_object_mut() {
            for key in &[
                "n",
                "presence_penalty",
                "frequency_penalty",
                "logit_bias",
                "stream_options",
                "parallel_tool_calls",
                "service_tier",
                "user",
                "reasoning_effort",
                "extra_body",
            ] {
                obj.remove(*key);
            }
        }

        Ok(())
    }

    /// Normalize an Anthropic Messages API response into OpenAI chat completion format.
    ///
    /// Anthropic response shape:
    /// ```json
    /// { "id": "msg_...", "type": "message", "role": "assistant",
    ///   "content": [{"type": "text", "text": "..."}],
    ///   "stop_reason": "end_turn",
    ///   "usage": {"input_tokens": N, "output_tokens": M} }
    /// ```
    fn transform_response(&self, body: &mut Value) -> Result<()> {
        // Only transform if this looks like an Anthropic response (has "stop_reason").
        if body.get("stop_reason").is_none() {
            return Ok(());
        }

        let id = body.get("id").cloned().unwrap_or(json!(""));
        let model = body.get("model").cloned().unwrap_or(json!(""));

        let content_blocks = body.get("content").and_then(|v| v.as_array()).cloned();

        // Extract text content from `type: "text"` blocks only.
        // Thinking blocks (`type: "thinking"`) are Anthropic's internal chain-of-thought
        // and are intentionally excluded from the user-facing content field.
        // Citation blocks (`type: "citation"`) are metadata — the text they reference
        // is already included in adjacent text blocks, so they are skipped here.
        let text_content: Option<String> = content_blocks.as_ref().map(|blocks| {
            blocks
                .iter()
                .filter(|b| b.get("type").and_then(|t| t.as_str()) == Some("text"))
                .filter_map(|b| b.get("text").and_then(|t| t.as_str()))
                .collect::<Vec<_>>()
                .join("")
        });

        // Extract tool_use blocks into OpenAI-format tool_calls.
        // Treat both "tool_use" and "server_tool_use" the same way.
        let tool_calls: Option<Vec<Value>> = content_blocks.as_ref().map(|blocks| {
            blocks
                .iter()
                .filter(|b| {
                    matches!(
                        b.get("type").and_then(|t| t.as_str()),
                        Some("tool_use") | Some("server_tool_use")
                    )
                })
                .map(|b| {
                    let arguments = serde_json::to_string(b.get("input").unwrap_or(&json!({}))).unwrap_or_default();
                    json!({
                        "id": b.get("id").cloned().unwrap_or(json!("")),
                        "type": "function",
                        "function": {
                            "name": b.get("name").cloned().unwrap_or(json!("")),
                            "arguments": arguments
                        }
                    })
                })
                .collect()
        });

        // Map Anthropic stop_reason → OpenAI finish_reason.
        let stop_reason = body.get("stop_reason").and_then(|v| v.as_str()).unwrap_or("end_turn");
        let finish_reason = map_stop_reason(stop_reason);

        // Map Anthropic usage → OpenAI usage.
        // Cache tokens count towards prompt tokens for billing purposes.
        let input_tokens = body
            .pointer("/usage/input_tokens")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let cache_creation_tokens = body
            .pointer("/usage/cache_creation_input_tokens")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let cache_read_tokens = body
            .pointer("/usage/cache_read_input_tokens")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let output_tokens = body
            .pointer("/usage/output_tokens")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let prompt_tokens = input_tokens + cache_creation_tokens + cache_read_tokens;

        // Build the message object — content is null when only tool_calls are present.
        let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
        let message_content = if has_tool_calls && text_content.as_deref().unwrap_or("").is_empty() {
            Value::Null
        } else {
            json!(text_content)
        };

        let mut message = json!({
            "role": "assistant",
            "content": message_content
        });

        if let (Some(tc), true) = (tool_calls, has_tool_calls) {
            message["tool_calls"] = json!(tc);
        }

        *body = json!({
            "id": id,
            "object": "chat.completion",
            "created": super::unix_timestamp_secs(),
            "model": model,
            "choices": [{
                "index": 0,
                "message": message,
                "finish_reason": finish_reason
            }],
            "usage": {
                "prompt_tokens": prompt_tokens,
                "completion_tokens": output_tokens,
                "total_tokens": prompt_tokens + output_tokens
            }
        });

        Ok(())
    }

    /// Parse an Anthropic SSE event into an OpenAI-compatible `ChatCompletionChunk`.
    ///
    /// Anthropic event types handled:
    /// - `message_start`: emits a role-only delta chunk.
    /// - `content_block_start`: emits empty delta (tool_use: emits tool_call header chunk).
    /// - `content_block_delta`: emits text, thinking, or tool input JSON delta.
    /// - `message_delta`: emits final chunk with finish_reason and usage.
    /// - `message_stop`: signals end of stream, returns `Ok(None)`.
    /// - `content_block_stop`, `ping`: skipped (returns `Ok(None)` — no content to emit).
    /// - `error`: returns `Err(LiterLlmError::Streaming)`.
    ///
    /// **Note:** The `id` and `model` fields are only populated on the first
    /// chunk (`message_start`).  Subsequent chunks emit empty strings for both
    /// fields because this parser is stateless — it cannot carry forward values
    /// from earlier events.  This differs from the OpenAI format where every
    /// chunk includes `id` and `model`.
    fn parse_stream_event(&self, event_data: &str) -> Result<Option<ChatCompletionChunk>> {
        // NOTE: `[DONE]` is handled at the SSE parser level; no check needed here.

        let event: Value = serde_json::from_str(event_data).map_err(|e| LiterLlmError::Streaming {
            message: format!("failed to parse Anthropic SSE event: {e}"),
        })?;

        let event_type = event.get("type").and_then(|t| t.as_str()).unwrap_or("");

        match event_type {
            "message_start" => {
                let msg = &event["message"];
                let id = msg.get("id").and_then(|v| v.as_str()).unwrap_or("").to_owned();
                let model = msg.get("model").and_then(|v| v.as_str()).unwrap_or("").to_owned();

                // Anthropic sends initial usage in message_start (input tokens only).
                // Also account for cache tokens in the prompt count.
                let input_tokens = msg.pointer("/usage/input_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
                let cache_creation = msg
                    .pointer("/usage/cache_creation_input_tokens")
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);
                let cache_read = msg
                    .pointer("/usage/cache_read_input_tokens")
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);
                let prompt_tokens = input_tokens + cache_creation + cache_read;

                let usage = if prompt_tokens > 0 {
                    Some(crate::types::Usage {
                        prompt_tokens,
                        completion_tokens: 0,
                        total_tokens: prompt_tokens,
                        prompt_tokens_details: None,
                    })
                } else {
                    None
                };

                Ok(Some(ChatCompletionChunk {
                    id,
                    object: "chat.completion.chunk".to_owned(),
                    created: super::unix_timestamp_secs(),
                    model,
                    choices: vec![StreamChoice {
                        index: 0,
                        delta: StreamDelta {
                            role: Some("assistant".to_owned()),
                            content: None,
                            tool_calls: None,
                            function_call: None,
                            refusal: None,
                        },
                        finish_reason: None,
                    }],
                    usage,
                    system_fingerprint: None,
                    service_tier: None,
                }))
            }

            "content_block_start" => {
                // For tool_use blocks, emit the tool_call header (id + name, empty arguments).
                let block = &event["content_block"];
                let block_type = block.get("type").and_then(|t| t.as_str()).unwrap_or("");
                // NOTE: We use Anthropic's block index directly, which counts ALL content blocks
                // (text + thinking + tool_use). This differs from OpenAI's sequential tool-call
                // indices (0, 1, 2...) but is safe because:
                // 1. The same index is used in both content_block_start and content_block_delta,
                //    so they are consistent with each other.
                // 2. OpenAI clients typically correlate tool calls by `id`, not by index.
                // 3. OpenAI's own streaming format allows index gaps in tool_calls[].
                // Known limitation: indices may have gaps when text/thinking blocks precede tool_use.
                let anthropic_index = event.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as u32;

                if block_type == "tool_use" || block_type == "server_tool_use" {
                    let tool_id = block.get("id").and_then(|v| v.as_str()).unwrap_or("").to_owned();
                    let tool_name = block.get("name").and_then(|v| v.as_str()).unwrap_or("").to_owned();

                    return Ok(Some(make_empty_chunk_with_tool_start(
                        anthropic_index,
                        tool_id,
                        tool_name,
                    )));
                }

                // Text / thinking block start — emit Ok(None) since there is no content yet.
                Ok(None)
            }

            "content_block_delta" => {
                let delta = &event["delta"];
                let delta_type = delta.get("type").and_then(|t| t.as_str()).unwrap_or("");
                let index = event.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as u32;

                match delta_type {
                    "text_delta" => {
                        let text = delta.get("text").and_then(|t| t.as_str()).unwrap_or("");
                        Ok(Some(make_text_chunk("", "", text)))
                    }
                    "thinking_delta" => {
                        // Extended thinking blocks are Anthropic's internal chain-of-thought.
                        // Non-streaming already filters thinking blocks from content, so
                        // streaming must be consistent: skip them entirely.
                        Ok(None)
                    }
                    "input_json_delta" => {
                        // Partial JSON for tool input — emit as tool_call arguments delta.
                        let partial_json = delta.get("partial_json").and_then(|v| v.as_str()).unwrap_or("");
                        Ok(Some(make_tool_arguments_delta(index, partial_json)))
                    }
                    _ => Ok(None),
                }
            }

            "message_delta" => {
                // Final chunk: carries stop_reason and output token count.
                let stop_reason = event.pointer("/delta/stop_reason").and_then(|v| v.as_str());
                let finish_reason = stop_reason.map(map_stop_reason);
                let output_tokens = event.pointer("/usage/output_tokens").and_then(|v| v.as_u64());

                let finish = finish_reason.map(|fr| match fr {
                    "stop" => FinishReason::Stop,
                    "length" => FinishReason::Length,
                    "tool_calls" => FinishReason::ToolCalls,
                    _ => FinishReason::Other,
                });

                let usage = output_tokens.map(|ct| crate::types::Usage {
                    prompt_tokens: 0,
                    completion_tokens: ct,
                    total_tokens: ct,
                    prompt_tokens_details: None,
                });

                Ok(Some(ChatCompletionChunk {
                    id: String::new(),
                    object: "chat.completion.chunk".to_owned(),
                    created: super::unix_timestamp_secs(),
                    model: String::new(),
                    choices: vec![StreamChoice {
                        index: 0,
                        delta: StreamDelta {
                            role: None,
                            content: None,
                            tool_calls: None,
                            function_call: None,
                            refusal: None,
                        },
                        finish_reason: finish,
                    }],
                    usage,
                    system_fingerprint: None,
                    service_tier: None,
                }))
            }

            "message_stop" => Ok(None),

            "content_block_stop" | "ping" => {
                // These events carry no delta content; return Ok(None).
                Ok(None)
            }

            "error" => {
                let message = event
                    .pointer("/error/message")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown Anthropic streaming error");
                Err(LiterLlmError::Streaming {
                    message: message.to_owned(),
                })
            }

            _ => {
                // Unknown event types are silently skipped.
                Ok(None)
            }
        }
    }
}

// ── Helper functions ──────────────────────────────────────────────────────────

/// Sanitize a tool_call_id so it only contains characters allowed by Anthropic: `[a-zA-Z0-9_-]`.
/// Any other character is replaced with `_`.
///
/// Convert an OpenAI `image_url` URL to an Anthropic image source block.
///
/// Handles two cases:
/// - Data URIs (`data:<media_type>;base64,<data>`) → base64 source.
/// - Plain URLs → url source.
fn convert_image_url_to_anthropic_source(url: &str) -> Value {
    if url.starts_with("data:")
        && let Some((header, data)) = url.split_once(',')
    {
        let media_type = header.trim_start_matches("data:").trim_end_matches(";base64");
        return json!({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": media_type,
                "data": data
            }
        });
    }
    json!({
        "type": "image",
        "source": {"type": "url", "url": url}
    })
}

/// Returns a borrowed `Cow` when the ID is already valid, avoiding allocation
/// on the common path (e.g. IDs starting with `toolu_`).
fn sanitize_tool_call_id(id: &str) -> Cow<'_, str> {
    if id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') {
        Cow::Borrowed(id)
    } else {
        Cow::Owned(
            id.chars()
                .map(|c| {
                    if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
                        c
                    } else {
                        '_'
                    }
                })
                .collect(),
        )
    }
}

/// Merge consecutive messages with the same role by concatenating their content blocks.
/// Anthropic requires strictly alternating user/assistant roles.
fn merge_consecutive_same_role(messages: Vec<Value>) -> Vec<Value> {
    let mut merged: Vec<Value> = Vec::new();

    for msg in messages {
        let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");

        if let Some(last) = merged.last_mut() {
            let last_role = last.get("role").and_then(|r| r.as_str()).unwrap_or("");
            if last_role == role {
                // Merge content blocks.
                let incoming_content = match msg.get("content") {
                    Some(Value::Array(arr)) => arr.clone(),
                    Some(Value::String(s)) => vec![json!({"type": "text", "text": s})],
                    Some(other) => vec![json!({"type": "text", "text": other.to_string()})],
                    None => vec![],
                };

                if let Some(Value::Array(existing)) = last.get_mut("content") {
                    existing.extend(incoming_content);
                } else {
                    // Normalize existing content to array first.
                    let existing_content = match last.get("content") {
                        Some(Value::String(s)) => vec![json!({"type": "text", "text": s.clone()})],
                        Some(Value::Array(arr)) => arr.clone(),
                        Some(other) => vec![json!({"type": "text", "text": other.to_string()})],
                        None => vec![],
                    };
                    let mut combined = existing_content;
                    combined.extend(incoming_content);
                    last["content"] = json!(combined);
                }
                continue;
            }
        }

        merged.push(msg);
    }

    merged
}

/// Convert an OpenAI-format message JSON value to Anthropic Messages API format.
fn convert_message_to_anthropic(msg: Value) -> Value {
    let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("");

    match role {
        "user" => {
            let content = convert_user_content_to_anthropic(msg.get("content"));
            let mut user_msg = json!({"role": "user", "content": content});
            // Propagate message-level cache_control to the last content block.
            if let Some(cc) = msg.get("cache_control")
                && let Some(blocks) = user_msg.get_mut("content").and_then(|c| c.as_array_mut())
                && let Some(last) = blocks.last_mut()
            {
                last["cache_control"] = cc.clone();
            }
            user_msg
        }
        "assistant" => {
            let mut blocks: Vec<Value> = Vec::new();

            // Text content block.
            if let Some(text) = msg.get("content").and_then(|c| c.as_str())
                && !text.is_empty()
            {
                let mut block = json!({"type": "text", "text": text});
                // Propagate cache_control from the assistant message to its text block.
                if let Some(cc) = msg.get("cache_control") {
                    block["cache_control"] = cc.clone();
                }
                blocks.push(block);
            }

            // Tool call blocks.
            if let Some(tool_calls) = msg.get("tool_calls").and_then(|tc| tc.as_array()) {
                for tc in tool_calls {
                    let id = tc.get("id").and_then(|v| v.as_str()).unwrap_or("");
                    let name = tc.pointer("/function/name").and_then(|v| v.as_str()).unwrap_or("");
                    let arguments_str = tc
                        .pointer("/function/arguments")
                        .and_then(|v| v.as_str())
                        .unwrap_or("{}");
                    let input: Value = serde_json::from_str(arguments_str).unwrap_or_else(|_| json!({}));
                    blocks.push(json!({
                        "type": "tool_use",
                        "id": id,
                        "name": name,
                        "input": input
                    }));
                }
            }

            // If no blocks were produced AND there are no tool_calls, emit an empty text block.
            // Do NOT inject empty text when tool_use blocks are present — Anthropic rejects it.
            let has_tool_use = blocks
                .iter()
                .any(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_use"));
            if blocks.is_empty() {
                blocks.push(json!({"type": "text", "text": ""}));
            } else if !has_tool_use {
                // only text blocks: fine as-is
            }
            // When there are tool_use blocks, do not add an empty text block.

            json!({"role": "assistant", "content": blocks})
        }
        "tool" => {
            // OpenAI tool message → Anthropic user message with tool_result block.
            let raw_id = msg.get("tool_call_id").and_then(|v| v.as_str()).unwrap_or("");
            let tool_use_id = sanitize_tool_call_id(raw_id);

            // Content may be a string or an array (e.g. images in tool results).
            let result_content = match msg.get("content") {
                Some(Value::Array(arr)) => {
                    // Pass through content array (may contain image blocks, etc.)
                    arr.iter()
                        .map(|part| {
                            let part_type = part.get("type").and_then(|t| t.as_str()).unwrap_or("text");
                            match part_type {
                                "image_url" => {
                                    let url = part.pointer("/image_url/url").and_then(|u| u.as_str()).unwrap_or("");
                                    convert_image_url_to_anthropic_source(url)
                                }
                                _ => {
                                    let text = part.get("text").and_then(|t| t.as_str()).unwrap_or("");
                                    json!({"type": "text", "text": text})
                                }
                            }
                        })
                        .collect::<Vec<_>>()
                }
                Some(Value::String(s)) => vec![json!({"type": "text", "text": s})],
                _ => vec![json!({"type": "text", "text": ""})],
            };

            let mut tool_result_block = json!({
                "type": "tool_result",
                "tool_use_id": tool_use_id,
                "content": result_content
            });

            // Propagate cache_control if present.
            if let Some(cc) = msg.get("cache_control") {
                tool_result_block["cache_control"] = cc.clone();
            }

            json!({
                "role": "user",
                "content": [tool_result_block]
            })
        }
        "function" => {
            // Deprecated function-role message — treat as a tool result.
            let name = msg.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let sanitized_name = sanitize_tool_call_id(name);
            let content_text = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
            json!({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": sanitized_name,
                    "content": [{"type": "text", "text": content_text}]
                }]
            })
        }
        _ => {
            // Unknown role — pass through as-is.
            msg
        }
    }
}

/// Convert OpenAI user content (string or content-part array) to Anthropic content blocks.
fn convert_user_content_to_anthropic(content: Option<&Value>) -> Value {
    match content {
        None => json!([]),
        Some(Value::String(s)) => json!([{"type": "text", "text": s}]),
        Some(Value::Array(parts)) => {
            let blocks: Vec<Value> = parts
                .iter()
                .filter_map(|part| {
                    let part_type = part.get("type").and_then(|t| t.as_str())?;
                    match part_type {
                        "text" => {
                            let text = part.get("text").and_then(|t| t.as_str()).unwrap_or("");
                            let mut block = json!({"type": "text", "text": text});
                            // Propagate cache_control if present.
                            if let Some(cc) = part.get("cache_control") {
                                block["cache_control"] = cc.clone();
                            }
                            Some(block)
                        }
                        "image_url" => {
                            let url = part.pointer("/image_url/url").and_then(|u| u.as_str())?;
                            let mut block = convert_image_url_to_anthropic_source(url);
                            if let Some(cc) = part.get("cache_control") {
                                block["cache_control"] = cc.clone();
                            }
                            Some(block)
                        }
                        "document" => {
                            // Convert ContentPart::Document to Anthropic document block.
                            let data = part.pointer("/document/data").and_then(|d| d.as_str())?;
                            let media_type = part
                                .pointer("/document/media_type")
                                .and_then(|m| m.as_str())
                                .unwrap_or("application/pdf");
                            let mut block = json!({
                                "type": "document",
                                "source": {
                                    "type": "base64",
                                    "media_type": media_type,
                                    "data": data
                                }
                            });
                            if let Some(cc) = part.get("cache_control") {
                                block["cache_control"] = cc.clone();
                            }
                            Some(block)
                        }
                        _ => {
                            // Unknown content part types: fall back to text representation
                            // and log a warning so callers can investigate.
                            #[cfg(feature = "tracing")]
                            tracing::warn!(
                                part_type = part_type,
                                "unrecognized user content part type; falling back to text"
                            );
                            let text = part.get("text").and_then(|t| t.as_str()).unwrap_or("");
                            if text.is_empty() {
                                None
                            } else {
                                Some(json!({"type": "text", "text": text}))
                            }
                        }
                    }
                })
                .collect();
            json!(blocks)
        }
        Some(other) => json!([{"type": "text", "text": other.to_string()}]),
    }
}

/// Map an OpenAI `tool_choice` value to Anthropic format.
///
/// Returns `None` when the tool_choice means "none" (tools should be removed entirely).
fn convert_tool_choice(tool_choice: &Value) -> Option<Value> {
    match tool_choice {
        Value::String(s) => match s.as_str() {
            "none" => None,
            "required" => Some(json!({"type": "any"})),
            _ => Some(json!({"type": "auto"})),
        },
        Value::Object(_) => {
            // {"type": "function", "function": {"name": "X"}} → {"type": "tool", "name": "X"}
            let name = tool_choice.pointer("/function/name").and_then(|v| v.as_str());
            if let Some(name) = name {
                Some(json!({"type": "tool", "name": name}))
            } else {
                Some(json!({"type": "auto"}))
            }
        }
        _ => Some(json!({"type": "auto"})),
    }
}

/// Convert an OpenAI tool definition to Anthropic format.
///
/// OpenAI: `{"type": "function", "function": {"name": "X", "description": "Y", "parameters": Z}}`
/// Anthropic: `{"name": "X", "description": "Y", "input_schema": Z}`
///
/// Also normalises `input_schema.type` to `"object"` if absent or mis-typed.
fn convert_tool_to_anthropic(tool: &Value) -> Value {
    let function = tool.get("function");
    let name = function.and_then(|f| f.get("name")).cloned().unwrap_or(json!(""));
    let description = function.and_then(|f| f.get("description")).cloned();
    let mut parameters = function
        .and_then(|f| f.get("parameters"))
        .cloned()
        .unwrap_or(json!({"type": "object", "properties": {}}));

    // Normalize input_schema.type to "object" — Anthropic rejects other values.
    if parameters.get("type").and_then(|t| t.as_str()) != Some("object") {
        parameters["type"] = json!("object");
    }

    let mut tool_def = json!({
        "name": name,
        "input_schema": parameters
    });

    if let Some(desc) = description {
        tool_def["description"] = desc;
    }

    // Propagate cache_control if present on the tool definition.
    if let Some(cc) = tool.get("cache_control") {
        tool_def["cache_control"] = cc.clone();
    } else if let Some(cc) = function.and_then(|f| f.get("cache_control")) {
        tool_def["cache_control"] = cc.clone();
    }

    tool_def
}

/// Check whether a tool type string represents an Anthropic hosted tool.
fn is_hosted_tool_type(tool_type: &str) -> bool {
    HOSTED_TOOL_TYPES.contains(&tool_type)
}

/// Return `true` if any `cache_control` key appears anywhere in the JSON body.
///
/// Searches messages, system blocks, and tool definitions recursively.
fn body_contains_cache_control(body: &Value) -> bool {
    match body {
        Value::Object(map) => {
            if map.contains_key("cache_control") {
                return true;
            }
            map.values().any(body_contains_cache_control)
        }
        Value::Array(arr) => arr.iter().any(body_contains_cache_control),
        _ => false,
    }
}

/// Return `true` if the body contains any content block with `"type": "document"`.
///
/// Scans the messages array for document content parts (PDF uploads, etc.).
fn body_contains_document_block(body: &Value) -> bool {
    if let Some(messages) = body.get("messages").and_then(|m| m.as_array()) {
        for msg in messages {
            if let Some(content) = msg.get("content").and_then(|c| c.as_array()) {
                for part in content {
                    if part.get("type").and_then(|t| t.as_str()) == Some("document") {
                        return true;
                    }
                }
            }
        }
    }
    false
}

/// Map an Anthropic `stop_reason` string to an OpenAI `finish_reason` string.
fn map_stop_reason(stop_reason: &str) -> &'static str {
    match stop_reason {
        "end_turn" | "stop_sequence" => "stop",
        "tool_use" => "tool_calls",
        "max_tokens" => "length",
        "content_filtered" | "refusal" => "content_filter",
        _ => "stop",
    }
}

/// Build a `ChatCompletionChunk` with a text content delta.
fn make_text_chunk(id: &str, model: &str, text: &str) -> ChatCompletionChunk {
    ChatCompletionChunk {
        id: id.to_owned(),
        object: "chat.completion.chunk".to_owned(),
        created: super::unix_timestamp_secs(),
        model: model.to_owned(),
        choices: vec![StreamChoice {
            index: 0,
            delta: StreamDelta {
                role: None,
                content: Some(text.to_owned()),
                tool_calls: None,
                function_call: None,
                refusal: None,
            },
            finish_reason: None,
        }],
        usage: None,
        system_fingerprint: None,
        service_tier: None,
    }
}

/// Build a `ChatCompletionChunk` that starts a tool call (id + name, no arguments yet).
fn make_empty_chunk_with_tool_start(tool_index: u32, tool_id: String, tool_name: String) -> ChatCompletionChunk {
    ChatCompletionChunk {
        id: String::new(),
        object: "chat.completion.chunk".to_owned(),
        created: super::unix_timestamp_secs(),
        model: String::new(),
        choices: vec![StreamChoice {
            index: 0,
            delta: StreamDelta {
                role: None,
                content: None,
                tool_calls: Some(vec![StreamToolCall {
                    index: tool_index,
                    id: Some(tool_id),
                    call_type: Some(crate::types::ToolType::Function),
                    function: Some(StreamFunctionCall {
                        name: Some(tool_name),
                        arguments: None,
                    }),
                }]),
                function_call: None,
                refusal: None,
            },
            finish_reason: None,
        }],
        usage: None,
        system_fingerprint: None,
        service_tier: None,
    }
}

/// Build a `ChatCompletionChunk` that carries a partial tool arguments JSON delta.
fn make_tool_arguments_delta(tool_index: u32, partial_json: &str) -> ChatCompletionChunk {
    ChatCompletionChunk {
        id: String::new(),
        object: "chat.completion.chunk".to_owned(),
        created: super::unix_timestamp_secs(),
        model: String::new(),
        choices: vec![StreamChoice {
            index: 0,
            delta: StreamDelta {
                role: None,
                content: None,
                tool_calls: Some(vec![StreamToolCall {
                    index: tool_index,
                    id: None,
                    call_type: None,
                    function: Some(StreamFunctionCall {
                        name: None,
                        arguments: Some(partial_json.to_owned()),
                    }),
                }]),
                function_call: None,
                refusal: None,
            },
            finish_reason: None,
        }],
        usage: None,
        system_fingerprint: None,
        service_tier: None,
    }
}

// ── Unit tests ────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    fn provider() -> AnthropicProvider {
        AnthropicProvider
    }

    // ── transform_request tests ───────────────────────────────────────────────

    #[test]
    fn transform_request_extracts_system_message() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": "Hello!"}
            ]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        // System messages lifted to top-level `system` field.
        assert_eq!(
            body["system"],
            json!([{"type": "text", "text": "You are a helpful assistant."}])
        );

        // Only the user message remains in `messages`.
        let messages = body["messages"].as_array().expect("messages should be an array");
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
    }

    #[test]
    fn transform_request_multiple_system_messages_merged() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "system", "content": "First instruction."},
                {"role": "system", "content": "Second instruction."},
                {"role": "user", "content": "Question"}
            ]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        let system = body["system"].as_array().expect("system should be an array");
        assert_eq!(system.len(), 2);
        assert_eq!(system[0]["text"], "First instruction.");
        assert_eq!(system[1]["text"], "Second instruction.");
    }

    #[test]
    fn transform_request_defaults_max_tokens() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["max_tokens"], json!(DEFAULT_MAX_TOKENS));
    }

    #[test]
    fn transform_request_preserves_explicit_max_tokens() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "max_tokens": 1024
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["max_tokens"], json!(1024u64));
    }

    #[test]
    fn transform_request_converts_stop_string_to_array() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "stop": "\n"
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["stop_sequences"], json!(["\n"]));
        assert!(body.get("stop").is_none(), "old `stop` key should be removed");
    }

    #[test]
    fn transform_request_stop_array_passes_through() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "stop": ["STOP", "END"]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["stop_sequences"], json!(["STOP", "END"]));
        assert!(body.get("stop").is_none());
    }

    #[test]
    fn transform_request_tool_choice_required_maps_to_any() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tool_choice": "required",
            "tools": [{"type": "function", "function": {"name": "f", "parameters": {}}}]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["tool_choice"], json!({"type": "any"}));
    }

    #[test]
    fn transform_request_tool_choice_none_removes_tools() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tool_choice": "none",
            "tools": [{"type": "function", "function": {"name": "f", "parameters": {}}}]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert!(body.get("tool_choice").is_none(), "tool_choice should be removed");
        assert!(
            body.get("tools").is_none(),
            "tools should be removed for tool_choice=none"
        );
    }

    #[test]
    fn transform_request_tool_choice_specific_function() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tool_choice": {"type": "function", "function": {"name": "my_tool"}},
            "tools": [{"type": "function", "function": {"name": "my_tool", "parameters": {}}}]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        assert_eq!(body["tool_choice"], json!({"type": "tool", "name": "my_tool"}));
    }

    #[test]
    fn transform_request_converts_tools_to_anthropic_format() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tools": [{
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get current weather",
                    "parameters": {"type": "object", "properties": {}}
                }
            }]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        let tools = body["tools"].as_array().expect("tools should be an array");
        assert_eq!(tools.len(), 1);
        assert_eq!(tools[0]["name"], "get_weather");
        assert_eq!(tools[0]["description"], "Get current weather");
        assert!(tools[0].get("input_schema").is_some());
        // OpenAI-style "function" wrapper should be gone.
        assert!(tools[0].get("function").is_none());
    }

    #[test]
    fn transform_request_removes_unsupported_fields() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "n": 2,
            "presence_penalty": 0.5,
            "frequency_penalty": 0.3,
            "logit_bias": {"1234": 5},
            "stream": true
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        for key in &["n", "presence_penalty", "frequency_penalty", "logit_bias"] {
            assert!(body.get(key).is_none(), "`{key}` should be removed");
        }
        // stream is intentionally preserved — Anthropic requires it for streaming.
        assert_eq!(body["stream"], true);
    }

    #[test]
    fn transform_request_converts_tool_message_to_tool_result() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "What is the weather?"},
                {"role": "assistant", "content": null, "tool_calls": [{
                    "id": "call_abc",
                    "type": "function",
                    "function": {"name": "get_weather", "arguments": "{\"location\": \"London\"}"}
                }]},
                {"role": "tool", "tool_call_id": "call_abc", "content": "15°C, sunny"}
            ]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        let messages = body["messages"].as_array().expect("messages should be an array");
        // tool message → user message with tool_result block
        let tool_result_msg = &messages[2];
        assert_eq!(tool_result_msg["role"], "user");
        let content = tool_result_msg["content"]
            .as_array()
            .expect("content should be an array");
        assert_eq!(content[0]["type"], "tool_result");
        assert_eq!(content[0]["tool_use_id"], "call_abc");
    }

    #[test]
    fn transform_request_converts_user_content_parts() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{
                "role": "user",
                "content": [
                    {"type": "text", "text": "What is in this image?"},
                    {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/abc=="}}
                ]
            }]
        });

        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");

        let messages = body["messages"].as_array().expect("messages should be an array");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["type"], "text");
        assert_eq!(content[1]["type"], "image");
        assert_eq!(content[1]["source"]["type"], "base64");
        assert_eq!(content[1]["source"]["media_type"], "image/jpeg");
    }

    // ── transform_response tests ──────────────────────────────────────────────

    #[test]
    fn transform_response_basic_text() {
        let mut body = json!({
            "id": "msg_01Xfn7",
            "type": "message",
            "role": "assistant",
            "content": [{"type": "text", "text": "Hello, world!"}],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "end_turn",
            "usage": {"input_tokens": 10, "output_tokens": 5}
        });

        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");

        assert_eq!(body["object"], "chat.completion");
        assert_eq!(body["id"], "msg_01Xfn7");
        let choice = &body["choices"][0];
        assert_eq!(choice["message"]["content"], "Hello, world!");
        assert_eq!(choice["finish_reason"], "stop");
        assert_eq!(body["usage"]["prompt_tokens"], 10);
        assert_eq!(body["usage"]["completion_tokens"], 5);
        assert_eq!(body["usage"]["total_tokens"], 15);
    }

    #[test]
    fn transform_response_stop_reason_max_tokens_maps_to_length() {
        let mut body = json!({
            "id": "msg_abc",
            "type": "message",
            "role": "assistant",
            "content": [{"type": "text", "text": "truncated"}],
            "model": "claude-3-haiku-20240307",
            "stop_reason": "max_tokens",
            "usage": {"input_tokens": 5, "output_tokens": 50}
        });

        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");

        assert_eq!(body["choices"][0]["finish_reason"], "length");
    }

    #[test]
    fn transform_response_tool_use_block() {
        let mut body = json!({
            "id": "msg_tool",
            "type": "message",
            "role": "assistant",
            "content": [{
                "type": "tool_use",
                "id": "toolu_01abc",
                "name": "get_weather",
                "input": {"location": "London"}
            }],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "tool_use",
            "usage": {"input_tokens": 20, "output_tokens": 10}
        });

        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");

        let choice = &body["choices"][0];
        assert_eq!(choice["finish_reason"], "tool_calls");
        assert_eq!(choice["message"]["content"], Value::Null);

        let tool_calls = choice["message"]["tool_calls"]
            .as_array()
            .expect("tool_calls should be an array");
        assert_eq!(tool_calls.len(), 1);
        assert_eq!(tool_calls[0]["id"], "toolu_01abc");
        assert_eq!(tool_calls[0]["function"]["name"], "get_weather");

        // arguments must be a JSON string
        let args_str = tool_calls[0]["function"]["arguments"]
            .as_str()
            .expect("arguments should be a string");
        let args: Value = serde_json::from_str(args_str).expect("arguments should be valid JSON");
        assert_eq!(args["location"], "London");
    }

    #[test]
    fn transform_response_is_noop_for_openai_format() {
        // A body without "stop_reason" should be left unchanged (already OpenAI format).
        let original = json!({
            "id": "chatcmpl-xxx",
            "object": "chat.completion",
            "choices": [{"index": 0, "message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}]
        });
        let mut body = original.clone();

        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");

        assert_eq!(body, original);
    }

    // ── parse_stream_event tests ──────────────────────────────────────────────

    #[test]
    fn parse_stream_event_done_is_handled_at_sse_level() {
        // `[DONE]` is now caught by the SSE parser before reaching the provider.
        // If it were to reach the provider, it would be invalid JSON.
        let result = provider().parse_stream_event("[DONE]");
        assert!(
            result.is_err(),
            "[DONE] is not valid JSON and should error if it reaches the provider"
        );
    }

    #[test]
    fn parse_stream_event_message_stop_returns_none() {
        let event = r#"{"type":"message_stop"}"#;
        let result = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail");
        assert!(result.is_none());
    }

    #[test]
    fn parse_stream_event_text_delta() {
        let event = r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("Hello"));
    }

    #[test]
    fn parse_stream_event_message_delta_with_finish_reason() {
        let event = r#"{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":12}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        assert_eq!(chunk.choices[0].finish_reason, Some(FinishReason::Stop));
        let usage = chunk.usage.expect("usage should be present");
        assert_eq!(usage.completion_tokens, 12);
    }

    #[test]
    fn parse_stream_event_message_delta_tool_use_stop_reason() {
        let event = r#"{"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":5}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        assert_eq!(chunk.choices[0].finish_reason, Some(FinishReason::ToolCalls));
    }

    #[test]
    fn parse_stream_event_message_start() {
        let event = r#"{"type":"message_start","message":{"id":"msg_abc","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet-20241022","stop_reason":null,"usage":{"input_tokens":25,"output_tokens":1}}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        assert_eq!(chunk.id, "msg_abc");
        assert_eq!(chunk.model, "claude-3-5-sonnet-20241022");
        assert_eq!(chunk.choices[0].delta.role.as_deref(), Some("assistant"));
        let usage = chunk.usage.expect("usage should be present");
        assert_eq!(usage.prompt_tokens, 25);
    }

    #[test]
    fn parse_stream_event_input_json_delta() {
        let event =
            r#"{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"loc"}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        let tc = &chunk.choices[0]
            .delta
            .tool_calls
            .as_ref()
            .expect("tool_calls should be present")[0];
        assert_eq!(
            tc.function
                .as_ref()
                .expect("function should be present")
                .arguments
                .as_deref(),
            Some("{\"loc")
        );
    }

    #[test]
    fn parse_stream_event_error_returns_err() {
        let event = r#"{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}"#;
        let result = provider().parse_stream_event(event);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("Overloaded"));
    }

    #[test]
    fn parse_stream_event_ping_returns_none() {
        let event = r#"{"type":"ping"}"#;
        let result = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail");
        assert!(result.is_none(), "ping should return Ok(None), not a chunk");
    }

    #[test]
    fn parse_stream_event_content_block_stop_returns_none() {
        let event = r#"{"type":"content_block_stop","index":0}"#;
        let result = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail");
        assert!(result.is_none(), "content_block_stop should return Ok(None)");
    }

    // ── chat_completions_path test ────────────────────────────────────────────

    #[test]
    fn chat_completions_path_is_messages() {
        assert_eq!(provider().chat_completions_path(), "/messages");
    }

    // ── new tests for fixed issues ────────────────────────────────────────────

    #[test]
    fn transform_request_empty_messages_returns_error() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": []
        });
        let result = provider().transform_request(&mut body);
        assert!(result.is_err(), "empty messages should return an error");
    }

    #[test]
    fn transform_request_sanitizes_tool_call_id() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "What is the weather?"},
                {"role": "assistant", "content": null, "tool_calls": [{
                    "id": "call_abc.123",
                    "type": "function",
                    "function": {"name": "get_weather", "arguments": "{}"}
                }]},
                {"role": "tool", "tool_call_id": "call_abc.123", "content": "Sunny"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        // The tool_result message is the last one (index 2 after merging nothing)
        let tool_result_msg = messages
            .iter()
            .find(|m| m["role"] == "user" && m["content"][0]["type"] == "tool_result")
            .expect("tool_result message should be present");
        // Dots should be replaced with underscores.
        assert_eq!(tool_result_msg["content"][0]["tool_use_id"], "call_abc_123");
    }

    #[test]
    fn transform_request_merges_consecutive_user_messages() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "First"},
                {"role": "user", "content": "Second"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        // Two consecutive user messages should be merged into one.
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0]["role"], "user");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content.len(), 2);
    }

    #[test]
    fn transform_request_system_content_array_passed_through() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "system", "content": [
                    {"type": "text", "text": "Block one"},
                    {"type": "text", "text": "Block two"}
                ]},
                {"role": "user", "content": "Hello"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let system = body["system"].as_array().expect("system should be an array");
        assert_eq!(system.len(), 2);
        assert_eq!(system[0]["text"], "Block one");
    }

    #[test]
    fn transform_request_system_cache_control_propagated() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "system", "content": "Cached instructions", "cache_control": {"type": "ephemeral"}},
                {"role": "user", "content": "Hi"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let system = body["system"].as_array().expect("system should be an array");
        assert_eq!(system[0]["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn transform_request_user_content_cache_control_propagated() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{
                "role": "user",
                "content": [
                    {"type": "text", "text": "Cached text", "cache_control": {"type": "ephemeral"}}
                ]
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn transform_request_tool_input_schema_type_normalized() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tools": [{
                "type": "function",
                "function": {
                    "name": "my_tool",
                    "parameters": {"properties": {}}
                    // no "type" field
                }
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let tools = body["tools"].as_array().expect("tools should be an array");
        assert_eq!(tools[0]["input_schema"]["type"], "object");
    }

    #[test]
    fn transform_request_max_completion_tokens_mapped() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "max_completion_tokens": 512
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert_eq!(body["max_tokens"], json!(512u64));
        assert!(body.get("max_completion_tokens").is_none());
    }

    #[test]
    fn transform_request_tool_result_content_array_preserved() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "Look"},
                {"role": "assistant", "content": null, "tool_calls": [{
                    "id": "call_img",
                    "type": "function",
                    "function": {"name": "get_image", "arguments": "{}"}
                }]},
                {"role": "tool", "tool_call_id": "call_img", "content": [
                    {"type": "text", "text": "Here is the image"},
                    {"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123"}}
                ]}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let tool_result_msg = messages
            .iter()
            .find(|m| {
                m["role"] == "user"
                    && m["content"]
                        .as_array()
                        .is_some_and(|c| c.first().is_some_and(|b| b["type"] == "tool_result"))
            })
            .expect("tool_result message with image should be present");
        let result_content = tool_result_msg["content"][0]["content"]
            .as_array()
            .expect("content should be an array");
        assert_eq!(result_content.len(), 2);
        assert_eq!(result_content[0]["type"], "text");
        assert_eq!(result_content[1]["type"], "image");
    }

    #[test]
    fn transform_response_thinking_block_excluded_from_content() {
        let mut body = json!({
            "id": "msg_think",
            "type": "message",
            "role": "assistant",
            "content": [
                {"type": "thinking", "thinking": "Let me reason..."},
                {"type": "text", "text": "The answer is 42."}
            ],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "end_turn",
            "usage": {"input_tokens": 10, "output_tokens": 20}
        });
        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");
        let content = body["choices"][0]["message"]["content"]
            .as_str()
            .expect("content should be a string");
        // Thinking blocks are internal chain-of-thought and must NOT appear in user-facing content.
        assert!(
            !content.contains("Let me reason..."),
            "thinking blocks should be filtered out"
        );
        assert_eq!(content, "The answer is 42.");
    }

    #[test]
    fn transform_response_server_tool_use_treated_as_tool_call() {
        let mut body = json!({
            "id": "msg_srv",
            "type": "message",
            "role": "assistant",
            "content": [{
                "type": "server_tool_use",
                "id": "srvtool_01",
                "name": "web_search",
                "input": {"query": "Rust programming"}
            }],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "tool_use",
            "usage": {"input_tokens": 5, "output_tokens": 5}
        });
        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");
        let tool_calls = body["choices"][0]["message"]["tool_calls"]
            .as_array()
            .expect("tool_calls should be an array");
        assert_eq!(tool_calls.len(), 1);
        assert_eq!(tool_calls[0]["id"], "srvtool_01");
        assert_eq!(tool_calls[0]["function"]["name"], "web_search");
    }

    #[test]
    fn transform_response_cache_tokens_counted_in_prompt() {
        let mut body = json!({
            "id": "msg_cache",
            "type": "message",
            "role": "assistant",
            "content": [{"type": "text", "text": "ok"}],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "end_turn",
            "usage": {
                "input_tokens": 100,
                "cache_creation_input_tokens": 50,
                "cache_read_input_tokens": 25,
                "output_tokens": 10
            }
        });
        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");
        // prompt_tokens = 100 + 50 + 25 = 175
        assert_eq!(body["usage"]["prompt_tokens"], 175u64);
        assert_eq!(body["usage"]["completion_tokens"], 10u64);
        assert_eq!(body["usage"]["total_tokens"], 185u64);
    }

    #[test]
    fn transform_response_tool_only_no_empty_text_block_in_request() {
        // When assistant has only tool_calls, the converted message should NOT inject empty text.
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "Call a tool"},
                {"role": "assistant", "tool_calls": [{
                    "id": "call_xyz",
                    "type": "function",
                    "function": {"name": "my_fn", "arguments": "{}"}
                }]},
                {"role": "tool", "tool_call_id": "call_xyz", "content": "result"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let assistant_msg = messages
            .iter()
            .find(|m| m["role"] == "assistant")
            .expect("assistant message should be present");
        let blocks = assistant_msg["content"].as_array().expect("content should be an array");
        // Only the tool_use block should be present; no empty text block.
        assert!(blocks.iter().all(|b| b["type"] != "text" || b["text"] != ""));
        assert!(blocks.iter().any(|b| b["type"] == "tool_use"));
    }

    #[test]
    fn parse_stream_event_thinking_delta_returns_none() {
        // Thinking blocks are filtered in both streaming and non-streaming for consistency.
        let event = r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"I am thinking..."}}"#;
        let result = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail");
        assert!(result.is_none(), "thinking_delta should be filtered (return None)");
    }

    #[test]
    fn parse_stream_event_message_start_cache_tokens_in_usage() {
        let event = r#"{"type":"message_start","message":{"id":"msg_x","model":"claude-opus","content":[],"usage":{"input_tokens":100,"cache_creation_input_tokens":50,"cache_read_input_tokens":25,"output_tokens":0}}}"#;
        let chunk = provider()
            .parse_stream_event(event)
            .expect("parse_stream_event should not fail")
            .expect("expected chunk");
        let usage = chunk.usage.expect("usage should be present");
        // prompt_tokens = 100 + 50 + 25 = 175
        assert_eq!(usage.prompt_tokens, 175);
    }

    #[test]
    fn sanitize_tool_call_id_replaces_invalid_chars() {
        assert_eq!(sanitize_tool_call_id("call.abc!123").as_ref(), "call_abc_123");
        assert_eq!(sanitize_tool_call_id("call-abc_123").as_ref(), "call-abc_123");
        assert_eq!(sanitize_tool_call_id("call abc").as_ref(), "call_abc");
        // Already-valid IDs should borrow (no allocation).
        assert!(matches!(sanitize_tool_call_id("toolu_01abc"), Cow::Borrowed(_)));
        // IDs with invalid chars should allocate.
        assert!(matches!(sanitize_tool_call_id("call.123"), Cow::Owned(_)));
    }

    #[test]
    fn map_stop_reason_content_filter() {
        assert_eq!(map_stop_reason("content_filtered"), "content_filter");
        assert_eq!(map_stop_reason("refusal"), "content_filter");
    }

    // ── Phase 1A: Extended thinking / reasoning effort ──────────────────────

    #[test]
    fn transform_request_reasoning_effort_low() {
        let mut body = json!({
            "model": "claude-sonnet-4-20250514",
            "messages": [{"role": "user", "content": "Think about this"}],
            "reasoning_effort": "low"
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert_eq!(body["thinking"]["type"], "enabled");
        assert_eq!(body["thinking"]["budget_tokens"], 1024);
        assert!(
            body.get("reasoning_effort").is_none(),
            "reasoning_effort should be removed"
        );
    }

    #[test]
    fn transform_request_reasoning_effort_medium() {
        let mut body = json!({
            "model": "claude-sonnet-4-20250514",
            "messages": [{"role": "user", "content": "Think about this"}],
            "reasoning_effort": "medium"
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert_eq!(body["thinking"]["type"], "enabled");
        assert_eq!(body["thinking"]["budget_tokens"], 4096);
    }

    #[test]
    fn transform_request_reasoning_effort_high() {
        let mut body = json!({
            "model": "claude-sonnet-4-20250514",
            "messages": [{"role": "user", "content": "Think deeply"}],
            "reasoning_effort": "high"
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert_eq!(body["thinking"]["type"], "enabled");
        assert_eq!(body["thinking"]["budget_tokens"], 16384);
    }

    #[test]
    fn transform_request_reasoning_effort_from_extra_body() {
        let mut body = json!({
            "model": "claude-sonnet-4-20250514",
            "messages": [{"role": "user", "content": "Think"}],
            "extra_body": {"reasoning_effort": "high"}
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert_eq!(body["thinking"]["type"], "enabled");
        assert_eq!(body["thinking"]["budget_tokens"], 16384);
    }

    // ── Phase 1A: Dynamic beta headers ──────────────────────────────────────

    #[test]
    fn dynamic_headers_thinking_beta() {
        let body = json!({
            "thinking": {"type": "enabled", "budget_tokens": 4096},
            "messages": [{"role": "user", "content": "Hi"}]
        });
        let headers = provider().dynamic_headers(&body);
        assert_eq!(headers.len(), 1);
        assert_eq!(headers[0].0, "anthropic-beta");
        assert!(headers[0].1.contains("thinking-2025-04-14"));
    }

    #[test]
    fn dynamic_headers_web_search_beta() {
        let body = json!({
            "tools": [{"type": "web_search_20250305", "name": "web_search"}],
            "messages": [{"role": "user", "content": "Search for Rust"}]
        });
        let headers = provider().dynamic_headers(&body);
        assert_eq!(headers.len(), 1);
        assert!(headers[0].1.contains("web-search-2025-03-05"));
    }

    #[test]
    fn dynamic_headers_multiple_betas_combined() {
        let body = json!({
            "thinking": {"type": "enabled", "budget_tokens": 4096},
            "tools": [
                {"type": "computer_use_20250124", "display_width_px": 1024, "display_height_px": 768},
                {"type": "web_search_20250305", "name": "web_search"}
            ]
        });
        let headers = provider().dynamic_headers(&body);
        assert_eq!(headers.len(), 1);
        let beta_value = &headers[0].1;
        assert!(beta_value.contains("thinking-2025-04-14"));
        assert!(beta_value.contains("computer-use-2025-01-24"));
        assert!(beta_value.contains("web-search-2025-03-05"));
    }

    #[test]
    fn dynamic_headers_no_betas_returns_empty() {
        let body = json!({
            "messages": [{"role": "user", "content": "Hi"}]
        });
        let headers = provider().dynamic_headers(&body);
        assert!(headers.is_empty());
    }

    #[test]
    fn dynamic_headers_code_execution_beta() {
        let body = json!({
            "tools": [{"type": "code_execution_20250522"}]
        });
        let headers = provider().dynamic_headers(&body);
        assert_eq!(headers.len(), 1);
        assert!(headers[0].1.contains("code-execution-2025-05-22"));
    }

    // ── Phase 1A: Prompt caching on messages + tools ────────────────────────

    #[test]
    fn transform_request_tool_cache_control_propagated() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Hi"}],
            "tools": [{
                "type": "function",
                "cache_control": {"type": "ephemeral"},
                "function": {
                    "name": "get_weather",
                    "description": "Get weather",
                    "parameters": {"type": "object", "properties": {}}
                }
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let tools = body["tools"].as_array().expect("tools should be an array");
        assert_eq!(tools[0]["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn transform_request_assistant_message_cache_control() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "user", "content": "Hi"},
                {"role": "assistant", "content": "Hello!", "cache_control": {"type": "ephemeral"}},
                {"role": "user", "content": "How are you?"}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let assistant_msg = messages
            .iter()
            .find(|m| m["role"] == "assistant")
            .expect("assistant message should be present");
        let content = assistant_msg["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn transform_request_user_message_level_cache_control() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{
                "role": "user",
                "content": "Hello",
                "cache_control": {"type": "ephemeral"}
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
    }

    // ── Phase 1A: Document/PDF handling ─────────────────────────────────────

    #[test]
    fn transform_request_document_content_part() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{
                "role": "user",
                "content": [
                    {"type": "text", "text": "Analyze this document"},
                    {"type": "document", "document": {
                        "data": "JVBERi0xLjQ=",
                        "media_type": "application/pdf"
                    }}
                ]
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["type"], "text");
        assert_eq!(content[1]["type"], "document");
        assert_eq!(content[1]["source"]["type"], "base64");
        assert_eq!(content[1]["source"]["media_type"], "application/pdf");
        assert_eq!(content[1]["source"]["data"], "JVBERi0xLjQ=");
    }

    #[test]
    fn transform_request_document_with_cache_control() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{
                "role": "user",
                "content": [
                    {"type": "document", "document": {
                        "data": "JVBERi0xLjQ=",
                        "media_type": "application/pdf"
                    }, "cache_control": {"type": "ephemeral"}}
                ]
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let messages = body["messages"].as_array().expect("messages should be an array");
        let content = messages[0]["content"].as_array().expect("content should be an array");
        assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
    }

    // ── Phase 1A: Response format -> JSON mode ──────────────────────────────

    #[test]
    fn transform_request_json_object_response_format() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Give me JSON"}],
            "response_format": {"type": "json_object"}
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert!(body.get("response_format").is_none());
        let system = body["system"].as_array().expect("system should be an array");
        assert!(
            system[0]["text"]
                .as_str()
                .expect("text should be a string")
                .contains("valid JSON")
        );
    }

    #[test]
    fn transform_request_json_schema_response_format() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Give me structured output"}],
            "response_format": {
                "type": "json_schema",
                "json_schema": {
                    "name": "person",
                    "schema": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "age": {"type": "integer"}
                        }
                    }
                }
            }
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        assert!(body.get("response_format").is_none());
        let system = body["system"].as_array().expect("system should be an array");
        let instruction = system[0]["text"].as_str().expect("text should be a string");
        assert!(instruction.contains("person"));
        assert!(instruction.contains("schema"));
    }

    #[test]
    fn transform_request_json_object_with_existing_system() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [
                {"role": "system", "content": "You are helpful."},
                {"role": "user", "content": "Give me JSON"}
            ],
            "response_format": {"type": "json_object"}
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let system = body["system"].as_array().expect("system should be an array");
        assert_eq!(system.len(), 2);
        assert!(
            system[0]["text"]
                .as_str()
                .expect("text should be a string")
                .contains("valid JSON")
        );
        assert_eq!(system[1]["text"], "You are helpful.");
    }

    // ── Phase 1A: Hosted tools ──────────────────────────────────────────────

    #[test]
    fn transform_request_hosted_tool_passed_through() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Search the web"}],
            "tools": [
                {"type": "web_search_20250305", "name": "web_search", "max_uses": 3},
                {"type": "function", "function": {
                    "name": "get_weather",
                    "parameters": {"type": "object", "properties": {}}
                }}
            ]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let tools = body["tools"].as_array().expect("tools should be an array");
        assert_eq!(tools.len(), 2);
        assert_eq!(tools[0]["type"], "web_search_20250305");
        assert_eq!(tools[0]["max_uses"], 3);
        assert_eq!(tools[1]["name"], "get_weather");
        assert!(tools[1].get("input_schema").is_some());
    }

    #[test]
    fn transform_request_computer_use_tool_passed_through() {
        let mut body = json!({
            "model": "claude-3-5-sonnet-20241022",
            "messages": [{"role": "user", "content": "Use the computer"}],
            "tools": [{
                "type": "computer_20241022",
                "display_width_px": 1024,
                "display_height_px": 768
            }]
        });
        provider()
            .transform_request(&mut body)
            .expect("transform_request should not fail");
        let tools = body["tools"].as_array().expect("tools should be an array");
        assert_eq!(tools[0]["type"], "computer_20241022");
        assert_eq!(tools[0]["display_width_px"], 1024);
    }

    // ── Phase 1A: Citation blocks in response ───────────────────────────────

    #[test]
    fn transform_response_citation_blocks_skipped() {
        let mut body = json!({
            "id": "msg_cite",
            "type": "message",
            "role": "assistant",
            "content": [
                {"type": "text", "text": "According to the document, "},
                {"type": "citation", "cited_text": "Rust is fast", "document_index": 0},
                {"type": "text", "text": "Rust is a fast language."}
            ],
            "model": "claude-3-5-sonnet-20241022",
            "stop_reason": "end_turn",
            "usage": {"input_tokens": 50, "output_tokens": 20}
        });
        provider()
            .transform_response(&mut body)
            .expect("transform_response should not fail");
        let content = body["choices"][0]["message"]["content"]
            .as_str()
            .expect("content should be a string");
        assert_eq!(content, "According to the document, Rust is a fast language.");
        assert!(!content.contains("citation"));
    }

    // ── Phase 1A: is_hosted_tool_type helper ────────────────────────────────

    #[test]
    fn is_hosted_tool_type_recognizes_all_types() {
        assert!(is_hosted_tool_type("computer_20241022"));
        assert!(is_hosted_tool_type("computer_use_20250124"));
        assert!(is_hosted_tool_type("web_search_20250305"));
        assert!(is_hosted_tool_type("code_execution_20250522"));
        assert!(!is_hosted_tool_type("function"));
        assert!(!is_hosted_tool_type("custom_tool"));
    }
}
