use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::types::content::ContentBlock;

/// ACP-compliant tool call content wrapper supporting multiple content types
///
/// The ACP protocol requires tool call content to be wrapped in a discriminated
/// union that supports three types:
/// - **Content** - Regular content blocks (text, images, etc.)
/// - **Diff** - File change diffs (oldText, newText)
/// - **Terminal** - Terminal session references
///
/// # Examples
///
/// ```rust
/// use dirigent_protocol::{ToolCallContent, ContentBlock};
///
/// // Create a text content wrapper
/// let content = ToolCallContent::from_content_block(ContentBlock::Text {
///     text: "Tool output".to_string()
/// });
///
/// // Create a diff
/// let diff = ToolCallContent::diff(
///     "/src/main.rs".to_string(),
///     Some("old code".to_string()),
///     "new code".to_string(),
/// );
///
/// // Create a terminal reference
/// let terminal = ToolCallContent::terminal("term_123".to_string());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolCallContent {
    /// Regular content (text, images, etc.)
    Content {
        content: ContentBlock,
    },

    /// File diff showing changes
    Diff {
        path: String,
        #[serde(rename = "oldText", skip_serializing_if = "Option::is_none")]
        old_text: Option<String>,
        #[serde(rename = "newText")]
        new_text: String,
    },

    /// Reference to a terminal session
    Terminal {
        #[serde(rename = "terminalId")]
        terminal_id: String,
    },
}

impl ToolCallContent {
    /// Create a content wrapper from a ContentBlock
    pub fn from_content_block(block: ContentBlock) -> Self {
        Self::Content { content: block }
    }

    /// Create a diff wrapper
    pub fn diff(path: String, old_text: Option<String>, new_text: String) -> Self {
        Self::Diff { path, old_text, new_text }
    }

    /// Create a terminal reference
    pub fn terminal(terminal_id: String) -> Self {
        Self::Terminal { terminal_id }
    }
}

/// Unique identifier for a tool call
pub type ToolCallId = String;

/// Status of a tool call in its lifecycle
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
    /// Tool call has been created but not yet started
    Pending,
    /// Tool call is currently executing
    #[serde(rename = "in_progress")]
    Running,
    /// Tool call completed successfully
    Completed,
    /// Tool call failed with an error
    #[serde(rename = "failed")]
    Error,
}

/// Origin of a tool execution
///
/// Distinguishes where the tool is actually executed:
/// - Internal: Dirigent runs the tool after user permission
/// - External: Agent runs tool directly (we observe)
/// - Forwarded: Upstream ACP server (transitionary)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ToolOrigin {
    /// Tool executed by Dirigent after user permission
    Internal,
    /// Tool executed by agent directly (we observe)
    #[default]
    External,
    /// Tool forwarded from upstream ACP server
    Forwarded,
}

/// Represents a tool call and its lifecycle
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
    /// Unique identifier for this tool call
    pub id: ToolCallId,
    /// Name of the tool being called
    pub tool_name: String,
    /// Current status of the tool call
    pub status: ToolCallStatus,
    /// Content associated with this tool call (wrapped in ACP-compliant format)
    ///
    /// Each content item is wrapped in a discriminated union that can be:
    /// - Content (text, images, etc.)
    /// - Diff (file changes)
    /// - Terminal (terminal session reference)
    #[serde(default)]
    pub content: Vec<ToolCallContent>,
    /// Raw input parameters (preserved for debugging/inspection)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub raw_input: Option<Value>,
    /// Raw output result (preserved for debugging/inspection)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub raw_output: Option<Value>,
    /// Optional title for the tool call
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Error message if status is Error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Additional metadata
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Value>,
    /// Origin of this tool execution
    #[serde(skip_serializing_if = "Option::is_none")]
    pub origin: Option<ToolOrigin>,
}

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

    #[test]
    fn test_tool_call_status_pending_serialization() {
        let status = ToolCallStatus::Pending;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, r#""pending""#);
    }

    #[test]
    fn test_tool_call_status_running_serialization() {
        let status = ToolCallStatus::Running;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, r#""in_progress""#);
    }

    #[test]
    fn test_tool_call_status_completed_serialization() {
        let status = ToolCallStatus::Completed;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, r#""completed""#);
    }

    #[test]
    fn test_tool_call_status_error_serialization() {
        let status = ToolCallStatus::Error;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, r#""failed""#);
    }

    #[test]
    fn test_tool_call_serialization_minimal() {
        let tool_call = ToolCall {
            id: "call_123".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 json = serde_json::to_string(&tool_call).unwrap();

        // Verify required fields are present
        assert!(json.contains(r#""id":"call_123""#));
        assert!(json.contains(r#""tool_name":"bash""#));
        assert!(json.contains(r#""status":"pending""#));
        assert!(json.contains(r#""content":[]"#));

        // Verify optional fields are NOT present when None
        assert!(!json.contains(r#""raw_input""#));
        assert!(!json.contains(r#""raw_output""#));
        assert!(!json.contains(r#""title""#));
        assert!(!json.contains(r#""error""#));
        assert!(!json.contains(r#""metadata""#));
    }

    #[test]
    fn test_tool_call_serialization_complete() {
        let tool_call = ToolCall {
            id: "call_456".to_string(),
            tool_name: "read_file".to_string(),
            status: ToolCallStatus::Completed,
            content: vec![ToolCallContent::from_content_block(ContentBlock::Text {
                text: "File contents".to_string(),
            })],
            raw_input: Some(json!({"path": "/tmp/test.txt"})),
            raw_output: Some(json!({"success": true})),
            title: Some("Read test file".to_string()),
            error: None,
            metadata: Some(json!({"duration_ms": 42})),
            origin: None,
        };

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

        // Verify all fields are present
        assert!(json.contains(r#""id":"call_456""#));
        assert!(json.contains(r#""tool_name":"read_file""#));
        assert!(json.contains(r#""status":"completed""#));
        assert!(json.contains(r#""text":"File contents""#));
        assert!(json.contains(r#""raw_input""#));
        assert!(json.contains(r#""raw_output""#));
        assert!(json.contains(r#""title":"Read test file""#));
        assert!(json.contains(r#""metadata""#));
        assert!(!json.contains(r#""error""#)); // Still None
    }

    #[test]
    fn test_tool_call_serialization_with_error() {
        let tool_call = ToolCall {
            id: "call_789".to_string(),
            tool_name: "write_file".to_string(),
            status: ToolCallStatus::Error,
            content: vec![],
            raw_input: Some(json!({"path": "/tmp/readonly.txt"})),
            raw_output: None,
            title: Some("Write to readonly file".to_string()),
            error: Some("Permission denied".to_string()),
            metadata: None,
            origin: None,
        };

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

        assert!(json.contains(r#""status":"failed""#));
        assert!(json.contains(r#""error":"Permission denied""#));
    }

    #[test]
    fn test_tool_call_roundtrip() {
        let original = ToolCall {
            id: "call_roundtrip".to_string(),
            tool_name: "test_tool".to_string(),
            status: ToolCallStatus::Running,
            content: vec![
                ToolCallContent::from_content_block(ContentBlock::Text {
                    text: "Output line 1".to_string(),
                }),
                ToolCallContent::from_content_block(ContentBlock::Text {
                    text: "Output line 2".to_string(),
                }),
            ],
            raw_input: Some(json!({"arg": "value"})),
            raw_output: None,
            title: Some("Test Tool Call".to_string()),
            error: None,
            metadata: Some(json!({"test": true})),
            origin: None,
        };

        let json = serde_json::to_string(&original).unwrap();
        let deserialized: ToolCall = serde_json::from_str(&json).unwrap();

        assert_eq!(original, deserialized);
    }

    #[test]
    fn test_tool_call_default_content() {
        // Test that content defaults to empty vec when not present in JSON
        let json = r#"{
            "id": "call_default",
            "tool_name": "test",
            "status": "pending"
        }"#;

        let tool_call: ToolCall = serde_json::from_str(json).unwrap();
        assert_eq!(tool_call.content, vec![]);
    }

    #[test]
    fn test_optional_fields_skip_when_none() {
        let tool_call = ToolCall {
            id: "call_skip".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,
        };

        let json = serde_json::to_value(&tool_call).unwrap();
        let obj = json.as_object().unwrap();

        // Verify optional fields are not in the serialized object
        assert!(!obj.contains_key("raw_input"));
        assert!(!obj.contains_key("raw_output"));
        assert!(!obj.contains_key("title"));
        assert!(!obj.contains_key("error"));
        assert!(!obj.contains_key("metadata"));

        // Verify required fields ARE in the serialized object
        assert!(obj.contains_key("id"));
        assert!(obj.contains_key("tool_name"));
        assert!(obj.contains_key("status"));
        assert!(obj.contains_key("content"));
    }

    // ============================================================================
    // ToolCallContent Tests
    // ============================================================================

    #[test]
    fn test_tool_call_content_wrapper_content_serialization() {
        let content = ToolCallContent::from_content_block(ContentBlock::Text {
            text: "test output".to_string(),
        });

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

        assert_eq!(json["type"], "content");
        assert!(json["content"].is_object());
        assert_eq!(json["content"]["type"], "text");
        assert_eq!(json["content"]["text"], "test output");
    }

    #[test]
    fn test_tool_call_content_wrapper_content_deserialization() {
        let json = json!({
            "type": "content",
            "content": {
                "type": "text",
                "text": "deserialized output"
            }
        });

        let content: ToolCallContent = serde_json::from_value(json).unwrap();

        match content {
            ToolCallContent::Content { content } => {
                match content {
                    ContentBlock::Text { text } => {
                        assert_eq!(text, "deserialized output");
                    }
                    _ => panic!("Expected Text content block"),
                }
            }
            _ => panic!("Expected Content variant"),
        }
    }

    #[test]
    fn test_tool_call_content_diff_serialization() {
        let diff = ToolCallContent::diff(
            "/src/main.rs".to_string(),
            Some("old code".to_string()),
            "new code".to_string(),
        );

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

        assert_eq!(json["type"], "diff");
        assert_eq!(json["path"], "/src/main.rs");
        assert_eq!(json["oldText"], "old code");
        assert_eq!(json["newText"], "new code");
    }

    #[test]
    fn test_tool_call_content_diff_without_old_text() {
        let diff = ToolCallContent::diff(
            "/src/new_file.rs".to_string(),
            None,
            "new file content".to_string(),
        );

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

        assert_eq!(json["type"], "diff");
        assert_eq!(json["path"], "/src/new_file.rs");
        assert!(json.get("oldText").is_none()); // Should be omitted when None
        assert_eq!(json["newText"], "new file content");
    }

    #[test]
    fn test_tool_call_content_diff_deserialization() {
        let json = json!({
            "type": "diff",
            "path": "/test/file.rs",
            "oldText": "before",
            "newText": "after"
        });

        let content: ToolCallContent = serde_json::from_value(json).unwrap();

        match content {
            ToolCallContent::Diff { path, old_text, new_text } => {
                assert_eq!(path, "/test/file.rs");
                assert_eq!(old_text, Some("before".to_string()));
                assert_eq!(new_text, "after");
            }
            _ => panic!("Expected Diff variant"),
        }
    }

    #[test]
    fn test_tool_call_content_terminal_serialization() {
        let terminal = ToolCallContent::terminal("term_123".to_string());

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

        assert_eq!(json["type"], "terminal");
        assert_eq!(json["terminalId"], "term_123");
    }

    #[test]
    fn test_tool_call_content_terminal_deserialization() {
        let json = json!({
            "type": "terminal",
            "terminalId": "term_456"
        });

        let content: ToolCallContent = serde_json::from_value(json).unwrap();

        match content {
            ToolCallContent::Terminal { terminal_id } => {
                assert_eq!(terminal_id, "term_456");
            }
            _ => panic!("Expected Terminal variant"),
        }
    }

    #[test]
    fn test_tool_call_content_roundtrip() {
        // Test all three variants
        let variants = vec![
            ToolCallContent::from_content_block(ContentBlock::Text {
                text: "test".to_string(),
            }),
            ToolCallContent::diff("path.rs".to_string(), Some("old".to_string()), "new".to_string()),
            ToolCallContent::terminal("term_789".to_string()),
        ];

        for original in variants {
            let json = serde_json::to_value(&original).unwrap();
            let deserialized: ToolCallContent = serde_json::from_value(json).unwrap();
            assert_eq!(original, deserialized);
        }
    }

    #[test]
    fn test_tool_call_with_mixed_content_types() {
        let tool_call = ToolCall {
            id: "call_mixed".to_string(),
            tool_name: "edit_tool".to_string(),
            status: ToolCallStatus::Completed,
            content: vec![
                ToolCallContent::from_content_block(ContentBlock::Text {
                    text: "Editing file...".to_string(),
                }),
                ToolCallContent::diff(
                    "/src/lib.rs".to_string(),
                    Some("fn old() {}".to_string()),
                    "fn new() {}".to_string(),
                ),
                ToolCallContent::from_content_block(ContentBlock::Text {
                    text: "Edit complete".to_string(),
                }),
            ],
            raw_input: None,
            raw_output: None,
            title: Some("Edit file".to_string()),
            error: None,
            metadata: None,
            origin: None,
        };

        // Serialize and deserialize
        let json = serde_json::to_value(&tool_call).unwrap();
        let deserialized: ToolCall = serde_json::from_value(json).unwrap();

        assert_eq!(tool_call, deserialized);
        assert_eq!(deserialized.content.len(), 3);
    }
}
