use crate::events::Event;
use crate::types::{ContentBlock, Meta, ProviderMeta, SessionUpdate, ToolCall, ToolCallStatus};
use crate::{
    Message, MessageMetadata, MessagePart, MessageRole, MessageStatus, Session, SessionMetadata,
};
use chrono::{DateTime, TimeZone, Utc};
use opencode_client::types as oc;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};

/// Stateful adapter that tracks message and part IDs to prevent duplicates
#[derive(Clone)]
pub struct OpenCodeAdapter {
    state: Arc<Mutex<AdapterState>>,
}

#[derive(Default)]
struct AdapterState {
    /// Track which messages we've sent "started" events for
    started_messages: HashSet<String>,
    /// Track which messages we've sent "completed" events for
    completed_messages: HashSet<String>,
    /// Track which parts we've seen (to differentiate new vs update)
    seen_parts: HashSet<String>,
    /// Track which sessions have had their system message set
    sessions_with_system: HashSet<String>,
    /// Track tool calls we've seen (for initial ToolCall vs ToolCallUpdate)
    tool_calls_seen: HashSet<String>,
    /// Track message roles to determine UserMessageChunk vs AgentMessageChunk
    message_roles: HashMap<String, MessageRole>,
}

#[derive(Debug)]
pub enum TranslationError {
    UnknownEvent,
    UnsupportedEvent,
    UnsupportedPartType,
    InvalidTimestamp,
    MissingField(String),
    Duplicate, // Event should be skipped (duplicate)
}

impl std::fmt::Display for TranslationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TranslationError::UnknownEvent => write!(f, "Unknown event type"),
            TranslationError::UnsupportedEvent => write!(f, "Unsupported event type"),
            TranslationError::UnsupportedPartType => write!(f, "Unsupported part type"),
            TranslationError::InvalidTimestamp => write!(f, "Invalid timestamp"),
            TranslationError::MissingField(field) => write!(f, "Missing required field: {}", field),
            TranslationError::Duplicate => write!(f, "Duplicate event (filtered)"),
        }
    }
}

impl std::error::Error for TranslationError {}

impl Default for OpenCodeAdapter {
    fn default() -> Self {
        Self::new()
    }
}

impl OpenCodeAdapter {
    pub fn new() -> Self {
        Self {
            state: Arc::new(Mutex::new(AdapterState::default())),
        }
    }

    /// Translate an OpenCode event to a Dirigent event
    pub fn translate_event(&self, oc_event: oc::Event) -> Result<Event, TranslationError> {
        match oc_event {
            oc::Event::SessionCreated { properties } => Ok(Event::SessionCreated {
                connector_id: String::new(), // Placeholder - connector will set this
                session: Self::translate_session(properties.info)?,
            }),

            oc::Event::SessionUpdated { properties } => Ok(Event::SessionUpdated {
                connector_id: String::new(), // Placeholder - connector will set this
                session: Self::translate_session(properties.info)?,
            }),

            oc::Event::SessionDeleted { properties } => Ok(Event::SessionDeleted {
                session_id: properties.info.id,
            }),

            oc::Event::SessionIdle { properties } => Ok(Event::SessionIdle {
                connector_id: String::new(), // Placeholder - connector will set this
                session_id: properties.session_id,
            }),

            oc::Event::MessageUpdated { properties } => {
                let msg_id = match &properties.info {
                    oc::Message::User(u) => u.id.clone(),
                    oc::Message::Assistant(a) => a.id.clone(),
                };

                // Extract session_id and system message before translation
                let (session_id, system_message) = match &properties.info {
                    oc::Message::Assistant(a) if !a.system.is_empty() => {
                        (a.session_id.clone(), a.system.first().cloned())
                    }
                    oc::Message::User(u) => (u.session_id.clone(), None),
                    _ => (String::new(), None),
                };

                // Check if completed
                let is_completed = if let oc::Message::Assistant(ref a) = properties.info {
                    a.time.completed.is_some()
                } else {
                    true // User messages are always completed
                };

                let msg = Self::translate_message(properties.info)?;

                // Store the message role for later part translation
                let mut state = self.state.lock().unwrap();
                state.message_roles.insert(msg_id.clone(), msg.role.clone());

                // Track system message for this session (only emit once)
                if let Some(sys_msg) = system_message {
                    if !state.sessions_with_system.contains(&session_id) {
                        state.sessions_with_system.insert(session_id.clone());
                        drop(state); // Release lock before returning

                        // Return SessionSystemMessageSet event instead of message event
                        // The message event will come on the next update
                        return Ok(Event::SessionSystemMessageSet {
                            session_id,
                            system_message: sys_msg,
                        });
                    }
                }

                if is_completed {
                    if state.completed_messages.contains(&msg_id) {
                        return Err(TranslationError::Duplicate);
                    }
                    state.completed_messages.insert(msg_id);
                    Ok(Event::MessageCompleted {
                        connector_id: String::new(), // Placeholder - connector will set this
                        message: msg,
                    })
                } else {
                    if state.started_messages.contains(&msg_id) {
                        return Err(TranslationError::Duplicate);
                    }
                    state.started_messages.insert(msg_id);
                    Ok(Event::MessageStarted {
                        connector_id: String::new(), // Placeholder - connector will set this
                        message: msg,
                    })
                }
            }

            oc::Event::MessagePartUpdated { properties } => {
                // Extract session_id from the part before translation
                let session_id = Self::extract_session_id_from_part(&properties.part)?;

                // Use the new translate_to_session_update method
                if let Some(update) =
                    self.translate_to_session_update(oc::Event::MessagePartUpdated { properties })?
                {
                    Ok(Event::SessionUpdate {
                        connector_id: String::new(), // Placeholder - connector will set this
                        session_id,
                        update,
                    })
                } else {
                    // Duplicate or non-delta update - skip
                    Err(TranslationError::Duplicate)
                }
            }

            oc::Event::ServerConnected { .. } => Ok(Event::Connected),

            oc::Event::Unknown => Err(TranslationError::UnknownEvent),

            _ => {
                // Log unhandled event
                eprintln!("[OpenCodeAdapter] Unhandled event type: {:?}", oc_event);
                Err(TranslationError::UnsupportedEvent)
            }
        }
    }

    /// Translate an OpenCode event to a SessionUpdate (ACP-style streaming updates)
    /// Returns Ok(None) if the event should be skipped (e.g., duplicates)
    pub fn translate_to_session_update(
        &self,
        oc_event: oc::Event,
    ) -> Result<Option<SessionUpdate>, TranslationError> {
        match oc_event {
            // Only translate MessagePartUpdated events for now
            // Other events will be handled in future tasks
            oc::Event::MessagePartUpdated { properties } => {
                self.translate_part_to_update(properties)
            }
            _ => Err(TranslationError::UnsupportedEvent),
        }
    }

    /// Extract session_id from an OpenCode part
    /// Returns an error if the part type doesn't contain a session_id
    fn extract_session_id_from_part(part: &oc::Part) -> Result<String, TranslationError> {
        let session_id = match part {
            oc::Part::Text(t) => &t.session_id,
            oc::Part::Reasoning(r) => &r.session_id,
            oc::Part::Tool(t) => &t.session_id,
            oc::Part::File(f) => &f.session_id,
            // For unsupported types, return error
            _ => return Err(TranslationError::UnsupportedPartType),
        };
        Ok(session_id.clone())
    }

    /// Build provider metadata from an OpenCode part
    /// Extracts session_id, message_id, and part_id for debugging and traceability
    fn build_meta(part: &oc::Part) -> Meta {
        let (session_id, message_id, part_id) = match part {
            oc::Part::Text(t) => (&t.session_id, &t.message_id, &t.id),
            oc::Part::Reasoning(r) => (&r.session_id, &r.message_id, &r.id),
            oc::Part::Tool(t) => (&t.session_id, &t.message_id, &t.id),
            oc::Part::File(f) => (&f.session_id, &f.message_id, &f.id),
            // For unsupported types, return empty meta (they'll fail translation anyway)
            _ => return Meta::default(),
        };

        Meta {
            provider: Some(ProviderMeta {
                name: "opencode".to_string(),
                original_ids: Some(HashMap::from([
                    ("session_id".to_string(), session_id.clone()),
                    ("message_id".to_string(), message_id.clone()),
                    ("part_id".to_string(), part_id.clone()),
                ])),
                raw_excerpt: None,
            }),
            extra: HashMap::new(),
        }
    }

    /// Translate a MessagePartUpdated event to a SessionUpdate
    /// Returns Ok(None) if this is a duplicate or should be skipped
    fn translate_part_to_update(
        &self,
        properties: oc::MessagePartEventInfo,
    ) -> Result<Option<SessionUpdate>, TranslationError> {
        let part = &properties.part;

        // Extract common fields from the part
        let (message_id, part_id) = match part {
            oc::Part::Text(t) => (t.message_id.clone(), t.id.clone()),
            oc::Part::Reasoning(r) => (r.message_id.clone(), r.id.clone()),
            oc::Part::Tool(t) => (t.message_id.clone(), t.id.clone()),
            oc::Part::File(f) => (f.message_id.clone(), f.id.clone()),
            _ => return Err(TranslationError::UnsupportedPartType),
        };

        // Track parts to avoid sending duplicates
        let mut state = self.state.lock().unwrap();
        let is_new_part = state.seen_parts.insert(part_id.clone());

        // If this is an update to an existing part with no delta, skip it
        if !is_new_part && properties.delta.is_none() {
            return Ok(None);
        }
        drop(state); // Release lock

        // Build metadata once for this part
        let meta = Self::build_meta(part);

        // Translate the part based on its type
        match part {
            oc::Part::Text(t) => {
                // Use delta instead of full text to avoid duplication
                let text = properties.delta.unwrap_or_else(|| t.text.clone());

                // Determine if this is a user or assistant message
                // by looking up the message_id in our tracked roles
                let state = self.state.lock().unwrap();
                let role = state.message_roles.get(&message_id).cloned();
                drop(state);

                match role {
                    Some(MessageRole::User) => Ok(Some(SessionUpdate::UserMessageChunk {
                        message_id,
                        content: ContentBlock::Text { text },
                        _meta: Some(meta),
                    })),
                    Some(MessageRole::Assistant) | None => {
                        // Default to assistant for backward compatibility
                        // (in case message wasn't tracked or is from old events)
                        Ok(Some(SessionUpdate::AgentMessageChunk {
                            message_id,
                            content: ContentBlock::Text { text },
                            _meta: Some(meta),
                        }))
                    }
                }
            }

            oc::Part::Reasoning(r) => {
                // Use delta instead of full text to avoid duplication
                let text = properties.delta.unwrap_or_else(|| r.text.clone());

                Ok(Some(SessionUpdate::AgentThoughtChunk {
                    message_id,
                    content: ContentBlock::Text { text },
                    _meta: Some(meta),
                }))
            }

            oc::Part::Tool(t) => {
                // Map OpenCode tool state to ToolCallStatus
                let (status, raw_input, raw_output, error) = match &t.state {
                    oc::ToolState::Pending => (ToolCallStatus::Pending, None, None, None),
                    oc::ToolState::Running { input, .. } => {
                        (ToolCallStatus::Running, Some(input.clone()), None, None)
                    }
                    oc::ToolState::Completed { input, output, .. } => (
                        ToolCallStatus::Completed,
                        Some(input.clone()),
                        Some(serde_json::Value::String(output.clone())),
                        None,
                    ),
                    oc::ToolState::Error { input, error, .. } => (
                        ToolCallStatus::Error,
                        Some(input.clone()),
                        None,
                        Some(error.clone()),
                    ),
                };

                let tool_call = ToolCall {
                    id: t.id.clone(),
                    tool_name: t.tool.clone(),
                    status,
                    content: vec![], // Tool output will be added as content blocks in future
                    raw_input,
                    raw_output,
                    title: None,
                    error,
                    metadata: t.metadata.clone(),
                    origin: None, // OpenCode tools - origin not specified
                };

                // Track tool calls: first occurrence → ToolCall, subsequent → ToolCallUpdate
                let mut state = self.state.lock().unwrap();
                let is_new_tool_call = state.tool_calls_seen.insert(t.id.clone());
                drop(state); // Release lock

                if is_new_tool_call {
                    Ok(Some(SessionUpdate::ToolCall {
                        message_id,
                        tool_call,
                        _meta: Some(meta),
                    }))
                } else {
                    Ok(Some(SessionUpdate::ToolCallUpdate {
                        message_id,
                        tool_call_id: t.id.clone(),
                        tool_call,
                        _meta: Some(meta),
                    }))
                }
            }

            oc::Part::File(f) => {
                // File parts become ResourceLink content blocks
                // Determine the appropriate chunk type (assume agent for now)
                Ok(Some(SessionUpdate::AgentMessageChunk {
                    message_id,
                    content: ContentBlock::ResourceLink {
                        uri: f.url.clone(),
                        name: f.filename.clone(),
                        mime_type: Some(f.mime.clone()),
                    },
                    _meta: Some(meta),
                }))
            }

            _ => Err(TranslationError::UnsupportedPartType),
        }
    }

    /// Translate an OpenCode session to a Dirigent session
    fn translate_session(oc_session: oc::Session) -> Result<Session, TranslationError> {
        Ok(Session {
            id: oc_session.id,
            title: oc_session.title,
            created_at: timestamp_to_datetime(oc_session.time.created)?,
            updated_at: timestamp_to_datetime(oc_session.time.updated)?,
            metadata: SessionMetadata {
                project_path: oc_session.directory,
                model: None,          // Could extract from assistant messages if needed
                total_messages: 0,    // Would need to be calculated separately
                system_message: None, // Will be set from first assistant message
                current_mode_id: None,
                _meta: None,
                project_id: None,
            },
            cwd: None, // OpenCode doesn't expose cwd separately from project_path
            models: None, // OpenCode doesn't provide ACP model state
            modes: None,         // OpenCode doesn't provide ACP mode state
            config_options: None,
            acp_client_id: None, // OpenCode doesn't have ACP client ID
        })
    }

    /// Translate an OpenCode message to a Dirigent message
    /// Note: System messages are extracted separately and stored at the session level
    fn translate_message(oc_msg: oc::Message) -> Result<Message, TranslationError> {
        let (id, session_id, role, created_at, status, metadata) = match oc_msg {
            oc::Message::User(u) => (
                u.id,
                u.session_id,
                MessageRole::User,
                timestamp_to_datetime(u.time.created)?,
                MessageStatus::Completed, // User messages are always complete
                None,                     // User messages don't have metadata
            ),
            oc::Message::Assistant(a) => {
                let status = if let Some(err) = a.error {
                    MessageStatus::Failed {
                        error: format_message_error(&err),
                    }
                } else if a.time.completed.is_some() {
                    MessageStatus::Completed
                } else {
                    MessageStatus::Streaming
                };

                // Extract metadata from assistant message
                let metadata = Some(MessageMetadata {
                    cost: Some(a.cost),
                    tokens_input: Some(a.tokens.input),
                    tokens_output: Some(a.tokens.output),
                    response_time_ms: None,
                    latency_ms: None,
                    model: a.model_id.clone(),
                    other: None,
                });

                (
                    a.id,
                    a.session_id,
                    MessageRole::Assistant,
                    timestamp_to_datetime(a.time.created)?,
                    status,
                    metadata,
                )
            }
        };

        Ok(Message {
            id,
            session_id,
            role,
            created_at,
            content: vec![], // Parts come separately via MessagePartUpdated events
            status,
            metadata,
        })
    }

    /// Translate an OpenCode part directly to a Dirigent message part (for list operations)
    /// This is used when loading message history, not for streaming updates.
    /// Returns just the MessagePart (session_id and message_id are on the message itself)
    pub fn translate_part_for_list(oc_part: oc::Part) -> Result<MessagePart, TranslationError> {
        let (_, _, _, part) = Self::translate_part(oc_part)?;
        Ok(part)
    }

    /// Translate an OpenCode part to a Dirigent message part (internal helper)
    /// Returns (session_id, message_id, part_id, part)
    fn translate_part(
        oc_part: oc::Part,
    ) -> Result<(String, String, String, MessagePart), TranslationError> {
        match oc_part {
            oc::Part::Text(t) => Ok((
                t.session_id.clone(),
                t.message_id.clone(),
                t.id,
                MessagePart::Text { text: t.text },
            )),

            oc::Part::Reasoning(r) => Ok((
                r.session_id.clone(),
                r.message_id.clone(),
                r.id,
                MessagePart::Thinking { text: r.text },
            )),

            oc::Part::Tool(t) => {
                let (input, output) = match t.state {
                    oc::ToolState::Pending => (serde_json::Value::Null, None),
                    oc::ToolState::Running { input, .. } => (input, None),
                    oc::ToolState::Completed { input, output, .. } => {
                        (input, Some(serde_json::Value::String(output)))
                    }
                    oc::ToolState::Error { input, error, .. } => {
                        (input, Some(serde_json::json!({ "error": error })))
                    }
                };

                Ok((
                    t.session_id.clone(),
                    t.message_id.clone(),
                    t.id,
                    MessagePart::Tool {
                        tool: t.tool,
                        tool_call_id: None,
                        input,
                        output,
                    },
                ))
            }

            oc::Part::File(f) => Ok((
                f.session_id.clone(),
                f.message_id.clone(),
                f.id,
                MessagePart::File {
                    path: f.filename.unwrap_or_else(|| f.url.clone()),
                    content: f.url, // In Dirigent protocol, we store the URL as content
                },
            )),

            // For now, we skip these part types - they could be added later
            oc::Part::StepStart(_)
            | oc::Part::StepFinish(_)
            | oc::Part::Snapshot(_)
            | oc::Part::Patch(_)
            | oc::Part::Agent(_)
            | oc::Part::Retry(_) => Err(TranslationError::UnsupportedPartType),

            oc::Part::Unknown => Err(TranslationError::UnsupportedPartType),
        }
    }
} // End impl OpenCodeAdapter

/// Convert Unix timestamp (milliseconds) to DateTime<Utc>
fn timestamp_to_datetime(timestamp: u64) -> Result<DateTime<Utc>, TranslationError> {
    Utc.timestamp_millis_opt(timestamp as i64)
        .single()
        .ok_or(TranslationError::InvalidTimestamp)
}

/// Format a message error into a user-friendly string
fn format_message_error(error: &oc::MessageError) -> String {
    match error {
        oc::MessageError::ProviderAuthError { data } => {
            format!(
                "Authentication error for {}: {}",
                data.provider_id, data.message
            )
        }
        oc::MessageError::UnknownError { data } => {
            format!("Unknown error: {}", data.message)
        }
        oc::MessageError::MessageOutputLengthError => "Message output length exceeded".to_string(),
        oc::MessageError::MessageAbortedError { data } => {
            format!("Message aborted: {}", data.message)
        }
        oc::MessageError::ApiError { data } => {
            if let Some(status) = data.status_code {
                format!("API error ({}): {}", status, data.message)
            } else {
                format!("API error: {}", data.message)
            }
        }
    }
}

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

    /// Helper to 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,
            }),
        })
    }

    /// Helper to 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,
            }),
        })
    }

    /// Helper to create a tool part for testing
    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,
        })
    }

    /// Helper to 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,
        })
    }

    #[test]
    fn test_translate_text_part_to_agent_message_chunk() {
        let adapter = OpenCodeAdapter::new();
        let part = create_text_part("sess1", "msg1", "part1", "Hello world");

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part,
                delta: Some("Hello world".to_string()),
            },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::AgentMessageChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(
                    content,
                    ContentBlock::Text {
                        text: "Hello world".to_string()
                    }
                );
                // Verify metadata is present with provider info
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part1");
            }
            _ => panic!("Expected AgentMessageChunk"),
        }
    }

    #[test]
    fn test_translate_reasoning_part_to_agent_thought_chunk() {
        let adapter = OpenCodeAdapter::new();
        let part = create_reasoning_part("sess1", "msg1", "part2", "Thinking about the problem...");

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part,
                delta: Some("Thinking about the problem...".to_string()),
            },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::AgentThoughtChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(
                    content,
                    ContentBlock::Text {
                        text: "Thinking about the problem...".to_string()
                    }
                );
                // Verify metadata is present with provider info
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part2");
            }
            _ => panic!("Expected AgentThoughtChunk"),
        }
    }

    #[test]
    fn test_translate_tool_part_pending_to_tool_call() {
        let adapter = OpenCodeAdapter::new();
        let part = create_tool_part("sess1", "msg1", "part3", "bash", oc::ToolState::Pending);

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo { part, delta: None },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::ToolCall {
                message_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(tool_call.id, "part3");
                assert_eq!(tool_call.tool_name, "bash");
                assert_eq!(tool_call.status, ToolCallStatus::Pending);
                assert_eq!(tool_call.raw_input, None);
                assert_eq!(tool_call.raw_output, None);
                assert_eq!(tool_call.error, None);
                // Verify metadata is present
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part3");
            }
            _ => panic!("Expected ToolCall"),
        }
    }

    #[test]
    fn test_translate_tool_part_running_to_tool_call() {
        let adapter = OpenCodeAdapter::new();
        let input = serde_json::json!({"command": "ls -la"});
        let part = create_tool_part(
            "sess1",
            "msg1",
            "part4",
            "bash",
            oc::ToolState::Running {
                input: input.clone(),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        );

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo { part, delta: None },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::ToolCall {
                message_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(tool_call.id, "part4");
                assert_eq!(tool_call.tool_name, "bash");
                assert_eq!(tool_call.status, ToolCallStatus::Running);
                assert_eq!(tool_call.raw_input, Some(input));
                assert_eq!(tool_call.raw_output, None);
                assert_eq!(tool_call.error, None);
                // Verify metadata is present
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part4");
            }
            _ => panic!("Expected ToolCall"),
        }
    }

    #[test]
    fn test_translate_tool_part_completed_to_tool_call_update() {
        let adapter = OpenCodeAdapter::new();
        let input = serde_json::json!({"command": "ls -la"});
        let output = "file1.txt\nfile2.txt";

        // First, send the pending state to mark it as seen
        let pending_part =
            create_tool_part("sess1", "msg1", "part5", "bash", oc::ToolState::Pending);
        let pending_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: pending_part,
                delta: None,
            },
        };
        let _ = adapter.translate_to_session_update(pending_event);

        // Now send the completed state
        let completed_part = create_tool_part(
            "sess1",
            "msg1",
            "part5",
            "bash",
            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 event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: completed_part,
                delta: Some(output.to_string()),
            },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::ToolCallUpdate {
                message_id,
                tool_call_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(tool_call_id, "part5");
                assert_eq!(tool_call.id, "part5");
                assert_eq!(tool_call.tool_name, "bash");
                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_eq!(tool_call.error, None);
                // Verify metadata is present
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part5");
            }
            _ => panic!("Expected ToolCallUpdate"),
        }
    }

    #[test]
    fn test_translate_tool_part_error_to_tool_call_update() {
        let adapter = OpenCodeAdapter::new();
        let input = serde_json::json!({"command": "invalid_command"});
        let error_msg = "Command not found";

        // First, send the pending state
        let pending_part =
            create_tool_part("sess1", "msg1", "part6", "bash", oc::ToolState::Pending);
        let pending_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: pending_part,
                delta: None,
            },
        };
        let _ = adapter.translate_to_session_update(pending_event);

        // Now send the error state
        let error_part = create_tool_part(
            "sess1",
            "msg1",
            "part6",
            "bash",
            oc::ToolState::Error {
                input: input.clone(),
                error: error_msg.to_string(),
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: Some(2000),
                },
            },
        );

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: error_part,
                delta: Some(error_msg.to_string()),
            },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::ToolCallUpdate {
                message_id,
                tool_call_id,
                tool_call,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(tool_call_id, "part6");
                assert_eq!(tool_call.id, "part6");
                assert_eq!(tool_call.tool_name, "bash");
                assert_eq!(tool_call.status, ToolCallStatus::Error);
                assert_eq!(tool_call.raw_input, Some(input));
                assert_eq!(tool_call.raw_output, None);
                assert_eq!(tool_call.error, Some(error_msg.to_string()));
                // Verify metadata is present
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part6");
            }
            _ => panic!("Expected ToolCallUpdate"),
        }
    }

    #[test]
    fn test_translate_file_part_to_resource_link() {
        let adapter = OpenCodeAdapter::new();
        let part = create_file_part(
            "sess1",
            "msg1",
            "part7",
            "test.txt",
            "file:///path/to/test.txt",
            "text/plain",
        );

        let event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo { part, delta: None },
        };

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

        let update = result.unwrap();
        assert!(update.is_some());

        match update.unwrap() {
            SessionUpdate::AgentMessageChunk {
                message_id,
                content,
                _meta,
            } => {
                assert_eq!(message_id, "msg1");
                assert_eq!(
                    content,
                    ContentBlock::ResourceLink {
                        uri: "file:///path/to/test.txt".to_string(),
                        name: Some("test.txt".to_string()),
                        mime_type: Some("text/plain".to_string()),
                    }
                );
                // Verify metadata is present
                assert!(_meta.is_some());
                let meta = _meta.unwrap();
                assert!(meta.provider.is_some());
                let provider = meta.provider.unwrap();
                assert_eq!(provider.name, "opencode");
                assert!(provider.original_ids.is_some());
                let ids = provider.original_ids.unwrap();
                assert_eq!(ids.get("session_id").unwrap(), "sess1");
                assert_eq!(ids.get("message_id").unwrap(), "msg1");
                assert_eq!(ids.get("part_id").unwrap(), "part7");
            }
            _ => panic!("Expected AgentMessageChunk with ResourceLink"),
        }
    }

    #[test]
    fn test_duplicate_part_without_delta_returns_none() {
        let adapter = OpenCodeAdapter::new();
        let part = create_text_part("sess1", "msg1", "part8", "Hello");

        // First update with delta
        let event1 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: part.clone(),
                delta: Some("Hello".to_string()),
            },
        };

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

        // Second update without delta (should be skipped)
        let event2 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo { part, delta: None },
        };

        let result2 = adapter.translate_to_session_update(event2);
        assert!(result2.is_ok());
        assert!(result2.unwrap().is_none()); // Should return None (skip)
    }

    #[test]
    fn test_unsupported_event_type_returns_error() {
        let adapter = OpenCodeAdapter::new();

        // SessionCreated is not supported in translate_to_session_update
        let 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(event);
        assert!(result.is_err());
        assert!(matches!(result, Err(TranslationError::UnsupportedEvent)));
    }

    #[test]
    fn test_unsupported_part_type_returns_error() {
        let adapter = OpenCodeAdapter::new();

        // Snapshot is not supported
        let 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 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo { part, delta: None },
        };

        let result = adapter.translate_to_session_update(event);
        assert!(result.is_err());
        assert!(matches!(result, Err(TranslationError::UnsupportedPartType)));
    }

    #[test]
    fn test_tool_call_lifecycle_tracking() {
        let adapter = OpenCodeAdapter::new();
        let tool_id = "tool_lifecycle_test";
        let message_id = "msg_lifecycle";
        let input = serde_json::json!({"command": "echo hello"});
        let output = "hello";

        // Step 1: Pending state (first occurrence) → ToolCall
        let pending_part =
            create_tool_part("sess1", message_id, tool_id, "bash", oc::ToolState::Pending);
        let pending_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: pending_part,
                delta: None,
            },
        };

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

        match update.unwrap() {
            SessionUpdate::ToolCall {
                message_id: msg_id,
                tool_call,
                ..
            } => {
                assert_eq!(msg_id, message_id);
                assert_eq!(tool_call.id, tool_id);
                assert_eq!(tool_call.status, ToolCallStatus::Pending);
            }
            _ => panic!("Expected ToolCall for first occurrence"),
        }

        // Step 2: Running state (update) → ToolCallUpdate
        let running_part = create_tool_part(
            "sess1",
            message_id,
            tool_id,
            "bash",
            oc::ToolState::Running {
                input: input.clone(),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        );
        let running_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: running_part,
                delta: Some("running".to_string()),
            },
        };

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

        match update.unwrap() {
            SessionUpdate::ToolCallUpdate {
                message_id: msg_id,
                tool_call_id,
                tool_call,
                ..
            } => {
                assert_eq!(msg_id, message_id);
                assert_eq!(tool_call_id, tool_id);
                assert_eq!(tool_call.id, tool_id);
                assert_eq!(tool_call.status, ToolCallStatus::Running);
                assert_eq!(tool_call.raw_input, Some(input.clone()));
            }
            _ => panic!("Expected ToolCallUpdate for second occurrence"),
        }

        // Step 3: Completed state (update) → ToolCallUpdate
        let completed_part = create_tool_part(
            "sess1",
            message_id,
            tool_id,
            "bash",
            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 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: completed_part,
                delta: Some(output.to_string()),
            },
        };

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

        match update.unwrap() {
            SessionUpdate::ToolCallUpdate {
                message_id: msg_id,
                tool_call_id,
                tool_call,
                ..
            } => {
                assert_eq!(msg_id, message_id);
                assert_eq!(tool_call_id, tool_id);
                assert_eq!(tool_call.id, tool_id);
                assert_eq!(tool_call.status, ToolCallStatus::Completed);
                assert_eq!(tool_call.raw_input, Some(input.clone()));
                assert_eq!(
                    tool_call.raw_output,
                    Some(serde_json::Value::String(output.to_string()))
                );
            }
            _ => panic!("Expected ToolCallUpdate for third occurrence"),
        }
    }

    #[test]
    fn test_tool_call_error_lifecycle() {
        let adapter = OpenCodeAdapter::new();
        let tool_id = "tool_error_test";
        let message_id = "msg_error";
        let input = serde_json::json!({"command": "invalid"});
        let error_msg = "Command failed";

        // Step 1: Pending → ToolCall
        let pending_part =
            create_tool_part("sess1", message_id, tool_id, "bash", oc::ToolState::Pending);
        let pending_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: pending_part,
                delta: None,
            },
        };

        let result = adapter.translate_to_session_update(pending_event);
        assert!(result.is_ok());
        assert!(matches!(
            result.unwrap(),
            Some(SessionUpdate::ToolCall { .. })
        ));

        // Step 2: Running → ToolCallUpdate
        let running_part = create_tool_part(
            "sess1",
            message_id,
            tool_id,
            "bash",
            oc::ToolState::Running {
                input: input.clone(),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        );
        let running_event = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: running_part,
                delta: Some("running".to_string()),
            },
        };

        let result = adapter.translate_to_session_update(running_event);
        assert!(result.is_ok());
        assert!(matches!(
            result.unwrap(),
            Some(SessionUpdate::ToolCallUpdate { .. })
        ));

        // Step 3: Error → ToolCallUpdate
        let error_part = create_tool_part(
            "sess1",
            message_id,
            tool_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 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: error_part,
                delta: Some(error_msg.to_string()),
            },
        };

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

        match update.unwrap() {
            SessionUpdate::ToolCallUpdate {
                message_id: msg_id,
                tool_call_id,
                tool_call,
                ..
            } => {
                assert_eq!(msg_id, message_id);
                assert_eq!(tool_call_id, tool_id);
                assert_eq!(tool_call.status, ToolCallStatus::Error);
                assert_eq!(tool_call.error, Some(error_msg.to_string()));
            }
            _ => panic!("Expected ToolCallUpdate for error state"),
        }
    }

    #[test]
    fn test_multiple_concurrent_tool_calls_tracked_independently() {
        let adapter = OpenCodeAdapter::new();
        let message_id = "msg_multi";

        // Tool 1: Pending → ToolCall
        let tool1_pending =
            create_tool_part("sess1", message_id, "tool1", "bash", oc::ToolState::Pending);
        let event1 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: tool1_pending,
                delta: None,
            },
        };
        let result1 = adapter.translate_to_session_update(event1);
        assert!(matches!(
            result1.unwrap(),
            Some(SessionUpdate::ToolCall { .. })
        ));

        // Tool 2: Pending → ToolCall (different tool call)
        let tool2_pending =
            create_tool_part("sess1", message_id, "tool2", "read", oc::ToolState::Pending);
        let event2 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: tool2_pending,
                delta: None,
            },
        };
        let result2 = adapter.translate_to_session_update(event2);
        assert!(matches!(
            result2.unwrap(),
            Some(SessionUpdate::ToolCall { .. })
        ));

        // Tool 1: Running → ToolCallUpdate
        let tool1_running = create_tool_part(
            "sess1",
            message_id,
            "tool1",
            "bash",
            oc::ToolState::Running {
                input: serde_json::json!({"cmd": "ls"}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        );
        let event3 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: tool1_running,
                delta: Some("running".to_string()),
            },
        };
        let result3 = adapter.translate_to_session_update(event3);
        assert!(matches!(
            result3.unwrap(),
            Some(SessionUpdate::ToolCallUpdate { .. })
        ));

        // Tool 2: Running → ToolCallUpdate
        let tool2_running = create_tool_part(
            "sess1",
            message_id,
            "tool2",
            "read",
            oc::ToolState::Running {
                input: serde_json::json!({"file": "test.txt"}),
                title: None,
                metadata: None,
                time: oc::PartTime {
                    start: 1000,
                    end: None,
                },
            },
        );
        let event4 = oc::Event::MessagePartUpdated {
            properties: oc::MessagePartEventInfo {
                part: tool2_running,
                delta: Some("running".to_string()),
            },
        };
        let result4 = adapter.translate_to_session_update(event4);
        assert!(matches!(
            result4.unwrap(),
            Some(SessionUpdate::ToolCallUpdate { .. })
        ));
    }
}
