/// ACP (Agent Client Protocol) type definitions.
///
/// Strongly-typed Rust representations of the ACP JSON-RPC 2.0 messages.
/// Both Hermes (`hermes acp`) and Reasonix share this protocol shape.
///
/// Reference:
/// - `reference-code/DeepSeek-Reasonix-main/src/acp/protocol.ts`
/// - `reference-code/hermes-vscode-main/src/protocol.ts`
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;

// ── JSON-RPC 2.0 basics ──────────────────────────────

pub type JsonRpcId = serde_json::Value; // number or string

// ── Initialize ───────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
    pub protocol_version: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_capabilities: Option<ClientCapabilities>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub client_info: Option<ClientInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fs: Option<FsCapabilities>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub terminal: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsCapabilities {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub read_text_file: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub write_text_file: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientInfo {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
    pub protocol_version: u64,
    pub agent_capabilities: AgentCapabilities,
    pub agent_info: AgentInfo,
    pub auth_methods: Vec<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub load_session: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prompt_capabilities: Option<PromptCapabilities>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mcp_capabilities: Option<McpCapabilities>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptCapabilities {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub image: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub audio: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub embedded_context: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpCapabilities {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sse: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentInfo {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    pub version: String,
}

// ── Session ──────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionNewParams {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mcp_servers: Option<Vec<McpServerSpec>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerSpec {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub args: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub env: Option<std::collections::HashMap<String, String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionNewResult {
    pub session_id: String,
}

// ── Session load ─────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionLoadParams {
    pub session_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mcp_servers: Option<Vec<McpServerSpec>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionLoadResult {
    pub session_id: String,
}

// ── Content blocks ───────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentBlock {
    #[serde(rename = "text")]
    Text { text: String },
    #[serde(rename = "resource")]
    Resource { resource: ResourceContent },
    #[serde(rename = "image")]
    Image { mime_type: String, data: String },
    #[serde(rename = "audio")]
    Audio { mime_type: String, data: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
    pub uri: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

// ── Session prompt ───────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionPromptParams {
    pub session_id: String,
    pub prompt: Vec<ContentBlock>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mnote_session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub actor_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trace_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub document_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mnote_capabilities: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionPromptResult {
    pub stop_reason: StopReason,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
    EndTurn,
    ToolUseComplete,
    Cancelled,
    Error,
}

// ── Session cancel (notification, no result) ─────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionCancelParams {
    pub session_id: String,
}

// ── Session update (notification from agent to client) ──

/// The `session/update` notification payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionUpdateParams {
    pub session_id: String,
    pub update: SessionUpdate,
}

#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum SessionUpdate {
    AgentMessageChunk {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "agent_message_chunk"
        content: TextContent,
    },
    AgentThoughtChunk {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "agent_thought_chunk"
        content: TextContent,
    },
    ToolCall {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "tool_call"
        #[serde(rename = "toolCallId")]
        tool_call_id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        title: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        kind: Option<ToolCallKind>,
        #[serde(skip_serializing_if = "Option::is_none")]
        status: Option<ToolCallStatus>,
        #[serde(skip_serializing_if = "Option::is_none")]
        raw_input: Option<Value>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        locations: Vec<ToolLocation>,
    },
    ToolCallUpdate {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "tool_call_update"
        #[serde(rename = "toolCallId")]
        tool_call_id: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        status: Option<ToolCallStatus>,
        #[serde(skip_serializing_if = "Option::is_none")]
        content: Option<Vec<ContentBlockWrapper>>,
    },
    Plan {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "plan"
        entries: Vec<PlanEntry>,
    },
    UsageUpdate {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "usage_update"
        used: u64,
        size: u64,
    },
    SessionInfoUpdate {
        #[serde(rename = "sessionUpdate")]
        session_update: String, // "session_info_update"
        title: String,
    },
    /// Catch-all for any future/unknown session update variants.
    Unknown {
        #[serde(rename = "sessionUpdate")]
        session_update: String,
        #[serde(flatten)]
        extra: std::collections::HashMap<String, Value>,
    },
}

impl<'de> Deserialize<'de> for SessionUpdate {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = Value::deserialize(deserializer)?;
        let kind = value
            .get("sessionUpdate")
            .and_then(Value::as_str)
            .ok_or_else(|| de::Error::missing_field("sessionUpdate"))?
            .to_string();

        match kind.as_str() {
            SessionUpdate::AGENT_MESSAGE_CHUNK => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    content: TextContent,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::AgentMessageChunk {
                    session_update: raw.session_update,
                    content: raw.content,
                })
            }
            SessionUpdate::AGENT_THOUGHT_CHUNK => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    content: TextContent,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::AgentThoughtChunk {
                    session_update: raw.session_update,
                    content: raw.content,
                })
            }
            SessionUpdate::TOOL_CALL => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    #[serde(rename = "toolCallId")]
                    tool_call_id: String,
                    title: Option<String>,
                    kind: Option<ToolCallKind>,
                    status: Option<ToolCallStatus>,
                    raw_input: Option<Value>,
                    #[serde(default)]
                    locations: Vec<ToolLocation>,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::ToolCall {
                    session_update: raw.session_update,
                    tool_call_id: raw.tool_call_id,
                    title: raw.title,
                    kind: raw.kind,
                    status: raw.status,
                    raw_input: raw.raw_input,
                    locations: raw.locations,
                })
            }
            SessionUpdate::TOOL_CALL_UPDATE => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    #[serde(rename = "toolCallId")]
                    tool_call_id: String,
                    status: Option<ToolCallStatus>,
                    content: Option<Vec<ContentBlockWrapper>>,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::ToolCallUpdate {
                    session_update: raw.session_update,
                    tool_call_id: raw.tool_call_id,
                    status: raw.status,
                    content: raw.content,
                })
            }
            SessionUpdate::PLAN => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    entries: Vec<PlanEntry>,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::Plan {
                    session_update: raw.session_update,
                    entries: raw.entries,
                })
            }
            SessionUpdate::USAGE_UPDATE => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    used: u64,
                    size: u64,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::UsageUpdate {
                    session_update: raw.session_update,
                    used: raw.used,
                    size: raw.size,
                })
            }
            SessionUpdate::SESSION_INFO_UPDATE => {
                #[derive(Deserialize)]
                #[serde(rename_all = "camelCase")]
                struct Raw {
                    #[serde(rename = "sessionUpdate")]
                    session_update: String,
                    title: String,
                }

                let raw: Raw = serde_json::from_value(value).map_err(de::Error::custom)?;
                Ok(SessionUpdate::SessionInfoUpdate {
                    session_update: raw.session_update,
                    title: raw.title,
                })
            }
            _ => {
                let mut extra = match value {
                    Value::Object(map) => map.into_iter().collect(),
                    _ => std::collections::HashMap::new(),
                };
                extra.remove("sessionUpdate");
                Ok(SessionUpdate::Unknown {
                    session_update: kind,
                    extra,
                })
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
    #[serde(rename = "type")]
    pub content_type: String, // "text"
    pub text: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentBlockWrapper {
    #[serde(rename = "type")]
    pub wrapper_type: String, // "content"
    pub content: TextContent,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallKind {
    Read,
    Edit,
    Search,
    Execute,
    Other,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
    Pending,
    InProgress,
    Completed,
    Failed,
}

/// A file path location referenced by a tool call.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolLocation {
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanEntry {
    pub content: String,
    pub priority: PlanPriority,
    pub status: PlanEntryStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlanPriority {
    High,
    Medium,
    Low,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlanEntryStatus {
    Pending,
    InProgress,
    Completed,
}

// ── Permission request (from agent to client) ────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionRequestParams {
    pub session_id: String,
    pub tool_call: PermissionToolCall,
    pub options: Vec<PermissionOption>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionToolCall {
    #[serde(rename = "toolCallId")]
    pub tool_call_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<ToolCallKind>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<ToolCallStatus>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub raw_input: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionOption {
    pub option_id: String,
    pub name: String,
    pub kind: PermissionOptionKind,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionOptionKind {
    AllowOnce,
    AllowAlways,
    RejectOnce,
    RejectAlways,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionRequestResult {
    pub outcome: PermissionOutcome,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PermissionOutcome {
    Selected { outcome: String, option_id: String },
    Cancelled { outcome: String },
}

// ── Error codes (JSON-RPC standard) ──────────────────

pub const ERR_PARSE: i64 = -32700;
pub const ERR_INVALID_REQUEST: i64 = -32600;
pub const ERR_METHOD_NOT_FOUND: i64 = -32601;
pub const ERR_INVALID_PARAMS: i64 = -32602;
pub const ERR_INTERNAL: i64 = -32603;

// ── Session update kind discriminants ────────────────

/// Constants for the `sessionUpdate` string field.
impl SessionUpdate {
    pub const AGENT_MESSAGE_CHUNK: &'static str = "agent_message_chunk";
    pub const AGENT_THOUGHT_CHUNK: &'static str = "agent_thought_chunk";
    pub const TOOL_CALL: &'static str = "tool_call";
    pub const TOOL_CALL_UPDATE: &'static str = "tool_call_update";
    pub const PLAN: &'static str = "plan";
    pub const USAGE_UPDATE: &'static str = "usage_update";
    pub const SESSION_INFO_UPDATE: &'static str = "session_info_update";
}

/// Parse the `sessionUpdate` string field from a raw JSON value and return the discriminant.
pub fn session_update_kind<'a>(value: &'a serde_json::Value) -> Option<&'a str> {
    value
        .get("update")
        .and_then(|u| u.get("sessionUpdate"))
        .and_then(|v| v.as_str())
}

// ── Helper: extract text from agent_message_chunk / agent_thought_chunk ──

/// Extract the text content from a session update's content block.
/// Returns `None` for non-text updates or malformed content.
///
/// Reference: `hermes-vscode-main/src/protocol.ts` `extractTextContent()`
pub fn extract_text_from_update(update: &SessionUpdate) -> Option<&str> {
    match update {
        SessionUpdate::AgentMessageChunk { content, .. }
        | SessionUpdate::AgentThoughtChunk { content, .. } => Some(&content.text),
        _ => None,
    }
}

// ── Tests ────────────────────────────────────────────

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

    #[test]
    fn test_initialize_params_roundtrip() {
        let params = InitializeParams {
            protocol_version: 1,
            client_capabilities: None,
            client_info: Some(ClientInfo {
                name: "mnote-web".into(),
                title: Some("MNote".into()),
                version: Some("0.1.0".into()),
            }),
        };
        let json = serde_json::to_value(&params).unwrap();
        assert_eq!(json["protocolVersion"], 1);
        assert_eq!(json["clientInfo"]["name"], "mnote-web");

        let deserialized: InitializeParams = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized.protocol_version, 1);
    }

    #[test]
    fn test_session_new_params() {
        let params = SessionNewParams {
            cwd: Some("/mnt/Data1T/mnote".into()),
            mcp_servers: Some(Vec::new()),
        };
        let json = serde_json::to_value(&params).unwrap();
        assert_eq!(json["cwd"], "/mnt/Data1T/mnote");
        assert_eq!(json["mcpServers"], serde_json::json!([]));
    }

    #[test]
    fn test_session_update_agent_message_chunk() {
        let json = serde_json::json!({
            "sessionId": "test_1",
            "update": {
                "sessionUpdate": "agent_message_chunk",
                "content": { "type": "text", "text": "Hello" }
            }
        });
        let parsed: SessionUpdateParams = serde_json::from_value(json).unwrap();
        assert_eq!(parsed.session_id, "test_1");
        match &parsed.update {
            SessionUpdate::AgentMessageChunk { content, .. } => {
                assert_eq!(content.text, "Hello");
            }
            _ => panic!("expected AgentMessageChunk"),
        }
    }

    #[test]
    fn test_session_update_agent_thought_chunk() {
        let json = serde_json::json!({
            "sessionId": "test_1",
            "update": {
                "sessionUpdate": "agent_thought_chunk",
                "content": { "type": "text", "text": "thinking" }
            }
        });
        let parsed: SessionUpdateParams = serde_json::from_value(json).unwrap();
        assert_eq!(parsed.session_id, "test_1");
        match &parsed.update {
            SessionUpdate::AgentThoughtChunk { content, .. } => {
                assert_eq!(content.text, "thinking");
            }
            _ => panic!("expected AgentThoughtChunk"),
        }
    }

    #[test]
    fn test_session_update_tool_call() {
        let json = serde_json::json!({
            "sessionId": "test_1",
            "update": {
                "sessionUpdate": "tool_call",
                "toolCallId": "tc_1",
                "title": "mnote.doc.fetch",
                "kind": "read",
                "status": "pending"
            }
        });
        let parsed: SessionUpdateParams = serde_json::from_value(json).unwrap();
        match &parsed.update {
            SessionUpdate::ToolCall {
                title,
                kind,
                locations,
                ..
            } => {
                assert_eq!(title.as_deref(), Some("mnote.doc.fetch"));
                assert!(matches!(kind, Some(ToolCallKind::Read)));
                assert!(locations.is_empty(), "no locations in this fixture");
            }
            _ => panic!("expected ToolCall"),
        }
    }

    #[test]
    fn test_session_update_tool_call_with_locations() {
        let json = serde_json::json!({
            "sessionId": "test_1",
            "update": {
                "sessionUpdate": "tool_call",
                "toolCallId": "tc_2",
                "title": "mnote.doc.read",
                "kind": "read",
                "status": "in_progress",
                "locations": [
                    { "path": "/mnt/Data1T/mnote/src/main.rs" },
                    { "path": "/mnt/Data1T/mnote/src/lib.rs" }
                ]
            }
        });
        let parsed: SessionUpdateParams = serde_json::from_value(json).unwrap();
        match &parsed.update {
            SessionUpdate::ToolCall {
                tool_call_id,
                locations,
                ..
            } => {
                assert_eq!(tool_call_id, "tc_2");
                assert_eq!(locations.len(), 2);
                assert_eq!(locations[0].path, "/mnt/Data1T/mnote/src/main.rs");
                assert_eq!(locations[1].path, "/mnt/Data1T/mnote/src/lib.rs");
            }
            _ => panic!("expected ToolCall"),
        }
    }

    #[test]
    fn test_session_update_usage() {
        let json = serde_json::json!({
            "sessionId": "test_1",
            "update": {
                "sessionUpdate": "usage_update",
                "used": 1500,
                "size": 4000
            }
        });
        let parsed: SessionUpdateParams = serde_json::from_value(json).unwrap();
        match &parsed.update {
            SessionUpdate::UsageUpdate { used, size, .. } => {
                assert_eq!(*used, 1500);
                assert_eq!(*size, 4000);
            }
            _ => panic!("expected UsageUpdate"),
        }
    }

    #[test]
    fn test_content_block_text() {
        let block = ContentBlock::Text {
            text: "hello".into(),
        };
        let json = serde_json::to_value(&block).unwrap();
        assert_eq!(json["type"], "text");
        assert_eq!(json["text"], "hello");
    }

    #[test]
    fn test_flatten_prompt() {
        let blocks = vec![
            ContentBlock::Text {
                text: "Hello".into(),
            },
            ContentBlock::Resource {
                resource: ResourceContent {
                    uri: "file:///test.md".into(),
                    mime_type: None,
                    text: Some(" world".into()),
                },
            },
        ];
        // flattenPrompt equivalent: concatenate text blocks + resource text
        let text: Vec<String> = blocks
            .iter()
            .map(|b| match b {
                ContentBlock::Text { text } => text.clone(),
                ContentBlock::Resource { resource } => resource.text.clone().unwrap_or_default(),
                _ => String::new(),
            })
            .collect();
        assert_eq!(text.join(""), "Hello world");
    }
}
