/// Comprehensive integration tests for OpenCode event → SessionUpdate translation
/// Tests realistic event sequences with proper ordering, metadata, and tool lifecycle tracking
use dirigent_protocol::adapters::{OpenCodeAdapter, TranslationError};
use dirigent_protocol::types::{ContentBlock, SessionUpdate, ToolCallStatus};
use opencode_client::types as oc;
use serde_json::json;

// ===== Helper Functions =====

/// Create a text part for testing
fn create_text_part(
    session_id: &str,
    message_id: &str,
    part_id: &str,
    text: &str,
) -> oc::Part {
    oc::Part::Text(oc::TextPart {
        id: part_id.to_string(),
        session_id: session_id.to_string(),
        message_id: message_id.to_string(),
        text: text.to_string(),
        synthetic: None,
        time: Some(oc::PartTime {
            start: 1000,
            end: None,
        }),
    })
}

/// Create a reasoning part for testing
fn create_reasoning_part(
    session_id: &str,
    message_id: &str,
    part_id: &str,
    text: &str,
) -> oc::Part {
    oc::Part::Reasoning(oc::ReasoningPart {
        id: part_id.to_string(),
        session_id: session_id.to_string(),
        message_id: message_id.to_string(),
        text: text.to_string(),
        time: Some(oc::PartTime {
            start: 1000,
            end: None,
        }),
    })
}

/// Create a tool part with given state
fn create_tool_part(
    session_id: &str,
    message_id: &str,
    part_id: &str,
    tool: &str,
    state: oc::ToolState,
) -> oc::Part {
    oc::Part::Tool(oc::ToolPart {
        id: part_id.to_string(),
        session_id: session_id.to_string(),
        message_id: message_id.to_string(),
        call_id: format!("call_{}", part_id),
        tool: tool.to_string(),
        state,
        metadata: None,
    })
}

/// Create a file part for testing
fn create_file_part(
    session_id: &str,
    message_id: &str,
    part_id: &str,
    filename: &str,
    url: &str,
    mime: &str,
) -> oc::Part {
    oc::Part::File(oc::FilePart {
        id: part_id.to_string(),
        session_id: session_id.to_string(),
        message_id: message_id.to_string(),
        mime: mime.to_string(),
        filename: Some(filename.to_string()),
        url: url.to_string(),
        source: None,
    })
}

/// Create a MessagePartUpdated event
fn create_part_event(part: oc::Part, delta: Option<String>) -> oc::Event {
    oc::Event::MessagePartUpdated {
        properties: oc::MessagePartEventInfo { part, delta },
    }
}

// ===== Test Case 1: Text Streaming =====

/// Test text streaming produces AgentMessageChunk sequence with proper metadata
#[test]
fn test_text_streaming_to_agent_message_chunks() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_text";
    let message_id = "msg_text";

    // Simulate streaming text in multiple chunks
    let chunks = vec![
        ("part_1", "Hello", "Hello"),
        ("part_2", "Hello world", " world"),
        ("part_3", "Hello world!", "!"),
    ];

    let mut updates = vec![];
    for (part_id, full_text, delta) in chunks {
        let part = create_text_part(session_id, message_id, part_id, full_text);
        let event = create_part_event(part, Some(delta.to_string()));

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok(), "Translation should succeed");

        let update = result.unwrap();
        assert!(update.is_some(), "Should return an update");
        updates.push(update.unwrap());
    }

    // Verify all updates are AgentMessageChunk
    assert_eq!(updates.len(), 3);

    for (i, update) in updates.iter().enumerate() {
        match update {
            SessionUpdate::AgentMessageChunk {
                message_id: msg_id,
                content,
                _meta,
            } => {
                assert_eq!(msg_id, message_id);

                // Verify content is Text
                match content {
                    ContentBlock::Text { text } => {
                        // Each chunk should contain the full text up to that point
                        assert!(!text.is_empty());
                    }
                    _ => panic!("Expected Text content"),
                }

                // Verify metadata is present
                assert!(_meta.is_some(), "Metadata should be present on chunk {}", i);
                let meta = _meta.as_ref().unwrap();
                assert!(meta.provider.is_some(), "Provider metadata should be present");

                let provider = meta.provider.as_ref().unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());

                let ids = provider.original_ids.as_ref().unwrap();
                assert_eq!(ids.get("session_id").unwrap(), session_id);
                assert_eq!(ids.get("message_id").unwrap(), message_id);
                assert!(ids.contains_key("part_id"));
            }
            _ => panic!("Expected AgentMessageChunk at index {}", i),
        }
    }
}

/// Test that chunks are created in order
#[test]
fn test_text_chunks_preserve_order() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_order";
    let message_id = "msg_order";

    let expected_order = vec!["First", "Second", "Third", "Fourth"];

    for (i, text) in expected_order.iter().enumerate() {
        let part_id = format!("part_{}", i);
        let part = create_text_part(session_id, message_id, &part_id, text);
        let event = create_part_event(part, Some(text.to_string()));

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok());

        let update = result.unwrap().unwrap();
        match update {
            SessionUpdate::AgentMessageChunk { content, .. } => {
                if let ContentBlock::Text { text: chunk_text } = content {
                    assert_eq!(chunk_text, *text);
                } else {
                    panic!("Expected Text content");
                }
            }
            _ => panic!("Expected AgentMessageChunk"),
        }
    }
}

// ===== Test Case 2: Reasoning Streaming =====

/// Test reasoning streaming produces AgentThoughtChunk sequence with metadata
#[test]
fn test_reasoning_streaming_to_agent_thought_chunks() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_reason";
    let message_id = "msg_reason";

    // Simulate streaming reasoning in chunks
    // Each chunk: (part_id, full_text, delta, expected_delta)
    let chunks = vec![
        ("reason_1", "I need to", "I need to"),
        ("reason_2", "I need to analyze", " analyze"),
        ("reason_3", "I need to analyze the problem", " the problem"),
    ];

    let mut updates = vec![];
    for (part_id, full_text, delta) in chunks.iter() {
        let part = create_reasoning_part(session_id, message_id, part_id, full_text);
        let event = create_part_event(part, Some(delta.to_string()));

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok());

        let update = result.unwrap();
        assert!(update.is_some());
        updates.push((update.unwrap(), delta));
    }

    // Verify all are AgentThoughtChunk with metadata
    assert_eq!(updates.len(), 3);

    for (i, (update, expected_delta)) in updates.iter().enumerate() {
        match update {
            SessionUpdate::AgentThoughtChunk {
                message_id: msg_id,
                content,
                _meta,
            } => {
                assert_eq!(msg_id, message_id);

                // Verify content contains the delta, not the full accumulated text
                match content {
                    ContentBlock::Text { text } => {
                        assert_eq!(text, *expected_delta, "Chunk {} should contain delta", i);
                    }
                    _ => panic!("Expected Text content"),
                }

                // Verify metadata
                assert!(_meta.is_some(), "Metadata should be present on chunk {}", i);
                let meta = _meta.as_ref().unwrap();
                assert!(meta.provider.is_some());

                let provider = meta.provider.as_ref().unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());

                let ids = provider.original_ids.as_ref().unwrap();
                assert_eq!(ids.get("session_id").unwrap(), session_id);
                assert_eq!(ids.get("message_id").unwrap(), message_id);
            }
            _ => panic!("Expected AgentThoughtChunk at index {}", i),
        }
    }
}

// ===== Test Case 3: Tool Lifecycle (Pending → Running → Completed) =====

/// Test complete tool lifecycle produces ToolCall followed by ToolCallUpdate events
#[test]
fn test_tool_lifecycle_pending_to_completed() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_tool";
    let message_id = "msg_tool";
    let part_id = "tool_1";
    let tool_name = "bash";

    let input = json!({"command": "ls -la"});
    let output = "file1.txt\nfile2.txt";

    // Step 1: Pending state → ToolCall
    let pending_part = create_tool_part(
        session_id,
        message_id,
        part_id,
        tool_name,
        oc::ToolState::Pending,
    );
    let pending_event = create_part_event(pending_part, None);

    let result = adapter.translate_to_session_update(pending_event);
    assert!(result.is_ok());
    let update = result.unwrap().unwrap();

    match &update {
        SessionUpdate::ToolCall {
            message_id: msg_id,
            tool_call,
            _meta,
        } => {
            assert_eq!(msg_id, message_id);
            assert_eq!(tool_call.id, part_id);
            assert_eq!(tool_call.tool_name, tool_name);
            assert_eq!(tool_call.status, ToolCallStatus::Pending);
            assert!(tool_call.raw_input.is_none());
            assert!(tool_call.raw_output.is_none());
            assert!(tool_call.error.is_none());

            // Verify metadata
            assert!(_meta.is_some());
            let meta = _meta.as_ref().unwrap();
            assert!(meta.provider.is_some());
            let provider = meta.provider.as_ref().unwrap();
            assert_eq!(provider.name, "opencode");
        }
        _ => panic!("Expected ToolCall for pending state"),
    }

    // Step 2: Running state → ToolCallUpdate
    let running_part = create_tool_part(
        session_id,
        message_id,
        part_id,
        tool_name,
        oc::ToolState::Running {
            input: input.clone(),
            title: None,
            metadata: None,
            time: oc::PartTime {
                start: 1000,
                end: None,
            },
        },
    );
    let running_event = create_part_event(running_part, Some("running".to_string()));

    let result = adapter.translate_to_session_update(running_event);
    assert!(result.is_ok());
    let update = result.unwrap().unwrap();

    match &update {
        SessionUpdate::ToolCallUpdate {
            message_id: msg_id,
            tool_call_id,
            tool_call,
            _meta,
        } => {
            assert_eq!(msg_id, message_id);
            assert_eq!(tool_call_id, part_id);
            assert_eq!(tool_call.id, part_id);
            assert_eq!(tool_call.tool_name, tool_name);
            assert_eq!(tool_call.status, ToolCallStatus::Running);
            assert_eq!(tool_call.raw_input, Some(input.clone()));
            assert!(tool_call.raw_output.is_none());
            assert!(tool_call.error.is_none());

            // Verify metadata throughout lifecycle
            assert!(_meta.is_some());
            let meta = _meta.as_ref().unwrap();
            assert!(meta.provider.is_some());
        }
        _ => panic!("Expected ToolCallUpdate for running state"),
    }

    // Step 3: Completed state → ToolCallUpdate
    let completed_part = create_tool_part(
        session_id,
        message_id,
        part_id,
        tool_name,
        oc::ToolState::Completed {
            input: input.clone(),
            output: output.to_string(),
            title: "bash command".to_string(),
            metadata: serde_json::Value::Null,
            time: oc::PartTime {
                start: 1000,
                end: Some(2000),
            },
            attachments: None,
        },
    );
    let completed_event = create_part_event(completed_part, Some(output.to_string()));

    let result = adapter.translate_to_session_update(completed_event);
    assert!(result.is_ok());
    let update = result.unwrap().unwrap();

    match &update {
        SessionUpdate::ToolCallUpdate {
            message_id: msg_id,
            tool_call_id,
            tool_call,
            _meta,
        } => {
            assert_eq!(msg_id, message_id);
            assert_eq!(tool_call_id, part_id);
            assert_eq!(tool_call.id, part_id);
            assert_eq!(tool_call.tool_name, tool_name);
            assert_eq!(tool_call.status, ToolCallStatus::Completed);
            assert_eq!(tool_call.raw_input, Some(input));
            assert_eq!(
                tool_call.raw_output,
                Some(serde_json::Value::String(output.to_string()))
            );
            assert!(tool_call.error.is_none());

            // Verify metadata in final state
            assert!(_meta.is_some());
        }
        _ => panic!("Expected ToolCallUpdate for completed state"),
    }
}

/// Test status transitions are properly tracked
#[test]
fn test_tool_status_transitions() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_status";
    let message_id = "msg_status";
    let part_id = "tool_status";

    let expected_statuses = vec![
        (oc::ToolState::Pending, ToolCallStatus::Pending),
        (
            oc::ToolState::Running {
                input: json!({}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
            ToolCallStatus::Running,
        ),
        (
            oc::ToolState::Completed {
                input: json!({}),
                output: "done".to_string(),
                title: "".to_string(),
                metadata: serde_json::Value::Null,
                time: oc::PartTime {
                    start: 1000,
                    end: Some(2000),
                },
                attachments: None,
            },
            ToolCallStatus::Completed,
        ),
    ];

    for (i, (oc_state, expected_status)) in expected_statuses.into_iter().enumerate() {
        let part = create_tool_part(session_id, message_id, part_id, "test", oc_state);
        let event = create_part_event(part, Some("delta".to_string()));

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok());
        let update = result.unwrap().unwrap();

        let actual_status = match &update {
            SessionUpdate::ToolCall { tool_call, .. } => &tool_call.status,
            SessionUpdate::ToolCallUpdate { tool_call, .. } => &tool_call.status,
            _ => panic!("Expected tool update at step {}", i),
        };

        assert_eq!(actual_status, &expected_status, "Status mismatch at step {}", i);
    }
}

// ===== Test Case 4: Tool Error =====

/// Test tool error produces ToolCallUpdate with Error status and error field
#[test]
fn test_tool_error_to_tool_call_update() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_error";
    let message_id = "msg_error";
    let part_id = "tool_error";
    let error_msg = "Command not found: invalid_cmd";
    let input = json!({"command": "invalid_cmd"});

    // First, create the tool with pending state
    let pending_part = create_tool_part(
        session_id,
        message_id,
        part_id,
        "bash",
        oc::ToolState::Pending,
    );
    let pending_event = create_part_event(pending_part, None);
    let _ = adapter.translate_to_session_update(pending_event);

    // Now send error state
    let error_part = create_tool_part(
        session_id,
        message_id,
        part_id,
        "bash",
        oc::ToolState::Error {
            input: input.clone(),
            error: error_msg.to_string(),
            metadata: None,
            time: oc::PartTime {
                start: 1000,
                end: Some(2000),
            },
        },
    );
    let error_event = create_part_event(error_part, Some(error_msg.to_string()));

    let result = adapter.translate_to_session_update(error_event);
    assert!(result.is_ok());
    let update = result.unwrap().unwrap();

    match &update {
        SessionUpdate::ToolCallUpdate {
            message_id: msg_id,
            tool_call_id,
            tool_call,
            _meta,
        } => {
            assert_eq!(msg_id, message_id);
            assert_eq!(tool_call_id, part_id);
            assert_eq!(tool_call.status, ToolCallStatus::Error);
            assert_eq!(tool_call.raw_input, Some(input));
            assert!(tool_call.raw_output.is_none());
            assert_eq!(tool_call.error, Some(error_msg.to_string()));

            // Verify error message is captured
            let err = tool_call.error.as_ref().unwrap();
            assert!(err.contains("Command not found"));

            // Verify metadata
            assert!(_meta.is_some());
        }
        _ => panic!("Expected ToolCallUpdate for error state"),
    }
}

// ===== Test Case 5: File Reference =====

/// Test file part produces ResourceLink in AgentMessageChunk
#[test]
fn test_file_reference_to_resource_link() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_file";
    let message_id = "msg_file";
    let part_id = "file_1";
    let filename = "test_document.pdf";
    let uri = "file:///home/user/documents/test_document.pdf";
    let mime_type = "application/pdf";

    let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type);
    let event = create_part_event(part, None);

    let result = adapter.translate_to_session_update(event);
    assert!(result.is_ok());
    let update = result.unwrap().unwrap();

    match &update {
        SessionUpdate::AgentMessageChunk {
            message_id: msg_id,
            content,
            _meta,
        } => {
            assert_eq!(msg_id, message_id);

            // Verify content is ResourceLink
            match content {
                ContentBlock::ResourceLink {
                    uri: content_uri,
                    name,
                    mime_type: content_mime,
                } => {
                    assert_eq!(content_uri, uri);
                    assert_eq!(name.as_ref().unwrap(), filename);
                    assert_eq!(content_mime.as_ref().unwrap(), mime_type);
                }
                _ => panic!("Expected ResourceLink content"),
            }

            // Verify metadata
            assert!(_meta.is_some());
            let meta = _meta.as_ref().unwrap();
            assert!(meta.provider.is_some());
            let provider = meta.provider.as_ref().unwrap();
            assert_eq!(provider.name, "opencode");
            assert!(provider.original_ids.is_some());
            let ids = provider.original_ids.as_ref().unwrap();
            assert_eq!(ids.get("part_id").unwrap(), part_id);
        }
        _ => panic!("Expected AgentMessageChunk with ResourceLink"),
    }
}

/// Test file with different mime types
#[test]
fn test_file_references_various_mime_types() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_files";
    let message_id = "msg_files";

    let test_cases = vec![
        ("file_txt", "data.txt", "file:///data.txt", "text/plain"),
        ("file_json", "config.json", "file:///config.json", "application/json"),
        ("file_img", "screenshot.png", "file:///screenshot.png", "image/png"),
    ];

    for (part_id, filename, uri, mime_type) in test_cases {
        let part = create_file_part(session_id, message_id, part_id, filename, uri, mime_type);
        let event = create_part_event(part, None);

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok());
        let update = result.unwrap().unwrap();

        match update {
            SessionUpdate::AgentMessageChunk { content, .. } => {
                if let ContentBlock::ResourceLink {
                    uri: content_uri,
                    name,
                    mime_type: content_mime,
                } = content
                {
                    assert_eq!(content_uri, uri);
                    assert_eq!(name.as_ref().unwrap(), filename);
                    assert_eq!(content_mime.as_ref().unwrap(), mime_type);
                } else {
                    panic!("Expected ResourceLink for {}", filename);
                }
            }
            _ => panic!("Expected AgentMessageChunk for {}", filename),
        }
    }
}

// ===== Test Case 6: Interleaved Updates Preserve Order =====

/// Test mix of text, reasoning, and tool updates maintain correct order
#[test]
fn test_interleaved_updates_preserve_order() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_interleaved";
    let message_id = "msg_interleaved";

    // Create a realistic sequence of interleaved events
    let events = vec![
        // Text chunk 1
        (
            "text",
            create_part_event(
                create_text_part(session_id, message_id, "part_1", "Let me think"),
                Some("Let me think".to_string()),
            ),
        ),
        // Reasoning chunk 1
        (
            "thought",
            create_part_event(
                create_reasoning_part(session_id, message_id, "reason_1", "Analyzing the task"),
                Some("Analyzing the task".to_string()),
            ),
        ),
        // Tool pending
        (
            "tool_pending",
            create_part_event(
                create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending),
                None,
            ),
        ),
        // Text chunk 2
        (
            "text",
            create_part_event(
                create_text_part(session_id, message_id, "part_2", "Running command"),
                Some("Running command".to_string()),
            ),
        ),
        // Tool running
        (
            "tool_running",
            create_part_event(
                create_tool_part(
                    session_id,
                    message_id,
                    "tool_1",
                    "bash",
                    oc::ToolState::Running {
                        input: json!({"cmd": "ls"}),
                        title: None,
                        metadata: None,
                        time: oc::PartTime {
                            start: 1000,
                            end: None,
                        },
                    },
                ),
                Some("running".to_string()),
            ),
        ),
        // Reasoning chunk 2
        (
            "thought",
            create_part_event(
                create_reasoning_part(session_id, message_id, "reason_2", "Command is executing"),
                Some("Command is executing".to_string()),
            ),
        ),
        // Tool completed
        (
            "tool_completed",
            create_part_event(
                create_tool_part(
                    session_id,
                    message_id,
                    "tool_1",
                    "bash",
                    oc::ToolState::Completed {
                        input: json!({"cmd": "ls"}),
                        output: "file1.txt".to_string(),
                        title: "".to_string(),
                        metadata: serde_json::Value::Null,
                        time: oc::PartTime {
                            start: 1000,
                            end: Some(2000),
                        },
                        attachments: None,
                    },
                ),
                Some("file1.txt".to_string()),
            ),
        ),
        // Text chunk 3
        (
            "text",
            create_part_event(
                create_text_part(session_id, message_id, "part_3", "Done!"),
                Some("Done!".to_string()),
            ),
        ),
    ];

    let expected_types = vec![
        "text",
        "thought",
        "tool_pending",
        "text",
        "tool_running",
        "thought",
        "tool_completed",
        "text",
    ];

    let mut updates = vec![];
    for (i, (_, event)) in events.into_iter().enumerate() {
        let result = adapter.translate_to_session_update(event);
        assert!(result.is_ok(), "Event {} should translate successfully", i);

        let update = result.unwrap();
        assert!(update.is_some(), "Event {} should produce an update", i);
        updates.push(update.unwrap());
    }

    // Verify order is preserved
    assert_eq!(updates.len(), expected_types.len());

    for (i, (update, expected_type)) in updates.iter().zip(expected_types.iter()).enumerate() {
        match (update, *expected_type) {
            (SessionUpdate::AgentMessageChunk { .. }, "text") => {}
            (SessionUpdate::AgentThoughtChunk { .. }, "thought") => {}
            (SessionUpdate::ToolCall { .. }, "tool_pending") => {}
            (SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_running") => {
                assert_eq!(tool_call.status, ToolCallStatus::Running);
            }
            (SessionUpdate::ToolCallUpdate { tool_call, .. }, "tool_completed") => {
                assert_eq!(tool_call.status, ToolCallStatus::Completed);
            }
            _ => panic!(
                "Type mismatch at position {}: expected {}, got {:?}",
                i, expected_type, update
            ),
        }
    }
}

/// Test correct variant for each update type in sequence
#[test]
fn test_correct_variant_for_each_type() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_variants";
    let message_id = "msg_variants";

    // Test text → AgentMessageChunk
    let text_event = create_part_event(
        create_text_part(session_id, message_id, "p1", "text"),
        Some("text".to_string()),
    );
    let result = adapter.translate_to_session_update(text_event).unwrap().unwrap();
    assert!(matches!(result, SessionUpdate::AgentMessageChunk { .. }));

    // Test reasoning → AgentThoughtChunk
    let reason_event = create_part_event(
        create_reasoning_part(session_id, message_id, "p2", "thinking"),
        Some("thinking".to_string()),
    );
    let result = adapter.translate_to_session_update(reason_event).unwrap().unwrap();
    assert!(matches!(result, SessionUpdate::AgentThoughtChunk { .. }));

    // Test tool pending → ToolCall
    let tool_pending_event = create_part_event(
        create_tool_part(session_id, message_id, "p3", "test", oc::ToolState::Pending),
        None,
    );
    let result = adapter.translate_to_session_update(tool_pending_event).unwrap().unwrap();
    assert!(matches!(result, SessionUpdate::ToolCall { .. }));

    // Test tool running → ToolCallUpdate
    let tool_running_event = create_part_event(
        create_tool_part(
            session_id,
            message_id,
            "p3",
            "test",
            oc::ToolState::Running {
                input: json!({}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        ),
        Some("running".to_string()),
    );
    let result = adapter.translate_to_session_update(tool_running_event).unwrap().unwrap();
    assert!(matches!(result, SessionUpdate::ToolCallUpdate { .. }));

    // Test file → AgentMessageChunk with ResourceLink
    let file_event = create_part_event(
        create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"),
        None,
    );
    let result = adapter.translate_to_session_update(file_event).unwrap().unwrap();
    match result {
        SessionUpdate::AgentMessageChunk { content, .. } => {
            assert!(matches!(content, ContentBlock::ResourceLink { .. }));
        }
        _ => panic!("Expected AgentMessageChunk for file"),
    }
}

// ===== Additional Edge Cases =====

/// Test duplicate detection for SessionUpdate translation
#[test]
fn test_duplicate_part_skipped_in_session_update() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_dup";
    let message_id = "msg_dup";
    let part_id = "part_dup";

    // First event with delta
    let part1 = create_text_part(session_id, message_id, part_id, "Hello");
    let event1 = create_part_event(part1, Some("Hello".to_string()));

    let result1 = adapter.translate_to_session_update(event1);
    assert!(result1.is_ok());
    assert!(result1.unwrap().is_some());

    // Second event without delta (completion marker) - should be skipped
    let part2 = create_text_part(session_id, message_id, part_id, "Hello");
    let event2 = create_part_event(part2, None);

    let result2 = adapter.translate_to_session_update(event2);
    assert!(result2.is_ok());
    assert!(result2.unwrap().is_none(), "Duplicate without delta should be skipped");
}

/// Test unsupported event types return error
#[test]
fn test_unsupported_events_return_error() {
    let adapter = OpenCodeAdapter::new();

    // SessionCreated is not supported in translate_to_session_update
    let session_event = oc::Event::SessionCreated {
        properties: oc::SessionEventInfo {
            info: oc::Session {
                id: "sess1".to_string(),
                project_id: "proj1".to_string(),
                directory: "/test".to_string(),
                parent_id: None,
                summary: None,
                share: None,
                title: "Test".to_string(),
                version: "1.0".to_string(),
                time: oc::SessionTime {
                    created: 1000,
                    updated: 1000,
                    compacting: None,
                },
                revert: None,
            },
        },
    };

    let result = adapter.translate_to_session_update(session_event);
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedEvent));
}

/// Test unsupported part types return error
#[test]
fn test_unsupported_part_types_return_error() {
    let adapter = OpenCodeAdapter::new();

    // Snapshot part is not supported
    let snapshot_part = oc::Part::Snapshot(oc::SnapshotPart {
        id: "snap1".to_string(),
        session_id: "sess1".to_string(),
        message_id: "msg1".to_string(),
        snapshot: "snapshot_data".to_string(),
    });

    let event = create_part_event(snapshot_part, None);
    let result = adapter.translate_to_session_update(event);
    assert!(result.is_err());
    assert!(matches!(result.unwrap_err(), TranslationError::UnsupportedPartType));
}

/// Test multiple concurrent tool calls tracked independently
#[test]
fn test_multiple_concurrent_tools_independent_tracking() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_multi";
    let message_id = "msg_multi";

    // Tool 1: Pending
    let tool1_pending = create_part_event(
        create_tool_part(session_id, message_id, "tool_1", "bash", oc::ToolState::Pending),
        None,
    );
    let result1 = adapter.translate_to_session_update(tool1_pending).unwrap().unwrap();
    assert!(matches!(result1, SessionUpdate::ToolCall { .. }));

    // Tool 2: Pending (different tool)
    let tool2_pending = create_part_event(
        create_tool_part(session_id, message_id, "tool_2", "read", oc::ToolState::Pending),
        None,
    );
    let result2 = adapter.translate_to_session_update(tool2_pending).unwrap().unwrap();
    assert!(matches!(result2, SessionUpdate::ToolCall { .. }));

    // Tool 1: Running → should be ToolCallUpdate
    let tool1_running = create_part_event(
        create_tool_part(
            session_id,
            message_id,
            "tool_1",
            "bash",
            oc::ToolState::Running {
                input: json!({}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        ),
        Some("r".to_string()),
    );
    let result3 = adapter.translate_to_session_update(tool1_running).unwrap().unwrap();
    match result3 {
        SessionUpdate::ToolCallUpdate { tool_call_id, .. } => {
            assert_eq!(tool_call_id, "tool_1");
        }
        _ => panic!("Expected ToolCallUpdate for tool_1"),
    }

    // Tool 2: Running → should also be ToolCallUpdate
    let tool2_running = create_part_event(
        create_tool_part(
            session_id,
            message_id,
            "tool_2",
            "read",
            oc::ToolState::Running {
                input: json!({}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        ),
        Some("r".to_string()),
    );
    let result4 = adapter.translate_to_session_update(tool2_running).unwrap().unwrap();
    match result4 {
        SessionUpdate::ToolCallUpdate { tool_call_id, .. } => {
            assert_eq!(tool_call_id, "tool_2");
        }
        _ => panic!("Expected ToolCallUpdate for tool_2"),
    }
}

/// Test metadata present in all update types
#[test]
fn test_metadata_present_in_all_updates() {
    let adapter = OpenCodeAdapter::new();
    let session_id = "sess_meta";
    let message_id = "msg_meta";

    // Text
    let text_event = create_part_event(
        create_text_part(session_id, message_id, "p1", "text"),
        Some("text".to_string()),
    );
    let text_update = adapter.translate_to_session_update(text_event).unwrap().unwrap();
    match text_update {
        SessionUpdate::AgentMessageChunk { _meta, .. } => {
            assert!(_meta.is_some(), "Text chunk should have metadata");
        }
        _ => panic!("Expected AgentMessageChunk"),
    }

    // Reasoning
    let reason_event = create_part_event(
        create_reasoning_part(session_id, message_id, "p2", "reason"),
        Some("reason".to_string()),
    );
    let reason_update = adapter.translate_to_session_update(reason_event).unwrap().unwrap();
    match reason_update {
        SessionUpdate::AgentThoughtChunk { _meta, .. } => {
            assert!(_meta.is_some(), "Thought chunk should have metadata");
        }
        _ => panic!("Expected AgentThoughtChunk"),
    }

    // Tool
    let tool_event = create_part_event(
        create_tool_part(session_id, message_id, "p3", "bash", oc::ToolState::Pending),
        None,
    );
    let tool_update = adapter.translate_to_session_update(tool_event).unwrap().unwrap();
    match tool_update {
        SessionUpdate::ToolCall { _meta, .. } => {
            assert!(_meta.is_some(), "ToolCall should have metadata");
        }
        _ => panic!("Expected ToolCall"),
    }

    // File
    let file_event = create_part_event(
        create_file_part(session_id, message_id, "p4", "f.txt", "file:///f.txt", "text/plain"),
        None,
    );
    let file_update = adapter.translate_to_session_update(file_event).unwrap().unwrap();
    match file_update {
        SessionUpdate::AgentMessageChunk { _meta, .. } => {
            assert!(_meta.is_some(), "File chunk should have metadata");
        }
        _ => panic!("Expected AgentMessageChunk"),
    }
}
