//! Core data models shared by agents, tools, and model adapters.
//!
//! The types in this module form the data contract between Anda runtimes,
//! model providers, agents, tools, and clients. They cover:
//! - agent and tool inputs/outputs ([`AgentInput`], [`AgentOutput`], [`ToolInput`], [`ToolOutput`]);
//! - chat messages and multimodal content ([`Message`], [`ContentPart`]);
//! - function-call metadata ([`FunctionDefinition`], [`ToolCall`]);
//! - request metadata and usage accounting ([`RequestMeta`], [`Usage`]);
//! - prompt documents and completion requests ([`Document`], [`Documents`], [`CompletionRequest`]).

use candid::Principal;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Map, json};
use std::{
    collections::{BTreeMap, HashMap},
    str::FromStr,
};

use crate::Json;
pub use ic_auth_types::{ByteArrayB64, ByteBufB64, Xid};

mod completion;
mod resource;

pub use completion::*;
pub use resource::*;

/// Request sent to an agent for processing.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AgentInput {
    /// Agent name. When empty, the runtime selects its default agent.
    pub name: String,

    /// User prompt or task message for the agent.
    pub prompt: String,

    /// The resources to process by the agent.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub resources: Vec<Resource>,

    /// The topics for the agent request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub topics: Option<Vec<String>>,

    /// Metadata for the agent request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<RequestMeta>,
}

impl AgentInput {
    /// Creates a new agent input with the given name and prompt.
    pub fn new(name: String, prompt: String) -> Self {
        Self {
            name,
            prompt,
            resources: Vec::new(),
            topics: None,
            meta: None,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum PromptCommand {
    #[default]
    Ping,
    Plain {
        prompt: String,
    },
    Command {
        command: String,
        prompt: String,
    },
}

impl From<String> for PromptCommand {
    fn from(prompt: String) -> Self {
        let trimmed = prompt.trim();
        if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("/ping") {
            return Self::Ping;
        }

        let Some(stripped) = trimmed.strip_prefix('/') else {
            return Self::Plain { prompt };
        };
        let command_end = stripped.find(char::is_whitespace).unwrap_or(stripped.len());
        let command = &stripped[..command_end];

        Self::Command {
            command: command.to_lowercase().to_string(),
            prompt,
        }
    }
}

/// Output produced by an agent execution.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AgentOutput {
    /// Final visible content from the agent. It may be empty.
    pub content: String,

    /// Optional intermediate reasoning text returned by providers that expose it.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thoughts: Option<String>,

    /// The usage statistics for the agent execution.
    pub usage: Usage,

    /// The usage statistics for each tool called by the agent.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub tools_usage: HashMap<String, Usage>,

    /// Failure reason if execution failed. `None` indicates success.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub failed_reason: Option<String>,

    /// Tool calls returned by the LLM function calling.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCall>,

    /// The history of the conversation.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub chat_history: Vec<Message>,

    /// Provider-specific conversation history used internally by model adapters.
    ///
    /// This is included in completion responses for follow-up calls, but should
    /// not be exposed as a stable engine API response.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub raw_history: Vec<Json>,

    /// A collection of artifacts generated by the agent during the execution of the task.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub artifacts: Vec<Resource>,

    /// The conversation ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation: Option<u64>,

    /// The session ID for the agent execution, if applicable.
    /// This is used to correlate related conversations or executions.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session: Option<String>,

    /// The model used by the agent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct PartialAgentOutput {
    pub content: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub thoughts: Option<String>,

    /// Failure reason if execution failed. `None` indicates success.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub failed_reason: Option<String>,

    /// The conversation ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation: Option<u64>,

    /// The session ID for the agent execution, if applicable.
    /// This is used to correlate related conversations or executions.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session: Option<String>,

    /// The model used by the agent.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
}

impl AgentOutput {
    pub fn into_tool_output(self) -> ToolOutput<Json> {
        let AgentOutput {
            content,
            thoughts,
            usage,
            tools_usage,
            failed_reason,
            artifacts,
            conversation,
            session,
            model,
            ..
        } = self;
        let has_metadata = thoughts.is_some()
            || failed_reason.is_some()
            || conversation.is_some()
            || session.is_some()
            || model.is_some();
        let output = if has_metadata {
            json!(PartialAgentOutput {
                content,
                thoughts,
                failed_reason,
                conversation,
                session,
                model,
            })
        } else {
            serde_json::from_str::<Json>(&content).unwrap_or(Json::String(content))
        };

        ToolOutput {
            output,
            artifacts,
            usage,
            tools_usage,
        }
    }
}

fn deserialize_content<'de, D>(deserializer: D) -> Result<Vec<ContentPart>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = Json::deserialize(deserializer)?;
    match value {
        Json::Null => Ok(Vec::new()),
        Json::String(s) => Ok(vec![ContentPart::Text { text: s }]),
        Json::Array(_) => Vec::<ContentPart>::deserialize(value).map_err(serde::de::Error::custom),
        _ => Err(serde::de::Error::custom(
            "expected a string or array for content",
        )),
    }
}

/// Chat message sent to or returned by an LLM provider.
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Message {
    /// Message role: "system", "user", "assistant", "tool".
    pub role: String,

    /// Message content parts.
    #[serde(default, deserialize_with = "deserialize_content")]
    pub content: Vec<ContentPart>,

    /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
    /// This field is not used by the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// The user ID of the message sender.
    /// This field is not used by the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<Principal>,

    /// The timestamp of the message.
    /// This field is not used by the model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<u64>,
}

impl Message {
    /// Returns all text content parts joined with blank lines.
    pub fn text(&self) -> Option<String> {
        let mut texts: Vec<&str> = Vec::new();
        for part in &self.content {
            if let ContentPart::Text { text } = part {
                texts.push(text);
            }
        }
        if texts.is_empty() {
            return None;
        }
        Some(texts.join("\n\n"))
    }

    /// Returns all reasoning content parts joined with blank lines.
    pub fn thoughts(&self) -> Option<String> {
        let mut thoughts: Vec<&str> = Vec::new();
        for part in &self.content {
            if let ContentPart::Reasoning { text } = part {
                thoughts.push(text);
            }
        }
        if thoughts.is_empty() {
            return None;
        }
        Some(thoughts.join("\n\n"))
    }

    /// Extracts tool calls from this message.
    pub fn tool_calls(&self) -> Vec<ToolCall> {
        let mut tool_calls: Vec<ToolCall> = Vec::new();
        for part in &self.content {
            if let ContentPart::ToolCall {
                name,
                args,
                call_id,
            } = part
            {
                tool_calls.push(ToolCall {
                    name: name.clone(),
                    args: args.clone(),
                    call_id: call_id.clone(),
                    result: None,
                    remote_id: None,
                });
            }
        }
        tool_calls
    }

    /// Removes non-visible content parts and appends a short pruning notice.
    pub fn prune_content(&mut self) -> usize {
        let original_len = self.content.len();
        self.content.retain(|part| {
            matches!(
                part,
                ContentPart::Text { .. }
                    | ContentPart::Reasoning { .. }
                    | ContentPart::Action { .. }
            )
        });
        let pruned = original_len - self.content.len();
        if pruned > 0 {
            self.content.push(ContentPart::Text {
                text: format!(
                    "[{} items (tool calls or files) pruned due to limits]",
                    pruned
                ),
            });
        }
        pruned
    }
}

/// A single content item inside a chat message.
///
/// The enum supports Anda's normalized content types while preserving unknown
/// provider-specific JSON payloads in [`ContentPart::Any`].
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all_fields = "camelCase")]
pub enum ContentPart {
    Text {
        text: String,
    },
    Reasoning {
        text: String,
    },
    FileData {
        file_uri: String,

        #[serde(skip_serializing_if = "Option::is_none")]
        mime_type: Option<String>,
    },
    InlineData {
        mime_type: String,
        data: ByteBufB64,
    },
    ToolCall {
        name: String,
        args: Json,

        #[serde(skip_serializing_if = "Option::is_none")]
        call_id: Option<String>,
    },
    ToolOutput {
        name: String,
        output: Json,

        #[serde(skip_serializing_if = "Option::is_none")]
        call_id: Option<String>,

        #[serde(skip_serializing_if = "Option::is_none")]
        remote_id: Option<Principal>,
    },
    Action {
        name: String,
        payload: Json,

        #[serde(skip_serializing_if = "Option::is_none")]
        recipients: Option<Vec<Principal>>,

        #[serde(skip_serializing_if = "Option::is_none")]
        signature: Option<ByteBufB64>,
    },
    #[serde(untagged)]
    Any(Json),
}

/// Converts a content part with inline data to a data URL string.
///
/// See <https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data>.
pub fn part_to_data_url(data: &ByteBufB64, mime_type: Option<&String>) -> String {
    format!(
        "data:{};base64,{}",
        mime_type.map(|m| m.as_str()).unwrap_or(""),
        data.to_base64()
    )
}

/// Parses a data URL string and extracts the inline data and MIME type, if applicable.
pub fn inline_data_from_data_url(data_url: &str) -> Option<(ByteBufB64, String)> {
    if let Some(stripped) = data_url.strip_prefix("data:") {
        let (meta, data_part) = stripped.split_once(",")?;

        if meta.ends_with(";base64") {
            if let Ok(data) = ByteBufB64::from_str(data_part) {
                let mime_type = meta
                    .strip_suffix(";base64")
                    .map(|s| s.to_string())
                    .or_else(|| infer2::get(&data).map(|t| t.mime_type().to_string()));
                Some((
                    data,
                    mime_type.unwrap_or_else(|| "application/octet-stream".to_string()),
                ))
            } else {
                None
            }
        } else {
            let data = decode_percent_encoded_bytes(data_part)?;
            Some((data, meta.to_string()))
        }
    } else if let Ok(data) = ByteBufB64::from_str(data_url) {
        let mime_type = infer2::get(&data).map(|t| t.mime_type().to_string());
        Some((
            data,
            mime_type.unwrap_or_else(|| "application/octet-stream".to_string()),
        ))
    } else {
        None
    }
}

impl<'de> Deserialize<'de> for ContentPart {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = Json::deserialize(deserializer)?;
        match &value {
            Json::String(s) => Ok(ContentPart::Text { text: s.clone() }),
            Json::Object(map)
                if matches!(
                    map.get("type").and_then(|t| t.as_str()),
                    Some(
                        "Text"
                            | "Reasoning"
                            | "FileData"
                            | "InlineData"
                            | "ToolCall"
                            | "ToolOutput"
                            | "Action"
                    )
                ) =>
            {
                #[derive(Deserialize)]
                #[serde(tag = "type", rename_all_fields = "camelCase")]
                enum Helper {
                    Text {
                        text: String,
                    },
                    Reasoning {
                        text: String,
                    },
                    FileData {
                        file_uri: String,
                        mime_type: Option<String>,
                    },
                    InlineData {
                        mime_type: String,
                        data: ByteBufB64,
                    },
                    ToolCall {
                        name: String,
                        args: Json,
                        call_id: Option<String>,
                    },
                    ToolOutput {
                        name: String,
                        output: Json,
                        call_id: Option<String>,
                        remote_id: Option<Principal>,
                    },
                    Action {
                        name: String,
                        payload: Json,
                        recipients: Option<Vec<Principal>>,
                        signature: Option<ByteBufB64>,
                    },
                }

                match serde_json::from_value::<Helper>(value) {
                    Ok(h) => Ok(match h {
                        Helper::Text { text } => ContentPart::Text { text },
                        Helper::Reasoning { text } => ContentPart::Reasoning { text },
                        Helper::FileData {
                            file_uri,
                            mime_type,
                        } => ContentPart::FileData {
                            file_uri,
                            mime_type,
                        },
                        Helper::InlineData { mime_type, data } => {
                            ContentPart::InlineData { mime_type, data }
                        }
                        Helper::ToolCall {
                            name,
                            args,
                            call_id,
                        } => ContentPart::ToolCall {
                            name,
                            args,
                            call_id,
                        },
                        Helper::ToolOutput {
                            name,
                            output,
                            call_id,
                            remote_id,
                        } => ContentPart::ToolOutput {
                            name,
                            output,
                            call_id,
                            remote_id,
                        },
                        Helper::Action {
                            name,
                            payload,
                            recipients,
                            signature,
                        } => ContentPart::Action {
                            name,
                            payload,
                            recipients,
                            signature,
                        },
                    }),
                    Err(_) => Err(serde::de::Error::custom("invalid ContentPart")),
                }
            }
            _ => Ok(ContentPart::Any(value)),
        }
    }
}

impl From<String> for ContentPart {
    fn from(text: String) -> Self {
        Self::Text { text }
    }
}

impl From<Json> for ContentPart {
    fn from(val: Json) -> Self {
        if let Json::Object(map) = &val
            && let Some(t) = map.get("type").and_then(|x| x.as_str())
        {
            match t {
                "Text" | "Reasoning" | "FileData" | "InlineData" | "ToolCall" | "ToolOutput"
                | "Action" | "Any" => {
                    if let Ok(part) = serde_json::from_value::<ContentPart>(val.clone()) {
                        return part;
                    }
                }
                _ => {}
            }
        }

        ContentPart::Any(val)
    }
}

impl TryFrom<Resource> for ContentPart {
    type Error = Resource;
    fn try_from(res: Resource) -> Result<Self, Self::Error> {
        if res.blob.as_ref().map(|v| !v.0.is_empty()).unwrap_or(false)
            && let Some(data) = res.blob
        {
            match String::from_utf8(data.0) {
                Ok(text) => Ok(ContentPart::Text { text }),
                Err(v) => {
                    let data: ByteBufB64 = v.into_bytes().into();
                    let mime_type = res.mime_type.unwrap_or_else(|| {
                        infer2::get(&data)
                            .map(|t| t.mime_type())
                            .unwrap_or("application/octet-stream")
                            .to_string()
                    });
                    Ok(ContentPart::InlineData { mime_type, data })
                }
            }
        } else if res
            .uri
            .as_ref()
            .map(|v| !v.trim().is_empty())
            .unwrap_or(false)
            && let Some(file_uri) = res.uri
        {
            Ok(ContentPart::FileData {
                file_uri,
                mime_type: res.mime_type,
            })
        } else {
            Err(res)
        }
    }
}

/// Request sent to a tool for processing.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolInput<T> {
    /// Tool name.
    pub name: String,

    /// Tool arguments.
    pub args: T,

    /// The resources to process by the tool.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub resources: Vec<Resource>,

    /// The metadata for the tool request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub meta: Option<RequestMeta>,
}

impl<T> ToolInput<T> {
    /// Creates a new tool input with the given name and arguments.
    pub fn new(name: String, args: T) -> Self {
        Self {
            name,
            args,
            resources: Vec::new(),
            meta: None,
        }
    }
}

/// Output produced by a tool execution.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolOutput<T> {
    /// The output from the tool.
    pub output: T,

    /// A collection of artifacts generated by the tool execution.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub artifacts: Vec<Resource>,

    /// The usage statistics for the tool execution.
    pub usage: Usage,

    /// The usage statistics for each tool called by the agent.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub tools_usage: HashMap<String, Usage>,
}

impl<T> ToolOutput<T> {
    /// Creates a new tool output with the given output value.
    pub fn new(output: T) -> Self {
        Self {
            output,
            artifacts: Vec::new(),
            usage: Usage::default(),
            tools_usage: HashMap::new(),
        }
    }
}

/// Metadata attached to an agent or tool request.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RequestMeta {
    /// The target engine principal for the request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub engine: Option<Principal>,

    /// User identifier supplied by the request context.
    /// Note: This is not verified and should not be used as a trusted identifier.
    /// For example, if triggered by a bot of X platform, this might be the username
    /// of the user interacting with the bot.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,

    /// Extra metadata key-value pairs.
    #[serde(flatten)]
    #[serde(skip_serializing_if = "Map::is_empty")]
    pub extra: Map<String, Json>,
}

impl RequestMeta {
    /// Gets an extra metadata value by key and deserializes it to the specified type.
    pub fn get_extra_as<T>(&self, key: &str) -> Option<T>
    where
        T: DeserializeOwned,
    {
        self.extra
            .get(key)
            .and_then(|value| serde_json::from_value(value.clone()).ok())
    }
}

/// Usage statistics for an agent, model, or tool execution.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Usage {
    /// Input tokens sent to the LLM.
    pub input_tokens: u64,

    /// Output tokens received from the LLM.
    pub output_tokens: u64,

    /// cached tokens used in the execution.
    #[serde(default)]
    pub cached_tokens: u64,

    /// Number of requests made to models, agents, or tools.
    pub requests: u64,
}

impl Usage {
    /// Accumulates the usage statistics from another usage object.
    pub fn accumulate(&mut self, other: &Usage) {
        self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
        self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
        self.cached_tokens = self.cached_tokens.saturating_add(other.cached_tokens);
        self.requests = self.requests.saturating_add(other.requests);
    }
}

/// Tool call requested by an LLM or returned by a tool execution pipeline.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolCall {
    /// Tool function name.
    pub name: String,

    /// Tool function arguments.
    pub args: Json,

    /// Tool result populated by the agent runtime when available.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<ToolOutput<Json>>,

    /// Provider-specific tool call ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub call_id: Option<String>,

    /// Remote engine principal that executed the tool, if any.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remote_id: Option<Principal>,
}

/// Represents a function definition with its metadata.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Function {
    /// Definition of the function.
    pub definition: FunctionDefinition,

    /// Resource tags supported by this function.
    pub supported_resource_tags: Vec<String>,
}

/// Defines a callable function with its metadata and schema.
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct FunctionDefinition {
    /// Name of the function.
    pub name: String,

    /// Description of what the function does.
    pub description: String,

    /// JSON schema defining the function's parameters.
    pub parameters: Json,

    /// Whether the model should strictly follow the parameter schema when calling the function.
    ///
    /// Provider support and the accepted JSON Schema subset vary by model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strict: Option<bool>,
}

impl FunctionDefinition {
    /// Modifies the function name with a prefix.
    pub fn name_with_prefix(mut self, prefix: &str) -> Self {
        self.name = format!("{}{}", prefix, self.name);
        self
    }
}

/// Estimates token count using a small, provider-independent heuristic.
pub fn estimate_tokens(content: &str) -> usize {
    content.len() / 3
}

/// A document with metadata and content.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Document {
    /// The metadata of the document.
    pub metadata: BTreeMap<String, Json>,

    /// The content of the document.
    pub content: Json,
}

impl Document {
    /// Creates a new text document with the given ID and text content.
    pub fn from_text(id: &str, text: &str) -> Self {
        Self {
            metadata: BTreeMap::from([
                ("id".to_string(), id.into()),
                ("type".to_string(), "Text".into()),
            ]),
            content: text.into(),
        }
    }
}

impl From<&Resource> for Document {
    fn from(res: &Resource) -> Self {
        let mut metadata = BTreeMap::from([
            ("id".to_string(), res._id.into()),
            ("type".to_string(), "Resource".into()),
        ]);
        if let Json::Object(mut val) = json!(res) {
            val.remove("blob");
            metadata.extend(val);
        };

        Self {
            metadata,
            content: Json::Null,
        }
    }
}

/// Collection of documents that can be injected into a completion prompt.
#[derive(Clone, Debug)]
pub struct Documents {
    /// The tag of the document collection. Defaults to "documents".
    tag: String,
    /// The documents in the collection.
    docs: Vec<Document>,
}

impl Default for Documents {
    fn default() -> Self {
        Self {
            tag: "documents".to_string(),
            docs: Vec::new(),
        }
    }
}

impl Documents {
    /// Creates a new document collection.
    pub fn new(tag: String, docs: Vec<Document>) -> Self {
        Self { tag, docs }
    }

    /// Sets the tag of the document collection.
    pub fn with_tag(self, tag: String) -> Self {
        Self { tag, ..self }
    }

    /// Returns the tag of the document collection.
    pub fn tag(&self) -> &str {
        &self.tag
    }

    /// Converts the document collection into a system-style user message.
    pub fn to_message(&self, rfc3339_datetime: &str) -> Option<Message> {
        if self.docs.is_empty() {
            return None;
        }

        Some(Message {
            role: "user".into(),
            content: vec![
                format!("Current Datetime: {}\n\n---\n\n{}", rfc3339_datetime, self).into(),
            ],
            name: Some("$system".into()),
            ..Default::default()
        })
    }

    /// Appends a document to the collection.
    pub fn append(&mut self, doc: Document) {
        self.docs.push(doc);
    }
}

impl From<Vec<String>> for Documents {
    fn from(texts: Vec<String>) -> Self {
        let mut docs = Vec::new();
        for (i, text) in texts.into_iter().enumerate() {
            docs.push(Document {
                content: text.into(),
                metadata: BTreeMap::from([
                    ("_id".to_string(), i.into()),
                    ("type".to_string(), "Text".into()),
                ]),
            });
        }
        Self {
            docs,
            ..Default::default()
        }
    }
}

impl From<Vec<Document>> for Documents {
    fn from(docs: Vec<Document>) -> Self {
        Self {
            docs,
            ..Default::default()
        }
    }
}

impl std::ops::Deref for Documents {
    type Target = Vec<Document>;

    fn deref(&self) -> &Self::Target {
        &self.docs
    }
}

impl std::ops::DerefMut for Documents {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.docs
    }
}

impl AsRef<Vec<Document>> for Documents {
    fn as_ref(&self) -> &Vec<Document> {
        &self.docs
    }
}

impl std::fmt::Display for Document {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        json!(self).fmt(f)
    }
}

impl std::fmt::Display for Documents {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.docs.is_empty() {
            return Ok(());
        }
        writeln!(f, "<{}>", self.tag)?;
        for doc in &self.docs {
            writeln!(f, "{}", doc)?;
        }
        write!(f, "</{}>", self.tag)
    }
}

pub fn prompt_with_resources(prompt: String, resources: &mut Vec<Resource>) -> String {
    let user_resources = text_resource_documents(resources);
    if user_resources.is_empty() {
        prompt
    } else {
        format!(
            "{prompt}\n\n{}",
            Documents::new("attachments".to_string(), user_resources)
        )
    }
}

pub fn text_resource_documents(resources: &mut Vec<Resource>) -> Vec<Document> {
    let res = select_resources(resources, &["text".to_string(), "md".to_string()]);
    let mut user_resources: Vec<Document> = Vec::with_capacity(res.len());
    for resource in res {
        if let Some(content) = resource
            .blob
            .and_then(|blob| String::from_utf8(blob.0).ok())
        {
            user_resources.push(Document::from_text(
                resource._id.to_string().as_str(),
                &content,
            ));
        }
    }

    user_resources
}

fn decode_percent_encoded_bytes(input: &str) -> Option<ByteBufB64> {
    fn decode_hex(byte: u8) -> Option<u8> {
        match byte {
            b'0'..=b'9' => Some(byte - b'0'),
            b'a'..=b'f' => Some(byte - b'a' + 10),
            b'A'..=b'F' => Some(byte - b'A' + 10),
            _ => None,
        }
    }

    let bytes = input.as_bytes();
    let mut decoded = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'%' => {
                let hi = *bytes.get(index + 1)?;
                let lo = *bytes.get(index + 2)?;
                decoded.push((decode_hex(hi)? << 4) | decode_hex(lo)?);
                index += 3;
            }
            byte => {
                decoded.push(byte);
                index += 1;
            }
        }
    }

    Some(decoded.into())
}

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

    fn resource(id: u64, tags: &[&str]) -> Resource {
        Resource {
            _id: id,
            name: format!("resource-{id}"),
            tags: tags.iter().map(|tag| tag.to_string()).collect(),
            ..Default::default()
        }
    }

    #[test]
    fn test_agent_and_tool_constructors_default_optional_fields() {
        let agent = AgentInput::new("planner".into(), "summarize this".into());
        assert_eq!(agent.name, "planner");
        assert_eq!(agent.prompt, "summarize this");
        assert!(agent.resources.is_empty());
        assert!(agent.topics.is_none());
        assert!(agent.meta.is_none());

        let tool = ToolInput::new("sum".into(), json!({"x": 1, "y": 2}));
        assert_eq!(tool.name, "sum");
        assert_eq!(tool.args, json!({"x": 1, "y": 2}));
        assert!(tool.resources.is_empty());
        assert!(tool.meta.is_none());

        let output = ToolOutput::new(json!("ok"));
        assert_eq!(output.output, json!("ok"));
        assert!(output.artifacts.is_empty());
        assert_eq!(output.usage.requests, 0);
        assert!(output.tools_usage.is_empty());
    }

    #[test]
    fn test_prompt_command_from_string_variants() {
        assert_eq!(PromptCommand::from("".to_string()), PromptCommand::Ping);
        assert_eq!(
            PromptCommand::from("  /PING  ".to_string()),
            PromptCommand::Ping
        );
        assert_eq!(
            PromptCommand::from("hello".to_string()),
            PromptCommand::Plain {
                prompt: "hello".into(),
            }
        );
        assert_eq!(
            PromptCommand::from("/Status  show details".to_string()),
            PromptCommand::Command {
                command: "status".into(),
                prompt: "/Status  show details".into(),
            }
        );
        assert_eq!(
            PromptCommand::from("/help".to_string()),
            PromptCommand::Command {
                command: "help".into(),
                prompt: "/help".into(),
            }
        );
    }

    #[test]
    fn test_agent_output_into_tool_output_handles_json_plain_text_and_metadata() {
        let mut tools_usage = HashMap::new();
        tools_usage.insert(
            "sum".into(),
            Usage {
                requests: 1,
                ..Default::default()
            },
        );

        let output = AgentOutput {
            content: r#"{"ok":true}"#.into(),
            usage: Usage {
                input_tokens: 2,
                output_tokens: 1,
                requests: 1,
                ..Default::default()
            },
            tools_usage: tools_usage.clone(),
            artifacts: vec![resource(7, &["text"])],
            ..Default::default()
        }
        .into_tool_output();
        assert_eq!(output.output, json!({"ok": true}));
        assert_eq!(output.artifacts.len(), 1);
        assert_eq!(output.artifacts[0]._id, 7);
        assert_eq!(output.usage.input_tokens, 2);
        assert_eq!(output.tools_usage.get("sum").unwrap().requests, 1);

        let output = AgentOutput {
            content: "not-json".into(),
            thoughts: Some("thinking".into()),
            session: Some("session-1".into()),
            model: Some("test-model".into()),
            ..Default::default()
        }
        .into_tool_output();
        assert_eq!(
            output.output,
            json!({
                "content": "not-json",
                "thoughts": "thinking",
                "session": "session-1",
                "model": "test-model"
            })
        );

        let output = AgentOutput {
            content: "still-not-json".into(),
            ..Default::default()
        }
        .into_tool_output();
        assert_eq!(output.output, json!("still-not-json"));
    }

    #[test]
    fn test_data_url_helpers_round_trip_and_invalid_inputs() {
        let data: ByteBufB64 = b"hello".to_vec().into();
        let mime_type = "text/plain".to_string();

        let data_url = part_to_data_url(&data, Some(&mime_type));
        assert_eq!(data_url, "data:text/plain;base64,aGVsbG8=");

        let (decoded, decoded_mime_type) = inline_data_from_data_url(&data_url).unwrap();
        assert_eq!(decoded, data);
        assert_eq!(decoded_mime_type, "text/plain");

        let (decoded, _) = inline_data_from_data_url("aGVsbG8=").unwrap();
        assert_eq!(decoded, data);

        let html_url = "data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E";
        let (decoded, decoded_mime_type) = inline_data_from_data_url(html_url).unwrap();
        let expected_html: ByteBufB64 = b"<h1>Hello, World!</h1>".to_vec().into();
        assert_eq!(decoded, expected_html);
        assert_eq!(decoded_mime_type, "text/html");

        let (decoded, decoded_mime_type) =
            inline_data_from_data_url("data:text/plain,hello").unwrap();
        let expected_text: ByteBufB64 = b"hello".to_vec().into();
        assert_eq!(decoded, expected_text);
        assert_eq!(decoded_mime_type, "text/plain");

        assert!(inline_data_from_data_url("data:text/plain,%GG").is_none());
        assert!(inline_data_from_data_url("not-base64%%%").is_none());
    }

    #[test]
    fn test_content_part_try_from_resource_variants() {
        let text = Resource {
            blob: Some(b"hello".to_vec().into()),
            ..resource(1, &["text"])
        };
        assert_eq!(
            ContentPart::try_from(text).unwrap(),
            ContentPart::Text {
                text: "hello".into(),
            }
        );

        let binary = Resource {
            blob: Some(vec![0xff, 0xd8, 0xff].into()),
            mime_type: Some("image/jpeg".into()),
            ..resource(2, &["image"])
        };
        assert_eq!(
            ContentPart::try_from(binary).unwrap(),
            ContentPart::InlineData {
                mime_type: "image/jpeg".into(),
                data: vec![0xff, 0xd8, 0xff].into(),
            }
        );

        let file = Resource {
            uri: Some("file:///tmp/a.txt".into()),
            mime_type: Some("text/plain".into()),
            ..resource(3, &["text"])
        };
        assert_eq!(
            ContentPart::try_from(file).unwrap(),
            ContentPart::FileData {
                file_uri: "file:///tmp/a.txt".into(),
                mime_type: Some("text/plain".into()),
            }
        );

        let empty_blob = Resource {
            blob: Some(Vec::<u8>::new().into()),
            ..resource(4, &["text"])
        };
        assert!(ContentPart::try_from(empty_blob).is_err());

        let empty_uri = Resource {
            uri: Some("   ".into()),
            ..resource(5, &["text"])
        };
        assert!(ContentPart::try_from(empty_uri).is_err());
    }

    #[test]
    fn test_request_meta_get_extra_as_and_usage_accumulate() {
        let mut extra = Map::new();
        extra.insert("numbers".into(), json!([1, 2, 3]));
        extra.insert("flag".into(), json!(true));
        let meta = RequestMeta {
            extra,
            ..Default::default()
        };

        assert_eq!(
            meta.get_extra_as::<Vec<u64>>("numbers"),
            Some(vec![1, 2, 3])
        );
        assert_eq!(meta.get_extra_as::<bool>("flag"), Some(true));
        assert_eq!(meta.get_extra_as::<String>("missing"), None);

        let mut usage = Usage {
            input_tokens: u64::MAX - 1,
            output_tokens: 2,
            cached_tokens: 3,
            requests: u64::MAX,
        };
        let other = Usage {
            input_tokens: 10,
            output_tokens: 5,
            cached_tokens: u64::MAX,
            requests: 1,
        };
        usage.accumulate(&other);

        assert_eq!(usage.input_tokens, u64::MAX);
        assert_eq!(usage.output_tokens, 7);
        assert_eq!(usage.cached_tokens, u64::MAX);
        assert_eq!(usage.requests, u64::MAX);
    }

    #[test]
    fn test_function_definition_and_document_helpers() {
        let definition = FunctionDefinition {
            name: "search".into(),
            description: "Find documents".into(),
            parameters: json!({"type": "object"}),
            strict: Some(true),
        }
        .name_with_prefix("tool_");
        assert_eq!(definition.name, "tool_search");
        assert_eq!(definition.description, "Find documents");
        assert_eq!(estimate_tokens("abcdef"), 2);
        assert_eq!(estimate_tokens(""), 0);

        let text_doc = Document::from_text("doc-1", "hello");
        assert_eq!(text_doc.metadata.get("id"), Some(&json!("doc-1")));
        assert_eq!(text_doc.metadata.get("type"), Some(&json!("Text")));
        assert_eq!(text_doc.content, json!("hello"));

        let resource = Resource {
            _id: 9,
            name: "note".into(),
            tags: vec!["text".into()],
            uri: Some("file:///tmp/note.txt".into()),
            blob: Some(b"hello".to_vec().into()),
            mime_type: Some("text/plain".into()),
            ..Default::default()
        };
        let doc = Document::from(&resource);
        assert_eq!(doc.metadata.get("id"), Some(&json!(9)));
        assert_eq!(doc.metadata.get("type"), Some(&json!("Resource")));
        assert_eq!(doc.metadata.get("_id"), Some(&json!(9)));
        assert_eq!(
            doc.metadata.get("uri"),
            Some(&json!("file:///tmp/note.txt"))
        );
        assert!(!doc.metadata.contains_key("blob"));
        assert_eq!(doc.content, Json::Null);
    }

    #[test]
    fn test_documents_and_resource_prompt_helpers() {
        let mut docs = Documents::new(
            "attachments".into(),
            vec![Document::from_text("1", "alpha")],
        );
        assert_eq!(docs.tag(), "attachments");
        docs.append(Document::from_text("2", "beta"));
        assert_eq!(docs.len(), 2);

        let message = docs.to_message("2026-05-16T00:00:00Z").unwrap();
        assert_eq!(message.role, "user");
        assert_eq!(message.name.as_deref(), Some("$system"));
        let text = message.text().unwrap();
        assert!(text.contains("Current Datetime: 2026-05-16T00:00:00Z"));
        assert!(text.contains("<attachments>"));
        assert!(text.contains("alpha"));
        assert!(text.contains("beta"));

        assert!(
            Documents::default()
                .to_message("2026-05-16T00:00:00Z")
                .is_none()
        );

        let from_strings: Documents = vec!["alpha".to_string(), "beta".to_string()].into();
        assert_eq!(
            from_strings[0],
            Document {
                metadata: BTreeMap::from([
                    ("_id".to_string(), json!(0)),
                    ("type".to_string(), json!("Text")),
                ]),
                content: json!("alpha"),
            }
        );
        assert_eq!(
            from_strings[1],
            Document {
                metadata: BTreeMap::from([
                    ("_id".to_string(), json!(1)),
                    ("type".to_string(), json!("Text")),
                ]),
                content: json!("beta"),
            }
        );

        let mut resources = vec![
            Resource {
                blob: Some(b"alpha".to_vec().into()),
                ..resource(1, &["text"])
            },
            Resource {
                blob: Some(vec![0xff, 0xfe].into()),
                ..resource(2, &["md"])
            },
            Resource {
                uri: Some("file:///tmp/image.png".into()),
                ..resource(3, &["image"])
            },
        ];

        let docs = text_resource_documents(&mut resources);
        assert_eq!(docs, vec![Document::from_text("1", "alpha")]);
        assert_eq!(resources.len(), 1);
        assert_eq!(resources[0]._id, 3);

        let mut prompt_resources = vec![Resource {
            blob: Some(b"beta".to_vec().into()),
            ..resource(4, &["text"])
        }];
        let prompt = prompt_with_resources("Base prompt".into(), &mut prompt_resources);
        assert!(prompt.starts_with("Base prompt\n\n<attachments>"));
        assert!(prompt.contains("beta"));
        assert!(prompt_resources.is_empty());

        let mut untouched_resources = vec![Resource {
            uri: Some("file:///tmp/only-image.png".into()),
            ..resource(5, &["image"])
        }];
        let prompt = prompt_with_resources("Base prompt".into(), &mut untouched_resources);
        assert_eq!(prompt, "Base prompt");
        assert_eq!(untouched_resources.len(), 1);
        assert_eq!(untouched_resources[0]._id, 5);
    }

    #[test]
    fn test_message_content_deserialize_rejects_non_string_non_array() {
        assert!(
            serde_json::from_value::<Message>(json!({
                "role": "user",
                "content": 123,
            }))
            .is_err()
        );
    }

    #[test]
    fn test_prompt() {
        let documents: Documents = vec![
            Document {
                metadata: BTreeMap::from([("_id".to_string(), 1.into())]),
                content: "Test document 1.".into(),
            },
            Document {
                metadata: BTreeMap::from([
                    ("_id".to_string(), 2.into()),
                    ("key".to_string(), "value".into()),
                    ("a".to_string(), "b".into()),
                ]),
                content: "Test document 2.".into(),
            },
        ]
        .into();
        // println!("{}", documents);

        let s = documents.to_string();
        let lines: Vec<&str> = s.lines().collect();
        assert_eq!(lines[0], "<documents>");
        assert_eq!(lines[3], "</documents>");

        let doc1: Json = serde_json::from_str(lines[1]).unwrap();
        assert_eq!(doc1.get("content").unwrap(), "Test document 1.");
        assert_eq!(doc1.get("metadata").unwrap().get("_id").unwrap(), 1);

        let doc2: Json = serde_json::from_str(lines[2]).unwrap();
        assert_eq!(doc2.get("content").unwrap(), "Test document 2.");
        assert_eq!(doc2.get("metadata").unwrap().get("_id").unwrap(), 2);
        assert_eq!(doc2.get("metadata").unwrap().get("key").unwrap(), "value");
        assert_eq!(doc2.get("metadata").unwrap().get("a").unwrap(), "b");

        let documents = documents.with_tag("my_docs".to_string());
        let s = documents.to_string();
        let lines: Vec<&str> = s.lines().collect();
        assert_eq!(lines[0], "<my_docs>");
        assert_eq!(lines[3], "</my_docs>");

        let doc1: Json = serde_json::from_str(lines[1]).unwrap();
        assert_eq!(doc1.get("content").unwrap(), "Test document 1.");
        assert_eq!(doc1.get("metadata").unwrap().get("_id").unwrap(), 1);

        let doc2: Json = serde_json::from_str(lines[2]).unwrap();
        assert_eq!(doc2.get("content").unwrap(), "Test document 2.");
        assert_eq!(doc2.get("metadata").unwrap().get("_id").unwrap(), 2);
        assert_eq!(doc2.get("metadata").unwrap().get("key").unwrap(), "value");
        assert_eq!(doc2.get("metadata").unwrap().get("a").unwrap(), "b");
    }

    #[test]
    fn test_content_part_text_serde_and_from() {
        let part: ContentPart = "hello".to_string().into();
        assert_eq!(
            part,
            ContentPart::Text {
                text: "hello".into()
            }
        );

        // serde round-trip
        let v = serde_json::to_value(&part).unwrap();
        assert_eq!(v.get("type").unwrap(), "Text");
        assert_eq!(v.get("text").unwrap(), "hello");

        let back: ContentPart = serde_json::from_value(v.clone()).unwrap();
        assert_eq!(back, part);
        let back: ContentPart = v.into();
        assert_eq!(back, part);

        let part: Vec<ContentPart> = serde_json::from_str(
            r#"
            [
                "hello",
                {
                    "type": "Text",
                    "text": "world"
                }
            ]
            "#,
        )
        .unwrap();
        assert_eq!(
            part,
            vec![
                ContentPart::Text {
                    text: "hello".into()
                },
                ContentPart::Text {
                    text: "world".into()
                }
            ]
        );
    }

    #[test]
    fn test_content_part_filedata_serde_optional() {
        // mime_type = None -> 不序列化
        let part = ContentPart::FileData {
            file_uri: "gs://bucket/file".into(),
            mime_type: None,
        };
        let v = serde_json::to_value(&part).unwrap();
        assert_eq!(v.get("type").unwrap(), "FileData");
        // 字段采用 camelCase
        assert_eq!(v.get("fileUri").unwrap(), "gs://bucket/file");
        assert!(v.get("mimeType").is_none());

        // mime_type = Some -> 出现
        let part2 = ContentPart::FileData {
            file_uri: "gs://bucket/file2".into(),
            mime_type: Some("image/png".into()),
        };
        let v2 = serde_json::to_value(&part2).unwrap();
        assert_eq!(v2.get("type").unwrap(), "FileData");
        assert_eq!(v2.get("fileUri").unwrap(), "gs://bucket/file2");
        assert_eq!(v2.get("mimeType").unwrap(), "image/png");

        // 反序列化校验
        let back: ContentPart = serde_json::from_value(v2.clone()).unwrap();
        assert_eq!(back, part2);
        let back: ContentPart = v2.into();
        assert_eq!(back, part2);
    }

    #[test]
    fn test_content_part_inlinedata_serde() {
        let part = ContentPart::InlineData {
            mime_type: "text/plain".into(),
            data: b"hello".to_vec().into(),
        };
        let v = serde_json::to_value(&part).unwrap();
        assert_eq!(v.get("type").unwrap(), "InlineData");
        assert_eq!(v.get("mimeType").unwrap(), "text/plain");
        assert_eq!(v.get("data").unwrap(), "aGVsbG8=");

        let back: ContentPart = serde_json::from_value(v.clone()).unwrap();
        assert_eq!(back, part);
        let back: ContentPart = v.into();
        assert_eq!(back, part);
    }

    #[test]
    fn test_content_part_any_serde() {
        let v = json!({
            "type": "text/plain",
            "data": "aGVsbG8=",
        });
        let part: ContentPart = v.clone().into();
        assert_eq!(part, ContentPart::Any(v));
        let v2 = serde_json::to_value(&part).unwrap();
        assert_eq!(v2.get("type").unwrap(), "text/plain");
        assert_eq!(v2.get("data").unwrap(), "aGVsbG8=");

        let part = ContentPart::Any(json!({
            "data": "aGVsbG8=",
        }));
        let v2 = serde_json::to_value(&part).unwrap();
        assert!(v2.get("type").is_none());
        assert_eq!(v2.get("data").unwrap(), "aGVsbG8=");
    }

    #[test]
    fn test_content_part_toolcall_and_tooloutput_serde() {
        let call = ContentPart::ToolCall {
            name: "sum".into(),
            args: serde_json::json!({"x":1, "y":2}),
            call_id: None,
        };
        let v_call = serde_json::to_value(&call).unwrap();
        assert_eq!(v_call.get("type").unwrap(), "ToolCall");
        assert_eq!(v_call.get("name").unwrap(), "sum");
        assert_eq!(
            v_call.get("args").unwrap(),
            &serde_json::json!({"x":1, "y":2})
        );
        // callId 省略
        assert!(v_call.get("callId").is_none());
        let back_call: ContentPart = serde_json::from_value(v_call.clone()).unwrap();
        assert_eq!(back_call, call);
        let back: ContentPart = v_call.into();
        assert_eq!(back, call);

        let out = ContentPart::ToolOutput {
            name: "sum".into(),
            output: serde_json::json!({"result":3}),
            call_id: Some("c1".into()),
            remote_id: None,
        };
        let v_out = serde_json::to_value(&out).unwrap();
        assert_eq!(v_out.get("type").unwrap(), "ToolOutput");
        assert_eq!(v_out.get("name").unwrap(), "sum");
        assert_eq!(
            v_out.get("output").unwrap(),
            &serde_json::json!({"result":3})
        );
        // callId 存在
        assert_eq!(v_out.get("callId").unwrap(), "c1");
        let back_out: ContentPart = serde_json::from_value(v_out.clone()).unwrap();
        assert_eq!(back_out, out);
        let back: ContentPart = v_out.into();
        assert_eq!(back, out);
    }

    #[test]
    fn test_message_text_collects_only_text_parts_in_order() {
        let msg = Message {
            role: "assistant".into(),
            content: vec![
                ContentPart::Reasoning {
                    text: "first thought".into(),
                },
                ContentPart::Text {
                    text: "first text".into(),
                },
                ContentPart::ToolCall {
                    name: "sum".into(),
                    args: serde_json::json!({"x":1, "y":2}),
                    call_id: Some("call_1".into()),
                },
                ContentPart::Text {
                    text: "second text".into(),
                },
                ContentPart::Action {
                    name: "notify".into(),
                    payload: serde_json::json!({"ok": true}),
                    recipients: None,
                    signature: None,
                },
            ],
            ..Default::default()
        };

        assert_eq!(msg.text().as_deref(), Some("first text\n\nsecond text"));

        let no_text = Message {
            role: "assistant".into(),
            content: vec![ContentPart::Reasoning {
                text: "thought only".into(),
            }],
            ..Default::default()
        };
        assert_eq!(no_text.text(), None);
    }

    #[test]
    fn test_message_thoughts_collects_only_reasoning_parts_in_order() {
        let msg = Message {
            role: "assistant".into(),
            content: vec![
                ContentPart::Text {
                    text: "visible text".into(),
                },
                ContentPart::Reasoning {
                    text: "first thought".into(),
                },
                ContentPart::ToolOutput {
                    name: "sum".into(),
                    output: serde_json::json!({"result": 3}),
                    call_id: Some("call_1".into()),
                    remote_id: None,
                },
                ContentPart::Reasoning {
                    text: "second thought".into(),
                },
            ],
            ..Default::default()
        };

        assert_eq!(
            msg.thoughts().as_deref(),
            Some("first thought\n\nsecond thought")
        );

        let no_reasoning = Message {
            role: "assistant".into(),
            content: vec![ContentPart::Text {
                text: "text only".into(),
            }],
            ..Default::default()
        };
        assert_eq!(no_reasoning.thoughts(), None);
    }

    #[test]
    fn test_message_tool_calls_extract_from_content_parts() {
        let parts = vec![
            ContentPart::Text {
                text: "hello".into(),
            },
            ContentPart::ToolCall {
                name: "sum".into(),
                args: serde_json::json!({"x":1, "y": 2}),
                call_id: Some("abc".into()),
            },
            ContentPart::ToolCall {
                name: "echo".into(),
                args: serde_json::json!({"text":"hi"}),
                call_id: None,
            },
            ContentPart::ToolOutput {
                name: "sum".into(),
                output: serde_json::json!({"result": 3}),
                call_id: Some("abc".into()),
                remote_id: None,
            },
        ];
        let msg = Message {
            role: "assistant".into(),
            content: parts,
            ..Default::default()
        };

        let calls = msg.tool_calls();
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].name, "sum");
        assert_eq!(calls[0].args, serde_json::json!({"x":1, "y":2}));
        assert_eq!(calls[0].call_id.as_deref(), Some("abc"));
        assert!(calls[0].result.is_none());
        assert!(calls[0].remote_id.is_none());
        assert_eq!(calls[1].name, "echo");
        assert_eq!(calls[1].args, serde_json::json!({"text":"hi"}));
        assert!(calls[1].call_id.is_none());
        assert!(calls[1].result.is_none());
        assert!(calls[1].remote_id.is_none());
    }

    #[test]
    fn test_message_prune_content_keeps_visible_parts_and_is_idempotent() {
        let action = ContentPart::Action {
            name: "delegate".into(),
            payload: serde_json::json!({"agent": "planner"}),
            recipients: None,
            signature: None,
        };
        let mut msg = Message {
            role: "assistant".into(),
            content: vec![
                ContentPart::Text {
                    text: "visible text".into(),
                },
                ContentPart::ToolCall {
                    name: "sum".into(),
                    args: serde_json::json!({"x":1, "y":2}),
                    call_id: Some("call_1".into()),
                },
                ContentPart::Reasoning {
                    text: "visible thought".into(),
                },
                ContentPart::FileData {
                    file_uri: "file:///tmp/a.txt".into(),
                    mime_type: None,
                },
                action.clone(),
                ContentPart::ToolOutput {
                    name: "sum".into(),
                    output: serde_json::json!({"result": 3}),
                    call_id: Some("call_1".into()),
                    remote_id: None,
                },
            ],
            ..Default::default()
        };

        assert_eq!(msg.prune_content(), 3);
        assert_eq!(
            msg.content,
            vec![
                ContentPart::Text {
                    text: "visible text".into(),
                },
                ContentPart::Reasoning {
                    text: "visible thought".into(),
                },
                action,
                ContentPart::Text {
                    text: "[3 items (tool calls or files) pruned due to limits]".into(),
                },
            ]
        );

        let pruned = msg.content.clone();
        assert_eq!(msg.prune_content(), 0);
        assert_eq!(msg.content, pruned);
    }

    #[test]
    fn test_message_content_deserialize_from_string() {
        // content as a plain string
        let msg: Message = serde_json::from_value(serde_json::json!({
            "role": "user",
            "content": "hello world"
        }))
        .unwrap();
        assert_eq!(msg.role, "user");
        assert_eq!(msg.content.len(), 1);
        assert_eq!(
            msg.content[0],
            ContentPart::Text {
                text: "hello world".into()
            }
        );

        // content as an array still works
        let msg2: Message = serde_json::from_value(serde_json::json!({
            "role": "assistant",
            "content": [{"type": "Text", "text": "hi"}]
        }))
        .unwrap();
        assert_eq!(msg2.content.len(), 1);
        assert_eq!(msg2.content[0], ContentPart::Text { text: "hi".into() });

        // missing content defaults to empty vec
        let msg3: Message = serde_json::from_value(serde_json::json!({
            "role": "system"
        }))
        .unwrap();
        assert!(msg3.content.is_empty());

        // null content is treated as empty content for provider compatibility
        let msg4: Message = serde_json::from_value(serde_json::json!({
            "role": "assistant",
            "content": null
        }))
        .unwrap();
        assert!(msg4.content.is_empty());
    }

    #[test]
    fn test_request_meta_extra_flatten_serde() {
        // empty extra should not serialize
        let meta = RequestMeta {
            engine: None,
            user: None,
            extra: Map::new(),
        };
        let v = serde_json::to_value(&meta).unwrap();
        assert_eq!(v, serde_json::json!({}));

        // extra should be flattened into the top-level object
        let mut extra = Map::new();
        extra.insert("foo".into(), serde_json::json!("bar"));
        extra.insert("n".into(), serde_json::json!(1));
        extra.insert("obj".into(), serde_json::json!({"x": true}));

        let meta2 = RequestMeta {
            engine: Some(Principal::from_text("aaaaa-aa").unwrap()),
            user: Some("alice".into()),
            extra,
        };

        let v2 = serde_json::to_value(&meta2).unwrap();
        assert_eq!(v2.get("engine").unwrap(), "aaaaa-aa");
        assert_eq!(v2.get("user").unwrap(), "alice");
        assert_eq!(v2.get("foo").unwrap(), "bar");
        assert_eq!(v2.get("n").unwrap(), 1);
        assert_eq!(v2.get("obj").unwrap(), &serde_json::json!({"x": true}));
        assert!(v2.get("extra").is_none());

        // deserialization: unknown fields go into extra
        let input = serde_json::json!({
            "engine": "aaaaa-aa",
            "user": "bob",
            "k1": "v1",
            "k2": 2,
            "nested": {"a": 1}
        });
        let back: RequestMeta = serde_json::from_value(input).unwrap();
        assert_eq!(back.engine.unwrap().to_text(), "aaaaa-aa");
        assert_eq!(back.user.as_deref(), Some("bob"));
        assert_eq!(back.extra.get("k1").unwrap(), "v1");
        assert_eq!(back.extra.get("k2").unwrap(), 2);
        assert_eq!(
            back.extra.get("nested").unwrap(),
            &serde_json::json!({"a": 1})
        );

        // round-trip (field-by-field)
        let back2: RequestMeta = serde_json::from_value(v2).unwrap();
        assert_eq!(back2.engine.unwrap().to_text(), "aaaaa-aa");
        assert_eq!(back2.user.as_deref(), Some("alice"));
        assert_eq!(back2.extra.get("foo").unwrap(), "bar");
        assert_eq!(back2.extra.get("n").unwrap(), 1);
        assert_eq!(
            back2.extra.get("obj").unwrap(),
            &serde_json::json!({"x": true})
        );
    }
}
