//! Content and Metadata Transformation Utilities
//!
//! This module provides helper functions for transforming content between
//! the internal Dirigent protocol and the ACP wire format.

use dirigent_protocol::ContentBlock;
use serde_json::json;

/// Extract 'kind' from tool call metadata (stored as acp_kind)
pub fn extract_kind(tool_call: &dirigent_protocol::ToolCall) -> Option<String> {
    tool_call
        .metadata
        .as_ref()
        .and_then(|m| m.get("acp_kind"))
        .and_then(|k| k.as_str())
        .map(String::from)
}

/// Convert ToolCallStatus to ACP string format
pub fn tool_call_status_to_string(status: &dirigent_protocol::ToolCallStatus) -> String {
    match status {
        dirigent_protocol::ToolCallStatus::Pending => "pending".to_string(),
        dirigent_protocol::ToolCallStatus::Running => "in_progress".to_string(),
        dirigent_protocol::ToolCallStatus::Completed => "completed".to_string(),
        dirigent_protocol::ToolCallStatus::Error => "failed".to_string(),
    }
}

/// Unwrap ToolCallContent wrappers to extract ContentBlocks for SSE output
///
/// The internal protocol uses ToolCallContent wrappers, but the SSE/ACP wire format
/// expects flat ContentBlock arrays. This function extracts Content variants only.
pub fn unwrap_tool_call_content(
    content: &[dirigent_protocol::ToolCallContent],
) -> Vec<ContentBlock> {
    content
        .iter()
        .filter_map(|wrapper| match wrapper {
            dirigent_protocol::ToolCallContent::Content { content } => Some(content.clone()),
            // Diff and Terminal variants are not yet supported in SSE output
            _ => None,
        })
        .collect()
}

/// Rebuild _meta for ACP output (ensures claudeCode.toolName is present)
pub fn rebuild_meta(
    meta: &Option<dirigent_protocol::Meta>,
    tool_call: &dirigent_protocol::ToolCall,
) -> Option<serde_json::Value> {
    let mut meta_obj = serde_json::Map::new();

    // Add claudeCode.toolName
    let mut claude_code = serde_json::Map::new();
    claude_code.insert("toolName".to_string(), json!(tool_call.tool_name));

    // Merge any existing _meta (preserves toolResponse, etc.)
    if let Some(existing_meta) = meta {
        // Convert Meta to JSON Value for merging
        if let Ok(meta_value) = serde_json::to_value(existing_meta) {
            if let Some(obj) = meta_value.as_object() {
                for (k, v) in obj {
                    if k == "claudeCode" {
                        // Merge into our claudeCode object
                        if let Some(cc) = v.as_object() {
                            for (ck, cv) in cc {
                                // Don't override toolName we just set
                                if ck != "toolName" {
                                    claude_code.insert(ck.clone(), cv.clone());
                                }
                            }
                        }
                    } else {
                        meta_obj.insert(k.clone(), v.clone());
                    }
                }
            }
        }
    }

    meta_obj.insert("claudeCode".to_string(), json!(claude_code));
    Some(json!(meta_obj))
}

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

    #[test]
    fn test_extract_kind_present() {
        let mut metadata = serde_json::Map::new();
        metadata.insert("acp_kind".to_string(), json!("search"));

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: dirigent_protocol::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: Some(json!(metadata)),
            origin: None,
        };

        assert_eq!(extract_kind(&tool_call), Some("search".to_string()));
    }

    #[test]
    fn test_extract_kind_missing_metadata() {
        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: dirigent_protocol::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        assert_eq!(extract_kind(&tool_call), None);
    }

    #[test]
    fn test_extract_kind_missing_field() {
        let mut metadata = serde_json::Map::new();
        metadata.insert("other_field".to_string(), json!("value"));

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: dirigent_protocol::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: Some(json!(metadata)),
            origin: None,
        };

        assert_eq!(extract_kind(&tool_call), None);
    }

    #[test]
    fn test_extract_kind_malformed_metadata() {
        // acp_kind is not a string
        let mut metadata = serde_json::Map::new();
        metadata.insert("acp_kind".to_string(), json!(123));

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: dirigent_protocol::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: Some(json!(metadata)),
            origin: None,
        };

        assert_eq!(extract_kind(&tool_call), None);
    }

    #[test]
    fn test_tool_call_status_to_string_all_variants() {
        assert_eq!(
            tool_call_status_to_string(&dirigent_protocol::ToolCallStatus::Pending),
            "pending"
        );
        assert_eq!(
            tool_call_status_to_string(&dirigent_protocol::ToolCallStatus::Running),
            "in_progress"
        );
        assert_eq!(
            tool_call_status_to_string(&dirigent_protocol::ToolCallStatus::Completed),
            "completed"
        );
        assert_eq!(
            tool_call_status_to_string(&dirigent_protocol::ToolCallStatus::Error),
            "failed"
        );
    }

    #[test]
    fn test_rebuild_meta_basic() {
        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "some_tool".to_string(),
            status: dirigent_protocol::ToolCallStatus::Pending,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&None, &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();
        assert_eq!(meta_obj["claudeCode"]["toolName"], "some_tool");
    }

    #[test]
    fn test_rebuild_meta_preserves_existing_fields() {
        let existing_meta = dirigent_protocol::Meta {
            provider: Some(dirigent_protocol::ProviderMeta {
                name: "test_provider".to_string(),
                original_ids: None,
                raw_excerpt: None,
            }),
            extra: std::collections::HashMap::from([(
                "customField".to_string(),
                json!("customValue"),
            )]),
        };

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "bash".to_string(),
            status: dirigent_protocol::ToolCallStatus::Running,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&Some(existing_meta), &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();

        // Verify claudeCode.toolName is present
        assert_eq!(meta_obj["claudeCode"]["toolName"], "bash");

        // Verify existing fields are preserved
        assert_eq!(meta_obj["provider"]["name"], "test_provider");
        assert_eq!(meta_obj["customField"], "customValue");
    }

    #[test]
    fn test_rebuild_meta_merges_claude_code_fields() {
        let mut existing_meta = dirigent_protocol::Meta::default();
        existing_meta.extra.insert(
            "claudeCode".to_string(),
            json!({
                "toolResponse": "some response",
                "otherField": "other value"
            }),
        );

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "test_tool".to_string(),
            status: dirigent_protocol::ToolCallStatus::Completed,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&Some(existing_meta), &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();

        // Verify toolName is present (we set it)
        assert_eq!(meta_obj["claudeCode"]["toolName"], "test_tool");

        // Verify existing claudeCode fields are preserved
        assert_eq!(meta_obj["claudeCode"]["toolResponse"], "some response");
        assert_eq!(meta_obj["claudeCode"]["otherField"], "other value");
    }

    #[test]
    fn test_rebuild_meta_does_not_override_tool_name() {
        // Existing meta has a different toolName
        let mut existing_meta = dirigent_protocol::Meta::default();
        existing_meta.extra.insert(
            "claudeCode".to_string(),
            json!({
                "toolName": "wrong_tool",
                "toolResponse": "response"
            }),
        );

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_123".to_string(),
            tool_name: "correct_tool".to_string(),
            status: dirigent_protocol::ToolCallStatus::Completed,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&Some(existing_meta), &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();

        // Verify our toolName is used (not the one from existing meta)
        assert_eq!(meta_obj["claudeCode"]["toolName"], "correct_tool");

        // Verify other fields are preserved
        assert_eq!(meta_obj["claudeCode"]["toolResponse"], "response");
    }

    #[test]
    fn test_rebuild_meta_preserves_tool_response() {
        // Test that rebuild_meta preserves toolResponse from incoming Claude updates
        let mut existing_meta = dirigent_protocol::Meta::default();
        existing_meta.extra.insert(
            "claudeCode".to_string(),
            json!({
                "toolResponse": {
                    "mode": "content",
                    "numFiles": 0,
                    "filenames": [],
                    "content": "some output",
                    "numLines": 58,
                    "appliedLimit": 100
                },
                "toolName": "OldToolName"
            }),
        );

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_456".to_string(),
            tool_name: "Grep".to_string(),
            status: dirigent_protocol::ToolCallStatus::Running,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&Some(existing_meta), &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();

        // toolName should be updated to current tool_call.tool_name
        assert_eq!(meta_obj["claudeCode"]["toolName"], "Grep");

        // toolResponse should be preserved completely
        assert!(meta_obj["claudeCode"]["toolResponse"].is_object());
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["mode"], "content");
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["numFiles"], 0);
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["numLines"], 58);
    }

    #[test]
    fn test_rebuild_meta_round_trip_no_data_loss() {
        // Test incoming → internal → outgoing preserves all fields
        let mut existing_meta = dirigent_protocol::Meta::default();
        existing_meta.extra.insert(
            "claudeCode".to_string(),
            json!({
                "toolResponse": {
                    "mode": "content",
                    "numFiles": 3,
                    "filenames": ["file1.rs", "file2.rs", "file3.rs"],
                    "content": "grep results here",
                    "numLines": 42,
                    "appliedLimit": 100,
                    "customField": "should be preserved",
                    "nestedObject": {
                        "deep": "value"
                    }
                },
                "additionalField": "also preserved"
            }),
        );
        existing_meta
            .extra
            .insert("customTopLevel".to_string(), json!("preserved too"));

        let tool_call = dirigent_protocol::ToolCall {
            id: "call_789".to_string(),
            tool_name: "Grep".to_string(),
            status: dirigent_protocol::ToolCallStatus::Completed,
            content: vec![],
            raw_input: None,
            raw_output: None,
            title: None,
            error: None,
            metadata: None,
            origin: None,
        };

        let meta = rebuild_meta(&Some(existing_meta), &tool_call);

        assert!(meta.is_some());
        let meta_obj = meta.unwrap();

        // Verify NO data loss
        assert_eq!(meta_obj["claudeCode"]["toolName"], "Grep");
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["mode"], "content");
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["numFiles"], 3);
        assert_eq!(meta_obj["claudeCode"]["toolResponse"]["numLines"], 42);
        assert_eq!(
            meta_obj["claudeCode"]["toolResponse"]["customField"],
            "should be preserved"
        );
        assert_eq!(
            meta_obj["claudeCode"]["toolResponse"]["nestedObject"]["deep"],
            "value"
        );
        assert_eq!(meta_obj["claudeCode"]["additionalField"], "also preserved");
        assert_eq!(meta_obj["customTopLevel"], "preserved too");
    }
}
