//! Anthropic Request Builder
//!
//! Converts Open Responses native Item model to Anthropic Messages API format.

use std::collections::BTreeSet;

use serde::Serialize;

use crate::llm_client::{
    Request,
    capability::{CacheStrategy, ModelCapability, ReasoningControl, ReasoningSupport},
    types::{ContentPart, Item, Role, ToolDefinition, parse_tool_arguments},
};

use super::AnthropicScheme;

fn is_false(value: &bool) -> bool {
    !*value
}

/// Anthropic API request body
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicRequest {
    pub model: String,
    pub max_tokens: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub system: Option<String>,
    pub messages: Vec<AnthropicMessage>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<AnthropicTool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub top_k: Option<u32>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub stop_sequences: Vec<String>,
    pub stream: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thinking: Option<AnthropicThinking>,
}

/// Anthropic extended thinking 指示。
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum AnthropicThinking {
    Enabled { budget_tokens: i32 },
}

/// Anthropic message
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicMessage {
    pub role: String,
    pub content: AnthropicContent,
}

/// Anthropic content
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum AnthropicContent {
    Text(String),
    Parts(Vec<AnthropicContentPart>),
}

/// `cache_control` marker attached to a content part, signalling that the
/// prefix up to and including this part should be cached.
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum CacheControl {
    Ephemeral,
}

/// Anthropic content part
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
pub(crate) enum AnthropicContentPart {
    #[serde(rename = "text")]
    Text {
        text: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControl>,
    },
    #[serde(rename = "thinking")]
    Thinking {
        thinking: String,
        signature: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControl>,
    },
    #[serde(rename = "redacted_thinking")]
    RedactedThinking {
        /// 暗号化済み reasoning blob。`Item::Reasoning::encrypted_content`
        /// から渡る。
        data: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControl>,
    },
    #[serde(rename = "tool_use")]
    ToolUse {
        id: String,
        name: String,
        input: serde_json::Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControl>,
    },
    #[serde(rename = "tool_result")]
    ToolResult {
        tool_use_id: String,
        content: String,
        #[serde(default, skip_serializing_if = "is_false")]
        is_error: bool,
        #[serde(skip_serializing_if = "Option::is_none")]
        cache_control: Option<CacheControl>,
    },
}

impl AnthropicContentPart {
    fn text(text: String) -> Self {
        Self::Text {
            text,
            cache_control: None,
        }
    }

    fn thinking(thinking: String, signature: String) -> Self {
        Self::Thinking {
            thinking,
            signature,
            cache_control: None,
        }
    }

    fn redacted_thinking(data: String) -> Self {
        Self::RedactedThinking {
            data,
            cache_control: None,
        }
    }

    fn tool_use(id: String, name: String, input: serde_json::Value) -> Self {
        Self::ToolUse {
            id,
            name,
            input,
            cache_control: None,
        }
    }

    fn tool_result(tool_use_id: String, content: String, is_error: bool) -> Self {
        Self::ToolResult {
            tool_use_id,
            content,
            is_error,
            cache_control: None,
        }
    }

    fn set_cache_control(&mut self, cc: CacheControl) {
        match self {
            Self::Text { cache_control, .. }
            | Self::Thinking { cache_control, .. }
            | Self::RedactedThinking { cache_control, .. }
            | Self::ToolUse { cache_control, .. }
            | Self::ToolResult { cache_control, .. } => {
                *cache_control = Some(cc);
            }
        }
    }
}

/// Anthropic tool definition
#[derive(Debug, Serialize)]
pub(crate) struct AnthropicTool {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub input_schema: serde_json::Value,
}

impl AnthropicScheme {
    /// Build Anthropic request from Request.
    ///
    /// `capability.prompt_caching` が [`CacheStrategy::Auto`] のときは
    /// `cache_control` マーカーを一切挿入しない（Ollama の `/v1/messages`
    /// 流用時など、サーバ側が `cache_control` を受け付けないケース）。
    pub(crate) fn build_request(
        &self,
        model: &str,
        request: &Request,
        capability: &ModelCapability,
    ) -> AnthropicRequest {
        let breakpoints = if matches!(capability.prompt_caching, CacheStrategy::Explicit { .. }) {
            compute_breakpoints(&request.items, request.cache_anchor)
        } else {
            BTreeSet::new()
        };
        let messages = self.convert_items_to_messages(&request.items, &breakpoints);
        let tools = request.tools.iter().map(|t| self.convert_tool(t)).collect();

        // Reasoning の投影: capability が BudgetTokens / Both をサポート
        // していて、request 側で budget_tokens が指定されているときだけ
        // thinking フィールドを付ける。
        let supports_budget_tokens = matches!(
            capability.reasoning,
            Some(ReasoningSupport::BudgetTokens | ReasoningSupport::Both),
        );
        let thinking = request
            .config
            .reasoning
            .as_ref()
            .filter(|_| supports_budget_tokens)
            .and_then(|rc| match rc {
                ReasoningControl::BudgetTokens(budget_tokens) => Some(AnthropicThinking::Enabled {
                    budget_tokens: *budget_tokens,
                }),
                ReasoningControl::Effort(_) => None,
            });

        AnthropicRequest {
            model: model.to_string(),
            max_tokens: request.config.max_tokens.unwrap_or(4096),
            system: request.system_prompt.clone(),
            messages,
            tools,
            temperature: request.config.temperature,
            top_p: request.config.top_p,
            top_k: request.config.top_k,
            stop_sequences: request.config.stop_sequences.clone(),
            stream: true,
            thinking,
        }
    }

    /// Convert Open Responses Items to Anthropic Messages and attach
    /// `cache_control` markers at each breakpoint item's last content
    /// part.
    ///
    /// Anthropic uses a message-based model where:
    /// - User messages have role "user"
    /// - Assistant messages have role "assistant"
    /// - Tool calls are content parts within assistant messages
    /// - Tool results are content parts within user messages
    ///
    /// Assistant-side items are accumulated until a user/system message or
    /// tool result boundary so one logical assistant burst becomes one
    /// Anthropic assistant message content array. Pending parts carry their
    /// origin item index; when flushed, the final part for each item records
    /// the `(msg_idx, part_idx)` used by breakpoint attachment. User/system
    /// `Message` items keep the single-text shorthand unless a breakpoint
    /// needs a concrete part to live on.
    fn convert_items_to_messages(
        &self,
        items: &[Item],
        breakpoints: &BTreeSet<usize>,
    ) -> Vec<AnthropicMessage> {
        let mut messages = Vec::new();
        // Pending parts carry their origin item index so we can record
        // (msg_idx, part_idx) when we flush them into a message.
        let mut pending_assistant: Vec<(usize, AnthropicContentPart)> = Vec::new();
        let mut pending_user: Vec<(usize, AnthropicContentPart)> = Vec::new();
        let mut locations: Vec<Option<(usize, usize)>> = vec![None; items.len()];

        for (i, item) in items.iter().enumerate() {
            match item {
                Item::Message { role, content, .. } => {
                    let parts: Vec<AnthropicContentPart> = content
                        .iter()
                        .map(|p| match p {
                            ContentPart::Text { text } => AnthropicContentPart::text(text.clone()),
                            ContentPart::Refusal { refusal } => {
                                AnthropicContentPart::text(refusal.clone())
                            }
                        })
                        .collect();

                    match role {
                        Role::Assistant => {
                            flush_pending(&mut messages, &mut pending_user, "user", &mut locations);
                            pending_assistant.extend(parts.into_iter().map(|part| (i, part)));
                        }
                        Role::User | Role::System => {
                            flush_pending(
                                &mut messages,
                                &mut pending_assistant,
                                "assistant",
                                &mut locations,
                            );
                            flush_pending(&mut messages, &mut pending_user, "user", &mut locations);

                            let force_parts = breakpoints.contains(&i);
                            let msg_idx = messages.len();

                            // Preserve the single-text shorthand unless a
                            // breakpoint needs a concrete part to live on.
                            if parts.len() == 1 && !force_parts {
                                if let AnthropicContentPart::Text { text, .. } = &parts[0] {
                                    messages.push(AnthropicMessage {
                                        role: "user".to_string(),
                                        content: AnthropicContent::Text(text.clone()),
                                    });
                                    continue;
                                }
                            }

                            let last_part_idx = parts.len().saturating_sub(1);
                            messages.push(AnthropicMessage {
                                role: "user".to_string(),
                                content: AnthropicContent::Parts(parts),
                            });
                            locations[i] = Some((msg_idx, last_part_idx));
                        }
                    }
                }

                Item::ToolCall {
                    call_id,
                    name,
                    arguments,
                    ..
                } => {
                    flush_pending(&mut messages, &mut pending_user, "user", &mut locations);
                    // Parse arguments JSON string to Value (defensive:
                    // normalize non-object / legacy "null" payloads to
                    // `{}` so the Anthropic API accepts it).
                    let input = parse_tool_arguments(arguments);
                    pending_assistant.push((
                        i,
                        AnthropicContentPart::tool_use(call_id.clone(), name.clone(), input),
                    ));
                }

                Item::ToolResult {
                    call_id,
                    summary,
                    content,
                    is_error,
                    ..
                } => {
                    flush_pending(
                        &mut messages,
                        &mut pending_assistant,
                        "assistant",
                        &mut locations,
                    );
                    let text = match content {
                        Some(c) => format!("{summary}\n{c}"),
                        None => summary.clone(),
                    };
                    pending_user.push((
                        i,
                        AnthropicContentPart::tool_result(call_id.clone(), text, *is_error),
                    ));
                }

                Item::Reasoning {
                    text,
                    encrypted_content,
                    signature,
                    ..
                } => {
                    flush_pending(&mut messages, &mut pending_user, "user", &mut locations);
                    // Anthropic はアシスタントターン中の `thinking` /
                    // `redacted_thinking` ブロックを必ず assistant role の
                    // content_part として送り返す必要がある。
                    //
                    // - signature あり: `thinking` content_part を投影
                    // - signature 無し + encrypted_content あり:
                    //   `redacted_thinking` content_part を投影
                    // - どちらも無い: 他 scheme（OpenAI 等）から流入した
                    //   素の reasoning text。Anthropic に投げる意味も
                    //   round-trip の根拠も無いので drop。
                    if let Some(sig) = signature.clone() {
                        pending_assistant
                            .push((i, AnthropicContentPart::thinking(text.clone(), sig)));
                    } else if let Some(data) = encrypted_content.clone() {
                        pending_assistant.push((i, AnthropicContentPart::redacted_thinking(data)));
                    }
                    // どちらも None なら何も pend せず、本 item は無視。
                }
            }
        }

        flush_pending(
            &mut messages,
            &mut pending_assistant,
            "assistant",
            &mut locations,
        );
        flush_pending(&mut messages, &mut pending_user, "user", &mut locations);

        // Apply cache_control markers at each breakpoint item's last part.
        for &bp in breakpoints {
            let Some((msg_idx, part_idx)) = locations.get(bp).copied().flatten() else {
                continue;
            };
            if let AnthropicContent::Parts(parts) = &mut messages[msg_idx].content {
                if let Some(part) = parts.get_mut(part_idx) {
                    part.set_cache_control(CacheControl::Ephemeral);
                }
            }
        }

        messages
    }

    fn convert_tool(&self, tool: &ToolDefinition) -> AnthropicTool {
        AnthropicTool {
            name: tool.name.clone(),
            description: tool.description.clone(),
            input_schema: tool.input_schema.clone(),
        }
    }
}

/// Flush a pending parts buffer into a new message, recording the
/// emitted `(msg_idx, part_idx)` for each originating item.
fn flush_pending(
    messages: &mut Vec<AnthropicMessage>,
    pending: &mut Vec<(usize, AnthropicContentPart)>,
    role: &str,
    locations: &mut [Option<(usize, usize)>],
) {
    if pending.is_empty() {
        return;
    }
    let msg_idx = messages.len();
    let taken: Vec<(usize, AnthropicContentPart)> = std::mem::take(pending);
    let mut parts = Vec::with_capacity(taken.len());
    for (part_idx, (origin, part)) in taken.into_iter().enumerate() {
        locations[origin] = Some((msg_idx, part_idx));
        parts.push(part);
    }
    messages.push(AnthropicMessage {
        role: role.to_string(),
        content: AnthropicContent::Parts(parts),
    });
}

/// Compute the set of item indices that should receive a cache_control
/// breakpoint:
///
/// 1. `cache_anchor` (stable prefix boundary — typically a post-compact
///    summary at index 0).
/// 2. The item immediately preceding the most recent `Role::User`
///    message (i.e. the end of the previous turn). This is the rewind
///    boundary: if a user re-issues the turn from scratch, the cache up
///    to here remains valid.
/// 3. The final item (head of the outgoing request, for the next
///    request in the same turn to read from).
///
/// Overlapping positions are collapsed to a single breakpoint.
fn compute_breakpoints(items: &[Item], cache_anchor: Option<usize>) -> BTreeSet<usize> {
    let mut bps = BTreeSet::new();
    if items.is_empty() {
        return bps;
    }
    if let Some(anchor) = cache_anchor {
        if anchor < items.len() {
            bps.insert(anchor);
        }
    }
    let last_user = items.iter().rposition(|it| {
        matches!(
            it,
            Item::Message {
                role: Role::User,
                ..
            }
        )
    });
    if let Some(i) = last_user {
        if i > 0 {
            bps.insert(i - 1);
        }
    }
    bps.insert(items.len() - 1);
    bps
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::llm_client::capability::{
        CacheStrategy, ReasoningEffort, StructuredOutput, ToolCallingSupport,
    };

    /// cache_control が有効になる既定の capability。
    fn cap_explicit() -> ModelCapability {
        ModelCapability {
            tool_calling: ToolCallingSupport::Parallel,
            structured_output: StructuredOutput::JsonSchema,
            reasoning: None,
            vision: false,
            prompt_caching: CacheStrategy::Explicit { max_breakpoints: 4 },
        }
    }

    /// cache_control を送らない capability（Ollama 等）。
    fn cap_auto() -> ModelCapability {
        ModelCapability {
            prompt_caching: CacheStrategy::Auto,
            ..cap_explicit()
        }
    }

    fn cap_budget_reasoning() -> ModelCapability {
        ModelCapability {
            reasoning: Some(ReasoningSupport::BudgetTokens),
            ..cap_explicit()
        }
    }

    #[test]
    fn test_build_simple_request() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .system("You are a helpful assistant.")
            .user("Hello!");

        let anthropic_req =
            scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        assert_eq!(anthropic_req.model, "claude-sonnet-4-20250514");
        assert_eq!(
            anthropic_req.system,
            Some("You are a helpful assistant.".to_string())
        );
        assert_eq!(anthropic_req.messages.len(), 1);
        assert!(anthropic_req.stream);
    }

    #[test]
    fn test_build_request_with_tool() {
        let scheme = AnthropicScheme::new();
        let request = Request::new().user("What's the weather?").tool(
            ToolDefinition::new("get_weather")
                .description("Get current weather")
                .input_schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "location": { "type": "string" }
                    },
                    "required": ["location"]
                })),
        );

        let anthropic_req =
            scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        assert_eq!(anthropic_req.tools.len(), 1);
        assert_eq!(anthropic_req.tools[0].name, "get_weather");
    }

    #[test]
    fn thinking_budget_projected_when_supported() {
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("think");
        request.config.reasoning = Some(ReasoningControl::BudgetTokens(4096));

        let req = scheme.build_request(
            "claude-sonnet-4-20250514",
            &request,
            &cap_budget_reasoning(),
        );
        let json = serde_json::to_value(&req).unwrap();

        assert_eq!(json["thinking"]["type"], "enabled");
        assert_eq!(json["thinking"]["budget_tokens"], 4096);
    }

    #[test]
    fn effort_reasoning_not_projected_to_anthropic() {
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("think");
        request.config.reasoning = Some(ReasoningControl::Effort(ReasoningEffort::High));

        let req = scheme.build_request(
            "claude-sonnet-4-20250514",
            &request,
            &cap_budget_reasoning(),
        );

        assert!(req.thinking.is_none());
    }

    #[test]
    fn test_tool_call_and_result() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("What's the weather?")
            .item(Item::tool_call(
                "call_123",
                "get_weather",
                r#"{"city":"Tokyo"}"#,
            ))
            .item(Item::tool_result("call_123", "Sunny, 25°C"));

        let anthropic_req =
            scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        assert_eq!(anthropic_req.messages.len(), 3);
        assert_eq!(anthropic_req.messages[0].role, "user");
        assert_eq!(anthropic_req.messages[1].role, "assistant");
        assert_eq!(anthropic_req.messages[2].role, "user");
    }

    /// Pull out the `cache_control` field from a part regardless of variant.
    fn part_cache_control(part: &AnthropicContentPart) -> Option<CacheControl> {
        match part {
            AnthropicContentPart::Text { cache_control, .. }
            | AnthropicContentPart::Thinking { cache_control, .. }
            | AnthropicContentPart::RedactedThinking { cache_control, .. }
            | AnthropicContentPart::ToolUse { cache_control, .. }
            | AnthropicContentPart::ToolResult { cache_control, .. } => *cache_control,
        }
    }

    /// All `(msg_idx, part_idx, cache_control)` triples whose `cache_control`
    /// is set, in iteration order over the output.
    fn breakpoint_positions(req: &AnthropicRequest) -> Vec<(usize, usize, CacheControl)> {
        let mut out = Vec::new();
        for (mi, msg) in req.messages.iter().enumerate() {
            if let AnthropicContent::Parts(parts) = &msg.content {
                for (pi, part) in parts.iter().enumerate() {
                    if let Some(cc) = part_cache_control(part) {
                        out.push((mi, pi, cc));
                    }
                }
            }
        }
        out
    }

    #[test]
    fn assistant_burst_bundles_reasoning_text_and_tool_call() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("question?")
            .item(Item::reasoning("thinking").with_signature("SIG-A"))
            .item(Item::assistant_message("answer"))
            .item(Item::tool_call("c1", "tool_a", r#"{"x":1}"#));

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        assert_eq!(req.messages.len(), 2, "messages: {:?}", req.messages);
        assert_eq!(req.messages[0].role, "user");
        assert_eq!(req.messages[1].role, "assistant");
        let AnthropicContent::Parts(parts) = &req.messages[1].content else {
            panic!("assistant burst must be emitted as content parts");
        };
        assert_eq!(parts.len(), 3, "parts: {:?}", parts);
        assert!(matches!(parts[0], AnthropicContentPart::Thinking { .. }));
        assert!(matches!(parts[1], AnthropicContentPart::Text { .. }));
        assert!(matches!(parts[2], AnthropicContentPart::ToolUse { .. }));
    }

    #[test]
    fn tool_result_and_user_messages_bound_assistant_bursts() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("question?")
            .item(Item::reasoning("thinking").with_signature("SIG-A"))
            .item(Item::assistant_message("answer"))
            .item(Item::tool_call("c1", "tool_a", "{}"))
            .item(Item::tool_result("c1", "result"))
            .item(Item::assistant_message("final"))
            .user("follow up");

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        let roles: Vec<&str> = req.messages.iter().map(|msg| msg.role.as_str()).collect();
        assert_eq!(
            roles,
            vec!["user", "assistant", "user", "assistant", "user"]
        );

        let AnthropicContent::Parts(first_assistant) = &req.messages[1].content else {
            panic!("first assistant burst must be content parts");
        };
        assert_eq!(first_assistant.len(), 3);
        assert!(matches!(
            first_assistant[0],
            AnthropicContentPart::Thinking { .. }
        ));
        assert!(matches!(
            first_assistant[1],
            AnthropicContentPart::Text { .. }
        ));
        assert!(matches!(
            first_assistant[2],
            AnthropicContentPart::ToolUse { .. }
        ));

        let AnthropicContent::Parts(tool_result) = &req.messages[2].content else {
            panic!("tool result must be content parts");
        };
        assert_eq!(tool_result.len(), 1);
        assert!(matches!(
            tool_result[0],
            AnthropicContentPart::ToolResult { .. }
        ));

        let AnthropicContent::Parts(second_assistant) = &req.messages[3].content else {
            panic!("second assistant burst must be content parts");
        };
        assert_eq!(second_assistant.len(), 1);
        assert!(matches!(
            second_assistant[0],
            AnthropicContentPart::Text { .. }
        ));
    }

    #[test]
    fn assistant_message_breakpoint_maps_to_text_part_inside_burst() {
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().items(vec![
            Item::user_message("question?"),
            Item::reasoning("thinking").with_signature("SIG-A"),
            Item::assistant_message("answer"),
            Item::tool_call("c1", "tool_a", "{}"),
            Item::user_message("next"),
        ]);
        request.cache_anchor = Some(2);

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let AnthropicContent::Parts(parts) = &req.messages[1].content else {
            panic!("assistant burst must be content parts");
        };

        assert!(matches!(parts[0], AnthropicContentPart::Thinking { .. }));
        assert!(matches!(parts[1], AnthropicContentPart::Text { .. }));
        assert!(matches!(parts[2], AnthropicContentPart::ToolUse { .. }));
        assert_eq!(part_cache_control(&parts[1]), Some(CacheControl::Ephemeral));
        assert_eq!(part_cache_control(&parts[2]), Some(CacheControl::Ephemeral));
    }

    /// Convenience: a turn that ends with one assistant text, one tool
    /// call/result pair, and a final assistant text. Produced at
    /// `history[head..]` indices shown alongside, so tests can reason
    /// about breakpoint positions.
    fn completed_turn() -> Vec<Item> {
        vec![
            Item::user_message("hello"),           // 0
            Item::assistant_message("hi"),         // 1
            Item::tool_call("c1", "tool_a", "{}"), // 2
            Item::tool_result("c1", "ok"),         // 3
            Item::assistant_message("done"),       // 4
        ]
    }

    #[test]
    fn three_breakpoints_when_compact_plus_prior_turn() {
        let scheme = AnthropicScheme::new();
        let mut items = vec![Item::system_message("[Compacted context summary]\n\nprior")];
        items.extend(completed_turn()); // now indices 1..=5
        items.push(Item::user_message("next turn"));
        // anchor=0, last user idx=6 → turn_end=5, head=6.
        let mut request = Request::new().items(items);
        request.cache_anchor = Some(0);

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 3, "expected 3 breakpoints, got {:?}", bps);
        for (_, _, cc) in bps {
            assert_eq!(cc, CacheControl::Ephemeral);
        }
    }

    #[test]
    fn two_breakpoints_without_compaction() {
        let scheme = AnthropicScheme::new();
        let mut items = completed_turn();
        items.push(Item::user_message("next turn")); // index 5 = latest user
        // cache_anchor=None, turn_end=4, head=5.
        let request = Request::new().items(items);

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 2, "expected 2 breakpoints, got {:?}", bps);
    }

    #[test]
    fn single_breakpoint_when_only_first_user_message() {
        let scheme = AnthropicScheme::new();
        let request = Request::new().user("first ever turn");
        // latest user at 0 → no turn_end; head=0; no anchor. Collapse → 1.
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 1, "expected 1 breakpoint, got {:?}", bps);
    }

    #[test]
    fn overlap_collapses_anchor_and_turn_end() {
        // items = [compact_summary(0, Role::System), user(1)]
        // anchor=0, latest user=1 → turn_end=0, head=1. Anchor∩turn_end at 0.
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().items(vec![
            Item::system_message("[Compacted context summary]\n\nprior"),
            Item::user_message("fresh user"),
        ]);
        request.cache_anchor = Some(0);

        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 2, "expected collapse to 2, got {:?}", bps);
    }

    #[test]
    fn breakpoint_on_tool_result_head() {
        // Mid-turn second call: items end with a tool_result. Head must
        // land on the ToolResult part.
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("run it")
            .item(Item::tool_call("c1", "t", "{}"))
            .item(Item::tool_result("c1", "result"));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 1);
        let (mi, pi, _) = bps[0];
        let part = match &req.messages[mi].content {
            AnthropicContent::Parts(parts) => &parts[pi],
            _ => panic!("expected Parts for breakpoint-bearing message"),
        };
        assert!(
            matches!(part, AnthropicContentPart::ToolResult { .. }),
            "expected ToolResult, got {:?}",
            part,
        );
    }

    #[test]
    fn single_text_message_uses_text_shorthand_without_breakpoint() {
        // Non-breakpoint single-text messages stay on the text shorthand
        // so we don't bloat requests with wrapper arrays. Here the Head
        // lands on items[1], leaving items[0] without a marker.
        let scheme = AnthropicScheme::new();
        let request = Request::new().user("hello").assistant("hi there");
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        assert!(
            matches!(req.messages[0].content, AnthropicContent::Text(_)),
            "non-breakpoint single-text message should use text shorthand",
        );
    }

    #[test]
    fn single_text_message_is_forced_to_parts_when_breakpoint() {
        // A breakpoint on a single-text message must be emitted in the
        // array form so cache_control has a part to attach to.
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("hello");
        request.cache_anchor = Some(0);
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        match &req.messages[0].content {
            AnthropicContent::Parts(parts) => {
                assert_eq!(parts.len(), 1);
                assert_eq!(part_cache_control(&parts[0]), Some(CacheControl::Ephemeral));
            }
            AnthropicContent::Text(_) => panic!("breakpoint item should use Parts form"),
        }
    }

    #[test]
    fn serialized_json_shape_matches_anthropic_spec() {
        // Single-sentence smoke test against the exact JSON key shape
        // Anthropic expects for cache_control.
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("hello");
        request.cache_anchor = Some(0);
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let json = serde_json::to_value(&req).unwrap();
        let part = &json["messages"][0]["content"][0];
        assert_eq!(part["type"], "text");
        assert_eq!(part["text"], "hello");
        assert_eq!(part["cache_control"]["type"], "ephemeral");
    }

    #[test]
    fn cache_anchor_out_of_range_is_ignored() {
        // Defensive: if a caller passes a stale anchor beyond items.len(),
        // we drop it silently rather than panicking.
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("one");
        request.cache_anchor = Some(99);
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        // Only the Head breakpoint survives.
        let bps = breakpoint_positions(&req);
        assert_eq!(bps.len(), 1);
    }

    #[test]
    fn empty_items_produce_no_breakpoints() {
        let scheme = AnthropicScheme::new();
        let req =
            scheme.build_request("claude-sonnet-4-20250514", &Request::new(), &cap_explicit());
        assert!(req.messages.is_empty());
        assert!(breakpoint_positions(&req).is_empty());
    }

    #[test]
    fn cache_auto_does_not_add_cache_control() {
        // Ollama のように `CacheStrategy::Auto` のときは cache_control
        // マーカーを一切付けない。breakpoint 計算も走らないこと。
        let scheme = AnthropicScheme::new();
        let mut request = Request::new().user("hello");
        request.cache_anchor = Some(0);
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_auto());
        assert!(breakpoint_positions(&req).is_empty());
    }

    fn collect_assistant_thinking_parts(req: &AnthropicRequest) -> Vec<&AnthropicContentPart> {
        let mut out = Vec::new();
        for msg in &req.messages {
            if msg.role != "assistant" {
                continue;
            }
            if let AnthropicContent::Parts(parts) = &msg.content {
                for part in parts {
                    if matches!(
                        part,
                        AnthropicContentPart::Thinking { .. }
                            | AnthropicContentPart::RedactedThinking { .. }
                    ) {
                        out.push(part);
                    }
                }
            }
        }
        out
    }

    #[test]
    fn reasoning_with_signature_projects_thinking_part() {
        // Item::Reasoning に signature があれば assistant role の
        // `thinking` content_part として送る。
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("hi")
            .item(Item::reasoning("step-by-step").with_signature("SIG-A"))
            .item(Item::assistant_message("done"));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let thinking_parts = collect_assistant_thinking_parts(&req);
        assert_eq!(thinking_parts.len(), 1);
        match thinking_parts[0] {
            AnthropicContentPart::Thinking {
                thinking,
                signature,
                ..
            } => {
                assert_eq!(thinking, "step-by-step");
                assert_eq!(signature, "SIG-A");
            }
            other => panic!("expected Thinking part, got {other:?}"),
        }
    }

    #[test]
    fn reasoning_with_only_encrypted_content_projects_redacted_thinking() {
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("hi")
            .item(Item::reasoning("").with_encrypted_content("opaque"))
            .item(Item::assistant_message("done"));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let parts = collect_assistant_thinking_parts(&req);
        assert_eq!(parts.len(), 1);
        match parts[0] {
            AnthropicContentPart::RedactedThinking { data, .. } => {
                assert_eq!(data, "opaque");
            }
            other => panic!("expected RedactedThinking, got {other:?}"),
        }
    }

    #[test]
    fn reasoning_without_signature_or_encrypted_is_dropped() {
        // 他 scheme から流入した素の reasoning は Anthropic に投げない。
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("hi")
            .item(Item::reasoning("plain text"))
            .item(Item::assistant_message("done"));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        // thinking part は 1 つも乗らない
        assert!(collect_assistant_thinking_parts(&req).is_empty());
    }

    #[test]
    fn thinking_part_lands_in_assistant_role_message() {
        // wire 構造の position 検証: thinking part は assistant role の
        // message 配列に並ぶ（user role には絶対に入らない）。
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("question?")
            .item(Item::reasoning("thinking inside").with_signature("SIG-A"))
            .item(Item::tool_call("c1", "tool_a", "{}"))
            .item(Item::tool_result("c1", "result"))
            .user("follow up");
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());

        // 全 thinking part が assistant role の message に存在すること
        let mut thinking_msg_indices = Vec::new();
        for (i, msg) in req.messages.iter().enumerate() {
            if let AnthropicContent::Parts(parts) = &msg.content {
                if parts
                    .iter()
                    .any(|p| matches!(p, AnthropicContentPart::Thinking { .. }))
                {
                    assert_eq!(
                        msg.role, "assistant",
                        "thinking part must be in assistant role, got {} at msg {}",
                        msg.role, i,
                    );
                    thinking_msg_indices.push(i);
                }
            }
        }
        assert!(
            !thinking_msg_indices.is_empty(),
            "expected at least one thinking part in messages: {:?}",
            req.messages,
        );

        // thinking part を含む assistant message は、それに続く tool_use を含む
        // assistant message より前 (= 先頭側) に位置すること
        // (Anthropic 仕様: 同一論理ターン内で thinking → tool_use の順)
        let mut tool_use_msg_indices = Vec::new();
        for (i, msg) in req.messages.iter().enumerate() {
            if let AnthropicContent::Parts(parts) = &msg.content {
                if parts
                    .iter()
                    .any(|p| matches!(p, AnthropicContentPart::ToolUse { .. }))
                {
                    tool_use_msg_indices.push(i);
                }
            }
        }
        assert!(!tool_use_msg_indices.is_empty(), "expected tool_use part");
        let first_thinking = thinking_msg_indices[0];
        let first_tool_use = tool_use_msg_indices[0];
        assert!(
            first_thinking <= first_tool_use,
            "thinking msg ({}) must precede tool_use msg ({})",
            first_thinking,
            first_tool_use,
        );
    }

    #[test]
    fn redacted_thinking_part_lands_in_assistant_role_message() {
        // RedactedThinking も同様に assistant role に置かれること。
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("ask")
            .item(Item::reasoning("").with_encrypted_content("opaque"))
            .item(Item::tool_call("c1", "tool_a", "{}"))
            .item(Item::tool_result("c1", "ok"));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        for msg in &req.messages {
            if let AnthropicContent::Parts(parts) = &msg.content {
                for part in parts {
                    if matches!(part, AnthropicContentPart::RedactedThinking { .. }) {
                        assert_eq!(msg.role, "assistant");
                    }
                }
            }
        }
    }

    #[test]
    fn tool_definitions_carry_no_cache_control() {
        // Tool JSON schema must serialise unchanged — no sneak-in of
        // cache_control at the tools-array level.
        let scheme = AnthropicScheme::new();
        let request = Request::new()
            .user("hello")
            .tool(ToolDefinition::new("noop").input_schema(serde_json::json!({
                "type": "object",
                "properties": {}
            })));
        let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
        let json = serde_json::to_value(&req).unwrap();
        let tool = &json["tools"][0];
        assert!(tool.get("cache_control").is_none());
    }
}
