use async_trait::async_trait;
use futures_util::stream;
use reqwest::Client;
use serde::Serialize;
use serde_json::Value;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::time::Duration;
use thiserror::Error;

use super::stream::SseFramer;
use super::traits::Usage;
use super::{
    ChatCompletionRequest, DynProviderError, FinishReason, LLMProvider, Message, ProviderChunk,
    ProviderStream, Tool,
};
use crate::bus::{ProviderReasoningState, message::ContentBlock};
use crate::storage::Storage;
use std::sync::Arc;

const LLM_REQUEST_TIMEOUT_SECS: u64 = 300;

#[derive(Serialize)]
struct CacheControl {
    #[serde(rename = "type")]
    cache_type: String,
}

impl CacheControl {
    fn ephemeral() -> Self {
        Self {
            cache_type: "ephemeral".to_string(),
        }
    }
}

fn convert_content_blocks(blocks: &[ContentBlock], cacheable: bool) -> Vec<serde_json::Value> {
    blocks
        .iter()
        .map(|b| match b {
            ContentBlock::Text { text } => {
                if cacheable {
                    serde_json::json!({
                        "type": "text",
                        "text": text,
                        "cache_control": CacheControl::ephemeral(),
                    })
                } else {
                    serde_json::json!({ "type": "text", "text": text })
                }
            }
            ContentBlock::ImageUrl { image_url } => convert_image_url_to_anthropic(&image_url.url),
        })
        .collect()
}

fn convert_image_url_to_anthropic(url: &str) -> serde_json::Value {
    // data:image/png;base64,... -> Anthropic image block
    if let Some(caps) = regex::Regex::new(r"data:(image/\w+);base64,(.+)")
        .ok()
        .and_then(|re| re.captures(url))
    {
        let media_type = caps.get(1).map(|m| m.as_str()).unwrap_or("image/png");
        let data = caps.get(2).map(|d| d.as_str()).unwrap_or("");
        return serde_json::json!({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": media_type,
                "data": data
            }
        });
    }
    // Regular URL -> Anthropic image block with url source
    serde_json::json!({
        "type": "image",
        "source": {
            "type": "url",
            "url": url
        }
    })
}

pub struct AnthropicProvider {
    client: Client,
    name: String,
    api_key: String,
    base_url: String,
    extra_headers: HashMap<String, String>,
    model_id: String,
    temperature: Option<f32>,
    max_tokens: Option<u32>,
    model_extra: HashMap<String, serde_json::Value>,
    storage: Option<Arc<Storage>>,
}

impl AnthropicProvider {
    // Keep this constructor aligned with OpenAIProvider and LLMProviderConfig.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        name: String,
        api_key: String,
        base_url: String,
        extra_headers: HashMap<String, String>,
        model_id: String,
        temperature: Option<f32>,
        max_tokens: Option<u32>,
        model_extra: HashMap<String, serde_json::Value>,
    ) -> Self {
        Self {
            client: Client::builder()
                .timeout(Duration::from_secs(LLM_REQUEST_TIMEOUT_SECS))
                .build()
                .unwrap_or_else(|_| Client::new()),
            name,
            api_key,
            base_url,
            extra_headers,
            model_id,
            temperature,
            max_tokens,
            model_extra,
            storage: None,
        }
    }

    pub fn set_storage(&mut self, storage: Arc<Storage>) {
        self.storage = Some(storage);
    }
}

#[derive(Serialize)]
struct AnthropicRequest {
    model: String,
    messages: Vec<AnthropicMessage>,
    max_tokens: u32,
    temperature: Option<f32>,
    stream: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    tools: Option<Vec<AnthropicTool>>,
    #[serde(flatten)]
    extra: HashMap<String, serde_json::Value>,
}

#[derive(Serialize)]
struct AnthropicMessage {
    role: String,
    content: Vec<serde_json::Value>,
}

fn convert_messages(messages: &[Message]) -> Vec<AnthropicMessage> {
    let mut converted = Vec::with_capacity(messages.len());
    let mut index = 0;

    while index < messages.len() {
        let message = &messages[index];

        // Anthropic requires all tool results for one assistant tool-use turn
        // to be carried in a single `role: user` content array.  Steering is
        // represented as a normal user message in PicoBot history, so merge
        // any immediately-following user messages into that same array at
        // the provider boundary.  Durable messages remain independent.
        if message.role == "tool" && message.tool_call_id.is_some() {
            let mut content = Vec::new();
            while index < messages.len()
                && messages[index].role == "tool"
                && messages[index].tool_call_id.is_some()
            {
                let tool = &messages[index];
                let tool_call_id = tool
                    .tool_call_id
                    .as_deref()
                    .expect("tool_call_id checked above");
                content.push(serde_json::json!({
                    "type": "tool_result",
                    "tool_use_id": tool_call_id,
                    "content": convert_content_blocks(&tool.content, false),
                }));
                index += 1;
            }

            // One turn may receive more than one steering message before the
            // next model request. Keep their order while emitting one native
            // Anthropic user message alongside the tool_result blocks.
            while index < messages.len() && messages[index].role == "user" {
                let steering = &messages[index];
                if let Some(native) = native_anthropic_content(steering) {
                    content.extend(native);
                } else {
                    content.extend(convert_content_blocks(&steering.content, false));
                }
                index += 1;
            }

            converted.push(AnthropicMessage {
                role: "user".to_string(),
                content,
            });
            continue;
        }

        let role = message.role.clone();
        let content = if let Some(native) = native_anthropic_content(message) {
            native
        } else {
            let mut blocks = convert_content_blocks(&message.content, message.role == "system");
            if let Some(tool_calls) = message
                .tool_calls
                .as_ref()
                .filter(|calls| !calls.is_empty())
            {
                for tool_call in tool_calls {
                    blocks.push(serde_json::json!({
                        "type": "tool_use",
                        "id": tool_call.id,
                        "name": tool_call.name,
                        "input": tool_call.arguments,
                    }));
                }
            }
            blocks
        };
        converted.push(AnthropicMessage { role, content });
        index += 1;
    }

    converted
}

fn native_anthropic_content(message: &Message) -> Option<Vec<Value>> {
    if message.role != "assistant" {
        return None;
    }
    let state = message.provider_state.as_ref()?;
    if state.provider != "anthropic" {
        return None;
    }
    let blocks = state.payload.get("content")?.as_array()?;
    blocks
        .iter()
        .all(|block| {
            block
                .as_object()
                .and_then(|value| value.get("type"))
                .is_some()
        })
        .then(|| blocks.clone())
}

#[derive(Serialize)]
struct AnthropicTool {
    name: String,
    description: String,
    input_schema: serde_json::Value,
    #[serde(skip_serializing_if = "Option::is_none")]
    cache_control: Option<CacheControl>,
}

#[derive(Debug, Error)]
enum AnthropicStreamError {
    #[error("invalid UTF-8 in Anthropic SSE event: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),
    #[error("invalid Anthropic SSE payload: {0}")]
    Json(#[from] serde_json::Error),
    #[error("Anthropic stream error: {0}")]
    Api(String),
    #[error("Anthropic stream ended without message_stop")]
    MissingFinish,
}

#[derive(Default)]
struct AnthropicSseDecoder {
    framer: SseFramer,
    blocks: BTreeMap<usize, Value>,
    tool_json: HashMap<usize, String>,
    usage: Usage,
    finish_reason: Option<FinishReason>,
    done_emitted: bool,
}

impl AnthropicSseDecoder {
    fn push(&mut self, bytes: &[u8]) -> Result<Vec<ProviderChunk>, AnthropicStreamError> {
        let frames = self.framer.push(bytes)?;
        self.decode_frames(frames)
    }

    fn finish(&mut self) -> Result<Vec<ProviderChunk>, AnthropicStreamError> {
        let frames = self.framer.finish()?;
        let chunks = self.decode_frames(frames)?;
        if !self.done_emitted {
            return Err(AnthropicStreamError::MissingFinish);
        }
        Ok(chunks)
    }

    fn decode_frames(
        &mut self,
        frames: Vec<String>,
    ) -> Result<Vec<ProviderChunk>, AnthropicStreamError> {
        let mut chunks = Vec::new();
        for data in frames {
            let payload: Value = serde_json::from_str(&data)?;
            match payload.get("type").and_then(Value::as_str) {
                Some("message_start") => self.message_start(&payload, &mut chunks),
                Some("content_block_start") => self.block_start(&payload, &mut chunks),
                Some("content_block_delta") => self.block_delta(&payload, &mut chunks),
                Some("content_block_stop") => self.block_stop(&payload),
                Some("message_delta") => self.message_delta(&payload, &mut chunks),
                Some("message_stop") => self.message_stop(&mut chunks),
                Some("error") => {
                    let message = payload
                        .pointer("/error/message")
                        .and_then(Value::as_str)
                        .unwrap_or("unknown streaming error");
                    return Err(AnthropicStreamError::Api(message.to_string()));
                }
                Some("ping") | None | Some(_) => {}
            }
        }
        Ok(chunks)
    }

    fn message_start(&mut self, payload: &Value, chunks: &mut Vec<ProviderChunk>) {
        let message = payload.get("message").unwrap_or(&Value::Null);
        let id = message
            .get("id")
            .and_then(Value::as_str)
            .unwrap_or_default();
        let model = message
            .get("model")
            .and_then(Value::as_str)
            .unwrap_or_default();
        chunks.push(ProviderChunk::Metadata {
            id: id.to_string(),
            model: model.to_string(),
        });
        if let Some(usage) = message.get("usage") {
            update_anthropic_usage(&mut self.usage, usage);
            chunks.push(ProviderChunk::Usage(self.usage.clone()));
        }
    }

    fn block_start(&mut self, payload: &Value, chunks: &mut Vec<ProviderChunk>) {
        let Some(index) = event_index(payload) else {
            return;
        };
        let Some(block) = payload.get("content_block") else {
            return;
        };
        self.blocks.insert(index, block.clone());
        match block.get("type").and_then(Value::as_str) {
            Some("text") => {
                if let Some(text) = block.get("text").and_then(Value::as_str)
                    && !text.is_empty()
                {
                    chunks.push(ProviderChunk::Text(text.to_string()));
                }
            }
            Some("thinking") => {
                if let Some(thinking) = block.get("thinking").and_then(Value::as_str)
                    && !thinking.is_empty()
                {
                    chunks.push(ProviderChunk::Reasoning(thinking.to_string()));
                }
            }
            Some("tool_use") => {
                chunks.push(ProviderChunk::ToolCallStart {
                    index,
                    id: block.get("id").and_then(Value::as_str).map(str::to_string),
                    name: block
                        .get("name")
                        .and_then(Value::as_str)
                        .map(str::to_string),
                });
                let input = block.get("input").cloned().unwrap_or(Value::Null);
                if !input.is_null() && input != serde_json::json!({}) {
                    chunks.push(ProviderChunk::ToolCallArguments {
                        index,
                        delta: input.to_string(),
                    });
                }
            }
            _ => {}
        }
    }

    fn block_delta(&mut self, payload: &Value, chunks: &mut Vec<ProviderChunk>) {
        let Some(index) = event_index(payload) else {
            return;
        };
        let Some(delta) = payload.get("delta") else {
            return;
        };
        match delta.get("type").and_then(Value::as_str) {
            Some("text_delta") => {
                if let Some(text) = delta.get("text").and_then(Value::as_str) {
                    append_block_string(&mut self.blocks, index, "text", text);
                    if !text.is_empty() {
                        chunks.push(ProviderChunk::Text(text.to_string()));
                    }
                }
            }
            Some("thinking_delta") => {
                if let Some(thinking) = delta.get("thinking").and_then(Value::as_str) {
                    append_block_string(&mut self.blocks, index, "thinking", thinking);
                    if !thinking.is_empty() {
                        chunks.push(ProviderChunk::Reasoning(thinking.to_string()));
                    }
                }
            }
            Some("signature_delta") => {
                if let Some(signature) = delta.get("signature").and_then(Value::as_str) {
                    append_block_string(&mut self.blocks, index, "signature", signature);
                }
            }
            Some("input_json_delta") => {
                if let Some(partial) = delta.get("partial_json").and_then(Value::as_str) {
                    self.tool_json.entry(index).or_default().push_str(partial);
                    if !partial.is_empty() {
                        chunks.push(ProviderChunk::ToolCallArguments {
                            index,
                            delta: partial.to_string(),
                        });
                    }
                }
            }
            _ => {}
        }
    }

    fn block_stop(&mut self, payload: &Value) {
        let Some(index) = event_index(payload) else {
            return;
        };
        let Some(json) = self.tool_json.remove(&index) else {
            return;
        };
        let input = serde_json::from_str(&json).unwrap_or(Value::Null);
        if let Some(block) = self.blocks.get_mut(&index)
            && let Some(object) = block.as_object_mut()
        {
            object.insert("input".to_string(), input);
        }
    }

    fn message_delta(&mut self, payload: &Value, chunks: &mut Vec<ProviderChunk>) {
        if let Some(reason) = payload
            .pointer("/delta/stop_reason")
            .and_then(Value::as_str)
        {
            self.finish_reason = Some(FinishReason::from_provider(reason));
        }
        if let Some(usage) = payload.get("usage") {
            update_anthropic_usage(&mut self.usage, usage);
            chunks.push(ProviderChunk::Usage(self.usage.clone()));
        }
    }

    fn message_stop(&mut self, chunks: &mut Vec<ProviderChunk>) {
        if self.done_emitted {
            return;
        }
        let content = self.blocks.values().cloned().collect::<Vec<_>>();
        chunks.push(ProviderChunk::ProviderState(ProviderReasoningState {
            provider: "anthropic".to_string(),
            payload: serde_json::json!({ "version": 1, "content": content }),
        }));
        chunks.push(ProviderChunk::Done(
            self.finish_reason.clone().unwrap_or(FinishReason::Stop),
        ));
        self.done_emitted = true;
    }
}

fn event_index(payload: &Value) -> Option<usize> {
    payload
        .get("index")
        .and_then(Value::as_u64)
        .and_then(|value| usize::try_from(value).ok())
}

fn append_block_string(blocks: &mut BTreeMap<usize, Value>, index: usize, key: &str, delta: &str) {
    let Some(object) = blocks.get_mut(&index).and_then(Value::as_object_mut) else {
        return;
    };
    let value = object
        .entry(key.to_string())
        .or_insert_with(|| Value::String(String::new()));
    if let Some(current) = value.as_str() {
        *value = Value::String(format!("{current}{delta}"));
    }
}

fn update_anthropic_usage(usage: &mut Usage, value: &Value) {
    if let Some(input) = json_u32(value, "input_tokens") {
        usage.prompt_tokens = input;
    }
    if let Some(output) = json_u32(value, "output_tokens") {
        usage.completion_tokens = output;
    }
    if let Some(cache_read) = json_u32(value, "cache_read_input_tokens") {
        usage.cached_tokens = Some(cache_read);
        usage.cache_read_input_tokens = Some(cache_read);
    }
    if let Some(cache_creation) = json_u32(value, "cache_creation_input_tokens") {
        usage.cache_creation_input_tokens = Some(cache_creation);
    }
    usage.total_tokens = usage.prompt_tokens.saturating_add(usage.completion_tokens);
}

fn json_u32(value: &Value, key: &str) -> Option<u32> {
    value
        .get(key)
        .and_then(Value::as_u64)
        .and_then(|number| u32::try_from(number).ok())
}

struct AnthropicHttpStream {
    response: reqwest::Response,
    decoder: AnthropicSseDecoder,
    pending: VecDeque<ProviderChunk>,
    reached_eof: bool,
}

async fn next_anthropic_chunk(
    mut state: AnthropicHttpStream,
) -> Result<Option<(ProviderChunk, AnthropicHttpStream)>, DynProviderError> {
    loop {
        if let Some(chunk) = state.pending.pop_front() {
            return Ok(Some((chunk, state)));
        }
        if state.reached_eof {
            return Ok(None);
        }
        match state.response.chunk().await? {
            Some(bytes) => state.pending.extend(state.decoder.push(&bytes)?),
            None => {
                state.pending.extend(state.decoder.finish()?);
                state.reached_eof = true;
            }
        }
    }
}

#[async_trait]
impl LLMProvider for AnthropicProvider {
    async fn stream(
        &self,
        request: ChatCompletionRequest,
    ) -> Result<ProviderStream, DynProviderError> {
        let start = std::time::Instant::now();
        let url = format!("{}/v1/messages", self.base_url);
        let max_tokens = request.max_tokens.or(self.max_tokens).unwrap_or(1024);

        let tools = request.tools.map(|tools| {
            tools
                .iter()
                .map(|t: &Tool| AnthropicTool {
                    name: t.function.name.clone(),
                    description: t.function.description.clone(),
                    input_schema: t.function.parameters.clone(),
                    cache_control: Some(CacheControl::ephemeral()),
                })
                .collect()
        });

        let body = AnthropicRequest {
            model: self.model_id.clone(),
            messages: convert_messages(&request.messages),
            max_tokens,
            temperature: request.temperature.or(self.temperature),
            stream: true,
            tools,
            extra: self.model_extra.clone(),
        };

        let mut req_builder = self
            .client
            .post(&url)
            .header("x-api-key", &self.api_key)
            .header("anthropic-version", "2023-06-01")
            .header("Content-Type", "application/json");

        for (key, value) in &self.extra_headers {
            req_builder = req_builder.header(key.as_str(), value.as_str());
        }

        let request_summary = super::stream::diagnostic_request_summary(
            &self.model_id,
            body.messages.len(),
            body.tools.as_ref().map_or(0, Vec::len),
        );
        tracing::debug!(
            message_count = body.messages.len(),
            tool_count = body.tools.as_ref().map_or(0, Vec::len),
            "Anthropic streaming request"
        );

        let resp = req_builder.json(&body).send().await.inspect_err(|e| {
            let is_timeout = e.is_timeout();
            tracing::error!(
                provider = %self.name,
                model = %self.model_id,
                url = %url,
                timeout = is_timeout,
                error = %e,
                elapsed_ms = %start.elapsed().as_millis(),
                "LLM API request failed"
            );
        })?;

        let status = resp.status();
        if !status.is_success() {
            let body_text = resp.text().await?;
            let error_msg = serde_json::from_str::<serde_json::Value>(&body_text)
                .ok()
                .and_then(|v| {
                    v.get("error")
                        .and_then(|e| e.get("message"))
                        .and_then(|m| m.as_str())
                        .map(|s| s.to_string())
                })
                .unwrap_or_else(|| body_text.clone());
            tracing::error!(
                provider = %self.name,
                model = %self.model_id,
                http_status = %status,
                error = %error_msg,
                elapsed_ms = %start.elapsed().as_millis(),
                "LLM API returned error"
            );
            if let Some(ref storage) = self.storage {
                let _ = storage
                    .append_llm_call(
                        &self.name,
                        &self.model_id,
                        &request_summary,
                        Some(&body_text),
                        Some(&error_msg),
                        start.elapsed().as_millis() as u64,
                    )
                    .await;
            }
            return Err(format!("API error ({}): {}", status.as_u16(), error_msg).into());
        }
        tracing::debug!(status = %status, "Anthropic streaming response started");
        Ok(Box::pin(stream::try_unfold(
            AnthropicHttpStream {
                response: resp,
                decoder: AnthropicSseDecoder::default(),
                pending: VecDeque::new(),
                reached_eof: false,
            },
            next_anthropic_chunk,
        )))
    }

    fn ptype(&self) -> &str {
        "anthropic"
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn model_id(&self) -> &str {
        &self.model_id
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::providers::ProviderResponseAccumulator;
    use serde_json::json;

    #[test]
    fn test_convert_content_blocks_adds_cache_control_for_system_text() {
        let blocks = vec![ContentBlock::text("hello")];
        let serialized = convert_content_blocks(&blocks, true);

        assert_eq!(serialized[0]["type"], "text");
        assert_eq!(serialized[0]["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn test_convert_content_blocks_leaves_user_text_uncached() {
        let blocks = vec![ContentBlock::text("hello")];
        let serialized = convert_content_blocks(&blocks, false);

        assert!(serialized[0].get("cache_control").is_none());
    }

    #[test]
    fn test_anthropic_tool_serializes_cache_control() {
        let tool = AnthropicTool {
            name: "alpha".to_string(),
            description: "desc".to_string(),
            input_schema: json!({}),
            cache_control: Some(CacheControl::ephemeral()),
        };

        let value = serde_json::to_value(tool).unwrap();
        assert_eq!(value["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn tool_result_preserves_native_image_blocks() {
        let messages = vec![Message {
            role: "tool".to_string(),
            content: vec![
                ContentBlock::text("image ready"),
                ContentBlock::image_url("data:image/png;base64,AAAA"),
            ],
            reasoning_content: None,
            provider_state: None,
            tool_call_id: Some("call_1".to_string()),
            name: Some("file_read".to_string()),
            tool_calls: None,
        }];

        let converted = convert_messages(&messages);
        let result = &converted[0].content[0];

        assert_eq!(converted[0].role, "user");
        assert_eq!(result["type"], "tool_result");
        assert_eq!(result["content"][0]["type"], "text");
        assert_eq!(result["content"][1]["type"], "image");
        assert_eq!(result["content"][1]["source"]["media_type"], "image/png");
        assert_eq!(result["content"][1]["source"]["data"], "AAAA");
    }

    #[test]
    fn tool_results_and_following_steering_share_one_user_content_array() {
        let messages = vec![
            Message::tool("call_1", "lookup", "first result"),
            Message::tool("call_2", "lookup", "second result"),
            Message::user("用户补充指令"),
            Message::user("再补充一条"),
            Message::assistant("最终回答"),
        ];

        let converted = convert_messages(&messages);

        assert_eq!(converted.len(), 2);
        assert_eq!(converted[0].role, "user");
        assert_eq!(converted[0].content.len(), 4);
        assert_eq!(converted[0].content[0]["type"], "tool_result");
        assert_eq!(converted[0].content[0]["tool_use_id"], "call_1");
        assert_eq!(converted[0].content[1]["tool_use_id"], "call_2");
        assert_eq!(
            converted[0].content[2],
            json!({"type": "text", "text": "用户补充指令"})
        );
        assert_eq!(
            converted[0].content[3],
            json!({"type": "text", "text": "再补充一条"})
        );
        assert_eq!(converted[1].role, "assistant");
    }

    #[test]
    fn native_stream_decodes_thinking_signature_tools_usage_and_replay_state() {
        let events = [
            json!({"type":"message_start","message":{"id":"msg_1","model":"claude-test","usage":{"input_tokens":11,"output_tokens":0,"cache_read_input_tokens":3}}}),
            json!({"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}),
            json!({"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"check "}}),
            json!({"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"facts"}}),
            json!({"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig=="}}),
            json!({"type":"content_block_stop","index":0}),
            json!({"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"tool_1","name":"lookup","input":{}}}),
            json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"q\":"}}),
            json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"rust\"}"}}),
            json!({"type":"content_block_stop","index":1}),
            json!({"type":"content_block_start","index":2,"content_block":{"type":"text","text":""}}),
            json!({"type":"content_block_delta","index":2,"delta":{"type":"text_delta","text":"answer"}}),
            json!({"type":"content_block_stop","index":2}),
            json!({"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":7}}),
            json!({"type":"message_stop"}),
        ];
        let wire = events
            .iter()
            .map(|event| format!("event: ignored\ndata: {event}\n\n"))
            .collect::<String>();
        let mut decoder = AnthropicSseDecoder::default();
        let mut accumulator = ProviderResponseAccumulator::default();
        for bytes in wire.as_bytes().chunks(7) {
            for chunk in decoder.push(bytes).unwrap() {
                accumulator.push(chunk);
            }
        }
        for chunk in decoder.finish().unwrap() {
            accumulator.push(chunk);
        }
        let response = accumulator.finish();

        assert_eq!(response.id, "msg_1");
        assert_eq!(response.model, "claude-test");
        assert_eq!(response.reasoning_content.as_deref(), Some("check facts"));
        assert_eq!(response.content, "answer");
        assert_eq!(response.tool_calls.len(), 1);
        assert_eq!(response.tool_calls[0].id, "tool_1");
        assert_eq!(response.tool_calls[0].arguments, json!({"q":"rust"}));
        assert_eq!(response.usage.prompt_tokens, 11);
        assert_eq!(response.usage.completion_tokens, 7);
        assert_eq!(response.usage.total_tokens, 18);
        assert_eq!(response.usage.cache_read_input_tokens, Some(3));

        let state = response.provider_state.unwrap();
        assert_eq!(state.provider, "anthropic");
        assert_eq!(state.payload["content"][0]["thinking"], "check facts");
        assert_eq!(state.payload["content"][0]["signature"], "sig==");
        assert_eq!(state.payload["content"][1]["input"], json!({"q":"rust"}));
        assert_eq!(state.payload["content"][2]["text"], "answer");
    }

    #[test]
    fn matching_provider_state_replays_native_blocks_without_generic_duplicates() {
        let native = json!([
            {"type":"thinking","thinking":"signed thought","signature":"sig=="},
            {"type":"tool_use","id":"tool_1","name":"lookup","input":{"q":"rust"}}
        ]);
        let message = Message {
            role: "assistant".into(),
            content: vec![ContentBlock::text("generic text must not be appended")],
            reasoning_content: Some("display copy".into()),
            provider_state: Some(ProviderReasoningState {
                provider: "anthropic".into(),
                payload: json!({"version":1,"content":native}),
            }),
            tool_call_id: None,
            name: None,
            tool_calls: Some(vec![crate::providers::ToolCall {
                id: "duplicate".into(),
                name: "duplicate".into(),
                arguments: json!({}),
            }]),
        };

        let converted = convert_messages(&[message]);

        assert_eq!(converted[0].content.len(), 2);
        assert_eq!(converted[0].content[0]["signature"], "sig==");
        assert_eq!(converted[0].content[1]["id"], "tool_1");
    }

    #[test]
    fn foreign_provider_state_is_not_replayed_to_anthropic() {
        let message = Message {
            role: "assistant".into(),
            content: vec![ContentBlock::text("answer")],
            reasoning_content: Some("unsigned display reasoning".into()),
            provider_state: Some(ProviderReasoningState {
                provider: "openai".into(),
                payload: json!({"private":"state"}),
            }),
            tool_call_id: None,
            name: None,
            tool_calls: None,
        };

        let converted = convert_messages(&[message]);

        assert_eq!(
            converted[0].content,
            vec![json!({"type":"text","text":"answer"})]
        );
    }

    #[test]
    fn stream_requires_message_stop() {
        let mut decoder = AnthropicSseDecoder::default();
        decoder
            .push(b"data: {\"type\":\"message_start\",\"message\":{}}\n\n")
            .unwrap();

        assert!(matches!(
            decoder.finish(),
            Err(AnthropicStreamError::MissingFinish)
        ));
    }
}
