use serde::{Deserialize, Serialize};

use crate::types::content::ContentBlock;
use crate::types::meta::Meta;
use crate::types::tool::ToolCall;

/// ACP-style session updates for streaming content
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SessionUpdate {
    /// User message content chunk
    UserMessageChunk {
        message_id: String,
        content: ContentBlock,
        #[serde(skip_serializing_if = "Option::is_none")]
        _meta: Option<Meta>,
    },
    /// Agent message content chunk
    AgentMessageChunk {
        message_id: String,
        content: ContentBlock,
        #[serde(skip_serializing_if = "Option::is_none")]
        _meta: Option<Meta>,
    },
    /// Agent thought content chunk (internal reasoning)
    AgentThoughtChunk {
        message_id: String,
        content: ContentBlock,
        #[serde(skip_serializing_if = "Option::is_none")]
        _meta: Option<Meta>,
    },
    /// Tool call created or initiated
    ToolCall {
        message_id: String,
        tool_call: ToolCall,
        #[serde(skip_serializing_if = "Option::is_none")]
        _meta: Option<Meta>,
    },
    /// Tool call update (status change, new content, etc.)
    ToolCallUpdate {
        message_id: String,
        tool_call_id: String,
        tool_call: ToolCall,
        #[serde(skip_serializing_if = "Option::is_none")]
        _meta: Option<Meta>,
    },
    /// Unknown update type (forward compatibility - pass through as raw JSON)
    #[serde(untagged)]
    Unknown {
        #[serde(flatten)]
        data: serde_json::Value,
    },
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ToolCallContent;
    use serde_json::json;

    #[test]
    fn test_user_message_chunk_serialization() {
        let update = SessionUpdate::UserMessageChunk {
            message_id: "msg_123".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_123"#));
        assert!(json.contains(r#""text":"Hello"#));
        assert!(!json.contains(r#""_meta""#));
    }

    #[test]
    fn test_user_message_chunk_deserialization() {
        let json = r#"{
            "type": "user_message_chunk",
            "message_id": "msg_123",
            "content": {
                "type": "text",
                "text": "Hello"
            }
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::UserMessageChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg_123");
                assert_eq!(
                    content,
                    ContentBlock::Text {
                        text: "Hello".to_string()
                    }
                );
                assert_eq!(_meta, None);
            }
            _ => panic!("Expected UserMessageChunk"),
        }
    }

    #[test]
    fn test_user_message_chunk_roundtrip() {
        let original = SessionUpdate::UserMessageChunk {
            message_id: "msg_456".to_string(),
            content: ContentBlock::Text {
                text: "Roundtrip test".to_string(),
            },
            _meta: None,
        };

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

    #[test]
    fn test_user_message_chunk_with_meta() {
        let update = SessionUpdate::UserMessageChunk {
            message_id: "msg_789".to_string(),
            content: ContentBlock::Text {
                text: "With meta".to_string(),
            },
            _meta: Some(Meta {
                provider: None,
                extra: std::collections::HashMap::new(),
            }),
        };

        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);
    }

    #[test]
    fn test_agent_message_chunk_serialization() {
        let update = SessionUpdate::AgentMessageChunk {
            message_id: "msg_agent_1".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_1"#));
        assert!(json.contains(r#""text":"Agent response"#));
    }

    #[test]
    fn test_agent_message_chunk_deserialization() {
        let json = r#"{
            "type": "agent_message_chunk",
            "message_id": "msg_agent_2",
            "content": {
                "type": "text",
                "text": "Agent here"
            }
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::AgentMessageChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg_agent_2");
                assert_eq!(
                    content,
                    ContentBlock::Text {
                        text: "Agent here".to_string()
                    }
                );
                assert_eq!(_meta, None);
            }
            _ => panic!("Expected AgentMessageChunk"),
        }
    }

    #[test]
    fn test_agent_message_chunk_roundtrip() {
        let original = SessionUpdate::AgentMessageChunk {
            message_id: "msg_agent_rt".to_string(),
            content: ContentBlock::ResourceLink {
                uri: "file:///test.txt".to_string(),
                name: Some("test.txt".to_string()),
                mime_type: Some("text/plain".to_string()),
            },
            _meta: None,
        };

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

    #[test]
    fn test_agent_message_chunk_with_meta() {
        let mut extra = std::collections::HashMap::new();
        extra.insert("timestamp".to_string(), json!("2025-11-10T12:00:00Z"));

        let update = SessionUpdate::AgentMessageChunk {
            message_id: "msg_agent_meta".to_string(),
            content: ContentBlock::Text {
                text: "With metadata".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""#));

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

    #[test]
    fn test_agent_thought_chunk_serialization() {
        let update = SessionUpdate::AgentThoughtChunk {
            message_id: "msg_thought_1".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_1"#));
        assert!(json.contains(r#""text":"Thinking..."#));
    }

    #[test]
    fn test_agent_thought_chunk_deserialization() {
        let json = r#"{
            "type": "agent_thought_chunk",
            "message_id": "msg_thought_2",
            "content": {
                "type": "text",
                "text": "Analyzing the problem..."
            }
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::AgentThoughtChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg_thought_2");
                assert_eq!(
                    content,
                    ContentBlock::Text {
                        text: "Analyzing the problem...".to_string()
                    }
                );
                assert_eq!(_meta, None);
            }
            _ => panic!("Expected AgentThoughtChunk"),
        }
    }

    #[test]
    fn test_agent_thought_chunk_roundtrip() {
        let original = SessionUpdate::AgentThoughtChunk {
            message_id: "msg_thought_rt".to_string(),
            content: ContentBlock::Text {
                text: "Internal reasoning".to_string(),
            },
            _meta: None,
        };

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

    #[test]
    fn test_agent_thought_chunk_with_meta() {
        let update = SessionUpdate::AgentThoughtChunk {
            message_id: "msg_thought_meta".to_string(),
            content: ContentBlock::Text {
                text: "Thought with meta".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);
    }

    #[test]
    fn test_tool_call_serialization() {
        let tool_call = ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: crate::types::tool::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: Some("Run bash command".to_string()),
            error: None,
            metadata: None,

            origin: None,

            };

        let update = SessionUpdate::ToolCall {
            message_id: "msg_tool_1".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_1"#));
        assert!(json.contains(r#""tool_name":"bash"#));
        assert!(json.contains(r#""status":"pending"#));
    }

    #[test]
    fn test_tool_call_deserialization() {
        let json = r#"{
            "type": "tool_call",
            "message_id": "msg_tool_2",
            "tool_call": {
                "id": "call_456",
                "tool_name": "read",
                "status": "in_progress",
                "content": []
            }
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::ToolCall {
                message_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg_tool_2");
                assert_eq!(tool_call.id, "call_456");
                assert_eq!(tool_call.tool_name, "read");
                assert_eq!(
                    tool_call.status,
                    crate::types::tool::ToolCallStatus::Running
                );
                assert_eq!(_meta, None);
            }
            _ => panic!("Expected ToolCall"),
        }
    }

    #[test]
    fn test_tool_call_roundtrip() {
        let tool_call = ToolCall {
            id: "call_rt".to_string(),
            tool_name: "write".to_string(),
            status: crate::types::tool::ToolCallStatus::Completed,
            content: vec![ToolCallContent::from_content_block(ContentBlock::Text {
                text: "File written".to_string(),
            })],
            raw_input: Some(json!({"path": "/tmp/test.txt"})),
            raw_output: Some(json!({"success": true})),
            title: Some("Write file".to_string()),
            error: None,
            metadata: None,

            origin: None,

            };

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

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

    #[test]
    fn test_tool_call_with_meta() {
        let tool_call = ToolCall {
            id: "call_meta".to_string(),
            tool_name: "bash".to_string(),
            status: crate::types::tool::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_meta".to_string(),
            tool_call,
            _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);
    }

    #[test]
    fn test_tool_call_update_serialization() {
        let tool_call = ToolCall {
            id: "call_update_1".to_string(),
            tool_name: "bash".to_string(),
            status: crate::types::tool::ToolCallStatus::Running,
            content: vec![ToolCallContent::from_content_block(ContentBlock::Text {
                text: "Output line 1".to_string(),
            })],
            raw_input: None,
            raw_output: None,
            title: Some("Running bash".to_string()),
            error: None,
            metadata: None,

            origin: None,

            };

        let update = SessionUpdate::ToolCallUpdate {
            message_id: "msg_update_1".to_string(),
            tool_call_id: "call_update_1".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_1"#));
        assert!(json.contains(r#""tool_call_id":"call_update_1"#));
        // ToolCallStatus::Running serializes as "in_progress"
        assert!(json.contains(r#""status":"in_progress"#));
    }

    #[test]
    fn test_tool_call_update_deserialization() {
        let json = r#"{
            "type": "tool_call_update",
            "message_id": "msg_update_2",
            "tool_call_id": "call_update_2",
            "tool_call": {
                "id": "call_update_2",
                "tool_name": "read",
                "status": "completed",
                "content": [
                    {
                        "type": "content",
                        "content": {
                            "type": "text",
                            "text": "File contents"
                        }
                    }
                ]
            }
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::ToolCallUpdate {
                message_id,
                tool_call_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg_update_2");
                assert_eq!(tool_call_id, "call_update_2");
                assert_eq!(tool_call.id, "call_update_2");
                assert_eq!(
                    tool_call.status,
                    crate::types::tool::ToolCallStatus::Completed
                );
                assert_eq!(
                    tool_call.content,
                    vec![crate::types::tool::ToolCallContent::from_content_block(ContentBlock::Text {
                        text: "File contents".to_string()
                    })]
                );
                assert_eq!(_meta, None);
            }
            _ => panic!("Expected ToolCallUpdate"),
        }
    }

    #[test]
    fn test_tool_call_update_roundtrip() {
        let tool_call = ToolCall {
            id: "call_update_rt".to_string(),
            tool_name: "bash".to_string(),
            status: crate::types::tool::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 original = SessionUpdate::ToolCallUpdate {
            message_id: "msg_update_rt".to_string(),
            tool_call_id: "call_update_rt".to_string(),
            tool_call,
            _meta: None,
        };

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

    #[test]
    fn test_tool_call_update_with_meta() {
        let tool_call = ToolCall {
            id: "call_update_meta".to_string(),
            tool_name: "write".to_string(),
            status: crate::types::tool::ToolCallStatus::Completed,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,

            origin: None,

            };

        let update = SessionUpdate::ToolCallUpdate {
            message_id: "msg_update_meta".to_string(),
            tool_call_id: "call_update_meta".to_string(),
            tool_call,
            _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);
    }

    #[test]
    fn test_all_variants_have_snake_case_type_tags() {
        // Test that each variant serializes with the correct snake_case type tag

        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"#));

        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: crate::types::tool::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"#));

        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: crate::types::tool::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"#));
    }
}
