//! Core types for parsing Claude Code JSONL session data.

use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Content types
// ---------------------------------------------------------------------------

/// Content is either a plain string or an array of content blocks.
///
/// Uses a custom deserializer so that `Blocks` variant applies lenient
/// deserialization — unknown content block types (e.g. `tool_reference`)
/// are silently skipped instead of failing the entire message.
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum Content {
    Text(String),
    Blocks(Vec<ContentBlock>),
}

impl<'de> serde::Deserialize<'de> for Content {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = serde_json::Value::deserialize(deserializer)?;
        match value {
            serde_json::Value::String(s) => Ok(Content::Text(s)),
            serde_json::Value::Array(arr) => {
                let blocks = arr
                    .into_iter()
                    .filter_map(|v| {
                        serde_json::from_value::<ContentBlock>(v.clone())
                            .ok()
                            .or_else(|| {
                                tracing::debug!(
                                    "Skipping unknown content block: {:?}",
                                    v.get("type")
                                );
                                None
                            })
                    })
                    .collect();
                Ok(Content::Blocks(blocks))
            }
            other => Err(serde::de::Error::custom(format!(
                "expected string or array for Content, got {}",
                match &other {
                    serde_json::Value::Null => "null",
                    serde_json::Value::Bool(_) => "bool",
                    serde_json::Value::Number(_) => "number",
                    serde_json::Value::Object(_) => "object",
                    _ => "unknown",
                }
            ))),
        }
    }
}

/// Typed content block inside messages.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
    Text {
        text: String,
    },
    ToolUse {
        id: String,
        name: String,
        input: serde_json::Value,
        #[serde(default)]
        caller: Option<serde_json::Value>,
    },
    ToolResult {
        tool_use_id: String,
        #[serde(default)]
        content: Option<Content>,
        #[serde(default)]
        is_error: bool,
    },
    Thinking {
        thinking: String,
    },
    Image {
        source: serde_json::Value,
    },
}

// ---------------------------------------------------------------------------
// Lenient content block deserialization
// ---------------------------------------------------------------------------

/// Deserializes a `Vec<ContentBlock>` leniently — unknown block types are
/// silently skipped instead of failing the entire message.
fn deserialize_content_blocks<'de, D>(
    deserializer: D,
) -> std::result::Result<Vec<ContentBlock>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::Deserialize as _;
    let raw: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
    Ok(raw
        .into_iter()
        .filter_map(|v| {
            serde_json::from_value::<ContentBlock>(v.clone()).ok().or_else(|| {
                tracing::debug!("Skipping unknown content block: {:?}", v.get("type"));
                None
            })
        })
        .collect())
}

// ---------------------------------------------------------------------------
// Top-level JSONL line discriminator
// ---------------------------------------------------------------------------

/// Top-level JSONL line discriminator.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum RawMessage {
    User(RawUserMessage),
    Assistant(RawAssistantMessage),
    Progress(RawProgressMessage),
    System(RawSystemMessage),
    QueueOperation(RawQueueOperation),
    FileHistorySnapshot(RawFileHistorySnapshot),
    LastPrompt(RawLastPrompt),
}

// ---------------------------------------------------------------------------
// User message
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawUserMessage {
    #[serde(default)]
    pub uuid: Option<String>,
    #[serde(default)]
    pub parent_uuid: Option<String>,
    #[serde(default)]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub cwd: Option<String>,
    #[serde(default)]
    pub version: Option<String>,
    #[serde(default)]
    pub git_branch: Option<String>,
    #[serde(default)]
    pub is_sidechain: bool,
    #[serde(default)]
    pub is_meta: Option<bool>,
    #[serde(default)]
    pub user_type: Option<String>,
    pub message: UserMessageInner,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserMessageInner {
    pub role: String,
    pub content: Content,
}

// ---------------------------------------------------------------------------
// Assistant message
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawAssistantMessage {
    #[serde(default)]
    pub uuid: Option<String>,
    #[serde(default)]
    pub parent_uuid: Option<String>,
    #[serde(default)]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub cwd: Option<String>,
    #[serde(default)]
    pub version: Option<String>,
    #[serde(default)]
    pub git_branch: Option<String>,
    #[serde(default)]
    pub is_sidechain: bool,
    #[serde(default)]
    pub request_id: Option<String>,
    pub message: AssistantInner,
}

// NOTE: AssistantInner is the Anthropic API response object nested inside
// the Claude Code JSONL wrapper. The API uses snake_case (stop_reason, etc.)
// unlike the outer JSONL wrapper which uses camelCase.
#[derive(Debug, Clone, Deserialize)]
pub struct AssistantInner {
    #[serde(default)]
    pub model: Option<String>,
    #[serde(default)]
    pub id: Option<String>,
    #[serde(default, rename = "type")]
    pub message_type: Option<String>,
    #[serde(default)]
    pub role: Option<String>,
    #[serde(default, deserialize_with = "deserialize_content_blocks")]
    pub content: Vec<ContentBlock>,
    #[serde(default)]
    pub stop_reason: Option<String>,
    #[serde(default)]
    pub stop_sequence: Option<String>,
    #[serde(default)]
    pub usage: Option<serde_json::Value>,
}

// ---------------------------------------------------------------------------
// Progress message
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawProgressMessage {
    #[serde(default)]
    pub uuid: Option<String>,
    #[serde(default)]
    pub parent_uuid: Option<String>,
    #[serde(default)]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub cwd: Option<String>,
    #[serde(default)]
    pub version: Option<String>,
    #[serde(default)]
    pub git_branch: Option<String>,
    #[serde(default)]
    pub is_sidechain: bool,
    #[serde(default)]
    pub data: Option<serde_json::Value>,
}

// ---------------------------------------------------------------------------
// System message
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawSystemMessage {
    #[serde(default)]
    pub uuid: Option<String>,
    #[serde(default)]
    pub parent_uuid: Option<String>,
    #[serde(default)]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub cwd: Option<String>,
    #[serde(default)]
    pub version: Option<String>,
    #[serde(default)]
    pub git_branch: Option<String>,
    #[serde(default)]
    pub is_sidechain: bool,
    #[serde(default)]
    pub data: Option<serde_json::Value>,
}

// ---------------------------------------------------------------------------
// Queue operation
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawQueueOperation {
    pub operation: String,
    #[serde(default)]
    pub timestamp: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
}

// ---------------------------------------------------------------------------
// File history snapshot
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawFileHistorySnapshot {
    #[serde(default)]
    pub message_id: Option<String>,
    #[serde(default)]
    pub is_snapshot_update: bool,
    #[serde(default)]
    pub snapshot: Option<serde_json::Value>,
}

// ---------------------------------------------------------------------------
// Last prompt
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawLastPrompt {
    #[serde(default)]
    pub last_prompt: Option<String>,
    #[serde(default)]
    pub session_id: Option<String>,
}

// ---------------------------------------------------------------------------
// Tool types (for correlation module later)
// ---------------------------------------------------------------------------

/// Known tool names used by Claude Code.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ToolName {
    Bash,
    Read,
    Write,
    Edit,
    Grep,
    Glob,
    Agent,
    Skill,
    WebSearch,
    WebFetch,
    TodoWrite,
    NotebookEdit,
    Other(String),
}

impl From<String> for ToolName {
    fn from(s: String) -> Self {
        match s.as_str() {
            "Bash" => ToolName::Bash,
            "Read" => ToolName::Read,
            "Write" => ToolName::Write,
            "Edit" => ToolName::Edit,
            "Grep" => ToolName::Grep,
            "Glob" => ToolName::Glob,
            "Agent" => ToolName::Agent,
            "Skill" => ToolName::Skill,
            "WebSearch" => ToolName::WebSearch,
            "WebFetch" => ToolName::WebFetch,
            "TodoWrite" => ToolName::TodoWrite,
            "NotebookEdit" => ToolName::NotebookEdit,
            other => ToolName::Other(other.to_string()),
        }
    }
}

/// A tool call extracted from an assistant message.
#[derive(Debug, Clone)]
pub struct ToolCall {
    pub id: String,
    pub name: ToolName,
    pub input: serde_json::Value,
    pub source_message_uuid: String,
}

/// A tool result extracted from a user message.
#[derive(Debug, Clone)]
pub struct ToolResultData {
    pub tool_use_id: String,
    pub content: Option<String>,
    pub is_error: bool,
    pub source_message_uuid: String,
}

/// A correlated tool call + result pair.
#[derive(Debug, Clone)]
pub struct ToolExchange {
    pub call: ToolCall,
    pub result: Option<ToolResultData>,
}

// ---------------------------------------------------------------------------
// Discovery types (for discovery module later)
// ---------------------------------------------------------------------------

/// A discovered Claude Code project directory.
#[derive(Debug, Clone)]
pub struct ClaudeProject {
    pub path: Utf8PathBuf,
    pub original_path: String,
    pub sessions: Vec<SessionRef>,
}

/// Reference to a session (not yet parsed).
#[derive(Debug, Clone)]
pub struct SessionRef {
    pub id: String,
    pub jsonl_path: Utf8PathBuf,
    pub artifacts_dir: Option<Utf8PathBuf>,
    pub index_entry: Option<SessionIndexEntry>,
}

/// From sessions-index.json (when available).
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionIndexEntry {
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub first_prompt: Option<String>,
    #[serde(default)]
    pub summary: Option<String>,
    #[serde(default)]
    pub message_count: Option<u32>,
    #[serde(default)]
    pub created: Option<serde_json::Value>,
    #[serde(default)]
    pub modified: Option<serde_json::Value>,
    #[serde(default)]
    pub git_branch: Option<String>,
    #[serde(default)]
    pub project_path: Option<String>,
}

// ---------------------------------------------------------------------------
// Sub-agent types
// ---------------------------------------------------------------------------

/// Sub-agent metadata from .meta.json.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubAgentMeta {
    #[serde(default)]
    pub agent_type: Option<String>,
}

/// A parsed sub-agent session.
#[derive(Debug, Clone)]
pub struct SubAgentSession {
    pub agent_id: String,
    pub meta: SubAgentMeta,
    pub messages: Vec<RawMessage>,
    pub parent_tool_call_id: Option<String>,
}

// ---------------------------------------------------------------------------
// MessageMeta (convenience, future use)
// ---------------------------------------------------------------------------

/// Common metadata extracted from any message. Defined for future consumers.
#[derive(Debug, Clone)]
pub struct MessageMeta {
    pub uuid: String,
    pub parent_uuid: Option<String>,
    pub timestamp: Option<String>,
    pub session_id: String,
    pub cwd: Option<String>,
    pub version: Option<String>,
    pub git_branch: Option<String>,
    pub is_sidechain: bool,
}

// ---------------------------------------------------------------------------
// ParsedSession
// ---------------------------------------------------------------------------

/// A fully parsed session with all correlations built.
#[derive(Debug)]
pub struct ParsedSession {
    pub messages: Vec<RawMessage>,
    pub tree: crate::tree::ConversationTree,
    pub tool_exchanges: Vec<ToolExchange>,
    pub subagents: Vec<SubAgentSession>,
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn parse_content_text_string() {
        let json = r#""Hello world""#;
        let content: Content = serde_json::from_str(json).unwrap();
        match content {
            Content::Text(s) => assert_eq!(s, "Hello world"),
            _ => panic!("Expected Content::Text"),
        }
    }

    #[test]
    fn parse_content_blocks() {
        let json = r#"[{"type": "text", "text": "Hello"}]"#;
        let content: Content = serde_json::from_str(json).unwrap();
        match content {
            Content::Blocks(blocks) => {
                assert_eq!(blocks.len(), 1);
                match &blocks[0] {
                    ContentBlock::Text { text } => assert_eq!(text, "Hello"),
                    _ => panic!("Expected ContentBlock::Text"),
                }
            }
            _ => panic!("Expected Content::Blocks"),
        }
    }

    #[test]
    fn parse_tool_use_block() {
        let json = r#"{"type": "tool_use", "id": "toolu_123", "name": "Bash", "input": {"command": "ls"}}"#;
        let block: ContentBlock = serde_json::from_str(json).unwrap();
        match block {
            ContentBlock::ToolUse { id, name, .. } => {
                assert_eq!(id, "toolu_123");
                assert_eq!(name, "Bash");
            }
            _ => panic!("Expected ContentBlock::ToolUse"),
        }
    }

    #[test]
    fn parse_tool_result_block() {
        let json = r#"{"type": "tool_result", "tool_use_id": "toolu_123", "content": "output text", "is_error": false}"#;
        let block: ContentBlock = serde_json::from_str(json).unwrap();
        match block {
            ContentBlock::ToolResult {
                tool_use_id,
                is_error,
                ..
            } => {
                assert_eq!(tool_use_id, "toolu_123");
                assert!(!is_error);
            }
            _ => panic!("Expected ContentBlock::ToolResult"),
        }
    }

    #[test]
    fn parse_thinking_block() {
        let json = r#"{"type": "thinking", "thinking": "Let me consider..."}"#;
        let block: ContentBlock = serde_json::from_str(json).unwrap();
        match block {
            ContentBlock::Thinking { thinking } => {
                assert_eq!(thinking, "Let me consider...");
            }
            _ => panic!("Expected ContentBlock::Thinking"),
        }
    }

    #[test]
    fn parse_queue_operation() {
        let json = r#"{"type": "queue-operation", "operation": "enqueue", "timestamp": "2026-03-14T21:15:17.531Z", "sessionId": "00f72d8d-fc54-485c-a082-310ffcabdb73"}"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::QueueOperation(op) => {
                assert_eq!(op.operation, "enqueue");
                assert_eq!(
                    op.session_id.as_deref(),
                    Some("00f72d8d-fc54-485c-a082-310ffcabdb73")
                );
            }
            _ => panic!("Expected RawMessage::QueueOperation"),
        }
    }

    #[test]
    fn parse_user_message_with_string_content() {
        let json = r#"{
            "parentUuid": "b1ab1ac7-fdb6-4e25-bc17-4c060b470b4a",
            "isSidechain": false,
            "userType": "external",
            "cwd": "G:\\dev\\projects\\dirigent",
            "sessionId": "00f72d8d-fc54-485c-a082-310ffcabdb73",
            "version": "2.1.71",
            "gitBranch": "main",
            "type": "user",
            "message": {
                "role": "user",
                "content": "Hello world"
            },
            "isMeta": false,
            "uuid": "1d843a4a-b99d-4c02-91a3-7cfe3dcac9f0",
            "timestamp": "2026-03-14T21:08:58.586Z"
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::User(u) => {
                assert_eq!(u.uuid.as_deref(), Some("1d843a4a-b99d-4c02-91a3-7cfe3dcac9f0"));
                assert_eq!(u.session_id.as_deref(), Some("00f72d8d-fc54-485c-a082-310ffcabdb73"));
                assert_eq!(u.is_meta, Some(false));
                match &u.message.content {
                    Content::Text(s) => assert_eq!(s, "Hello world"),
                    _ => panic!("Expected Content::Text"),
                }
            }
            _ => panic!("Expected RawMessage::User"),
        }
    }

    #[test]
    fn parse_assistant_message_with_tool_use() {
        let json = r#"{
            "parentUuid": "77793647-f957-4aec-8b04-a9c07e01e37b",
            "isSidechain": false,
            "userType": "external",
            "cwd": "G:\\dev\\projects\\dirigent",
            "sessionId": "00f72d8d-fc54-485c-a082-310ffcabdb73",
            "version": "2.1.71",
            "gitBranch": "main",
            "message": {
                "model": "claude-opus-4-6",
                "id": "msg_01NcwYjEydGEyZCSCgwmcnYd",
                "type": "message",
                "role": "assistant",
                "content": [
                    {
                        "type": "tool_use",
                        "id": "toolu_01DP5mkAQnAi2o54idq24cPn",
                        "name": "Agent",
                        "input": {
                            "description": "Investigate config sources of truth",
                            "subagent_type": "Explore",
                            "prompt": "test prompt"
                        },
                        "caller": { "type": "direct" }
                    }
                ],
                "stop_reason": null,
                "stop_sequence": null,
                "usage": {
                    "input_tokens": 3,
                    "cache_creation_input_tokens": 20147,
                    "cache_read_input_tokens": 0,
                    "output_tokens": 9,
                    "service_tier": "standard"
                }
            },
            "requestId": "req_011CZ3fYWGjcQCgh5d58d2k8",
            "type": "assistant",
            "uuid": "6cad0d13-d0ae-47fa-a6b1-b7b45a2b5e0b",
            "timestamp": "2026-03-14T21:15:27.916Z"
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::Assistant(a) => {
                assert_eq!(a.uuid.as_deref(), Some("6cad0d13-d0ae-47fa-a6b1-b7b45a2b5e0b"));
                assert_eq!(a.message.model.as_deref(), Some("claude-opus-4-6"));
                assert_eq!(a.message.content.len(), 1);
                match &a.message.content[0] {
                    ContentBlock::ToolUse { name, id, .. } => {
                        assert_eq!(name, "Agent");
                        assert_eq!(id, "toolu_01DP5mkAQnAi2o54idq24cPn");
                    }
                    _ => panic!("Expected ContentBlock::ToolUse"),
                }
                assert!(a.message.stop_reason.is_none());
                assert!(a.message.usage.is_some());
            }
            _ => panic!("Expected RawMessage::Assistant"),
        }
    }

    #[test]
    fn unknown_content_block_type_skipped_in_assistant() {
        let json = r#"{
            "parentUuid": null,
            "isSidechain": false,
            "sessionId": "test",
            "message": {
                "role": "assistant",
                "content": [
                    {"type": "text", "text": "known"},
                    {"type": "future_type", "data": "something"}
                ]
            },
            "type": "assistant",
            "uuid": "test-uuid",
            "timestamp": "2026-01-01T00:00:00Z"
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::Assistant(a) => {
                assert_eq!(a.message.content.len(), 1);
                match &a.message.content[0] {
                    ContentBlock::Text { text } => assert_eq!(text, "known"),
                    _ => panic!("Expected ContentBlock::Text"),
                }
            }
            _ => panic!("Expected RawMessage::Assistant"),
        }
    }

    // -----------------------------------------------------------------------
    // Regression tests for parse failure audit (2026-04-04)
    // -----------------------------------------------------------------------

    #[test]
    fn tool_reference_in_tool_result_content_does_not_fail() {
        // Suggestion 1 & 3: tool_reference blocks inside tool_result.content
        // should be silently skipped, not fail the entire message.
        let json = r#"{
            "type": "user",
            "uuid": "test-uuid",
            "parentUuid": null,
            "isSidechain": false,
            "sessionId": "test-session",
            "message": {
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": "toolu_abc123",
                        "content": [
                            {"type": "text", "text": "File contents here"},
                            {"type": "tool_reference", "tool_name": "TodoWrite"}
                        ],
                        "is_error": false
                    }
                ]
            }
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::User(u) => {
                match &u.message.content {
                    Content::Blocks(blocks) => {
                        assert_eq!(blocks.len(), 1);
                        match &blocks[0] {
                            ContentBlock::ToolResult { tool_use_id, content, .. } => {
                                assert_eq!(tool_use_id, "toolu_abc123");
                                // The inner content should have 1 block (text), tool_reference skipped
                                match content.as_ref().unwrap() {
                                    Content::Blocks(inner) => {
                                        assert_eq!(inner.len(), 1);
                                        match &inner[0] {
                                            ContentBlock::Text { text } => {
                                                assert_eq!(text, "File contents here");
                                            }
                                            _ => panic!("Expected inner ContentBlock::Text"),
                                        }
                                    }
                                    _ => panic!("Expected inner Content::Blocks"),
                                }
                            }
                            _ => panic!("Expected ContentBlock::ToolResult"),
                        }
                    }
                    _ => panic!("Expected Content::Blocks"),
                }
            }
            _ => panic!("Expected RawMessage::User"),
        }
    }

    #[test]
    fn file_history_snapshot_parses() {
        // Suggestion 2: file-history-snapshot lines should parse, not fail.
        let json = r#"{
            "type": "file-history-snapshot",
            "messageId": "abc-123",
            "isSnapshotUpdate": false,
            "snapshot": {
                "messageId": "abc-123",
                "trackedFileBackups": {
                    "src/main.rs": {"backupFileName": "main.rs.bak", "backupTime": "2026-01-01T00:00:00Z", "version": "1"}
                },
                "timestamp": "2026-01-01T00:00:00Z"
            }
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::FileHistorySnapshot(s) => {
                assert_eq!(s.message_id.as_deref(), Some("abc-123"));
                assert!(!s.is_snapshot_update);
                assert!(s.snapshot.is_some());
            }
            _ => panic!("Expected RawMessage::FileHistorySnapshot"),
        }
    }

    #[test]
    fn last_prompt_parses() {
        // Suggestion 2: last-prompt lines should parse, not fail.
        let json = r#"{
            "type": "last-prompt",
            "lastPrompt": "Fix the bug in auth middleware",
            "sessionId": "session-uuid-123"
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::LastPrompt(lp) => {
                assert_eq!(lp.last_prompt.as_deref(), Some("Fix the bug in auth middleware"));
                assert_eq!(lp.session_id.as_deref(), Some("session-uuid-123"));
            }
            _ => panic!("Expected RawMessage::LastPrompt"),
        }
    }

    #[test]
    fn unknown_content_block_in_user_message_skipped() {
        // Suggestion 3: Unknown block types in user message content
        // should be silently skipped (lenient everywhere).
        let json = r#"{
            "type": "user",
            "uuid": "test-uuid",
            "isSidechain": false,
            "sessionId": "test",
            "message": {
                "role": "user",
                "content": [
                    {"type": "text", "text": "known"},
                    {"type": "future_unknown_type", "data": "something"}
                ]
            }
        }"#;
        let msg: RawMessage = serde_json::from_str(json).unwrap();
        match msg {
            RawMessage::User(u) => {
                match &u.message.content {
                    Content::Blocks(blocks) => {
                        assert_eq!(blocks.len(), 1);
                        match &blocks[0] {
                            ContentBlock::Text { text } => assert_eq!(text, "known"),
                            _ => panic!("Expected ContentBlock::Text"),
                        }
                    }
                    _ => panic!("Expected Content::Blocks"),
                }
            }
            _ => panic!("Expected RawMessage::User"),
        }
    }

    #[test]
    fn tool_name_from_string() {
        assert_eq!(ToolName::from("Bash".to_string()), ToolName::Bash);
        assert_eq!(ToolName::from("Read".to_string()), ToolName::Read);
        assert_eq!(ToolName::from("Agent".to_string()), ToolName::Agent);
        assert_eq!(ToolName::from("WebSearch".to_string()), ToolName::WebSearch);
        assert_eq!(
            ToolName::from("CustomTool".to_string()),
            ToolName::Other("CustomTool".to_string())
        );
    }
}
