/// Comprehensive edge case tests for SessionUpdate variants
use dirigent_protocol::types::{
    ContentBlock, Meta, SessionUpdate, ToolCall, ToolCallContent, ToolCallStatus,
};
use serde_json::json;

// ===== UserMessageChunk Tests =====

/// Test UserMessageChunk minimal (no _meta)
#[test]
fn test_user_message_chunk_minimal() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: "msg_001".to_string(),
        content: ContentBlock::Text {
            text: "Hello".to_string(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"user_message_chunk"#));
    assert!(json.contains(r#""message_id":"msg_001"#));
    assert!(!json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test UserMessageChunk with empty message_id
#[test]
fn test_user_message_chunk_empty_message_id() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: String::new(),
        content: ContentBlock::Text {
            text: "Test".to_string(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""message_id":"""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    match deserialized {
        SessionUpdate::UserMessageChunk { message_id, .. } => {
            assert_eq!(message_id, "");
        }
        _ => panic!("Expected UserMessageChunk"),
    }
}

/// Test UserMessageChunk with ResourceLink content
#[test]
fn test_user_message_chunk_with_resource_link() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: "msg_002".to_string(),
        content: ContentBlock::ResourceLink {
            uri: "file:///path/to/file.txt".to_string(),
            name: Some("file.txt".to_string()),
            mime_type: Some("text/plain".to_string()),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"user_message_chunk"#));
    assert!(json.contains(r#""type":"resource_link"#)); // nested type
    assert!(json.contains(r#""uri":"file:///path/to/file.txt"#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test UserMessageChunk with _meta
#[test]
fn test_user_message_chunk_with_meta() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: "msg_003".to_string(),
        content: ContentBlock::Text {
            text: "Test".to_string(),
        },
        _meta: Some(Meta::default()),
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""_meta":{}"#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

// ===== AgentMessageChunk Tests =====

/// Test AgentMessageChunk minimal
#[test]
fn test_agent_message_chunk_minimal() {
    let update = SessionUpdate::AgentMessageChunk {
        message_id: "msg_agent_001".to_string(),
        content: ContentBlock::Text {
            text: "Agent response".to_string(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"agent_message_chunk"#));
    assert!(json.contains(r#""message_id":"msg_agent_001"#));
    assert!(!json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test AgentMessageChunk with empty message_id
#[test]
fn test_agent_message_chunk_empty_message_id() {
    let update = SessionUpdate::AgentMessageChunk {
        message_id: String::new(),
        content: ContentBlock::Text {
            text: "Response".to_string(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();

    match deserialized {
        SessionUpdate::AgentMessageChunk { message_id, .. } => {
            assert_eq!(message_id, "");
        }
        _ => panic!("Expected AgentMessageChunk"),
    }
}

/// Test AgentMessageChunk with complex meta
#[test]
fn test_agent_message_chunk_with_complex_meta() {
    let mut extra = std::collections::HashMap::new();
    extra.insert("timestamp".to_string(), json!("2025-11-10T12:00:00Z"));
    extra.insert("duration_ms".to_string(), json!(123));

    let update = SessionUpdate::AgentMessageChunk {
        message_id: "msg_agent_002".to_string(),
        content: ContentBlock::Text {
            text: "Response".to_string(),
        },
        _meta: Some(Meta {
            provider: None,
            extra,
        }),
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""_meta""#));
    assert!(json.contains(r#""timestamp""#));
    assert!(json.contains(r#""duration_ms""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

// ===== AgentThoughtChunk Tests =====

/// Test AgentThoughtChunk minimal
#[test]
fn test_agent_thought_chunk_minimal() {
    let update = SessionUpdate::AgentThoughtChunk {
        message_id: "msg_thought_001".to_string(),
        content: ContentBlock::Text {
            text: "Thinking...".to_string(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"agent_thought_chunk"#));
    assert!(json.contains(r#""message_id":"msg_thought_001"#));
    assert!(!json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test AgentThoughtChunk with empty text
#[test]
fn test_agent_thought_chunk_empty_text() {
    let update = SessionUpdate::AgentThoughtChunk {
        message_id: "msg_thought_002".to_string(),
        content: ContentBlock::Text {
            text: String::new(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""text":"""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test AgentThoughtChunk with very long text
#[test]
fn test_agent_thought_chunk_long_text() {
    let long_text = "Analyzing the problem...\n".repeat(1000);
    let update = SessionUpdate::AgentThoughtChunk {
        message_id: "msg_thought_003".to_string(),
        content: ContentBlock::Text {
            text: long_text.clone(),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();

    match deserialized {
        SessionUpdate::AgentThoughtChunk { content, .. } => {
            if let ContentBlock::Text { text } = content {
                assert_eq!(text.len(), long_text.len());
            } else {
                panic!("Expected Text content");
            }
        }
        _ => panic!("Expected AgentThoughtChunk"),
    }
}

// ===== ToolCall Tests =====

/// Test ToolCall variant minimal
#[test]
fn test_tool_call_variant_minimal() {
    let tool_call = ToolCall {
        id: "call_001".to_string(),
        tool_name: "bash".to_string(),
        status: ToolCallStatus::Pending,
        content: vec![],
        raw_input: None,
        raw_output: None,
        title: None,
        error: None,
        metadata: None,
        origin: None,
    };

    let update = SessionUpdate::ToolCall {
        message_id: "msg_tool_001".to_string(),
        tool_call,
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"tool_call"#));
    assert!(json.contains(r#""message_id":"msg_tool_001"#));
    assert!(json.contains(r#""tool_call""#));
    assert!(!json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test ToolCall variant with complex nested ToolCall
#[test]
fn test_tool_call_variant_complex() {
    let tool_call = ToolCall {
        id: "call_002".to_string(),
        tool_name: "read_file".to_string(),
        status: ToolCallStatus::Completed,
        content: vec![
            ToolCallContent::from_content_block(ContentBlock::Text {
                text: "Line 1".to_string(),
            }),
            ToolCallContent::from_content_block(ContentBlock::Text {
                text: "Line 2".to_string(),
            }),
        ],
        raw_input: Some(json!({"path": "/tmp/test.txt"})),
        raw_output: Some(json!({"bytes": 1024})),
        title: Some("Read file".to_string()),
        error: None,
        metadata: Some(json!({"duration_ms": 42})),
        origin: None,
    };

    let update = SessionUpdate::ToolCall {
        message_id: "msg_tool_002".to_string(),
        tool_call,
        _meta: Some(Meta::default()),
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"tool_call"#));
    assert!(json.contains(r#""tool_call""#));
    assert!(json.contains(r#""raw_input""#));
    assert!(json.contains(r#""raw_output""#));
    assert!(json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test ToolCall variant with Error status
#[test]
fn test_tool_call_variant_with_error() {
    let tool_call = ToolCall {
        id: "call_003".to_string(),
        tool_name: "bash".to_string(),
        status: ToolCallStatus::Error,
        content: vec![],
        raw_input: Some(json!({"command": "invalid"})),
        raw_output: None,
        title: Some("Failed command".to_string()),
        error: Some("Command not found".to_string()),
        metadata: None,
        origin: None,
    };

    let update = SessionUpdate::ToolCall {
        message_id: "msg_tool_003".to_string(),
        tool_call,
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""status":"failed""#));
    assert!(json.contains(r#""error":"Command not found""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

// ===== ToolCallUpdate Tests =====

/// Test ToolCallUpdate variant minimal
#[test]
fn test_tool_call_update_minimal() {
    let tool_call = ToolCall {
        id: "call_004".to_string(),
        tool_name: "bash".to_string(),
        status: ToolCallStatus::Running,
        content: vec![],
        raw_input: None,
        raw_output: None,
        title: None,
        error: None,
        metadata: None,
        origin: None,
    };

    let update = SessionUpdate::ToolCallUpdate {
        message_id: "msg_update_001".to_string(),
        tool_call_id: "call_004".to_string(),
        tool_call,
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""type":"tool_call_update"#));
    assert!(json.contains(r#""message_id":"msg_update_001"#));
    assert!(json.contains(r#""tool_call_id":"call_004"#));
    assert!(json.contains(r#""tool_call""#));
    assert!(!json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test ToolCallUpdate with mismatched IDs
#[test]
fn test_tool_call_update_mismatched_ids() {
    // This is technically allowed by the type system, though semantically odd
    let tool_call = ToolCall {
        id: "call_005".to_string(),
        tool_name: "test".to_string(),
        status: ToolCallStatus::Running,
        content: vec![],
        raw_input: None,
        raw_output: None,
        title: None,
        error: None,
        metadata: None,
        origin: None,
    };

    let update = SessionUpdate::ToolCallUpdate {
        message_id: "msg_update_002".to_string(),
        tool_call_id: "call_DIFFERENT".to_string(), // Different from tool_call.id
        tool_call,
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""tool_call_id":"call_DIFFERENT"#));
    assert!(json.contains(r#""id":"call_005"#)); // nested id

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

/// Test ToolCallUpdate with completed status
#[test]
fn test_tool_call_update_completed() {
    let tool_call = ToolCall {
        id: "call_006".to_string(),
        tool_name: "read".to_string(),
        status: ToolCallStatus::Completed,
        content: vec![ToolCallContent::from_content_block(ContentBlock::Text {
            text: "File contents".to_string(),
        })],
        raw_input: Some(json!({"path": "/tmp/file"})),
        raw_output: Some(json!({"success": true})),
        title: Some("Read operation".to_string()),
        error: None,
        metadata: Some(json!({"lines": 42})),
        origin: None,
    };

    let update = SessionUpdate::ToolCallUpdate {
        message_id: "msg_update_003".to_string(),
        tool_call_id: "call_006".to_string(),
        tool_call,
        _meta: Some(Meta::default()),
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""status":"completed""#));
    assert!(json.contains(r#""raw_output""#));
    assert!(json.contains(r#""_meta""#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

// ===== Type Tag Tests =====

/// Test all variants have correct snake_case type tags
#[test]
fn test_all_type_tags_snake_case() {
    let user_chunk = SessionUpdate::UserMessageChunk {
        message_id: "m1".to_string(),
        content: ContentBlock::Text {
            text: "test".to_string(),
        },
        _meta: None,
    };
    let json = serde_json::to_string(&user_chunk).unwrap();
    assert!(json.contains(r#""type":"user_message_chunk"#));
    assert!(!json.contains(r#""type":"UserMessageChunk"#));

    let agent_chunk = SessionUpdate::AgentMessageChunk {
        message_id: "m2".to_string(),
        content: ContentBlock::Text {
            text: "test".to_string(),
        },
        _meta: None,
    };
    let json = serde_json::to_string(&agent_chunk).unwrap();
    assert!(json.contains(r#""type":"agent_message_chunk"#));

    let thought_chunk = SessionUpdate::AgentThoughtChunk {
        message_id: "m3".to_string(),
        content: ContentBlock::Text {
            text: "test".to_string(),
        },
        _meta: None,
    };
    let json = serde_json::to_string(&thought_chunk).unwrap();
    assert!(json.contains(r#""type":"agent_thought_chunk"#));

    let tool_call = SessionUpdate::ToolCall {
        message_id: "m4".to_string(),
        tool_call: ToolCall {
            id: "c1".to_string(),
            tool_name: "test".to_string(),
            status: ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        },
        _meta: None,
    };
    let json = serde_json::to_string(&tool_call).unwrap();
    assert!(json.contains(r#""type":"tool_call"#));
    assert!(!json.contains(r#""type":"ToolCall"#));

    let tool_call_update = SessionUpdate::ToolCallUpdate {
        message_id: "m5".to_string(),
        tool_call_id: "c2".to_string(),
        tool_call: ToolCall {
            id: "c2".to_string(),
            tool_name: "test".to_string(),
            status: ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        },
        _meta: None,
    };
    let json = serde_json::to_string(&tool_call_update).unwrap();
    assert!(json.contains(r#""type":"tool_call_update"#));
}

// ===== Deserialization Error Cases =====

/// Test missing type field
#[test]
fn test_missing_type_field() {
    let json = r#"{
        "message_id": "msg_001",
        "content": {
            "type": "text",
            "text": "Hello"
        }
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail without type field");
}

/// Test invalid type value
#[test]
fn test_invalid_type_value() {
    let json = r#"{
        "type": "invalid_message_chunk",
        "message_id": "msg_001",
        "content": {
            "type": "text",
            "text": "Hello"
        }
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail with invalid type");
}

/// Test missing message_id
#[test]
fn test_missing_message_id() {
    let json = r#"{
        "type": "user_message_chunk",
        "content": {
            "type": "text",
            "text": "Hello"
        }
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail without message_id");
}

/// Test missing content field
#[test]
fn test_missing_content_field() {
    let json = r#"{
        "type": "user_message_chunk",
        "message_id": "msg_001"
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail without content");
}

/// Test missing tool_call field
#[test]
fn test_missing_tool_call_field() {
    let json = r#"{
        "type": "tool_call",
        "message_id": "msg_001"
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail without tool_call");
}

/// Test missing tool_call_id in ToolCallUpdate
#[test]
fn test_missing_tool_call_id() {
    let json = r#"{
        "type": "tool_call_update",
        "message_id": "msg_001",
        "tool_call": {
            "id": "call_001",
            "tool_name": "test",
            "status": "running"
        }
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail without tool_call_id");
}

/// Test null values for required fields
#[test]
fn test_null_required_values() {
    let json = r#"{
        "type": "user_message_chunk",
        "message_id": null,
        "content": {
            "type": "text",
            "text": "Hello"
        }
    }"#;

    let result: Result<SessionUpdate, _> = serde_json::from_str(json);
    assert!(result.is_err(), "Should fail with null message_id");
}

/// Test null _meta (should deserialize as None)
#[test]
fn test_null_meta() {
    let json = r#"{
        "type": "user_message_chunk",
        "message_id": "msg_001",
        "content": {
            "type": "text",
            "text": "Hello"
        },
        "_meta": null
    }"#;

    let update: SessionUpdate = serde_json::from_str(json).unwrap();
    match update {
        SessionUpdate::UserMessageChunk { _meta, .. } => {
            assert!(_meta.is_none());
        }
        _ => panic!("Expected UserMessageChunk"),
    }
}

// ===== Roundtrip Tests =====

/// Test roundtrip for all variants
#[test]
fn test_all_variants_roundtrip() {
    let variants = vec![
        SessionUpdate::UserMessageChunk {
            message_id: "msg_1".to_string(),
            content: ContentBlock::Text {
                text: "User message".to_string(),
            },
            _meta: None,
        },
        SessionUpdate::AgentMessageChunk {
            message_id: "msg_2".to_string(),
            content: ContentBlock::Text {
                text: "Agent response".to_string(),
            },
            _meta: Some(Meta::default()),
        },
        SessionUpdate::AgentThoughtChunk {
            message_id: "msg_3".to_string(),
            content: ContentBlock::Text {
                text: "Thinking...".to_string(),
            },
            _meta: None,
        },
        SessionUpdate::ToolCall {
            message_id: "msg_4".to_string(),
            tool_call: ToolCall {
                id: "call_1".to_string(),
                tool_name: "bash".to_string(),
                status: ToolCallStatus::Pending,
                content: vec![],
                raw_input: Some(json!({"cmd": "ls"})),
                raw_output: None,
                title: Some("List files".to_string()),
                error: None,
                metadata: None,
                origin: None,
            },
            _meta: None,
        },
        SessionUpdate::ToolCallUpdate {
            message_id: "msg_5".to_string(),
            tool_call_id: "call_1".to_string(),
            tool_call: ToolCall {
                id: "call_1".to_string(),
                tool_name: "bash".to_string(),
                status: ToolCallStatus::Completed,
                content: vec![ToolCallContent::from_content_block(ContentBlock::Text {
                    text: "file1.txt\nfile2.txt".to_string(),
                })],
                raw_input: Some(json!({"cmd": "ls"})),
                raw_output: Some(json!({"exit_code": 0})),
                title: Some("List files".to_string()),
                error: None,
                metadata: Some(json!({"duration_ms": 100})),
                origin: None,
            },
            _meta: Some(Meta::default()),
        },
    ];

    for variant in variants {
        let json = serde_json::to_string(&variant).unwrap();
        let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
        assert_eq!(variant, deserialized);
    }
}

// ===== Edge Cases: Complex Content =====

/// Test UserMessageChunk with complex nested content
#[test]
fn test_complex_nested_content() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: "msg_complex".to_string(),
        content: ContentBlock::ResourceLink {
            uri: "data:text/plain;base64,SGVsbG8gV29ybGQh".to_string(),
            name: Some("embedded.txt".to_string()),
            mime_type: Some("text/plain; charset=utf-8".to_string()),
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    assert_eq!(update, deserialized);
}

// ===== Clone and Debug =====

/// Test SessionUpdate clone
#[test]
fn test_session_update_clone() {
    let original = SessionUpdate::UserMessageChunk {
        message_id: "msg_clone".to_string(),
        content: ContentBlock::Text {
            text: "Test".to_string(),
        },
        _meta: None,
    };

    let cloned = original.clone();
    assert_eq!(original, cloned);
}

/// Test SessionUpdate debug formatting
#[test]
fn test_session_update_debug() {
    let update = SessionUpdate::UserMessageChunk {
        message_id: "msg_debug".to_string(),
        content: ContentBlock::Text {
            text: "Debug test".to_string(),
        },
        _meta: None,
    };

    let debug_str = format!("{:?}", update);
    assert!(debug_str.contains("UserMessageChunk"));
    assert!(debug_str.contains("msg_debug"));
}

// ===== Edge Cases: Empty Collections =====

/// Test ToolCall with empty content array persists correctly
#[test]
fn test_tool_call_empty_content_persists() {
    let update = SessionUpdate::ToolCall {
        message_id: "msg_empty".to_string(),
        tool_call: ToolCall {
            id: "call_empty".to_string(),
            tool_name: "test".to_string(),
            status: ToolCallStatus::Pending,
            content: vec![], // Explicitly empty
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        },
        _meta: None,
    };

    let json = serde_json::to_string(&update).unwrap();
    assert!(json.contains(r#""content":[]"#));

    let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap();
    match deserialized {
        SessionUpdate::ToolCall { tool_call, .. } => {
            assert_eq!(tool_call.content.len(), 0);
        }
        _ => panic!("Expected ToolCall"),
    }
}
