#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

//! RCT tests for core types
//!
//! These tests verify the serialization/deserialization contracts.

use super::*;
use serde_json::json;

fn schema_for<T: schemars::JsonSchema>() -> Value {
    let schema = schemars::schema_for!(T);
    let mut value = serde_json::to_value(&schema).unwrap_or(Value::Null);

    // Ensure object schemas always have `properties` and `required` keys.
    if let Value::Object(ref mut obj) = value
        && obj.get("type").and_then(Value::as_str) == Some("object")
    {
        obj.entry("properties".to_string())
            .or_insert_with(|| Value::Object(serde_json::Map::new()));
        obj.entry("required".to_string())
            .or_insert_with(|| Value::Array(Vec::new()));
    }

    value
}

#[test]
fn test_session_id_encoding() {
    // UUID v7 format test
    let id = SessionId::new();
    let json = serde_json::to_string(&id).unwrap();

    // Should be a valid UUID string in JSON
    assert!(json.starts_with('"'));
    assert!(json.ends_with('"'));

    // Should roundtrip
    let parsed: SessionId = serde_json::from_str(&json).unwrap();
    assert_eq!(id, parsed);

    // Parse from string should work
    let id_str = id.0.to_string();
    let parsed_from_str = SessionId::parse(&id_str).unwrap();
    assert_eq!(id, parsed_from_str);
}

#[test]
fn test_message_json_schema() {
    // System message
    let system = Message::System(SystemMessage::new("You are a helpful assistant."));
    let json = serde_json::to_value(&system).unwrap();
    assert_eq!(json["role"], "system");
    assert_eq!(json["content"], "You are a helpful assistant.");

    // User message
    let user = Message::User(UserMessage::text("Hello!"));
    let json = serde_json::to_value(&user).unwrap();
    assert_eq!(json["role"], "user");
    assert_eq!(json["content"], "Hello!");
    assert!(json.get("render_metadata").is_none());

    // System notice message
    let notice = Message::SystemNotice(SystemNoticeMessage::new(
        SystemNoticeKind::BackgroundJob,
        "Background job still running.",
    ));
    let json = serde_json::to_value(&notice).unwrap();
    assert_eq!(json["role"], "system_notice");
    assert_eq!(json["kind"], "background_job");
    assert_eq!(json["body"], "Background job still running.");

    // Assistant message
    let assistant = Message::Assistant(AssistantMessage {
        content: "Hi there!".to_string(),
        tool_calls: vec![],
        stop_reason: StopReason::EndTurn,
        usage: Usage::default(),
        created_at: message_timestamp_now(),
    });
    let json = serde_json::to_value(&assistant).unwrap();
    assert_eq!(json["role"], "assistant");
    assert_eq!(json["content"], "Hi there!");
    assert_eq!(json["stop_reason"], "end_turn");

    // Tool results
    let tool_results = Message::tool_results(vec![ToolResult::new(
        "tool_123".to_string(),
        "Result content".to_string(),
        false,
    )]);
    let json = serde_json::to_value(&tool_results).unwrap();
    assert_eq!(json["role"], "tool_results");
    assert!(json["results"].is_array());
}

#[test]
fn test_user_message_render_metadata_serialization() {
    let user = Message::User(UserMessage::text_with_render_metadata(
        "notice",
        Some(RenderMetadata {
            class: RenderClass::ToolScopeNotice,
            salience: RenderSalience::Normal,
        }),
    ));

    let json = serde_json::to_value(&user).unwrap();
    assert_eq!(json["role"], "user");
    assert_eq!(json["render_metadata"]["class"], "tool_scope_notice");
    assert_eq!(json["render_metadata"]["salience"], "normal");
}

#[test]
fn test_legacy_user_notice_deserializes_as_user_text() {
    let parsed: Message = serde_json::from_value(json!({
        "role": "user",
        "content": "[SYSTEM NOTICE][TOOL_SCOPE] Tool configuration changed.",
        "render_metadata": {
            "class": "tool_scope_notice",
            "salience": "normal"
        }
    }))
    .unwrap();

    match parsed {
        Message::User(user) => {
            assert_eq!(
                user.text_content(),
                "[SYSTEM NOTICE][TOOL_SCOPE] Tool configuration changed."
            );
        }
        other => panic!("expected user message, got {other:?}"),
    }
}

#[test]
fn test_reserved_prefix_user_text_stays_user_without_notice_metadata() {
    let parsed: Message = serde_json::from_value(json!({
        "role": "user",
        "content": "[SYSTEM NOTICE][TOOL_SCOPE] pasted by a real user"
    }))
    .unwrap();

    match parsed {
        Message::User(user) => {
            assert_eq!(
                user.text_content(),
                "[SYSTEM NOTICE][TOOL_SCOPE] pasted by a real user"
            );
        }
        other => panic!("expected user message, got {other:?}"),
    }
}

#[test]
fn test_future_system_notice_block_deserializes_as_unknown_meta() {
    let parsed: Message = serde_json::from_value(json!({
        "role": "system_notice",
        "kind": "generic",
        "body": "Future runtime fact.",
        "blocks": [
            {
                "type": "future_runtime_fact",
                "summary": "Future fact",
                "payload": {
                    "alpha": 1
                },
                "extra": true
            }
        ]
    }))
    .unwrap();

    match parsed {
        Message::SystemNotice(notice) => {
            assert_eq!(notice.kind, SystemNoticeKind::Generic);
            assert_eq!(notice.body.as_deref(), Some("Future runtime fact."));
            assert_eq!(notice.blocks.len(), 1);
            match &notice.blocks[0] {
                SystemNoticeBlock::Unknown { summary, payload } => {
                    assert_eq!(summary.as_deref(), Some("Future fact"));
                    let payload = payload.as_ref().expect("unknown block payload");
                    assert_eq!(payload["type"], "future_runtime_fact");
                    assert_eq!(payload["payload"]["alpha"], 1);
                    assert_eq!(payload["extra"], true);
                }
                other => panic!("expected unknown block, got {other:?}"),
            }
        }
        other => panic!("expected system notice, got {other:?}"),
    }
}

#[test]
fn test_system_notice_block_without_type_is_invalid() {
    let err = serde_json::from_value::<Message>(json!({
        "role": "system_notice",
        "kind": "generic",
        "blocks": [
            {
                "summary": "Malformed block"
            }
        ]
    }))
    .expect_err("missing block type should remain invalid");

    assert!(
        err.to_string().contains("missing system notice block type"),
        "unexpected error: {err}"
    );
}

#[test]
fn test_tool_call_serialization() {
    let tool_call = ToolCall::new(
        "tc_abc123".to_string(),
        "read_file".to_string(),
        json!({"path": "/tmp/test.txt"}),
    );

    let json = serde_json::to_string(&tool_call).unwrap();
    let parsed: ToolCall = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed.id, "tc_abc123");
    assert_eq!(parsed.name, "read_file");
    assert_eq!(parsed.args["path"], "/tmp/test.txt");
}

#[test]
fn test_tool_result_serialization() {
    let result = ToolResult::new(
        "tc_abc123".to_string(),
        "File contents here".to_string(),
        false,
    );

    let json = serde_json::to_string(&result).unwrap();
    let parsed: ToolResult = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed.tool_use_id, "tc_abc123");
    assert_eq!(parsed.text_content(), "File contents here");
    assert!(!parsed.is_error);

    // Error result
    let error_result = ToolResult::new(
        "tc_abc124".to_string(),
        "Permission denied".to_string(),
        true,
    );

    let json = serde_json::to_string(&error_result).unwrap();
    let parsed: ToolResult = serde_json::from_str(&json).unwrap();
    assert!(parsed.is_error);
}

#[test]
fn test_tool_result_from_tool_call() {
    let tool_call = ToolCall::new("tc_123".to_string(), "test_tool".to_string(), json!({}));

    let result = ToolResult::from_tool_call(&tool_call, "output".to_string(), false);

    assert_eq!(result.tool_use_id, "tc_123");
    assert_eq!(result.text_content(), "output");
    assert!(!result.is_error);
}

#[test]
fn test_stop_reason_mapping() {
    // All variants should serialize to snake_case
    let reasons = vec![
        (StopReason::EndTurn, "end_turn"),
        (StopReason::ToolUse, "tool_use"),
        (StopReason::MaxTokens, "max_tokens"),
        (StopReason::StopSequence, "stop_sequence"),
        (StopReason::ContentFilter, "content_filter"),
        (StopReason::Cancelled, "cancelled"),
    ];

    for (reason, expected_str) in reasons {
        let json = serde_json::to_value(reason).unwrap();
        assert_eq!(json.as_str().unwrap(), expected_str);

        // Roundtrip
        let parsed: StopReason = serde_json::from_value(json).unwrap();
        assert_eq!(parsed, reason);
    }
}

#[test]
fn test_usage_accumulation() {
    let mut total = Usage::default();

    let turn1 = Usage {
        input_tokens: 100,
        output_tokens: 50,
        cache_creation_tokens: Some(10),
        cache_read_tokens: None,
    };

    let turn2 = Usage {
        input_tokens: 150,
        output_tokens: 75,
        cache_creation_tokens: None,
        cache_read_tokens: Some(10),
    };

    total.add(&turn1);
    assert_eq!(total.input_tokens, 100);
    assert_eq!(total.output_tokens, 50);
    assert_eq!(total.cache_creation_tokens, Some(10));
    assert_eq!(total.cache_read_tokens, None);

    total.add(&turn2);
    assert_eq!(total.input_tokens, 250);
    assert_eq!(total.output_tokens, 125);
    assert_eq!(total.cache_creation_tokens, Some(10));
    assert_eq!(total.cache_read_tokens, Some(10));
    assert_eq!(total.total_tokens(), 375);
}

#[test]
fn test_run_result_json_schema() {
    let result = RunResult {
        text: "Task completed".to_string(),
        session_id: SessionId::new(),
        usage: Usage {
            input_tokens: 1000,
            output_tokens: 500,
            cache_creation_tokens: None,
            cache_read_tokens: None,
        },
        turns: 3,
        tool_calls: 5,
        terminal_cause_kind: None,
        structured_output: None,
        extraction_error: None,
        schema_warnings: None,
        skill_diagnostics: None,
    };

    let json = serde_json::to_value(&result).unwrap();

    assert_eq!(json["text"], "Task completed");
    assert!(json["session_id"].is_string());
    assert_eq!(json["usage"]["input_tokens"], 1000);
    assert_eq!(json["usage"]["output_tokens"], 500);
    assert_eq!(json["turns"], 3);
    assert_eq!(json["tool_calls"], 5);

    // Roundtrip
    let parsed: RunResult = serde_json::from_value(json).unwrap();
    assert_eq!(parsed.text, "Task completed");
    assert_eq!(parsed.turns, 3);
}

#[test]
fn test_tool_def_serialization() {
    #[derive(schemars::JsonSchema)]
    #[allow(dead_code)]
    struct TestToolDefInput {
        arg1: String,
    }

    let tool_def = ToolDef {
        name: "test_tool".into(),
        description: "A test tool".to_string(),
        input_schema: schema_for::<TestToolDefInput>(),
        provenance: None,
    };

    let json = serde_json::to_string(&tool_def).unwrap();
    let parsed: ToolDef = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed.name, "test_tool");
    assert_eq!(parsed.description, "A test tool");
    assert_eq!(parsed.input_schema["type"], "object");
    assert_eq!(parsed.input_schema["required"], json!(["arg1"]));
}

#[test]
fn test_tool_def_empty_schema_serialization() {
    #[derive(schemars::JsonSchema)]
    struct EmptyObject {}

    let tool_def = ToolDef {
        name: "empty_tool".into(),
        description: "An empty tool".to_string(),
        input_schema: schema_for::<EmptyObject>(),
        provenance: None,
    };

    let json = serde_json::to_string(&tool_def).unwrap();
    let parsed: ToolDef = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed.name, "empty_tool");
    assert_eq!(parsed.input_schema["type"], "object");
    assert_eq!(parsed.input_schema["required"], json!([]));
}

#[test]
fn test_artifact_ref_serialization() {
    let artifact = ArtifactRef {
        id: "artifact_123".to_string(),
        session_id: SessionId::new(),
        size_bytes: 1024,
        ttl_seconds: Some(3600),
        version: 1,
    };

    let json = serde_json::to_string(&artifact).unwrap();
    let parsed: ArtifactRef = serde_json::from_str(&json).unwrap();

    assert_eq!(parsed.id, "artifact_123");
    assert_eq!(parsed.size_bytes, 1024);
    assert_eq!(parsed.ttl_seconds, Some(3600));
    assert_eq!(parsed.version, 1);

    // Without TTL
    let permanent = ArtifactRef {
        id: "artifact_456".to_string(),
        session_id: SessionId::new(),
        size_bytes: 2048,
        ttl_seconds: None,
        version: 1,
    };

    let json = serde_json::to_value(&permanent).unwrap();
    assert!(!json.as_object().unwrap().contains_key("ttl_seconds"));
}

#[test]
fn test_session_checkpoint_empty() {
    // Empty session should serialize correctly
    let messages: Vec<Message> = vec![];
    let json = serde_json::to_string(&messages).unwrap();
    assert_eq!(json, "[]");

    let parsed: Vec<Message> = serde_json::from_str(&json).unwrap();
    assert!(parsed.is_empty());
}

#[test]
fn test_session_checkpoint_complex() {
    // Complex session with 50+ messages
    let mut messages = Vec::new();

    // Add system message
    messages.push(Message::System(SystemMessage::new(
        "You are a helpful coding assistant.",
    )));

    // Add 25 user/assistant pairs with tool calls
    for i in 0..25 {
        messages.push(Message::User(UserMessage::text(format!("Request {i}"))));

        if i % 3 == 0 {
            // With tool calls
            messages.push(Message::Assistant(AssistantMessage {
                content: format!("Let me help with request {i}"),
                tool_calls: vec![ToolCall::new(
                    format!("tc_{i}"),
                    "test_tool".to_string(),
                    json!({"index": i}),
                )],
                stop_reason: StopReason::ToolUse,
                usage: Usage {
                    input_tokens: 100 + i as u64,
                    output_tokens: 50 + i as u64,
                    cache_creation_tokens: None,
                    cache_read_tokens: None,
                },
                created_at: message_timestamp_now(),
            }));

            messages.push(Message::tool_results(vec![ToolResult::new(
                format!("tc_{i}"),
                format!("Tool result for {i}"),
                false,
            )]));

            messages.push(Message::Assistant(AssistantMessage {
                content: format!("Completed request {i} with tool result"),
                tool_calls: vec![],
                stop_reason: StopReason::EndTurn,
                usage: Usage {
                    input_tokens: 150 + i as u64,
                    output_tokens: 75 + i as u64,
                    cache_creation_tokens: None,
                    cache_read_tokens: None,
                },
                created_at: message_timestamp_now(),
            }));
        } else {
            // Without tool calls
            messages.push(Message::Assistant(AssistantMessage {
                content: format!("Response to request {i}"),
                tool_calls: vec![],
                stop_reason: StopReason::EndTurn,
                usage: Usage {
                    input_tokens: 100 + i as u64,
                    output_tokens: 50 + i as u64,
                    cache_creation_tokens: None,
                    cache_read_tokens: None,
                },
                created_at: message_timestamp_now(),
            }));
        }
    }

    assert!(messages.len() >= 50, "Should have at least 50 messages");

    // Serialize and deserialize
    let json = serde_json::to_string(&messages).unwrap();
    let parsed: Vec<Message> = serde_json::from_str(&json).unwrap();

    assert_eq!(messages.len(), parsed.len());

    // Verify first and last messages
    match &parsed[0] {
        Message::System(s) => assert!(s.content.contains("helpful")),
        _ => panic!("First message should be System"),
    }
}

#[test]
fn test_session_meta_timestamps() {
    use chrono::{DateTime, Utc};

    // Timestamps should be ISO8601 compatible
    let now = std::time::SystemTime::now();
    let datetime: DateTime<Utc> = now.into();
    let iso_string = datetime.to_rfc3339();

    // Should parse back
    let parsed = DateTime::parse_from_rfc3339(&iso_string).unwrap();
    assert_eq!(datetime.timestamp(), parsed.timestamp());
}

#[test]
fn test_output_schema_new() -> Result<(), Box<dyn std::error::Error>> {
    let schema = json!({
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer"}
        },
        "required": ["name", "age"]
    });

    let output_schema = OutputSchema::new(schema.clone())?;

    assert_eq!(output_schema.schema.as_value(), &schema);
    assert!(output_schema.name.is_none());
    assert!(!output_schema.strict);
    Ok(())
}

#[test]
fn test_output_schema_with_name() -> Result<(), Box<dyn std::error::Error>> {
    let schema = json!({"type": "string"});
    let output_schema = OutputSchema::new(schema)?.with_name("my_output");

    assert_eq!(output_schema.name, Some("my_output".to_string()));
    Ok(())
}

#[test]
fn test_output_schema_strict() -> Result<(), Box<dyn std::error::Error>> {
    let schema = json!({"type": "string"});
    let output_schema = OutputSchema::new(schema)?.strict();

    assert!(output_schema.strict);
    Ok(())
}

#[test]
fn test_output_schema_serde_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
    let schema = json!({
        "type": "object",
        "properties": {"field": {"type": "string"}}
    });
    let output_schema = OutputSchema::new(schema.clone())?
        .with_name("test_schema")
        .strict();

    let json = serde_json::to_string(&output_schema).unwrap();
    let parsed: OutputSchema = serde_json::from_str(&json).unwrap();

    let mut expected = schema;
    expected["required"] = json!([]);
    assert_eq!(parsed.schema.as_value(), &expected);
    assert_eq!(parsed.name, Some("test_schema".to_string()));
    assert!(parsed.strict);
    Ok(())
}

#[test]
fn test_output_schema_serde_skip_none_name() -> Result<(), Box<dyn std::error::Error>> {
    let schema = json!({"type": "string"});
    let output_schema = OutputSchema::new(schema)?;

    let json = serde_json::to_value(&output_schema).unwrap();

    // name should be skipped when None
    assert!(json.get("name").is_none() || json.get("name") == Some(&Value::Null));
    Ok(())
}

#[test]
fn test_output_schema_deserialize_wrapper() -> Result<(), Box<dyn std::error::Error>> {
    let input = json!({
        "schema": {"type": "object"},
        "name": "wrapped",
        "strict": true,
        "compat": "lossy",
        "format": "meerkat_v1"
    });
    let parsed: OutputSchema = serde_json::from_value(input)?;
    assert_eq!(parsed.name, Some("wrapped".to_string()));
    assert!(parsed.strict);
    Ok(())
}

#[test]
fn test_output_schema_deserialize_raw() -> Result<(), Box<dyn std::error::Error>> {
    let input = json!({"type": "object"});
    let parsed: OutputSchema = serde_json::from_value(input)?;
    assert!(parsed.name.is_none());
    assert_eq!(parsed.schema.as_value()["type"], "object");
    Ok(())
}

#[test]
fn test_output_schema_deserialize_raw_with_schema_key() -> Result<(), Box<dyn std::error::Error>> {
    let input = json!({
        "type": "object",
        "schema": {"type": "string"},
        "properties": {"value": {"type": "string"}}
    });
    let parsed: OutputSchema = serde_json::from_value(input)?;
    assert!(parsed.name.is_none());
    assert_eq!(parsed.schema.as_value()["type"], "object");
    Ok(())
}

#[test]
fn test_output_schema_from_type() -> Result<(), Box<dyn std::error::Error>> {
    #[derive(Debug, Clone, serde::Serialize, schemars::JsonSchema)]
    struct Payload {
        label: String,
        count: u32,
    }

    let output_schema = OutputSchema::from_type::<Payload>()?;
    let schema = output_schema.schema.as_value();

    assert_eq!(schema["type"], "object");
    assert!(schema["properties"]["label"].is_object());
    assert!(schema["properties"]["count"].is_object());
    Ok(())
}

#[test]
fn test_run_result_with_structured_output() {
    let result = RunResult {
        text: "Here's the structured data".to_string(),
        session_id: SessionId::new(),
        usage: Usage::default(),
        turns: 2,
        tool_calls: 0,
        terminal_cause_kind: None,
        structured_output: Some(json!({"name": "Alice", "age": 30})),
        extraction_error: None,
        schema_warnings: None,
        skill_diagnostics: None,
    };

    let json = serde_json::to_value(&result).unwrap();
    assert_eq!(json["structured_output"]["name"], "Alice");
    assert_eq!(json["structured_output"]["age"], 30);

    // Roundtrip
    let parsed: RunResult = serde_json::from_value(json).unwrap();
    assert!(parsed.structured_output.is_some());
    assert_eq!(parsed.structured_output.unwrap()["name"], "Alice");
}

#[test]
fn test_run_result_without_structured_output_skips_field() {
    let result = RunResult {
        text: "Regular response".to_string(),
        session_id: SessionId::new(),
        usage: Usage::default(),
        turns: 1,
        tool_calls: 0,
        terminal_cause_kind: None,
        structured_output: None,
        extraction_error: None,
        schema_warnings: None,
        skill_diagnostics: None,
    };

    let json = serde_json::to_value(&result).unwrap();

    // structured_output should be skipped when None
    assert!(
        json.get("structured_output").is_none()
            || json.get("structured_output") == Some(&Value::Null)
    );
    assert!(
        json.get("schema_warnings").is_none() || json.get("schema_warnings") == Some(&Value::Null)
    );
}

// ===========================================================================
// New tests for ordered transcript types (spec section 3.1)
// ===========================================================================

mod ordered_transcript_types {
    use super::*;
    use serde_json::value::RawValue;

    // -----------------------------------------------------------------------
    // ProviderMeta serialization round-trip tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_provider_meta_anthropic_roundtrip() {
        let meta = ProviderMeta::Anthropic {
            signature: "opaque_sig_abc123".to_string(),
        };

        let json = serde_json::to_string(&meta).unwrap();
        let parsed: ProviderMeta = serde_json::from_str(&json).unwrap();

        assert_eq!(meta, parsed);
        // Verify tagged enum structure
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["provider"], "anthropic");
        assert_eq!(value["signature"], "opaque_sig_abc123");
    }

    #[test]
    fn test_provider_meta_gemini_roundtrip() {
        let meta = ProviderMeta::Gemini {
            thought_signature: "gemini_thought_xyz".to_string(),
        };

        let json = serde_json::to_string(&meta).unwrap();
        let parsed: ProviderMeta = serde_json::from_str(&json).unwrap();

        assert_eq!(meta, parsed);
        // Verify tagged enum structure with camelCase rename
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["provider"], "gemini");
        assert_eq!(value["thoughtSignature"], "gemini_thought_xyz");
    }

    #[test]
    fn test_provider_meta_openai_roundtrip() {
        let meta = ProviderMeta::OpenAi {
            id: "reasoning_item_123".to_string(),
            encrypted_content: Some("encrypted_tokens_abc".to_string()),
            phase: Some("reasoning".to_string()),
        };

        let json = serde_json::to_string(&meta).unwrap();
        let parsed: ProviderMeta = serde_json::from_str(&json).unwrap();

        assert_eq!(meta, parsed);
        // Verify tagged enum structure
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["provider"], "open_ai");
        assert_eq!(value["id"], "reasoning_item_123");
        assert_eq!(value["encrypted_content"], "encrypted_tokens_abc");
        assert_eq!(value["phase"], "reasoning");
    }

    #[test]
    fn test_provider_meta_openai_without_encrypted_content() {
        let meta = ProviderMeta::OpenAi {
            id: "reasoning_item_456".to_string(),
            encrypted_content: None,
            phase: None,
        };

        let json = serde_json::to_string(&meta).unwrap();
        let value: Value = serde_json::from_str(&json).unwrap();

        // encrypted_content should be skipped when None
        assert!(
            value.get("encrypted_content").is_none()
                || value.get("encrypted_content") == Some(&Value::Null)
        );

        // Roundtrip still works
        let parsed: ProviderMeta = serde_json::from_str(&json).unwrap();
        assert_eq!(meta, parsed);
    }

    // -----------------------------------------------------------------------
    // AssistantBlock serialization round-trip tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_assistant_block_text_roundtrip() {
        let block = AssistantBlock::Text {
            text: "Hello, world!".to_string(),
            meta: None,
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        assert_eq!(block, parsed);
        // Verify adjacently tagged enum structure
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["block_type"], "text");
        assert_eq!(value["data"]["text"], "Hello, world!");
    }

    #[test]
    fn test_assistant_block_text_with_gemini_meta() {
        let block = AssistantBlock::Text {
            text: "Response with signature".to_string(),
            meta: Some(Box::new(ProviderMeta::Gemini {
                thought_signature: "trailing_sig".to_string(),
            })),
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        assert_eq!(block, parsed);
    }

    #[test]
    fn test_assistant_block_reasoning_roundtrip() {
        let block = AssistantBlock::Reasoning {
            text: "Let me think about this...".to_string(),
            meta: Some(Box::new(ProviderMeta::Anthropic {
                signature: "thinking_sig_abc".to_string(),
            })),
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        assert_eq!(block, parsed);
        // Verify adjacently tagged enum structure
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["block_type"], "reasoning");
        assert_eq!(value["data"]["text"], "Let me think about this...");
        assert!(value["data"]["meta"].is_object());
    }

    #[test]
    fn test_assistant_block_reasoning_empty_text() {
        // OpenAI may have empty text with only encrypted_content
        let block = AssistantBlock::Reasoning {
            text: String::new(),
            meta: Some(Box::new(ProviderMeta::OpenAi {
                id: "reasoning_123".to_string(),
                encrypted_content: Some("encrypted".to_string()),
                phase: None,
            })),
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        assert_eq!(block, parsed);
    }

    #[test]
    fn reasoning_with_provider_metadata_counts_as_actionable_output() {
        let blocks = vec![AssistantBlock::Reasoning {
            text: String::new(),
            meta: Some(Box::new(ProviderMeta::OpenAi {
                id: "reasoning_123".to_string(),
                encrypted_content: Some("encrypted".to_string()),
                phase: None,
            })),
        }];

        assert!(
            crate::assistant_blocks_have_visible_or_actionable_output(&blocks),
            "reasoning-only completions with provider continuity metadata are valid output"
        );
    }

    #[test]
    fn empty_reasoning_without_metadata_is_not_actionable_output() {
        let blocks = vec![AssistantBlock::Reasoning {
            text: String::new(),
            meta: None,
        }];

        assert!(
            !crate::assistant_blocks_have_visible_or_actionable_output(&blocks),
            "empty reasoning without continuity metadata is still an empty completion"
        );
    }

    #[test]
    fn test_assistant_block_tool_use_roundtrip() {
        let args = RawValue::from_string(r#"{"path":"/tmp/test.txt"}"#.to_string()).unwrap();
        let block = AssistantBlock::ToolUse {
            id: "tool_123".to_string(),
            name: "read_file".into(),
            args,
            meta: None,
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        // Verify adjacently tagged enum structure
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["block_type"], "tool_use");
        assert_eq!(value["data"]["id"], "tool_123");
        assert_eq!(value["data"]["name"], "read_file");

        // Check args round-trip (compare as JSON values)
        match parsed {
            AssistantBlock::ToolUse { id, name, args, .. } => {
                assert_eq!(id, "tool_123");
                assert_eq!(name, "read_file");
                let args_value: Value = serde_json::from_str(args.get()).unwrap();
                assert_eq!(args_value["path"], "/tmp/test.txt");
            }
            _ => panic!("Expected ToolUse block"),
        }
    }

    fn assert_tool_use_message_roundtrip(
        raw_message: &Message,
        expected_args: &Value,
        expected_id: &str,
    ) {
        match raw_message {
            Message::BlockAssistant(parsed_message) => {
                assert_eq!(parsed_message.blocks.len(), 2);
                assert_eq!(parsed_message.stop_reason, StopReason::ToolUse);
                match &parsed_message.blocks[1] {
                    AssistantBlock::ToolUse { id, name, args, .. } => {
                        assert_eq!(id, expected_id);
                        assert_eq!(name, "read_dir");
                        let args_value: Value = serde_json::from_str(args.get()).unwrap();
                        assert_eq!(&args_value, expected_args);
                    }
                    _ => panic!("Expected ToolUse block"),
                }
            }
            _ => panic!("Expected BlockAssistant message"),
        }
    }

    #[test]
    fn test_assistant_block_tool_use_with_gemini_meta() {
        let args = RawValue::from_string(r#"{"query":"test"}"#.to_string()).unwrap();
        let block = AssistantBlock::ToolUse {
            id: "tool_456".to_string(),
            name: "search".into(),
            args,
            meta: Some(Box::new(ProviderMeta::Gemini {
                thought_signature: "first_parallel_call_sig".to_string(),
            })),
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        match parsed {
            AssistantBlock::ToolUse { meta, .. } => {
                assert!(meta.is_some());
                match *meta.unwrap() {
                    ProviderMeta::Gemini { thought_signature } => {
                        assert_eq!(thought_signature, "first_parallel_call_sig");
                    }
                    _ => panic!("Expected Gemini meta"),
                }
            }
            _ => panic!("Expected ToolUse block"),
        }
    }

    // -----------------------------------------------------------------------
    // AssistantBlock::Transcript variant
    // -----------------------------------------------------------------------

    #[test]
    fn test_assistant_block_transcript_spoken_roundtrip() {
        let block = AssistantBlock::Transcript {
            text: "spoken response".to_string(),
            source: TranscriptSource::Spoken,
            meta: None,
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();

        assert_eq!(block, parsed);
        let value: Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["block_type"], "transcript");
        assert_eq!(value["data"]["text"], "spoken response");
        assert_eq!(value["data"]["source"], "spoken");
    }

    #[test]
    fn test_assistant_block_transcript_with_provider_meta_roundtrip() {
        let block = AssistantBlock::Transcript {
            text: "with meta".to_string(),
            source: TranscriptSource::Spoken,
            meta: Some(Box::new(ProviderMeta::OpenAi {
                id: "item_abc".to_string(),
                encrypted_content: None,
                phase: Some("response".to_string()),
            })),
        };

        let json = serde_json::to_string(&block).unwrap();
        let parsed: AssistantBlock = serde_json::from_str(&json).unwrap();
        assert_eq!(block, parsed);
    }

    #[test]
    fn test_message_block_assistant_mixed_text_transcript_roundtrip() {
        // Mixed Text + Transcript + Text preserves order and lane provenance.
        let message = Message::BlockAssistant(BlockAssistantMessage::new(
            vec![
                AssistantBlock::Text {
                    text: "display first".to_string(),
                    meta: None,
                },
                AssistantBlock::Transcript {
                    text: "spoken middle".to_string(),
                    source: TranscriptSource::Spoken,
                    meta: None,
                },
                AssistantBlock::Text {
                    text: "display last".to_string(),
                    meta: None,
                },
            ],
            StopReason::EndTurn,
        ));

        let json = serde_json::to_string(&message).unwrap();
        let parsed: Message = serde_json::from_str(&json).unwrap();

        match &parsed {
            Message::BlockAssistant(parsed_message) => {
                assert_eq!(parsed_message.blocks.len(), 3);
                assert!(matches!(
                    &parsed_message.blocks[0],
                    AssistantBlock::Text { text, .. } if text == "display first"
                ));
                match &parsed_message.blocks[1] {
                    AssistantBlock::Transcript { text, source, .. } => {
                        assert_eq!(text, "spoken middle");
                        assert_eq!(*source, TranscriptSource::Spoken);
                    }
                    other => panic!("expected Transcript block, got {other:?}"),
                }
                assert!(matches!(
                    &parsed_message.blocks[2],
                    AssistantBlock::Text { text, .. } if text == "display last"
                ));
                // Display projects both lanes to the same text stream.
                assert_eq!(
                    parsed_message.to_string(),
                    "display firstspoken middledisplay last"
                );
                // text_blocks() iterator includes Transcript text as well.
                let collected: Vec<&str> = parsed_message.text_blocks().collect();
                assert_eq!(
                    collected,
                    vec!["display first", "spoken middle", "display last"]
                );
            }
            other => panic!("expected BlockAssistant, got {other:?}"),
        }
    }

    #[test]
    fn test_transcript_source_roundtrip_snake_case() {
        let source = TranscriptSource::Spoken;
        let json = serde_json::to_string(&source).unwrap();
        assert_eq!(json, "\"spoken\"");
        let parsed: TranscriptSource = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, TranscriptSource::Spoken);
    }

    #[test]
    fn test_message_block_assistant_tool_use_roundtrip() {
        for (case_id, args_json) in [
            ("tool_001", r#"{"path":"/tmp","recursive":false}"#),
            ("tool_002", r"{}"),
            ("tool_003", r#"{"config":{"nested":true}}"#),
            ("tool_004", r"[1,2,3]"),
            ("tool_005", r#""line1\nline2\\\"quoted\\\"""#),
        ] {
            let expected_args: Value = serde_json::from_str(args_json).unwrap();
            let message = Message::BlockAssistant(BlockAssistantMessage::new(
                vec![
                    AssistantBlock::Text {
                        text: "Checking files".to_string(),
                        meta: None,
                    },
                    AssistantBlock::ToolUse {
                        id: case_id.to_string(),
                        name: "read_dir".into(),
                        args: RawValue::from_string(args_json.to_string()).unwrap(),
                        meta: None,
                    },
                ],
                StopReason::ToolUse,
            ));

            let json = serde_json::to_string(&message).unwrap();
            let parsed: Message = serde_json::from_str(&json).unwrap();
            assert_tool_use_message_roundtrip(&parsed, &expected_args, case_id);

            let persisted_json = serde_json::json!({
                "role": "block_assistant",
                "blocks": [
                    {
                        "block_type": "text",
                        "data": {"text": "Checking files"}
                    },
                    {
                        "block_type": "tool_use",
                        "data": {
                            "id": case_id,
                            "name": "read_dir",
                            "args": expected_args
                        }
                    }
                ],
                "stop_reason": "tool_use"
            });
            let persisted_roundtrip: Message = serde_json::from_value(persisted_json).unwrap();
            assert_tool_use_message_roundtrip(&persisted_roundtrip, &expected_args, case_id);
        }
    }

    // -----------------------------------------------------------------------
    // ToolCallView::parse_args tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_tool_call_view_parse_args() {
        #[derive(Debug, serde::Deserialize, PartialEq)]
        struct ReadFileArgs {
            path: String,
            #[serde(default)]
            encoding: Option<String>,
        }

        let args =
            RawValue::from_string(r#"{"path":"/tmp/test.txt","encoding":"utf-8"}"#.to_string())
                .unwrap();
        let view = ToolCallView {
            id: "tc_123",
            name: "read_file",
            args: &args,
        };

        let parsed: ReadFileArgs = view.parse_args().unwrap();
        assert_eq!(parsed.path, "/tmp/test.txt");
        assert_eq!(parsed.encoding, Some("utf-8".to_string()));
    }

    #[test]
    fn test_tool_call_view_parse_args_error() {
        #[derive(Debug, serde::Deserialize)]
        struct ExpectedArgs {
            #[allow(dead_code)]
            required_field: String,
        }

        let args = RawValue::from_string(r#"{"wrong_field":"value"}"#.to_string()).unwrap();
        let view = ToolCallView {
            id: "tc_456",
            name: "some_tool",
            args: &args,
        };

        let result: Result<ExpectedArgs, _> = view.parse_args();
        assert!(result.is_err());
    }

    // -----------------------------------------------------------------------
    // AssistantMessage::tool_calls() iterator tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_assistant_message_tool_calls_iterator() {
        let args1 = RawValue::from_string(r#"{"path":"/a"}"#.to_string()).unwrap();
        let args2 = RawValue::from_string(r#"{"query":"test"}"#.to_string()).unwrap();

        let msg = BlockAssistantMessage {
            blocks: vec![
                AssistantBlock::Text {
                    text: "Let me help".to_string(),
                    meta: None,
                },
                AssistantBlock::ToolUse {
                    id: "tc_1".to_string(),
                    name: "read_file".into(),
                    args: args1,
                    meta: None,
                },
                AssistantBlock::Reasoning {
                    text: "thinking...".to_string(),
                    meta: None,
                },
                AssistantBlock::ToolUse {
                    id: "tc_2".to_string(),
                    name: "search".into(),
                    args: args2,
                    meta: None,
                },
            ],
            stop_reason: StopReason::ToolUse,
            created_at: message_timestamp_now(),
        };

        let tool_calls: Vec<_> = msg.tool_calls().collect();
        assert_eq!(tool_calls.len(), 2);
        assert_eq!(tool_calls[0].id, "tc_1");
        assert_eq!(tool_calls[0].name, "read_file");
        assert_eq!(tool_calls[1].id, "tc_2");
        assert_eq!(tool_calls[1].name, "search");
    }

    #[test]
    fn test_assistant_message_tool_calls_empty() {
        let msg = BlockAssistantMessage {
            blocks: vec![AssistantBlock::Text {
                text: "No tools needed".to_string(),
                meta: None,
            }],
            stop_reason: StopReason::EndTurn,
            created_at: message_timestamp_now(),
        };

        let tool_calls: Vec<_> = msg.tool_calls().collect();
        assert!(tool_calls.is_empty());
    }

    #[test]
    fn test_assistant_message_has_tool_calls() {
        let args = RawValue::from_string(r"{}".to_string()).unwrap();

        let msg_with_tools = BlockAssistantMessage {
            blocks: vec![AssistantBlock::ToolUse {
                id: "tc_1".to_string(),
                name: "test".into(),
                args,
                meta: None,
            }],
            stop_reason: StopReason::ToolUse,
            created_at: message_timestamp_now(),
        };
        assert!(msg_with_tools.has_tool_calls());

        let msg_without_tools = BlockAssistantMessage {
            blocks: vec![AssistantBlock::Text {
                text: "Hello".to_string(),
                meta: None,
            }],
            stop_reason: StopReason::EndTurn,
            created_at: message_timestamp_now(),
        };
        assert!(!msg_without_tools.has_tool_calls());
    }

    #[test]
    fn test_assistant_message_get_tool_use() {
        let args1 = RawValue::from_string(r#"{"a":1}"#.to_string()).unwrap();
        let args2 = RawValue::from_string(r#"{"b":2}"#.to_string()).unwrap();

        let msg = BlockAssistantMessage {
            blocks: vec![
                AssistantBlock::ToolUse {
                    id: "tc_first".to_string(),
                    name: "tool_a".into(),
                    args: args1,
                    meta: None,
                },
                AssistantBlock::ToolUse {
                    id: "tc_second".to_string(),
                    name: "tool_b".into(),
                    args: args2,
                    meta: None,
                },
            ],
            stop_reason: StopReason::ToolUse,
            created_at: message_timestamp_now(),
        };

        let found = msg.get_tool_use("tc_second");
        assert!(found.is_some());
        let view = found.unwrap();
        assert_eq!(view.name, "tool_b");

        let not_found = msg.get_tool_use("tc_nonexistent");
        assert!(not_found.is_none());
    }

    // -----------------------------------------------------------------------
    // AssistantMessage Display impl tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_assistant_message_display() {
        let args = RawValue::from_string(r"{}".to_string()).unwrap();

        let msg = BlockAssistantMessage {
            blocks: vec![
                AssistantBlock::Text {
                    text: "First part. ".to_string(),
                    meta: None,
                },
                AssistantBlock::Reasoning {
                    text: "This should not appear".to_string(),
                    meta: None,
                },
                AssistantBlock::Text {
                    text: "Second part.".to_string(),
                    meta: None,
                },
                AssistantBlock::ToolUse {
                    id: "tc_1".to_string(),
                    name: "test".into(),
                    args,
                    meta: None,
                },
            ],
            stop_reason: StopReason::ToolUse,
            created_at: message_timestamp_now(),
        };

        let display = format!("{msg}");
        assert_eq!(display, "First part. Second part.");
    }

    #[test]
    fn test_assistant_message_display_empty() {
        let msg = BlockAssistantMessage {
            blocks: vec![],
            stop_reason: StopReason::EndTurn,
            created_at: message_timestamp_now(),
        };

        let display = format!("{msg}");
        assert_eq!(display, "");
    }

    // -----------------------------------------------------------------------
    // AssistantMessage text_blocks iterator tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_assistant_message_text_blocks() {
        let args = RawValue::from_string(r"{}".to_string()).unwrap();

        let msg = BlockAssistantMessage {
            blocks: vec![
                AssistantBlock::Text {
                    text: "Hello".to_string(),
                    meta: None,
                },
                AssistantBlock::Reasoning {
                    text: "thinking".to_string(),
                    meta: None,
                },
                AssistantBlock::Text {
                    text: "World".to_string(),
                    meta: None,
                },
                AssistantBlock::ToolUse {
                    id: "tc_1".to_string(),
                    name: "test".into(),
                    args,
                    meta: None,
                },
            ],
            stop_reason: StopReason::EndTurn,
            created_at: message_timestamp_now(),
        };

        let text_blocks: Vec<_> = msg.text_blocks().collect();
        assert_eq!(text_blocks.len(), 2);
        assert_eq!(text_blocks[0], "Hello");
        assert_eq!(text_blocks[1], "World");
    }
}

// ===========================================================================
// Multimodal ContentBlock + ContentInput tests
// ===========================================================================

mod content_block_tests {
    use super::*;

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

    #[test]
    fn content_block_image_roundtrip() {
        let block = ContentBlock::Image {
            media_type: "image/png".to_string(),
            data: "iVBOR".into(),
        };
        let json = serde_json::to_value(&block).unwrap();
        assert_eq!(json["type"], "image");
        assert_eq!(json["media_type"], "image/png");
        assert_eq!(json["source"], "inline");
        let parsed: ContentBlock = serde_json::from_value(json).unwrap();
        assert_eq!(parsed, block);
    }

    #[test]
    fn image_block_serializes_inline_source_discriminator() {
        let block = ContentBlock::Image {
            media_type: "image/png".to_string(),
            data: "abc".into(),
        };
        let json = serde_json::to_string(&block).unwrap();
        assert!(
            json.contains("\"source\":\"inline\""),
            "inline discriminator missing: {json}"
        );
    }

    #[test]
    fn image_block_requires_source_discriminator() {
        let json = r#"{"type":"image","media_type":"image/png","data":"abc"}"#;
        assert!(serde_json::from_str::<ContentBlock>(json).is_err());
    }

    #[test]
    fn content_block_image_roundtrips_blob_source() {
        let block = ContentBlock::Image {
            media_type: "image/jpeg".to_string(),
            data: crate::types::ImageData::Blob {
                blob_id: crate::BlobId::new("sha256:abc"),
            },
        };
        let json = serde_json::to_value(&block).unwrap();
        assert_eq!(json["source"], "blob");
        assert_eq!(json["blob_id"], "sha256:abc");
        let parsed: ContentBlock = serde_json::from_value(json).unwrap();
        assert_eq!(parsed, block);
    }

    #[test]
    fn content_block_video_roundtrip() {
        let block = ContentBlock::Video {
            media_type: "video/mp4".to_string(),
            duration_ms: 12_000,
            data: crate::types::VideoData::Inline {
                data: "AAA".to_string(),
            },
        };
        let json = serde_json::to_value(&block).unwrap();
        assert_eq!(json["type"], "video");
        assert_eq!(json["media_type"], "video/mp4");
        assert_eq!(json["duration_ms"], 12000);
        assert_eq!(json["source"], "inline");
        let parsed: ContentBlock = serde_json::from_value(json).unwrap();
        assert_eq!(parsed, block);
    }

    #[test]
    fn text_projection_text_returns_text() {
        let block = ContentBlock::Text {
            text: "hello".to_string(),
        };
        assert_eq!(block.text_projection().as_ref(), "hello");
    }

    #[test]
    fn text_projection_image_excludes_source_path() {
        let block = ContentBlock::Image {
            media_type: "image/png".to_string(),
            data: "d".into(),
        };
        assert_eq!(block.text_projection().as_ref(), "[image: image/png]");
    }

    #[test]
    fn text_projection_video_returns_placeholder() {
        let block = ContentBlock::Video {
            media_type: "video/mp4".to_string(),
            duration_ms: 12_000,
            data: crate::types::VideoData::Inline {
                data: "d".to_string(),
            },
        };
        assert_eq!(block.text_projection().as_ref(), "[video: video/mp4]");
    }

    #[test]
    fn content_block_video_requires_duration_ms() {
        let json = r#"{"type":"video","media_type":"video/mp4","source":"inline","data":"abc"}"#;
        assert!(serde_json::from_str::<ContentBlock>(json).is_err());
    }

    #[test]
    fn tool_result_deserialize_legacy_string() {
        let json = json!({"tool_use_id": "tc_1", "content": "hello", "is_error": false});
        let result: ToolResult = serde_json::from_value(json).unwrap();
        assert_eq!(result.content.len(), 1);
        assert!(matches!(&result.content[0], ContentBlock::Text { text } if text == "hello"));
    }

    #[test]
    fn tool_result_deserialize_array() {
        let json = json!({
            "tool_use_id": "tc_1",
            "content": [{"type": "text", "text": "desc"}, {"type": "image", "media_type": "image/png", "source": "inline", "data": "abc"}],
            "is_error": false
        });
        let result: ToolResult = serde_json::from_value(json).unwrap();
        assert_eq!(result.content.len(), 2);
    }

    #[test]
    fn tool_result_serialize_text_only_string() {
        let result = ToolResult::new("tc_1".to_string(), "hello".to_string(), false);
        let json = serde_json::to_value(&result).unwrap();
        assert_eq!(json["content"], "hello");
    }

    #[test]
    fn tool_result_serialize_with_image_array() {
        let result = ToolResult::with_blocks(
            "tc_1".to_string(),
            vec![
                ContentBlock::Text {
                    text: "desc".to_string(),
                },
                ContentBlock::Image {
                    media_type: "image/png".to_string(),
                    data: "abc".into(),
                },
            ],
            false,
        );
        let json = serde_json::to_value(&result).unwrap();
        assert!(json["content"].is_array());
    }

    #[test]
    fn tool_result_text_content_concatenates() {
        let result = ToolResult::with_blocks(
            "tc_1".to_string(),
            vec![
                ContentBlock::Text {
                    text: "line1".to_string(),
                },
                ContentBlock::Image {
                    media_type: "image/png".to_string(),
                    data: "d".into(),
                },
                ContentBlock::Text {
                    text: "line2".to_string(),
                },
            ],
            false,
        );
        assert_eq!(result.text_content(), "line1\n[image: image/png]\nline2");
    }

    #[test]
    fn tool_result_has_images() {
        let with = ToolResult::with_blocks(
            "t".into(),
            vec![ContentBlock::Image {
                media_type: "image/png".into(),
                data: "d".into(),
            }],
            false,
        );
        assert!(with.has_images());
        let without = ToolResult::new("t".into(), "text".into(), false);
        assert!(!without.has_images());
    }

    #[test]
    fn tool_result_has_video() {
        let with = ToolResult::with_blocks(
            "t".into(),
            vec![ContentBlock::Video {
                media_type: "video/mp4".into(),
                duration_ms: 12_000,
                data: crate::types::VideoData::Inline {
                    data: "abc".to_string(),
                },
            }],
            false,
        );
        assert!(with.has_video());
        let without = ToolResult::new("t".into(), "text".into(), false);
        assert!(!without.has_video());
    }

    #[test]
    fn user_message_deserialize_legacy_string() {
        let json = json!({"role": "user", "content": "hello"});
        let msg: Message = serde_json::from_value(json).unwrap();
        match msg {
            Message::User(u) => assert_eq!(u.text_content(), "hello"),
            _ => panic!("Expected User"),
        }
    }

    #[test]
    fn user_message_serialize_text_only_string() {
        let msg = UserMessage::text("hello");
        let json = serde_json::to_value(&msg).unwrap();
        assert_eq!(json["content"], "hello");
    }

    #[test]
    fn user_message_serialize_with_image_array() {
        let msg = UserMessage::with_blocks(vec![
            ContentBlock::Text {
                text: "look".to_string(),
            },
            ContentBlock::Image {
                media_type: "image/png".into(),
                data: "d".into(),
            },
        ]);
        let json = serde_json::to_value(&msg).unwrap();
        assert!(json["content"].is_array());
    }

    #[test]
    fn indexable_text_user_with_image_uses_projection() {
        let msg = Message::User(UserMessage::with_blocks(vec![
            ContentBlock::Text {
                text: "describe this".to_string(),
            },
            ContentBlock::Image {
                media_type: "image/png".into(),
                data: "d".into(),
            },
        ]));
        let text = msg.as_indexable_text();
        assert!(text.contains("describe this"));
        assert!(text.contains("[image: image/png]"));
    }

    // ContentInput tests
    #[test]
    fn content_input_from_string() {
        let input = ContentInput::from("hello".to_string());
        assert_eq!(input.text_content(), "hello");
        assert!(!input.has_images());
        assert!(!input.has_video());
        assert!(!input.has_non_text_content());
    }

    #[test]
    fn content_input_from_blocks() {
        let input = ContentInput::Blocks(vec![
            ContentBlock::Text {
                text: "desc".to_string(),
            },
            ContentBlock::Image {
                media_type: "image/png".into(),
                data: "d".into(),
            },
        ]);
        assert!(input.has_images());
        assert!(input.has_non_text_content());
        assert!(input.text_content().contains("[image: image/png]"));
    }

    #[test]
    fn content_input_video_helpers_work() {
        let input = ContentInput::Blocks(vec![
            ContentBlock::Text {
                text: "watch".to_string(),
            },
            ContentBlock::Video {
                media_type: "video/mp4".into(),
                duration_ms: 12_000,
                data: crate::types::VideoData::Inline {
                    data: "abc".to_string(),
                },
            },
        ]);
        assert!(input.has_video());
        assert!(input.has_non_text_content());
        assert!(input.text_content().contains("[video: video/mp4]"));
    }

    #[test]
    fn content_input_deserialize_string() {
        let input: ContentInput = serde_json::from_value(json!("hello")).unwrap();
        assert_eq!(input.text_content(), "hello");
    }

    #[test]
    fn content_input_deserialize_array() {
        let input: ContentInput = serde_json::from_value(json!([
            {"type": "text", "text": "look"},
            {"type": "image", "media_type": "image/png", "source": "inline", "data": "abc"}
        ]))
        .unwrap();
        assert!(input.has_images());
    }

    #[test]
    fn content_input_serialize_text_as_string() {
        let input = ContentInput::Text("hello".to_string());
        let json = serde_json::to_value(&input).unwrap();
        assert_eq!(json, "hello");
    }

    #[test]
    fn tool_result_serialize_multi_text_blocks_as_array() {
        let result = ToolResult::with_blocks(
            "tc_1".into(),
            vec![
                ContentBlock::Text {
                    text: "line1".into(),
                },
                ContentBlock::Text {
                    text: "line2".into(),
                },
            ],
            false,
        );
        let json = serde_json::to_value(&result).unwrap();
        // Multiple text blocks should serialize as array, not collapsed string
        assert!(json["content"].is_array());
        assert_eq!(json["content"].as_array().unwrap().len(), 2);

        // Round-trip should preserve both blocks
        let parsed: ToolResult = serde_json::from_value(json).unwrap();
        assert_eq!(parsed.content.len(), 2);
        assert!(matches!(&parsed.content[0], ContentBlock::Text { text } if text == "line1"));
        assert!(matches!(&parsed.content[1], ContentBlock::Text { text } if text == "line2"));
    }

    #[test]
    fn content_block_display_text() {
        let block = ContentBlock::Text {
            text: "hello world".to_string(),
        };
        assert_eq!(format!("{block}"), "hello world");
    }

    #[test]
    fn content_block_display_image() {
        let block = ContentBlock::Image {
            media_type: "image/png".to_string(),
            data: "abc".into(),
        };
        assert_eq!(format!("{block}"), "[image: image/png]");
    }

    #[test]
    fn content_block_display_video() {
        let block = ContentBlock::Video {
            media_type: "video/mp4".to_string(),
            duration_ms: 12_000,
            data: crate::types::VideoData::Inline {
                data: "abc".to_string(),
            },
        };
        assert_eq!(format!("{block}"), "[video: video/mp4]");
    }

    // -----------------------------------------------------------------------
    // ToolProvenance / ToolDef provenance
    // -----------------------------------------------------------------------

    #[test]
    fn tool_name_and_set_keep_string_wire_shape() {
        use super::{ToolName, ToolNameSet};

        let name = ToolName::new("read_file");
        assert_eq!(serde_json::to_string(&name).unwrap(), "\"read_file\"");
        let parsed: ToolName = serde_json::from_str("\"read_file\"").unwrap();
        assert_eq!(parsed.as_str(), "read_file");

        let names: ToolNameSet = ["read_file", "shell"].into_iter().collect();
        let value = serde_json::to_value(&names).unwrap();
        let array = value
            .as_array()
            .expect("tool name set remains array-shaped");
        assert_eq!(array.len(), 2);
        assert!(array.contains(&serde_json::json!("read_file")));
        assert!(array.contains(&serde_json::json!("shell")));

        let parsed: ToolNameSet = serde_json::from_value(value).unwrap();
        assert!(parsed.contains("read_file"));
        assert!(parsed.contains("shell"));
    }

    #[test]
    fn tool_provenance_roundtrip_all_kinds() {
        use super::{ToolProvenance, ToolSourceKind};

        let kinds = [
            ToolSourceKind::Builtin,
            ToolSourceKind::Shell,
            ToolSourceKind::Comms,
            ToolSourceKind::Memory,
            ToolSourceKind::Schedule,
            ToolSourceKind::Mob,
            ToolSourceKind::Callback,
            ToolSourceKind::Mcp,
            ToolSourceKind::RustBundle,
        ];
        for kind in kinds {
            let prov = ToolProvenance {
                kind: kind.clone(),
                source_id: "test-source".into(),
            };
            let json = serde_json::to_string(&prov).unwrap();
            let parsed: ToolProvenance = serde_json::from_str(&json).unwrap();
            assert_eq!(parsed, prov);
        }
    }

    #[test]
    fn tool_def_without_provenance_backward_compat() {
        // Existing JSON without provenance field should deserialize fine
        let json = r#"{"name":"test","description":"a tool","input_schema":{"type":"object"}}"#;
        let def: ToolDef = serde_json::from_str(json).unwrap();
        assert_eq!(def.name, "test");
        assert!(def.provenance.is_none());
    }

    #[test]
    fn tool_def_with_provenance_roundtrip() {
        use super::{ToolProvenance, ToolSourceKind};

        let def = ToolDef::new("my_tool", "does things", json!({"type": "object"}))
            .with_provenance(ToolProvenance {
                kind: ToolSourceKind::Mcp,
                source_id: "code-server".into(),
            });

        let json = serde_json::to_string(&def).unwrap();
        let parsed: ToolDef = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.name, "my_tool");
        let prov = parsed.provenance.unwrap();
        assert_eq!(prov.kind, ToolSourceKind::Mcp);
        assert_eq!(prov.source_id, "code-server");
    }

    #[test]
    fn tool_def_without_provenance_skips_serialization() {
        let def = ToolDef::new("t", "d", json!({}));
        let json = serde_json::to_string(&def).unwrap();
        assert!(!json.contains("provenance"));
    }

    #[test]
    fn tool_def_new_constructor() {
        let def = ToolDef::new("name", "desc", json!({"type": "object"}));
        assert_eq!(def.name, "name");
        assert_eq!(def.description, "desc");
        assert!(def.provenance.is_none());
    }
}
