use crate::session::{ConfigOption, SessionModeState, SessionModelState};
use crate::{Message, Session, SessionUpdate};
use serde::{Deserialize, Serialize};

/// Reason why the turn was marked complete (for debugging/observability)
///
/// This enum indicates **how** the system determined that a turn has completed.
/// Different connector types use different strategies:
///
/// - **OpenCode Connector**: Uses `ExplicitSignal` (upstream session.idle event)
/// - **ACP Connector (stdio)**: Uses `ResponseReceived` (JSON-RPC response is final)
/// - **Gateway Connector**: Uses `OperationsComplete` (tracks pending tool calls)
/// - **Fallback**: Uses `IdleTimeout` when no other signal available
///
/// # Consumer Usage
///
/// Most consumers should treat all triggers the same (turn is complete).
/// The trigger type is primarily for debugging and observability.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TurnCompleteTrigger {
    /// Explicit signal from upstream provider (e.g., OpenCode session.idle event)
    ///
    /// This is the most reliable trigger as it comes directly from the agent system.
    ExplicitSignal,

    /// JSON-RPC response received (ACP stdio transport)
    ///
    /// In ACP stdio mode, the response message is the last message in the turn.
    ResponseReceived,

    /// All tracked operations completed (e.g., pending tool calls resolved)
    ///
    /// Used when the connector tracks operation state and can determine
    /// completion by monitoring tool call statuses.
    OperationsComplete,

    /// Timeout-based idle detection (fallback mechanism)
    ///
    /// Used when no other completion signal is available.
    /// The duration indicates how long the system waited before declaring completion.
    IdleTimeout { duration_ms: u64 },
}

/// A single node in an inspector snapshot (protocol-level DTO).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectorSnapshotNode {
    pub id: String,
    pub parent: Option<String>,
    pub children: Vec<String>,
    pub label: String,
    pub kind: String,
    pub state: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_detail: Option<String>,
    pub properties: std::collections::BTreeMap<String, serde_json::Value>,
    pub created_at: String,
    pub last_updated: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", content = "data")]
pub enum Event {
    // Session events
    SessionsListed {
        connector_id: String,
        sessions: Vec<Session>,
    },
    SessionCreated {
        connector_id: String,
        session: Session,
    },
    SessionUpdated {
        connector_id: String,
        session: Session,
    },
    SessionMetadataUpdated {
        connector_id: String,
        session_id: String,
        title: Option<String>,
        total_messages: Option<u32>,
        model: Option<String>,
    },
    SessionDeleted {
        session_id: String,
    },
    /// Session was closed (agent released resources, session remains in list).
    /// The session can be loaded again later via session/load.
    SessionClosed {
        connector_id: String,
        session_id: String,
    },
    SessionSystemMessageSet {
        session_id: String,
        system_message: String,
    },
    SessionIdle {
        connector_id: String,
        session_id: String,
    },
    /// Session mode/model metadata received from an ACP connector
    ///
    /// Emitted when metadata is received from a connector (e.g., after session/new or session/load).
    /// This event is separate from SessionCreated to support:
    /// - Session takeover scenarios (session already exists, but metadata is new)
    /// - Specific subscriptions to metadata changes
    /// - Connectors that provide metadata asynchronously
    ///
    /// # Fields
    /// - `models`: UNSTABLE in ACP spec but used by Claude-ACP
    /// - `modes`: Stable in ACP spec
    ///
    /// Both fields are optional since not all connectors provide this data.
    SessionMetadataReceived {
        /// Connector that provided the metadata
        connector_id: String,
        /// Session the metadata belongs to
        session_id: String,
        /// Available models and current model (UNSTABLE in ACP spec)
        #[serde(skip_serializing_if = "Option::is_none")]
        models: Option<SessionModelState>,
        /// Available modes and current mode
        #[serde(skip_serializing_if = "Option::is_none")]
        modes: Option<SessionModeState>,
        /// ACP config options (replaces modes/models in future ACP versions)
        #[serde(default, skip_serializing_if = "Option::is_none")]
        config_options: Option<Vec<ConfigOption>>,
    },

    /// **All content for this turn/message has been received.**
    ///
    /// This is the **primary signal** for finalization actions (archiving, UI state lock).
    /// Emitted BEFORE `SessionIdle` to ensure proper event ordering.
    ///
    /// # Event Semantics
    ///
    /// - **`MessageCompleted`**: Message metadata is ready (informational)
    ///   - Purpose: UI status updates ("Assistant is typing" → "Complete")
    ///   - Timing: Sent when message record exists, content may still be streaming
    ///   - Consumer action: Update UI state, show completion status
    ///
    /// - **`TurnComplete`**: All content received (actionable)
    ///   - Purpose: Signal that the entire turn is finalized
    ///   - Timing: Sent AFTER all content chunks, tool calls, and metadata
    ///   - Consumer action: Finalize storage, lock state, trigger post-processing
    ///
    /// - **`SessionIdle`**: No recent activity (informational)
    ///   - Purpose: UI spinner control, activity indication
    ///   - Timing: Sent AFTER `TurnComplete` as final safety signal
    ///   - Consumer action: Hide spinners, update activity indicators
    ///
    /// # Event Ordering
    ///
    /// ```text
    /// 1. MessageStarted         (message created)
    /// 2. SessionUpdate::*Chunk  (content streaming)
    /// 3. SessionUpdate::ToolCall* (tool execution)
    /// 4. MessageCompleted       (metadata ready)
    /// 5. TurnComplete          ← YOU ARE HERE (finalize!)
    /// 6. SessionIdle           (activity stopped)
    /// ```
    ///
    /// # Consumer Behavior
    ///
    /// | Consumer | MessageCompleted | TurnComplete | SessionIdle |
    /// |----------|------------------|--------------|-------------|
    /// | **Archivist** | Ignore | **Finalize and write** | Safety net |
    /// | **UI Cache** | Update status | **Lock state** | Hide spinner |
    /// | **Conductor Bridge** | - | **Flush response** | Fallback flush |
    ///
    /// # Example Usage
    ///
    /// ```rust
    /// use dirigent_protocol::{Event, TurnCompleteTrigger};
    ///
    /// match event {
    ///     Event::TurnComplete { session_id, message_id, trigger, .. } => {
    ///         // Finalize the message in your storage
    ///         archivist.finalize_message(&session_id, &message_id).await?;
    ///
    ///         // Lock UI state
    ///         ui_cache.lock_message(&message_id);
    ///
    ///         // Log trigger for debugging
    ///         println!("Turn complete via {:?}", trigger);
    ///     }
    ///     _ => {}
    /// }
    /// ```
    TurnComplete {
        connector_id: String,
        session_id: String,
        message_id: String,
        trigger: TurnCompleteTrigger,
    },
    /// Session-level error that can be displayed in the chat UI.
    /// Used when a connector encounters an error during session operations.
    ///
    /// # Fields
    ///
    /// - `error_message`: Human-readable error summary
    /// - `is_recoverable`: Whether the session can continue after this error
    /// - `error_code`: Optional categorization code (e.g., "TRANSPORT_PARSE_FAILED")
    /// - `technical_details`: Optional full technical details (truncated if large)
    /// - `context`: Optional JSON blob with structured error context for debugging
    SessionError {
        connector_id: String,
        session_id: String,
        error_message: String,
        /// Whether the session can continue after this error.
        /// If false, the session should be considered terminated.
        is_recoverable: bool,
        /// Error categorization code for UI grouping and filtering.
        /// Examples: "TRANSPORT_PARSE_FAILED", "SESSION_NOT_FOUND", "TIMEOUT"
        #[serde(skip_serializing_if = "Option::is_none")]
        error_code: Option<String>,
        /// Full technical details including stack traces, received content, etc.
        /// May be truncated if the original content was very large.
        #[serde(skip_serializing_if = "Option::is_none")]
        technical_details: Option<String>,
        /// Structured error context for debug view (JSON blob).
        /// Contains machine-readable error information.
        #[serde(skip_serializing_if = "Option::is_none")]
        context: Option<serde_json::Value>,
    },
    /// Session was transferred from one connector to another
    ///
    /// Emitted by CoreRuntime when a session transfer completes successfully.
    /// ACP Server should update client mappings on receiving this event.
    SessionTransferred {
        /// Source connector ID (where transfer originated)
        from_connector: String,
        /// Source session ID
        from_session: String,
        /// Target connector ID (where session is now active)
        to_connector: String,
        /// New session ID in target connector
        to_session: String,
        /// Whether a new session was created (true) or existing loaded (false)
        is_new_session: bool,
        /// Available models and current model from the new connector (optional)
        #[serde(skip_serializing_if = "Option::is_none")]
        models: Option<SessionModelState>,
        /// Available modes and current mode from the new connector (optional)
        #[serde(skip_serializing_if = "Option::is_none")]
        modes: Option<SessionModeState>,
    },
    /// Emitted by archivist when a session registration is durable and list-stable.
    ///
    /// Frontend can use this to refresh the session list with confidence that
    /// the session will appear (it's been written to the archive index).
    /// This replaces any timeout-based delay hacks after session creation.
    ///
    /// # Fields
    ///
    /// - `connector_id`: The connector that owns this session
    /// - `session_id`: The native session ID from the connector
    /// - `scroll_id`: The archivist's canonical scroll_id for this session
    SessionRegistered {
        connector_id: String,
        session_id: String,
        /// The archivist's canonical scroll_id for this session
        scroll_id: String,
    },

    /// A forwarded session encountered a failure
    ///
    /// Emitted when a connector that received a transferred session fails.
    /// Clients should be routed back to Gateway automatically.
    ForwardingPanic {
        /// Connector that failed
        connector_id: String,
        /// Session that was affected
        session_id: String,
        /// Human-readable reason for the failure
        reason: String,
        /// ID of the Gateway session to fall back to (if available)
        fallback_gateway_session: Option<String>,
    },
    /// New ACP-style session update (replaces MessagePartAdded for new consumers)
    SessionUpdate {
        connector_id: String,
        session_id: String,
        update: SessionUpdate,
    },
    /// Agent-initiated request requiring client response (e.g., permission prompt)
    ///
    /// Emitted when an agent sends a request (like session/request_permission) that
    /// requires user input. The client should respond via the appropriate API endpoint.
    ///
    /// # Permission Flow Routing
    ///
    /// - `is_forwarded: false` - Internal session (UI-owned) → Show modal in web UI
    /// - `is_forwarded: true` - External session (ACP client-owned) → Forward to EventBridge, DO NOT show UI modal
    ///
    /// The `is_forwarded` field determines whether this permission request should be handled
    /// by the Dirigent web UI or forwarded to an external ACP client that owns the session.
    AgentRequest {
        /// ID of the connector that received the request
        connector_id: String,
        /// Session ID from the request parameters
        session_id: String,
        /// Request ID from the agent (for correlating the response)
        request_id: serde_json::Value,
        /// Method being requested (e.g., "session/request_permission")
        method: String,
        /// Request parameters from the agent
        params: serde_json::Value,
        /// Whether this is a forwarded (external) session.
        ///
        /// If `true`, the UI MUST NOT show a permission modal. Instead, the EventBridge
        /// should forward this request to the external ACP client that owns the session.
        ///
        /// If `false`, this is an internal session and the UI should show the permission modal.
        is_forwarded: bool,
    },

    // ACP Client Connection Events (for UI visibility of incoming connections)
    /// An ACP client has connected to the server
    ///
    /// Emitted when a new client connects via the ACP Server.
    /// Used by UI to show incoming connections in the sidebar.
    AcpClientConnected {
        /// Unique client identifier (UUID7)
        client_id: String,
        /// When the client connected (ISO 8601 timestamp)
        connected_at: String,
        /// Optional client capabilities from the initialize handshake
        capabilities: Option<serde_json::Value>,
        /// The Acceptor connector's UID (for archivist meta session creation)
        connector_uid: String,
    },
    /// An ACP client has disconnected from the server
    ///
    /// Emitted when a client disconnects (explicitly or due to connection loss).
    /// The client record should be marked as disconnected, not removed (for history).
    AcpClientDisconnected {
        /// Unique client identifier
        client_id: String,
        /// When the client disconnected (ISO 8601 timestamp)
        disconnected_at: String,
        /// Optional reason for disconnection
        reason: Option<String>,
    },
    /// An ACP client has opened a new session via Gateway
    ///
    /// Emitted when a client creates a new session through the ACP Server.
    /// This adds an entry to the connection history.
    AcpClientSessionOpened {
        /// Client that opened the session
        client_id: String,
        /// The Gateway session ID (or initial session before routing)
        gateway_session_id: String,
        /// The client-facing session ID
        client_session_id: String,
        /// When this occurred (ISO 8601 timestamp)
        timestamp: String,
    },
    /// An ACP client's session was routed to a different connector
    ///
    /// Emitted when a session is transferred from Gateway to another connector.
    /// This adds an entry to the connection history showing the route change.
    AcpClientSessionRouted {
        /// Client whose session was routed
        client_id: String,
        /// Original session ID (typically Gateway session)
        from_session_id: String,
        /// New session ID in the target connector
        to_session_id: String,
        /// Target connector ID
        connector_id: String,
        /// Target connector title (for display)
        connector_title: String,
        /// Connector kind (e.g., "opencode", "acp", "gateway")
        #[serde(default)]
        connector_kind: Option<String>,
        /// Current model being used (if known)
        #[serde(default)]
        model: Option<String>,
        /// Agent version/name info (if available)
        #[serde(default)]
        agent_info: Option<String>,
        /// When this occurred (ISO 8601 timestamp)
        timestamp: String,
    },

    // Message events
    MessagesListed {
        messages: Vec<Message>,
    },
    MessageStarted {
        connector_id: String,
        message: Message,
    },
    MessageCompleted {
        connector_id: String,
        message: Message,
    },
    MessageFailed {
        message_id: String,
        error: String,
    },

    // Connector lifecycle events
    ConnectorCreated {
        connector_id: String,
        kind: String,
        title: String,
    },
    ConnectorRemoved {
        connector_id: String,
    },
    ConnectorStateChanged {
        connector_id: String,
        state: String,
        /// Machine-readable error classification ("offline", "unstable", "connection_failed")
        #[serde(default, skip_serializing_if = "Option::is_none")]
        error_kind: Option<String>,
    },

    // System events
    Connected,
    Disconnected,
    Error {
        message: String,
    },

    // Inspector events (runtime tree visualization)
    /// Full snapshot of the inspector tree — sent on initial connection
    /// and can be requested via server function.
    InspectorSnapshot {
        /// ISO 8601 timestamp of the snapshot
        timestamp: String,
        /// All nodes in the tree
        nodes: Vec<InspectorSnapshotNode>,
        /// Total node count
        node_count: usize,
    },
    /// A new node was registered in the inspector tree
    InspectorNodeRegistered {
        id: String,
        parent: String,
        kind: String,
    },
    /// A node was removed from the inspector tree
    InspectorNodeRemoved {
        id: String,
    },
    /// A node's lifecycle state changed
    InspectorStateChanged {
        id: String,
        old: String,
        new: String,
    },
    /// A node's properties were updated
    InspectorPropertiesUpdated {
        id: String,
        keys: Vec<String>,
    },

    // System task events
    /// A background system task changed status (completed, failed, cancelled).
    ///
    /// Emitted by the SystemTaskRegistry when a task reaches a terminal state.
    /// Allows the UI to react to task completion without polling.
    SystemTaskStatusChanged {
        /// Unique task identifier (UUIDv7)
        task_id: String,
        /// What kind of operation (e.g., "ClaudeImport")
        kind: String,
        /// Terminal status: "completed", "failed", or "cancelled"
        status: String,
        /// JSON result payload (present when status == "completed")
        #[serde(skip_serializing_if = "Option::is_none")]
        result_json: Option<String>,
        /// Error message (present when status == "failed")
        #[serde(skip_serializing_if = "Option::is_none")]
        error: Option<String>,
    },
}

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

    #[test]
    fn test_session_update_variant_serialization() {
        let update = SessionUpdate::UserMessageChunk {
            message_id: "msg_123".to_string(),
            content: ContentBlock::Text {
                text: "Hello from event".to_string(),
            },
            _meta: None,
        };

        let event = Event::SessionUpdate {
            connector_id: "conn_123".to_string(),
            session_id: "session_456".to_string(),
            update,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""event":"SessionUpdate"#));
        assert!(json.contains(r#""session_id":"session_456"#));
        assert!(json.contains(r#""type":"user_message_chunk"#));
        assert!(json.contains(r#""message_id":"msg_123"#));
        assert!(json.contains(r#""text":"Hello from event"#));
    }

    #[test]
    fn test_session_update_variant_deserialization() {
        let json = r#"{
            "event": "SessionUpdate",
            "data": {
                "connector_id": "conn_123",
                "session_id": "session_789",
                "update": {
                    "type": "agent_message_chunk",
                    "message_id": "msg_789",
                    "content": {
                        "type": "text",
                        "text": "Agent response"
                    }
                }
            }
        }"#;

        let event: Event = serde_json::from_str(json).unwrap();
        match event {
            Event::SessionUpdate {
                connector_id,
                session_id,
                update,
            } => {
                assert_eq!(connector_id, "conn_123");
                assert_eq!(session_id, "session_789");
                match update {
                    SessionUpdate::AgentMessageChunk {
                        message_id,
                        content,
                        _meta,
                    } => {
                        assert_eq!(message_id, "msg_789");
                        assert_eq!(
                            content,
                            ContentBlock::Text {
                                text: "Agent response".to_string()
                            }
                        );
                        assert_eq!(_meta, None);
                    }
                    _ => panic!("Expected AgentMessageChunk"),
                }
            }
            _ => panic!("Expected SessionUpdate event"),
        }
    }

    #[test]
    fn test_session_update_variant_roundtrip() {
        let original = Event::SessionUpdate {
            connector_id: "conn_roundtrip".to_string(),
            session_id: "session_roundtrip".to_string(),
            update: SessionUpdate::AgentThoughtChunk {
                message_id: "msg_thought".to_string(),
                content: ContentBlock::Text {
                    text: "Thinking...".to_string(),
                },
                _meta: None,
            },
        };

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

        match (&original, &deserialized) {
            (
                Event::SessionUpdate {
                    connector_id: cid1,
                    session_id: sid1,
                    update: update1,
                },
                Event::SessionUpdate {
                    connector_id: cid2,
                    session_id: sid2,
                    update: update2,
                },
            ) => {
                assert_eq!(cid1, cid2);
                assert_eq!(sid1, sid2);
                assert_eq!(update1, update2);
            }
            _ => panic!("Roundtrip failed"),
        }
    }

    #[test]
    fn test_existing_events_still_work() {
        // Verify that existing event variants are not affected
        use crate::SessionMetadata;
        use chrono::Utc;

        let now = Utc::now();
        let session_created = Event::SessionCreated {
            connector_id: "conn_test".to_string(),
            session: Session {
                id: "session_123".to_string(),
                title: "Test Session".to_string(),
                created_at: now,
                updated_at: now,
                metadata: SessionMetadata {
                    project_path: "/test".to_string(),
                    model: Some("gpt-4".to_string()),
                    total_messages: 0,
                    system_message: None,
                    current_mode_id: None,
                    _meta: None,
                    project_id: None,
                },
                cwd: None,
                models: None,
                modes: None,
                config_options: None,
                acp_client_id: None,
            },
        };

        let json = serde_json::to_string(&session_created).unwrap();
        let _deserialized: Event = serde_json::from_str(&json).unwrap();
    }

    #[test]
    fn test_session_error_serialization() {
        let event = Event::SessionError {
            connector_id: "acp_conn_1".to_string(),
            session_id: "session_456".to_string(),
            error_message: "Session not found".to_string(),
            is_recoverable: false,
            error_code: None,
            technical_details: None,
            context: None,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""event":"SessionError"#));
        assert!(json.contains(r#""connector_id":"acp_conn_1"#));
        assert!(json.contains(r#""session_id":"session_456"#));
        assert!(json.contains(r#""error_message":"Session not found"#));
        assert!(json.contains(r#""is_recoverable":false"#));
        // Optional fields should not be present when None
        assert!(!json.contains(r#""error_code"#));
        assert!(!json.contains(r#""technical_details"#));
        assert!(!json.contains(r#""context"#));
    }

    #[test]
    fn test_session_error_with_details_serialization() {
        let event = Event::SessionError {
            connector_id: "acp_conn_1".to_string(),
            session_id: "session_456".to_string(),
            error_message: "Transport parse failed".to_string(),
            is_recoverable: true,
            error_code: Some("TRANSPORT_PARSE_FAILED".to_string()),
            technical_details: Some("Failed to parse JSON: expected value at line 1".to_string()),
            context: Some(serde_json::json!({
                "received_bytes": 1024,
                "received_preview": "option { key: ...",
                "expected": "JSON-RPC message"
            })),
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""error_code":"TRANSPORT_PARSE_FAILED"#));
        assert!(json.contains(r#""technical_details"#));
        assert!(json.contains(r#""context"#));
        assert!(json.contains(r#""received_bytes":1024"#));
    }

    #[test]
    fn test_session_error_deserialization() {
        // Test backward compatibility - old format without new fields
        let json = r#"{
            "event": "SessionError",
            "data": {
                "connector_id": "conn_test",
                "session_id": "session_789",
                "error_message": "Connection timeout",
                "is_recoverable": true
            }
        }"#;

        let event: Event = serde_json::from_str(json).unwrap();
        match event {
            Event::SessionError {
                connector_id,
                session_id,
                error_message,
                is_recoverable,
                error_code,
                technical_details,
                context,
            } => {
                assert_eq!(connector_id, "conn_test");
                assert_eq!(session_id, "session_789");
                assert_eq!(error_message, "Connection timeout");
                assert!(is_recoverable);
                assert!(error_code.is_none());
                assert!(technical_details.is_none());
                assert!(context.is_none());
            }
            _ => panic!("Expected SessionError event"),
        }
    }

    #[test]
    fn test_session_error_roundtrip() {
        let original = Event::SessionError {
            connector_id: "roundtrip_conn".to_string(),
            session_id: "roundtrip_session".to_string(),
            error_message: "API rate limit exceeded".to_string(),
            is_recoverable: true,
            error_code: Some("RATE_LIMITED".to_string()),
            technical_details: Some("429 Too Many Requests".to_string()),
            context: Some(serde_json::json!({"retry_after": 60})),
        };

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

        match (&original, &deserialized) {
            (
                Event::SessionError {
                    connector_id: cid1,
                    session_id: sid1,
                    error_message: err1,
                    is_recoverable: rec1,
                    error_code: code1,
                    technical_details: details1,
                    context: ctx1,
                },
                Event::SessionError {
                    connector_id: cid2,
                    session_id: sid2,
                    error_message: err2,
                    is_recoverable: rec2,
                    error_code: code2,
                    technical_details: details2,
                    context: ctx2,
                },
            ) => {
                assert_eq!(cid1, cid2);
                assert_eq!(sid1, sid2);
                assert_eq!(code1, code2);
                assert_eq!(details1, details2);
                assert_eq!(ctx1, ctx2);
                assert_eq!(err1, err2);
                assert_eq!(rec1, rec2);
            }
            _ => panic!("Roundtrip failed"),
        }
    }

    #[test]
    fn test_session_transferred_serialization() {
        let event = Event::SessionTransferred {
            from_connector: "gateway-1".to_string(),
            from_session: "session-old".to_string(),
            to_connector: "opencode-1".to_string(),
            to_session: "session-new".to_string(),
            is_new_session: true,
            models: None,
            modes: None,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""event":"SessionTransferred"#));
        assert!(json.contains(r#""from_connector":"gateway-1"#));
        assert!(json.contains(r#""to_connector":"opencode-1"#));

        let deserialized: Event = serde_json::from_str(&json).unwrap();
        // Verify roundtrip
        match deserialized {
            Event::SessionTransferred {
                from_connector,
                from_session,
                to_connector,
                to_session,
                is_new_session,
                models,
                modes,
            } => {
                assert_eq!(from_connector, "gateway-1");
                assert_eq!(from_session, "session-old");
                assert_eq!(to_connector, "opencode-1");
                assert_eq!(to_session, "session-new");
                assert!(is_new_session);
                assert!(models.is_none());
                assert!(modes.is_none());
            }
            _ => panic!("Expected SessionTransferred event"),
        }
    }

    #[test]
    fn test_forwarding_panic_serialization() {
        let event = Event::ForwardingPanic {
            connector_id: "opencode-1".to_string(),
            session_id: "session-123".to_string(),
            reason: "Connection lost".to_string(),
            fallback_gateway_session: Some("gateway-session-1".to_string()),
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""event":"ForwardingPanic"#));
        assert!(json.contains(r#""connector_id":"opencode-1"#));
        assert!(json.contains(r#""session_id":"session-123"#));
        assert!(json.contains(r#""reason":"Connection lost"#));

        let deserialized: Event = serde_json::from_str(&json).unwrap();
        // Verify roundtrip
        match deserialized {
            Event::ForwardingPanic {
                connector_id,
                session_id,
                reason,
                fallback_gateway_session,
            } => {
                assert_eq!(connector_id, "opencode-1");
                assert_eq!(session_id, "session-123");
                assert_eq!(reason, "Connection lost");
                assert_eq!(
                    fallback_gateway_session,
                    Some("gateway-session-1".to_string())
                );
            }
            _ => panic!("Expected ForwardingPanic event"),
        }
    }

    #[test]
    fn test_session_metadata_received_full() {
        use crate::session::{ModelInfo, SessionMode, SessionModeState, SessionModelState};

        let event = Event::SessionMetadataReceived {
            connector_id: "claude-acp-1".to_string(),
            session_id: "session-123".to_string(),
            models: Some(SessionModelState {
                available_models: vec![
                    ModelInfo {
                        model_id: "default".to_string(),
                        name: "Default (recommended)".to_string(),
                        description: Some("Opus 4.5".to_string()),
                    },
                    ModelInfo {
                        model_id: "sonnet".to_string(),
                        name: "Sonnet".to_string(),
                        description: None,
                    },
                ],
                current_model_id: "default".to_string(),
            }),
            modes: Some(SessionModeState {
                current_mode_id: "default".to_string(),
                available_modes: vec![
                    SessionMode {
                        id: "default".to_string(),
                        name: "Always Ask".to_string(),
                        description: Some("Prompts for permission".to_string()),
                    },
                    SessionMode {
                        id: "plan".to_string(),
                        name: "Plan Mode".to_string(),
                        description: None,
                    },
                ],
            }),
            config_options: None,
        };

        let json = serde_json::to_string(&event).unwrap();
        assert!(json.contains(r#""event":"SessionMetadataReceived"#));
        assert!(json.contains(r#""connector_id":"claude-acp-1"#));
        assert!(json.contains(r#""session_id":"session-123"#));
        // Check camelCase in nested types
        assert!(json.contains("availableModels"));
        assert!(json.contains("currentModelId"));
        assert!(json.contains("availableModes"));
        assert!(json.contains("currentModeId"));

        let deserialized: Event = serde_json::from_str(&json).unwrap();
        match deserialized {
            Event::SessionMetadataReceived {
                connector_id,
                session_id,
                models,
                modes,
                ..
            } => {
                assert_eq!(connector_id, "claude-acp-1");
                assert_eq!(session_id, "session-123");
                assert!(models.is_some());
                assert!(modes.is_some());
                let models = models.unwrap();
                assert_eq!(models.current_model_id, "default");
                assert_eq!(models.available_models.len(), 2);
                let modes = modes.unwrap();
                assert_eq!(modes.current_mode_id, "default");
                assert_eq!(modes.available_modes.len(), 2);
            }
            _ => panic!("Expected SessionMetadataReceived event"),
        }
    }

    #[test]
    fn test_session_metadata_received_partial() {
        // Test with only modes (models is None)
        use crate::session::{SessionMode, SessionModeState};

        let event = Event::SessionMetadataReceived {
            connector_id: "gateway-1".to_string(),
            session_id: "session-456".to_string(),
            models: None,
            modes: Some(SessionModeState {
                current_mode_id: "default".to_string(),
                available_modes: vec![SessionMode {
                    id: "default".to_string(),
                    name: "Default".to_string(),
                    description: None,
                }],
            }),
            config_options: None,
        };

        let json = serde_json::to_string(&event).unwrap();
        // models should be skipped when None
        assert!(!json.contains("availableModels"));
        assert!(json.contains("availableModes"));

        let deserialized: Event = serde_json::from_str(&json).unwrap();
        match deserialized {
            Event::SessionMetadataReceived { models, modes, .. } => {
                assert!(models.is_none());
                assert!(modes.is_some());
            }
            _ => panic!("Expected SessionMetadataReceived event"),
        }
    }

    #[test]
    fn test_session_metadata_received_empty() {
        // Test with both None (connector provides no metadata)
        let event = Event::SessionMetadataReceived {
            connector_id: "generic-1".to_string(),
            session_id: "session-789".to_string(),
            models: None,
            modes: None,
            config_options: None,
        };

        let json = serde_json::to_string(&event).unwrap();
        // Both should be skipped when None
        assert!(!json.contains("models"));
        assert!(!json.contains("modes"));

        let deserialized: Event = serde_json::from_str(&json).unwrap();
        match deserialized {
            Event::SessionMetadataReceived { models, modes, .. } => {
                assert!(models.is_none());
                assert!(modes.is_none());
            }
            _ => panic!("Expected SessionMetadataReceived event"),
        }
    }
}
