//! Anthropic API client implementation for chat and completion functionality.
//!
//! This module provides integration with Anthropic's Claude models through their API.

use std::collections::HashMap;

/// Tool name prefix used for OAuth requests to avoid conflicts with server-side tools
const TOOL_PREFIX: &str = "mcp_";

/// OAuth system prompt
const OAUTH_SYSTEM_PROMPT: &str = "You are Claude Code, Anthropic's official CLI for Claude.";

use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use http::{
    Method, Request, Response,
    header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
};
use querymt::{
    FunctionCall, HTTPLLMProvider, ToolCall, Usage,
    auth::ApiKeyResolver,
    chat::{
        ChatMessage, ChatResponse, ChatRole, Content, FinishReason, ReasoningEffort, Tool,
        ToolChoice,
        http::{ChatStreamParser, HTTPChatProvider},
    },
    completion::{CompletionRequest, CompletionResponse, http::HTTPCompletionProvider},
    embedding::http::HTTPEmbeddingProvider,
    error::LLMError,
    handle_http_error,
};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::sync::Arc;
use url::Url;

/// Authentication type for Anthropic API
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AuthType {
    /// Standard API key authentication (x-api-key header)
    #[serde(rename = "api_key")]
    ApiKey,
    /// OAuth token authentication (Authorization: Bearer header)
    #[serde(rename = "oauth")]
    OAuth,
}

/// Determines the authentication type based on an explicit setting or by auto-detecting from the API key format.
///
/// If `explicit_auth_type` is `Some`, returns that value directly.
/// Otherwise, auto-detects based on the token prefix:
/// - OAuth tokens: `sk-ant-oat<digits>-...` (e.g., sk-ant-oat01-...)
/// - API keys: `sk-ant-api<digits>-...` (e.g., sk-ant-api03-...)
///
/// If the token format is unrecognized, logs a warning and defaults to API key authentication.
pub fn detect_auth_type(api_key: &str, explicit_auth_type: Option<AuthType>) -> AuthType {
    if let Some(auth_type) = explicit_auth_type {
        return auth_type;
    }

    // Check for OAuth token pattern: sk-ant-oat<digits>-
    if api_key.starts_with("sk-ant-oat") {
        if let Some(rest) = api_key.strip_prefix("sk-ant-oat")
            && rest.chars().next().is_some_and(|c| c.is_ascii_digit())
        {
            return AuthType::OAuth;
        }
    }

    // Check for API key pattern: sk-ant-api<digits>-
    if api_key.starts_with("sk-ant-api") {
        if let Some(rest) = api_key.strip_prefix("sk-ant-api")
            && rest.chars().next().is_some_and(|c| c.is_ascii_digit())
        {
            return AuthType::ApiKey;
        }
    }

    // Fallback: Check for generic sk-ant- prefix (backward compatibility)
    if api_key.starts_with("sk-ant-") {
        eprintln!(
            "Warning: Anthropic token format not recognized (expected 'sk-ant-oat<N>-' or 'sk-ant-api<N>-'). \
            Defaulting to API key authentication. Consider setting 'auth_type' explicitly."
        );
        return AuthType::ApiKey;
    }

    // Token doesn't match Anthropic format at all
    eprintln!(
        "Warning: Token does not match expected Anthropic format (should start with 'sk-ant-'). \
        Defaulting to API key authentication. This may cause authentication failures."
    );
    AuthType::ApiKey
}

/// Client for interacting with Anthropic's API.
///
/// Provides methods for chat and completion requests using Anthropic's models.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct Anthropic {
    pub api_key: String,
    /// Optional: Explicitly specify authentication type.
    /// If not provided, will auto-detect based on api_key format:
    /// - OAuth tokens: `sk-ant-oat<digits>-...` (e.g., sk-ant-oat01-...)
    /// - API keys: `sk-ant-api<digits>-...` (e.g., sk-ant-api03-...)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auth_type: Option<AuthType>,
    pub model: String,
    pub max_tokens: u32,
    pub temperature: Option<f32>,
    pub timeout_seconds: Option<u64>,
    pub system: Option<AnthropicSystemPrompt>,
    pub stream: Option<bool>,
    pub top_p: Option<f32>,
    pub top_k: Option<u32>,
    pub tools: Option<Vec<Tool>>,
    pub tool_choice: Option<ToolChoice>,
    pub reasoning_effort: Option<ReasoningEffort>,
    pub reasoning_budget_tokens: Option<u32>,
    /// Optional resolver for dynamic credential refresh (e.g., OAuth tokens).
    #[serde(skip)]
    #[schemars(skip)]
    pub key_resolver: Option<Arc<dyn ApiKeyResolver>>,
}

/// Per-block accumulator used while streaming tool-use content.
///
/// Anthropic's SSE protocol emits tool arguments as a sequence of
/// `input_json_delta` events followed by a `content_block_stop`.  We
/// accumulate the partial JSON here and emit a `ToolUseComplete` chunk
/// when the block closes.
#[derive(Debug, Default)]
struct AnthropicToolUseState {
    id: String,
    name: String,
    arguments_buffer: String,
}

/// Per-block accumulator for Anthropic thinking signature deltas.
#[derive(Debug, Default)]
struct AnthropicThinkingState {
    signature: String,
}

/// Anthropic-specific tool format that matches their API structure
#[derive(Serialize, Debug)]
struct AnthropicTool<'a> {
    name: String,
    description: &'a str,
    #[serde(rename = "input_schema")]
    schema: &'a serde_json::Value,
}

/// Configuration for the thinking feature
#[derive(Serialize, Debug)]
struct ThinkingConfig {
    #[serde(rename = "type")]
    thinking_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    budget_tokens: Option<u32>,
}

/// Request payload for Anthropic's messages API endpoint.
#[derive(Serialize, Debug)]
struct AnthropicCompleteRequest<'a> {
    messages: Vec<AnthropicMessage>,
    model: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    system: Option<AnthropicSystemPrompt>,
    #[serde(skip_serializing_if = "Option::is_none")]
    stream: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_k: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tools: Option<Vec<AnthropicTool<'a>>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tool_choice: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    thinking: Option<ThinkingConfig>,
}

/// Individual message in an Anthropic chat conversation.
#[derive(Serialize, Debug)]
struct AnthropicMessage {
    role: &'static str,
    content: Vec<MessageContent>,
}

/// A content block sent to the Anthropic API.
///
/// Uses `#[serde(untagged)]` so each variant controls its own `type` field.
/// This avoids needing a single flat struct with many `Option` fields.
#[derive(Serialize, Debug)]
#[serde(untagged)]
enum MessageContent {
    Text {
        #[serde(rename = "type")]
        content_type: &'static str, // "text"
        text: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    Image {
        #[serde(rename = "type")]
        content_type: &'static str, // "image"
        source: ImageSource,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    ImageUrl {
        #[serde(rename = "type")]
        content_type: &'static str, // "image_url"
        image_url: ImageUrlContent,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    Document {
        #[serde(rename = "type")]
        content_type: &'static str, // "document"
        source: ImageSource,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    Thinking {
        #[serde(rename = "type")]
        content_type: &'static str, // "thinking"
        thinking: String,
        signature: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    ToolUse {
        #[serde(rename = "type")]
        content_type: &'static str, // "tool_use"
        id: String,
        name: String,
        input: Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
    ToolResult {
        #[serde(rename = "type")]
        content_type: &'static str, // "tool_result"
        tool_use_id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        is_error: Option<bool>,
        content: Vec<ToolResultContent>,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControlEphemeral>,
    },
}

/// Content block inside a tool_result (Anthropic supports text + image).
#[derive(Serialize, Debug)]
#[serde(untagged)]
enum ToolResultContent {
    Text {
        #[serde(rename = "type")]
        content_type: &'static str,
        text: String,
    },
    Image {
        #[serde(rename = "type")]
        content_type: &'static str,
        source: ImageSource,
    },
}

impl MessageContent {
    fn set_cache_control(&mut self, cc: CacheControlEphemeral) {
        match self {
            MessageContent::Text { cache_control, .. }
            | MessageContent::Image { cache_control, .. }
            | MessageContent::ImageUrl { cache_control, .. }
            | MessageContent::Document { cache_control, .. }
            | MessageContent::Thinking { cache_control, .. }
            | MessageContent::ToolUse { cache_control, .. }
            | MessageContent::ToolResult { cache_control, .. } => {
                *cache_control = Some(cc);
            }
        }
    }
}

#[derive(Serialize, Debug)]
struct ImageUrlContent {
    url: String,
}

#[derive(Serialize, Debug)]
struct ImageSource {
    #[serde(rename = "type")]
    source_type: &'static str,
    media_type: String,
    data: String,
}

// --- System prompt types (Anthropic API union: string | TextBlockParam[]) ---

/// Time-to-live for cache control breakpoints.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub enum CacheTTL {
    /// 5 minutes
    #[serde(rename = "5m")]
    FiveMinutes,
    /// 1 hour
    #[serde(rename = "1h")]
    OneHour,
}

/// Cache control configuration for ephemeral caching.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CacheControlEphemeral {
    #[serde(rename = "type")]
    pub control_type: String,
    /// Time-to-live for the cache control breakpoint. Defaults to 5m.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<CacheTTL>,
}

/// Citation referencing a character location within a document.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CitationCharLocationParam {
    pub cited_text: String,
    pub document_index: u64,
    pub document_title: String,
    pub end_char_index: u64,
    pub start_char_index: u64,
}

/// Citation referencing a page location within a document.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CitationPageLocationParam {
    pub cited_text: String,
    pub document_index: u64,
    pub document_title: String,
    pub end_page_number: u64,
    pub start_page_number: u64,
}

/// Citation referencing a content block location within a document.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CitationContentBlockLocationParam {
    pub cited_text: String,
    pub document_index: u64,
    pub document_title: String,
    pub end_block_index: u64,
    pub start_block_index: u64,
}

/// Citation referencing a web search result location.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CitationWebSearchResultLocationParam {
    pub cited_text: String,
    pub encrypted_index: String,
    pub title: String,
    pub url: String,
}

/// Citation referencing a search result location.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct CitationSearchResultLocationParam {
    pub cited_text: String,
    pub end_block_index: u64,
    pub search_result_index: u64,
    pub source: String,
    pub start_block_index: u64,
    pub title: String,
}

/// Union of all citation parameter types, discriminated by the `type` field.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum TextCitationParam {
    #[serde(rename = "char_location")]
    CharLocation(CitationCharLocationParam),
    #[serde(rename = "page_location")]
    PageLocation(CitationPageLocationParam),
    #[serde(rename = "content_block_location")]
    ContentBlockLocation(CitationContentBlockLocationParam),
    #[serde(rename = "web_search_result_location")]
    WebSearchResultLocation(CitationWebSearchResultLocationParam),
    #[serde(rename = "search_result_location")]
    SearchResultLocation(CitationSearchResultLocationParam),
}

/// A text content block used in system prompts, with optional cache control and citations.
#[derive(Debug, Clone, Deserialize, JsonSchema, Serialize, PartialEq)]
pub struct TextBlockParam {
    #[serde(rename = "type")]
    pub block_type: String,
    pub text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cache_control: Option<CacheControlEphemeral>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub citations: Option<Vec<TextCitationParam>>,
}

/// Anthropic system prompt: either a plain string or an array of TextBlockParam.
///
/// Deserializes from three JSON shapes:
/// - `"string"` → `Text(String)`
/// - `["s1", "s2"]` → `Blocks` with each string wrapped as a `TextBlockParam`
/// - `[{"type":"text","text":"...","cache_control":{...}}]` → `Blocks(Vec<TextBlockParam>)`
#[derive(Debug, Clone, JsonSchema, Serialize, PartialEq)]
#[serde(untagged)]
pub enum AnthropicSystemPrompt {
    /// Plain text system prompt
    Text(String),
    /// Array of text content blocks with optional cache control and citations
    Blocks(Vec<TextBlockParam>),
}

impl<'de> Deserialize<'de> for AnthropicSystemPrompt {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = Value::deserialize(deserializer)?;
        match value {
            Value::String(s) => Ok(AnthropicSystemPrompt::Text(s)),
            Value::Array(arr) => {
                // Try to deserialize as Vec<TextBlockParam> first (objects with "type"/"text")
                // If that fails, try as Vec<String> (plain string array from LLMParams)
                let blocks_result: Result<Vec<TextBlockParam>, _> =
                    serde_json::from_value(Value::Array(arr.clone()));
                if let Ok(blocks) = blocks_result {
                    return Ok(AnthropicSystemPrompt::Blocks(blocks));
                }

                // Try as array of plain strings
                let strings: Vec<String> = arr
                    .into_iter()
                    .map(|v| match v {
                        Value::String(s) => Ok(s),
                        other => Err(serde::de::Error::custom(format!(
                            "expected string or TextBlockParam object in system array, got {}",
                            other
                        ))),
                    })
                    .collect::<Result<_, _>>()?;
                Ok(AnthropicSystemPrompt::Blocks(
                    strings
                        .into_iter()
                        .map(|text| TextBlockParam {
                            block_type: "text".to_string(),
                            text,
                            cache_control: None,
                            citations: None,
                        })
                        .collect(),
                ))
            }
            other => Err(serde::de::Error::custom(format!(
                "expected string or array for system prompt, got {}",
                other
            ))),
        }
    }
}

/// Response from Anthropic's messages API endpoint.
#[derive(Deserialize, Debug)]
struct AnthropicCompleteResponse {
    content: Vec<AnthropicContent>,
    stop_reason: String,
    usage: Option<Usage>,
}

#[derive(Deserialize, Debug)]
struct AnthropicStreamResponse {
    #[serde(rename = "type")]
    response_type: String,
    /// Index of the content block (for content_block_start, content_block_delta, content_block_stop)
    index: Option<usize>,
    /// Content block for content_block_start events
    content_block: Option<AnthropicStreamContentBlock>,
    /// Delta for content_block_delta and message_delta events
    delta: Option<AnthropicDelta>,
    /// Usage information (present on message_delta events; only output_tokens is populated)
    usage: Option<Usage>,
    /// Nested message object (present on message_start events)
    message: Option<AnthropicStreamMessage>,
}

/// Nested message object within an Anthropic `message_start` SSE event.
#[derive(Deserialize, Debug)]
struct AnthropicStreamMessage {
    /// Usage information present in the initial message_start event.
    usage: Option<Usage>,
}

/// Content block within an Anthropic streaming content_block_start event.
#[derive(Deserialize, Debug)]
struct AnthropicStreamContentBlock {
    #[serde(rename = "type")]
    block_type: String,
    /// Tool use ID (for tool_use blocks)
    id: Option<String>,
    /// Tool name (for tool_use blocks)
    name: Option<String>,
    /// Initial text (for text blocks, usually empty)
    #[allow(dead_code)]
    text: Option<String>,
}

/// Delta content within an Anthropic streaming response.
#[derive(Deserialize, Debug)]
struct AnthropicDelta {
    #[serde(rename = "type")]
    delta_type: Option<String>,
    /// Text content (for text_delta)
    text: Option<String>,
    /// Partial JSON string (for input_json_delta)
    partial_json: Option<String>,
    /// Thinking content (for thinking_delta)
    thinking: Option<String>,
    /// Thinking signature (for signature_delta)
    signature: Option<String>,
    /// Stop reason (for message_delta)
    stop_reason: Option<String>,
}

/// Content block within an Anthropic API response.
#[derive(Serialize, Deserialize, Debug)]
struct AnthropicContent {
    text: Option<String>,
    #[serde(rename = "type")]
    content_type: Option<String>,
    thinking: Option<String>,
    name: Option<String>,
    input: Option<serde_json::Value>,
    id: Option<String>,
}

impl std::fmt::Display for AnthropicCompleteResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for content in self.content.iter() {
            match content.content_type {
                Some(ref t) if t == "tool_use" => write!(
                    f,
                    "{{\n \"name\": {}, \"input\": {}\n}}",
                    content.name.clone().unwrap_or_default(),
                    content.input.clone().unwrap_or(serde_json::Value::Null)
                )?,
                Some(ref t) if t == "thinking" => {
                    write!(f, "{}", content.thinking.clone().unwrap_or_default())?
                }
                _ => write!(
                    f,
                    "{}",
                    self.content
                        .iter()
                        .map(|c| c.text.clone().unwrap_or_default())
                        .collect::<Vec<_>>()
                        .join("\n")
                )?,
            }
        }
        Ok(())
    }
}

impl ChatResponse for AnthropicCompleteResponse {
    fn text(&self) -> Option<String> {
        Some(
            self.content
                .iter()
                .filter_map(|c| {
                    if c.content_type == Some("text".to_string()) || c.content_type.is_none() {
                        c.text.clone()
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>()
                .join("\n"),
        )
    }

    fn thinking(&self) -> Option<String> {
        self.content
            .iter()
            .find(|c| c.content_type == Some("thinking".to_string()))
            .and_then(|c| c.thinking.clone())
    }

    fn tool_calls(&self) -> Option<Vec<ToolCall>> {
        match self
            .content
            .iter()
            .filter_map(|c| {
                if c.content_type == Some("tool_use".to_string()) {
                    Some(ToolCall {
                        id: c.id.clone().unwrap_or_default(),
                        call_type: "function".to_string(),
                        function: FunctionCall {
                            name: c.name.clone().unwrap_or_default(),
                            arguments: serde_json::to_string(
                                &c.input.clone().unwrap_or(serde_json::Value::Null),
                            )
                            .unwrap_or_default(),
                        },
                    })
                } else {
                    None
                }
            })
            .collect::<Vec<ToolCall>>()
        {
            v if v.is_empty() => None,
            v => Some(v),
        }
    }

    fn usage(&self) -> Option<Usage> {
        self.usage.clone()
    }

    fn finish_reason(&self) -> Option<FinishReason> {
        Some(match self.stop_reason.as_ref() {
            "end_turn" => FinishReason::Stop,
            "max_tokens" => FinishReason::Length,
            "stop_sequence" => FinishReason::Stop,
            "tool_use" => FinishReason::ToolCalls,
            "refusal" | "pause_turn" => FinishReason::Other,
            _ => FinishReason::Unknown,
        })
    }
}

impl Anthropic {
    /// Map a raw Anthropic `stop_reason` string to a typed `FinishReason`.
    ///
    /// Reuses the same mapping logic as `AnthropicCompleteResponse::finish_reason()`.
    pub(crate) fn map_stop_reason(stop_reason: &str) -> FinishReason {
        match stop_reason {
            "end_turn" | "stop_sequence" => FinishReason::Stop,
            "max_tokens" => FinishReason::Length,
            "tool_use" => FinishReason::ToolCalls,
            "refusal" | "pause_turn" => FinishReason::Other,
            _ => FinishReason::Unknown,
        }
    }

    fn default_base_url() -> Url {
        Url::parse("https://api.anthropic.com/v1/").unwrap()
    }

    /// Returns the current API key, using the resolver if available.
    fn resolved_key(&self) -> String {
        if let Some(ref resolver) = self.key_resolver {
            resolver.current()
        } else {
            self.api_key.clone()
        }
    }

    /// Determines the authentication type to use.
    /// Delegates to `detect_auth_type` for the actual logic.
    fn determine_auth_type(&self) -> AuthType {
        let key = self.resolved_key();
        detect_auth_type(&key, self.auth_type.clone())
    }

    /// Returns true if using OAuth authentication
    fn is_oauth(&self) -> bool {
        self.determine_auth_type() == AuthType::OAuth
    }

    /// Sanitizes the system prompt for OAuth requests.
    ///
    /// For OAuth: always prepends `OAUTH_SYSTEM_PROMPT` as the first block,
    /// converting a plain `Text` prompt to `Blocks` in the process.
    /// When no system prompt is configured and OAuth is active, returns the
    /// `OAUTH_SYSTEM_PROMPT` const wrapped in a single-element `Blocks`.
    ///
    /// For non-OAuth: returns the configured system prompt unchanged.
    fn sanitize_system_prompt(&self) -> Option<AnthropicSystemPrompt> {
        if !self.is_oauth() {
            return self.system.clone();
        }

        let oauth_block = TextBlockParam {
            block_type: "text".to_string(),
            text: OAUTH_SYSTEM_PROMPT.to_string(),
            cache_control: None,
            citations: None,
        };

        match &self.system {
            None => Some(AnthropicSystemPrompt::Blocks(vec![oauth_block])),
            Some(AnthropicSystemPrompt::Text(s)) => Some(AnthropicSystemPrompt::Blocks(vec![
                oauth_block,
                TextBlockParam {
                    block_type: "text".to_string(),
                    text: s.clone(),
                    cache_control: None,
                    citations: None,
                },
            ])),
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                let mut result = Vec::with_capacity(1 + blocks.len());
                result.push(oauth_block);
                result.extend(blocks.iter().cloned());
                Some(AnthropicSystemPrompt::Blocks(result))
            }
        }
    }

    /// Prefixes a tool name with TOOL_PREFIX if using OAuth
    fn prefix_tool_name(&self, name: &str) -> String {
        if self.is_oauth() {
            format!("{}{}", TOOL_PREFIX, name)
        } else {
            name.to_string()
        }
    }

    /// Returns true for models that support Anthropic adaptive thinking mode.
    fn is_adaptive_reasoning_model(&self) -> bool {
        ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"]
            .iter()
            .any(|needle| self.model.contains(needle))
    }

    /// Maps generic reasoning effort to Anthropic default budget tokens.
    fn effort_budget_tokens(effort: ReasoningEffort) -> u32 {
        match effort {
            ReasoningEffort::Low => 1_024,
            ReasoningEffort::Medium => 8_000,
            ReasoningEffort::High => 16_000,
            ReasoningEffort::Max => 31_999,
        }
    }

    /// Strips the TOOL_PREFIX from a tool name if present (used for responses)
    fn strip_tool_prefix(name: &str) -> String {
        name.strip_prefix(TOOL_PREFIX).unwrap_or(name).to_string()
    }

    /// Adds authentication headers to the request builder based on auth type
    fn add_auth_headers(&self, builder: http::request::Builder) -> http::request::Builder {
        let key = self.resolved_key();
        let auth_type = self.determine_auth_type();
        let builder = match auth_type {
            AuthType::OAuth => builder
                .header(AUTHORIZATION, format!("Bearer {}", key))
                .header(
                    "anthropic-beta",
                    "oauth-2025-04-20,interleaved-thinking-2025-05-14",
                )
                .header(USER_AGENT, "claude-cli/2.1.2 (external, cli)"),
            AuthType::ApiKey => builder.header("x-api-key", &key),
        };
        builder.header("anthropic-version", "2023-06-01")
    }
}

impl HTTPChatProvider for Anthropic {
    fn chat_request(
        &self,
        messages: &[ChatMessage],
        tools: Option<&[Tool]>,
    ) -> Result<Request<Vec<u8>>, LLMError> {
        if self.resolved_key().is_empty() {
            return Err(LLMError::AuthError("Missing Anthropic API key".to_string()));
        }

        let anthropic_messages: Vec<AnthropicMessage> = messages
            .iter()
            .map(|m| {
                let mut content: Vec<MessageContent> = Vec::new();

                for block in &m.content {
                    match block {
                        Content::Text { text } => {
                            // Avoid Anthropic API error: "text content blocks must be non-empty"
                            if !text.is_empty() {
                                content.push(MessageContent::Text {
                                    content_type: "text",
                                    text: text.clone(),
                                    cache_control: None,
                                });
                            }
                        }
                        Content::Thinking { text, signature } => {
                            // Anthropic requires signed thinking blocks when replaying
                            // assistant history. Skip unsigned legacy blocks.
                            if let Some(signature) = signature {
                                content.push(MessageContent::Thinking {
                                    content_type: "thinking",
                                    thinking: text.clone(),
                                    signature: signature.clone(),
                                    cache_control: None,
                                });
                            }
                        }
                        Content::Image { mime_type, data } => {
                            content.push(MessageContent::Image {
                                content_type: "image",
                                source: ImageSource {
                                    source_type: "base64",
                                    media_type: mime_type.clone(),
                                    data: BASE64.encode(data),
                                },
                                cache_control: None,
                            });
                        }
                        Content::ImageUrl { url } => {
                            content.push(MessageContent::ImageUrl {
                                content_type: "image_url",
                                image_url: ImageUrlContent { url: url.clone() },
                                cache_control: None,
                            });
                        }
                        Content::Pdf { data } => {
                            content.push(MessageContent::Document {
                                content_type: "document",
                                source: ImageSource {
                                    source_type: "base64",
                                    media_type: "application/pdf".to_string(),
                                    data: BASE64.encode(data),
                                },
                                cache_control: None,
                            });
                        }
                        Content::ToolUse {
                            id,
                            name,
                            arguments,
                        } => {
                            // Anthropic API requires `input` to be a JSON object, never null.
                            let input = if arguments.is_object() {
                                arguments.clone()
                            } else {
                                Value::Object(Default::default())
                            };
                            content.push(MessageContent::ToolUse {
                                content_type: "tool_use",
                                id: id.clone(),
                                name: self.prefix_tool_name(name),
                                input,
                                cache_control: None,
                            });
                        }
                        Content::ToolResult {
                            id,
                            is_error,
                            content: inner,
                            ..
                        } => {
                            // Anthropic supports multi-content tool results natively:
                            // { type: "tool_result", tool_use_id, content: [text, image, ...] }
                            let tool_content: Vec<ToolResultContent> = inner
                                .iter()
                                .filter_map(|c| match c {
                                    Content::Text { text } => Some(ToolResultContent::Text {
                                        content_type: "text",
                                        text: text.clone(),
                                    }),
                                    Content::Image { mime_type, data } => {
                                        Some(ToolResultContent::Image {
                                            content_type: "image",
                                            source: ImageSource {
                                                source_type: "base64",
                                                media_type: mime_type.clone(),
                                                data: BASE64.encode(data),
                                            },
                                        })
                                    }
                                    _ => None, // Skip unsupported nested types
                                })
                                .collect();
                            content.push(MessageContent::ToolResult {
                                content_type: "tool_result",
                                tool_use_id: id.clone(),
                                is_error: if *is_error { Some(true) } else { None },
                                content: tool_content,
                                cache_control: None,
                            });
                        }
                        // Audio, ResourceLink — skip (not supported by Anthropic)
                        _ => {}
                    }
                }

                // Apply cache_control to the last content block if present
                if let Some(cache_hint) = &m.cache {
                    if let Some(last) = content.last_mut() {
                        let cc = match cache_hint {
                            querymt::chat::CacheHint::Ephemeral { ttl_seconds } => {
                                CacheControlEphemeral {
                                    control_type: "ephemeral".to_string(),
                                    ttl: match ttl_seconds {
                                        Some(s) if *s > 300 => Some(CacheTTL::OneHour),
                                        Some(_) => Some(CacheTTL::FiveMinutes),
                                        None => None,
                                    },
                                }
                            }
                        };
                        last.set_cache_control(cc);
                    }
                }

                AnthropicMessage {
                    role: match m.role {
                        ChatRole::User => "user",
                        ChatRole::Assistant => "assistant",
                    },
                    content,
                }
            })
            .collect();

        let maybe_tool_slice: Option<&[Tool]> = tools.or(self.tools.as_deref());
        let anthropic_tools = maybe_tool_slice.map(|slice| {
            slice
                .iter()
                .map(|tool| AnthropicTool {
                    name: self.prefix_tool_name(&tool.function.name),
                    description: &tool.function.description,
                    schema: &tool.function.parameters,
                })
                .collect::<Vec<_>>()
        });

        let tool_choice = match self.tool_choice {
            Some(ToolChoice::Auto) => {
                Some(HashMap::from([("type".to_string(), "auto".to_string())]))
            }
            Some(ToolChoice::Any) => Some(HashMap::from([("type".to_string(), "any".to_string())])),
            Some(ToolChoice::Tool(ref tool_name)) => Some(HashMap::from([
                ("type".to_string(), "tool".to_string()),
                ("name".to_string(), self.prefix_tool_name(tool_name)),
            ])),
            Some(ToolChoice::None) => {
                Some(HashMap::from([("type".to_string(), "none".to_string())]))
            }
            None => None,
        };

        let final_tool_choice = if anthropic_tools.is_some() {
            tool_choice.clone()
        } else {
            None
        };

        let thinking = self.reasoning_effort.map(|effort| {
            if self.is_adaptive_reasoning_model() {
                // Newer Anthropic models support adaptive reasoning and ignore explicit budgets.
                ThinkingConfig {
                    thinking_type: "adaptive".to_string(),
                    budget_tokens: None,
                }
            } else {
                let budget_tokens = self
                    .reasoning_budget_tokens
                    .unwrap_or_else(|| Self::effort_budget_tokens(effort));
                ThinkingConfig {
                    thinking_type: "enabled".to_string(),
                    budget_tokens: Some(budget_tokens),
                }
            }
        });

        // Use sanitized system prompt for OAuth requests
        let sanitized_system = self.sanitize_system_prompt();

        let req_body = AnthropicCompleteRequest {
            messages: anthropic_messages,
            model: &self.model,
            max_tokens: Some(self.max_tokens),
            temperature: if self.reasoning_effort.is_some() {
                // NOTE: Anthropic reasoning mode expects fixed temperature = 1.0.
                Some(1.0)
            } else {
                self.temperature
            },
            system: sanitized_system,
            stream: self.stream,
            top_p: self.top_p,
            top_k: self.top_k,
            tools: anthropic_tools,
            tool_choice: final_tool_choice,
            thinking,
        };

        let json_req = serde_json::to_vec(&req_body)?;
        let mut url = Anthropic::default_base_url().join("messages")?;

        // Add beta query parameter for OAuth requests
        if self.is_oauth() {
            url.query_pairs_mut().append_pair("beta", "true");
        }

        let builder = Request::builder()
            .method(Method::POST)
            .uri(url.as_str())
            .header(CONTENT_TYPE, "application/json");

        let builder = self.add_auth_headers(builder);

        Ok(builder.body(json_req)?)
    }

    fn chat_stream_request(
        &self,
        messages: &[ChatMessage],
        tools: Option<&[Tool]>,
    ) -> Result<Request<Vec<u8>>, LLMError> {
        let mut cfg = self.clone();
        cfg.stream = Some(true);
        cfg.chat_request(messages, tools)
    }

    fn parse_chat(&self, resp: Response<Vec<u8>>) -> Result<Box<dyn ChatResponse>, LLMError> {
        handle_http_error!(resp);

        let mut json_resp: AnthropicCompleteResponse = serde_json::from_slice(resp.body())
            .map_err(|e| LLMError::HttpError(format!("Failed to parse JSON: {}", e)))?;

        // Strip tool prefix from tool names in response (for OAuth)
        if self.is_oauth() {
            for content in &mut json_resp.content {
                if let Some(ref mut name) = content.name {
                    *name = Self::strip_tool_prefix(name);
                }
            }
        }

        Ok(Box::new(json_resp))
    }

    fn supports_streaming(&self) -> bool {
        true
    }

    fn chat_stream_parser(&self) -> Result<Box<dyn ChatStreamParser>, LLMError> {
        Ok(Box::new(AnthropicStreamParser {
            oauth: self.is_oauth(),
            tool_state_buffer: HashMap::new(),
            thinking_state_buffer: HashMap::new(),
        }))
    }
}

struct AnthropicStreamParser {
    oauth: bool,
    tool_state_buffer: HashMap<usize, AnthropicToolUseState>,
    thinking_state_buffer: HashMap<usize, AnthropicThinkingState>,
}

impl ChatStreamParser for AnthropicStreamParser {
    fn parse_chunk(&mut self, chunk: &[u8]) -> Result<Vec<querymt::chat::StreamChunk>, LLMError> {
        let text = std::str::from_utf8(chunk).map_err(|e| LLMError::GenericError(e.to_string()))?;
        let mut chunks = Vec::new();

        for line in text.lines() {
            if let Some(data) = line.strip_prefix("data: ") {
                let data = data.trim();
                if data.is_empty() || data == "[DONE]" {
                    continue;
                }

                let stream_resp: AnthropicStreamResponse =
                    serde_json::from_str(data).map_err(|e| LLMError::ResponseFormatError {
                        message: format!("Failed to parse Anthropic stream data: {}", e),
                        raw_response: data.to_string(),
                    })?;

                match stream_resp.response_type.as_str() {
                    "message_start" => {
                        if let Some(usage) = stream_resp.message.and_then(|m| m.usage) {
                            chunks.push(querymt::chat::StreamChunk::Usage(usage));
                        }
                    }
                    "content_block_start" => {
                        if let (Some(index), Some(block)) =
                            (stream_resp.index, stream_resp.content_block)
                        {
                            if block.block_type == "tool_use" {
                                let name = block.name.unwrap_or_default();
                                let name = if self.oauth {
                                    Anthropic::strip_tool_prefix(&name)
                                } else {
                                    name
                                };
                                let id = block.id.unwrap_or_default();

                                self.tool_state_buffer.insert(
                                    index,
                                    AnthropicToolUseState {
                                        id: id.clone(),
                                        name: name.clone(),
                                        arguments_buffer: String::new(),
                                    },
                                );

                                chunks.push(querymt::chat::StreamChunk::ToolUseStart {
                                    index,
                                    id,
                                    name,
                                });
                            } else if block.block_type == "thinking" {
                                self.thinking_state_buffer
                                    .insert(index, AnthropicThinkingState::default());
                            }
                        }
                    }
                    "content_block_delta" => {
                        if let (Some(index), Some(delta)) = (stream_resp.index, stream_resp.delta) {
                            if let Some(text) = delta.text {
                                chunks.push(querymt::chat::StreamChunk::Text(text));
                            } else if let Some(thinking) = delta.thinking {
                                chunks.push(querymt::chat::StreamChunk::Thinking(thinking));
                            } else if let Some(signature) = delta.signature {
                                if let Some(state) = self.thinking_state_buffer.get_mut(&index) {
                                    state.signature.push_str(&signature);
                                }
                            } else if let Some(partial_json) = delta.partial_json {
                                if let Some(state) = self.tool_state_buffer.get_mut(&index) {
                                    state.arguments_buffer.push_str(&partial_json);
                                }

                                chunks.push(querymt::chat::StreamChunk::ToolUseInputDelta {
                                    index,
                                    partial_json,
                                });
                            }
                        }
                    }
                    "content_block_stop" => {
                        if let Some(index) = stream_resp.index {
                            if let Some(state) = self.tool_state_buffer.remove(&index) {
                                chunks.push(querymt::chat::StreamChunk::ToolUseComplete {
                                    index,
                                    tool_call: querymt::ToolCall {
                                        id: state.id,
                                        call_type: "function".to_string(),
                                        function: querymt::FunctionCall {
                                            name: state.name,
                                            arguments: if state.arguments_buffer.is_empty() {
                                                "{}".to_string()
                                            } else {
                                                state.arguments_buffer
                                            },
                                        },
                                    },
                                });
                            }

                            if let Some(state) = self.thinking_state_buffer.remove(&index)
                                && !state.signature.is_empty()
                            {
                                chunks.push(querymt::chat::StreamChunk::ThinkingSignature(
                                    state.signature,
                                ));
                            }
                        }
                    }
                    "message_delta" => {
                        if let Some(usage) = stream_resp.usage {
                            chunks.push(querymt::chat::StreamChunk::Usage(usage));
                        }

                        if let Some(delta) = stream_resp.delta
                            && let Some(stop_reason) = delta.stop_reason
                        {
                            let finish_reason = Anthropic::map_stop_reason(&stop_reason);
                            chunks.push(querymt::chat::StreamChunk::Done { finish_reason });
                        }
                    }
                    _ => {}
                }
            }
        }
        Ok(chunks)
    }
}

impl HTTPCompletionProvider for Anthropic {
    fn complete_request(&self, _req: &CompletionRequest) -> Result<Request<Vec<u8>>, LLMError> {
        unimplemented!()
    }

    fn parse_complete(&self, _resp: Response<Vec<u8>>) -> Result<CompletionResponse, LLMError> {
        unimplemented!()
    }
}

impl HTTPEmbeddingProvider for Anthropic {
    fn embed_request(&self, _inputs: &[String]) -> Result<Request<Vec<u8>>, LLMError> {
        Err(LLMError::ProviderError(
            "Embedding not supported".to_string(),
        ))
    }

    fn parse_embed(&self, _resp: Response<Vec<u8>>) -> Result<Vec<Vec<f32>>, LLMError> {
        Err(LLMError::ProviderError(
            "Embedding not supported".to_string(),
        ))
    }
}

impl HTTPLLMProvider for Anthropic {
    fn tools(&self) -> Option<&[Tool]> {
        self.tools.as_deref()
    }

    fn key_resolver(&self) -> Option<&Arc<dyn ApiKeyResolver>> {
        self.key_resolver.as_ref()
    }

    fn set_key_resolver(&mut self, resolver: Arc<dyn ApiKeyResolver>) {
        self.key_resolver = Some(resolver);
    }
}

mod factory;
pub use factory::create_http_factory;

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

    /// Build a minimal `Anthropic` instance for use in unit tests.
    fn test_anthropic(api_key: &str) -> Anthropic {
        Anthropic {
            api_key: api_key.to_string(),
            auth_type: None,
            model: "claude-3-7-sonnet-20250219".to_string(),
            max_tokens: 100,
            temperature: Some(1.0),
            timeout_seconds: None,
            system: None,
            stream: None,
            top_p: None,
            top_k: None,
            tools: None,
            tool_choice: None,
            reasoning_effort: None,
            reasoning_budget_tokens: None,
            key_resolver: None,
        }
    }

    #[test]
    fn test_oauth_token_detection() {
        let anthropic = test_anthropic("sk-ant-oat01-abc123");
        assert_eq!(anthropic.determine_auth_type(), AuthType::OAuth);
    }

    #[test]
    fn test_api_key_detection() {
        let anthropic = test_anthropic("sk-ant-api03-xyz789");
        assert_eq!(anthropic.determine_auth_type(), AuthType::ApiKey);
    }

    #[test]
    fn test_explicit_auth_type_override() {
        // Even with an oat token, explicit auth_type should take precedence
        let mut anthropic = test_anthropic("sk-ant-oat01-abc123");
        anthropic.auth_type = Some(AuthType::ApiKey);
        assert_eq!(anthropic.determine_auth_type(), AuthType::ApiKey);
    }

    #[test]
    fn test_fallback_to_api_key_for_unknown_format() {
        let anthropic = test_anthropic("sk-ant-unknown-format");
        // Should default to API key and print warning
        assert_eq!(anthropic.determine_auth_type(), AuthType::ApiKey);
    }

    #[test]
    fn test_version_number_flexibility() {
        // Test with different version numbers
        let anthropic_oat99 = test_anthropic("sk-ant-oat99-future");
        assert_eq!(anthropic_oat99.determine_auth_type(), AuthType::OAuth);

        let anthropic_api15 = test_anthropic("sk-ant-api15-future");
        assert_eq!(anthropic_api15.determine_auth_type(), AuthType::ApiKey);
    }

    #[test]
    fn test_reasoning_budget_tokens_from_config_is_used_for_thinking_budget() {
        use querymt::chat::ChatMessage;

        let cfg = serde_json::json!({
            "api_key": "sk-ant-api03-test",
            "model": "claude-3-7-sonnet-20250219",
            "max_tokens": 2500,
            "reasoning_effort": "high",
            "reasoning_budget_tokens": 1024
        });

        let anthropic: Anthropic = serde_json::from_value(cfg)
            .expect("reasoning_budget_tokens should be accepted in Anthropic config");

        let messages = vec![
            ChatMessage::user()
                .text("How many r in strawberry?")
                .build(),
        ];
        let req = anthropic
            .chat_request(&messages, None)
            .expect("chat request should build");

        let body: serde_json::Value =
            serde_json::from_slice(req.body()).expect("request body should be valid JSON");
        assert_eq!(body["max_tokens"], serde_json::json!(2500));
        assert_eq!(body["thinking"]["type"], serde_json::json!("enabled"));
        assert_eq!(body["thinking"]["budget_tokens"], serde_json::json!(1024));
    }

    #[test]
    fn test_system_prompt_deserialize_string() {
        let json = serde_json::json!({
            "api_key": "sk-ant-api03-test",
            "model": "claude-3-7-sonnet-20250219",
            "max_tokens": 100,
            "system": "You are a helpful assistant."
        });
        let anthropic: Anthropic = serde_json::from_value(json).unwrap();
        assert_eq!(
            anthropic.system,
            Some(AnthropicSystemPrompt::Text(
                "You are a helpful assistant.".to_string()
            ))
        );
    }

    #[test]
    fn test_system_prompt_deserialize_blocks() {
        let json = serde_json::json!({
            "api_key": "sk-ant-api03-test",
            "model": "claude-3-7-sonnet-20250219",
            "max_tokens": 100,
            "system": [
                {
                    "type": "text",
                    "text": "You are a helpful assistant.",
                    "cache_control": {
                        "type": "ephemeral"
                    }
                }
            ]
        });
        let anthropic: Anthropic = serde_json::from_value(json).unwrap();
        match &anthropic.system {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                assert_eq!(blocks.len(), 1);
                assert_eq!(blocks[0].text, "You are a helpful assistant.");
                assert_eq!(blocks[0].block_type, "text");
                assert!(blocks[0].cache_control.is_some());
                let cc = blocks[0].cache_control.as_ref().unwrap();
                assert_eq!(cc.control_type, "ephemeral");
                assert_eq!(cc.ttl, None);
            }
            other => panic!("Expected Blocks variant, got {:?}", other),
        }
    }

    #[test]
    fn test_system_prompt_deserialize_blocks_with_ttl() {
        let json = serde_json::json!({
            "api_key": "sk-ant-api03-test",
            "model": "claude-3-7-sonnet-20250219",
            "max_tokens": 100,
            "system": [
                {
                    "type": "text",
                    "text": "You are a helpful assistant.",
                    "cache_control": {
                        "type": "ephemeral",
                        "ttl": "1h"
                    }
                }
            ]
        });
        let anthropic: Anthropic = serde_json::from_value(json).unwrap();
        match &anthropic.system {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                let cc = blocks[0].cache_control.as_ref().unwrap();
                assert_eq!(cc.ttl, Some(CacheTTL::OneHour));
            }
            other => panic!("Expected Blocks variant, got {:?}", other),
        }
    }

    #[test]
    fn test_system_prompt_serialize_string() {
        let prompt = AnthropicSystemPrompt::Text("Hello".to_string());
        let json = serde_json::to_value(&prompt).unwrap();
        assert_eq!(json, serde_json::json!("Hello"));
    }

    #[test]
    fn test_system_prompt_serialize_blocks() {
        let prompt = AnthropicSystemPrompt::Blocks(vec![TextBlockParam {
            block_type: "text".to_string(),
            text: "Hello".to_string(),
            cache_control: Some(CacheControlEphemeral {
                control_type: "ephemeral".to_string(),
                ttl: Some(CacheTTL::FiveMinutes),
            }),
            citations: None,
        }]);
        let json = serde_json::to_value(&prompt).unwrap();
        assert_eq!(
            json,
            serde_json::json!([
                {
                    "type": "text",
                    "text": "Hello",
                    "cache_control": {
                        "type": "ephemeral",
                        "ttl": "5m"
                    }
                }
            ])
        );
    }

    #[test]
    fn test_system_prompt_deserialize_blocks_with_citations() {
        let json = serde_json::json!({
            "api_key": "sk-ant-api03-test",
            "model": "claude-3-7-sonnet-20250219",
            "max_tokens": 100,
            "system": [
                {
                    "type": "text",
                    "text": "Context from document.",
                    "citations": [
                        {
                            "type": "char_location",
                            "cited_text": "some text",
                            "document_index": 0,
                            "document_title": "doc.pdf",
                            "start_char_index": 0,
                            "end_char_index": 9
                        },
                        {
                            "type": "page_location",
                            "cited_text": "page text",
                            "document_index": 1,
                            "document_title": "doc2.pdf",
                            "start_page_number": 1,
                            "end_page_number": 3
                        }
                    ]
                }
            ]
        });
        let anthropic: Anthropic = serde_json::from_value(json).unwrap();
        match &anthropic.system {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                assert_eq!(blocks.len(), 1);
                let citations = blocks[0].citations.as_ref().unwrap();
                assert_eq!(citations.len(), 2);
                match &citations[0] {
                    TextCitationParam::CharLocation(c) => {
                        assert_eq!(c.cited_text, "some text");
                        assert_eq!(c.start_char_index, 0);
                        assert_eq!(c.end_char_index, 9);
                    }
                    other => panic!("Expected CharLocation, got {:?}", other),
                }
                match &citations[1] {
                    TextCitationParam::PageLocation(p) => {
                        assert_eq!(p.cited_text, "page text");
                        assert_eq!(p.start_page_number, 1);
                        assert_eq!(p.end_page_number, 3);
                    }
                    other => panic!("Expected PageLocation, got {:?}", other),
                }
            }
            other => panic!("Expected Blocks variant, got {:?}", other),
        }
    }

    #[test]
    fn test_sanitize_system_prompt_oauth_no_system() {
        // OAuth with no system prompt → OAUTH_SYSTEM_PROMPT injected as the sole block
        let anthropic = test_anthropic("sk-ant-oat01-abc123");
        let sanitized = anthropic.sanitize_system_prompt();
        match sanitized {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                assert_eq!(blocks.len(), 1);
                assert_eq!(blocks[0].text, OAUTH_SYSTEM_PROMPT);
                assert!(blocks[0].cache_control.is_none());
            }
            other => panic!("Expected Blocks with one OAuth block, got {:?}", other),
        }
    }

    #[test]
    fn test_sanitize_system_prompt_oauth_text() {
        // OAuth with a plain Text prompt → OAUTH_SYSTEM_PROMPT prepended, original preserved
        let mut anthropic = test_anthropic("sk-ant-oat01-abc123");
        anthropic.system = Some(AnthropicSystemPrompt::Text(
            "You are QueryMT assistant.".to_string(),
        ));
        let sanitized = anthropic.sanitize_system_prompt();
        match sanitized {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                assert_eq!(blocks.len(), 2);
                assert_eq!(blocks[0].text, OAUTH_SYSTEM_PROMPT);
                assert_eq!(blocks[1].text, "You are QueryMT assistant.");
            }
            other => panic!("Expected Blocks with two entries, got {:?}", other),
        }
    }

    #[test]
    fn test_sanitize_system_prompt_oauth_blocks() {
        // OAuth with existing Blocks → OAUTH_SYSTEM_PROMPT prepended, original blocks preserved
        let mut anthropic = test_anthropic("sk-ant-oat01-abc123");
        anthropic.system = Some(AnthropicSystemPrompt::Blocks(vec![TextBlockParam {
            block_type: "text".to_string(),
            text: "You are QueryMT assistant.".to_string(),
            cache_control: Some(CacheControlEphemeral {
                control_type: "ephemeral".to_string(),
                ttl: None,
            }),
            citations: None,
        }]));
        let sanitized = anthropic.sanitize_system_prompt();
        match sanitized {
            Some(AnthropicSystemPrompt::Blocks(blocks)) => {
                assert_eq!(blocks.len(), 2);
                assert_eq!(blocks[0].text, OAUTH_SYSTEM_PROMPT);
                assert!(blocks[0].cache_control.is_none());
                assert_eq!(blocks[1].text, "You are QueryMT assistant.");
                // cache_control on the original block should be preserved
                assert!(blocks[1].cache_control.is_some());
            }
            other => panic!("Expected Blocks with two entries, got {:?}", other),
        }
    }

    #[test]
    fn test_sanitize_system_prompt_non_oauth_passthrough() {
        // Non-OAuth: system prompt returned unchanged, no blocks injected
        let mut anthropic = test_anthropic("sk-ant-api03-xyz789");
        anthropic.system = Some(AnthropicSystemPrompt::Text(
            "You are QueryMT assistant.".to_string(),
        ));
        let sanitized = anthropic.sanitize_system_prompt();
        assert_eq!(
            sanitized,
            Some(AnthropicSystemPrompt::Text(
                "You are QueryMT assistant.".to_string()
            ))
        );
    }

    #[test]
    fn test_usage_deserialization_with_cache() {
        // Real fixture from Anthropic API response with cache creation and read tokens
        let json = r#"{
            "input_tokens": 12,
            "cache_creation_input_tokens": 1495,
            "cache_read_input_tokens": 0,
            "cache_creation": {
                "ephemeral_5m_input_tokens": 1495,
                "ephemeral_1h_input_tokens": 0
            },
            "output_tokens": 1024,
            "service_tier": "standard"
        }"#;

        let usage: Usage = serde_json::from_str(json).unwrap();

        // Verify standard token counts
        assert_eq!(usage.input_tokens, 12);
        assert_eq!(usage.output_tokens, 1024);

        // Verify cache-related fields (using Anthropic aliases)
        assert_eq!(usage.cache_write, 1495); // from cache_creation_input_tokens
        assert_eq!(usage.cache_read, 0); // from cache_read_input_tokens

        // Verify default values for fields not in the JSON
        assert_eq!(usage.reasoning_tokens, 0);

        // Note: Extra fields like "cache_creation" and "service_tier" are
        // silently ignored during deserialization as they're not part of Usage struct
    }

    /// Feed a sequence of raw SSE lines through a fresh stream parser and
    /// collect all emitted `StreamChunk`s.
    fn collect_chunks(anthropic: &Anthropic, lines: &[&str]) -> Vec<querymt::chat::StreamChunk> {
        let mut parser = anthropic
            .chat_stream_parser()
            .expect("stream parser should initialize");
        let mut out = Vec::new();
        for line in lines {
            let bytes = line.as_bytes();
            let parsed = parser.parse_chunk(bytes).unwrap();
            out.extend(parsed);
        }
        out
    }

    #[test]
    fn test_streaming_tool_call_assembled() {
        // Simulate the Anthropic SSE events for a single tool call streamed across
        // multiple chunks:
        //   content_block_start  → ToolUseStart
        //   content_block_delta  → ToolUseInputDelta (×2)
        //   content_block_stop   → ToolUseComplete  (with assembled JSON)
        //   message_delta        → Done
        let anthropic = test_anthropic("sk-ant-api03-test");

        let lines = [
            r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01","name":"read_file"}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"path\":"}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\"foo.txt\"}"}}"#,
            r#"data: {"type":"content_block_stop","index":0}"#,
            r#"data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null}}"#,
        ];

        let chunks = collect_chunks(&anthropic, &lines);

        // ToolUseStart
        assert!(
            matches!(
                &chunks[0],
                querymt::chat::StreamChunk::ToolUseStart { index: 0, id, name }
                if id == "toolu_01" && name == "read_file"
            ),
            "expected ToolUseStart, got {:?}",
            chunks[0]
        );

        // Two ToolUseInputDelta chunks
        assert!(
            matches!(
                &chunks[1],
                querymt::chat::StreamChunk::ToolUseInputDelta { index: 0, partial_json }
                if partial_json == r#"{"path":"#
            ),
            "expected first ToolUseInputDelta, got {:?}",
            chunks[1]
        );
        assert!(
            matches!(
                &chunks[2],
                querymt::chat::StreamChunk::ToolUseInputDelta { index: 0, partial_json }
                if partial_json == r#""foo.txt"}"#
            ),
            "expected second ToolUseInputDelta, got {:?}",
            chunks[2]
        );

        // ToolUseComplete with fully assembled arguments
        match &chunks[3] {
            querymt::chat::StreamChunk::ToolUseComplete { index, tool_call } => {
                assert_eq!(*index, 0);
                assert_eq!(tool_call.id, "toolu_01");
                assert_eq!(tool_call.function.name, "read_file");
                assert_eq!(tool_call.function.arguments, r#"{"path":"foo.txt"}"#);
            }
            other => panic!("expected ToolUseComplete, got {:?}", other),
        }

        // Done
        assert!(
            matches!(
                &chunks[4],
                querymt::chat::StreamChunk::Done { finish_reason }
                if *finish_reason == FinishReason::ToolCalls
            ),
            "expected Done, got {:?}",
            chunks[4]
        );

        // Parser state is per-stream and dropped with the parser instance.
    }

    #[test]
    fn test_streaming_multiple_tool_calls() {
        // Two tool calls at indices 0 and 1 (parallel tool calls)
        let anthropic = test_anthropic("sk-ant-api03-test");

        let lines = [
            r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01","name":"read_file"}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"path\":\"a.txt\"}"}}"#,
            r#"data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_02","name":"write_file"}}"#,
            r#"data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":\"b.txt\"}"}}"#,
            r#"data: {"type":"content_block_stop","index":0}"#,
            r#"data: {"type":"content_block_stop","index":1}"#,
            r#"data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null}}"#,
        ];

        let chunks = collect_chunks(&anthropic, &lines);

        let complete_chunks: Vec<_> = chunks
            .iter()
            .filter(|c| matches!(c, querymt::chat::StreamChunk::ToolUseComplete { .. }))
            .collect();

        assert_eq!(
            complete_chunks.len(),
            2,
            "expected 2 ToolUseComplete chunks"
        );

        // Verify each complete chunk has the right tool
        let names: std::collections::HashSet<&str> = complete_chunks
            .iter()
            .filter_map(|c| {
                if let querymt::chat::StreamChunk::ToolUseComplete { tool_call, .. } = c {
                    Some(tool_call.function.name.as_str())
                } else {
                    None
                }
            })
            .collect();
        assert!(names.contains("read_file"));
        assert!(names.contains("write_file"));

        // Parser state is per-stream and dropped with the parser instance.
    }

    #[test]
    fn test_streaming_message_start_usage() {
        // Full "Hello" response from the example in the Anthropic docs.
        // message_start carries input_tokens; message_delta carries output_tokens.
        let anthropic = test_anthropic("sk-ant-api03-test");

        let lines = [
            r#"data: {"type":"message_start","message":{"id":"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY","type":"message","role":"assistant","content":[],"model":"claude-opus-4-6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25,"output_tokens":1}}}"#,
            r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#,
            r#"data: {"type":"ping"}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}}"#,
            r#"data: {"type":"content_block_stop","index":0}"#,
            r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":15}}"#,
            r#"data: {"type":"message_stop"}"#,
        ];

        let chunks = collect_chunks(&anthropic, &lines);

        // First chunk must be Usage from message_start (input_tokens)
        match &chunks[0] {
            querymt::chat::StreamChunk::Usage(u) => {
                assert_eq!(u.input_tokens, 25);
                assert_eq!(u.output_tokens, 1);
            }
            other => panic!("expected Usage from message_start, got {:?}", other),
        }

        // Exactly two Usage chunks total: one from message_start, one from message_delta
        let usage_chunks: Vec<_> = chunks
            .iter()
            .filter(|c| matches!(c, querymt::chat::StreamChunk::Usage(_)))
            .collect();
        assert_eq!(usage_chunks.len(), 2, "expected 2 Usage chunks");

        // The second Usage chunk comes from message_delta and carries output_tokens
        match usage_chunks[1] {
            querymt::chat::StreamChunk::Usage(u) => {
                assert_eq!(u.output_tokens, 15);
            }
            other => panic!("expected second Usage from message_delta, got {:?}", other),
        }

        // Text chunks should have been emitted
        let text: String = chunks
            .iter()
            .filter_map(|c| {
                if let querymt::chat::StreamChunk::Text(t) = c {
                    Some(t.as_str())
                } else {
                    None
                }
            })
            .collect();
        assert_eq!(text, "Hello!");
    }

    #[test]
    fn test_streaming_usage_merge_max_gives_correct_totals() {
        // Verifies that applying Usage::merge_max across the two Usage chunks
        // emitted by the Anthropic provider yields the correct final totals.
        // This mirrors what transitions.rs now does for every streaming response.
        let anthropic = test_anthropic("sk-ant-api03-test");

        let lines = [
            r#"data: {"type":"message_start","message":{"id":"msg_test","type":"message","role":"assistant","content":[],"model":"claude-opus-4-6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25,"output_tokens":1}}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}"#,
            r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":15}}"#,
        ];

        let chunks = collect_chunks(&anthropic, &lines);

        // Collect all Usage chunks and fold them with merge_max (same logic as transitions.rs)
        let merged = chunks.iter().fold(None::<Usage>, |acc, chunk| {
            if let querymt::chat::StreamChunk::Usage(u) = chunk {
                Some(match acc {
                    Some(prev) => prev.merge_max(u.clone()),
                    None => u.clone(),
                })
            } else {
                acc
            }
        });

        let merged = merged.expect("at least one Usage chunk expected");
        assert_eq!(
            merged.input_tokens, 25,
            "input_tokens must come from message_start"
        );
        assert_eq!(
            merged.output_tokens, 15,
            "output_tokens must be the cumulative value from message_delta"
        );
        assert_eq!(merged.cache_read, 0);
        assert_eq!(merged.cache_write, 0);
    }

    #[test]
    fn test_streaming_text_only_no_tool_complete() {
        // Pure text response — no ToolUseComplete should be emitted
        let anthropic = test_anthropic("sk-ant-api03-test");

        let lines = [
            r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#,
            r#"data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"}}"#,
            r#"data: {"type":"content_block_stop","index":0}"#,
            r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}}"#,
        ];

        let chunks = collect_chunks(&anthropic, &lines);

        assert!(
            !chunks
                .iter()
                .any(|c| matches!(c, querymt::chat::StreamChunk::ToolUseComplete { .. })),
            "no ToolUseComplete expected for a text-only response"
        );
        assert!(
            chunks
                .iter()
                .any(|c| matches!(c, querymt::chat::StreamChunk::Text(t) if t == "Hello!")),
            "expected a Text chunk"
        );
        // Parser state is per-stream and dropped with the parser instance.
    }
}
