//! Connector abstraction layer
//!
//! This module provides the connector abstraction that allows dirigent_core to
//! manage long-lived connections to external agent systems (OpenCode, ACP, etc.).
//!
//! # Architecture
//!
//! Each connector:
//! - Runs in its own async task with lifecycle management
//! - Has a command channel (mpsc) for receiving control commands
//! - Has an event broadcast channel for publishing events to subscribers
//! - Tracks its own state (Initializing, Connecting, Ready, Error, Stopped)
//! - Is owned by a specific user (for authorization)
//!
//! # Usage
//!
//! ```no_run
//! use dirigent_core::connectors::{Connector, ConnectorCommand};
//! use tokio::sync::broadcast;
//!
//! async fn example(connector: impl Connector) {
//!     // Subscribe to connector events
//!     let mut event_rx = connector.subscribe();
//!
//!     // Send a command
//!     let cmd_tx = connector.command_tx();
//!     cmd_tx.send(ConnectorCommand::ListSessions).await.ok();
//!
//!     // Receive events
//!     while let Ok(event) = event_rx.recv().await {
//!         println!("Received event: {:?}", event);
//!     }
//! }
//! ```

use crate::types::{ConnectorErrorKind, ConnectorId, ConnectorKind, ConnectorState, UserId};
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc, RwLock};
use tokio::task::JoinHandle;

// Connector implementations
pub mod acceptor;
pub mod acp;
pub mod fingerprint;
pub mod gateway;
pub mod opencode;

pub use fingerprint::compute_fingerprint;

/// Commands that can be sent to a connector
///
/// These commands control connector behavior and trigger operations on the
/// underlying agent system. Commands are sent via the connector's command
/// channel and processed asynchronously by the connector's task loop.
#[derive(Clone, Debug)]
pub enum ConnectorCommand {
    /// Request a list of all sessions
    ///
    /// The connector will query the underlying agent system and emit
    /// a SessionsListed event with the results.
    ListSessions,

    /// Request messages for a specific session
    ///
    /// The connector will fetch messages for the given session and emit
    /// a MessagesListed event with the results.
    ListMessages {
        /// ID of the session to list messages from
        session_id: String,
    },

    /// Create a new session
    ///
    /// The connector will create a new session with the agent system and emit
    /// a SessionCreated event with the new session information.
    CreateSession {
        /// Optional current working directory for the session
        cwd: Option<String>,
        /// Optional project ID to associate with the session
        project_id: Option<String>,
        /// Session ownership model (internal UI session vs external forwarded session)
        ownership: dirigent_protocol::SessionOwnership,
    },

    /// Load an existing session
    ///
    /// The connector will load an existing session (if the protocol supports it)
    /// and replay its history. This may emit multiple SessionUpdate events during
    /// replay.
    LoadSession {
        /// ID of the session to load
        session_id: String,
        /// Working directory (project path from archivist metadata or session/list)
        cwd: String,
        /// MCP server configurations for the agent
        mcp_servers: Option<serde_json::Value>,
    },

    /// Send a message to a session
    ///
    /// The connector will send the message to the agent system and emit
    /// streaming events as the response is generated.
    SendMessage {
        /// ID of the session to send to
        session_id: String,
        /// Message text content
        text: String,
    },

    /// Cancel generation in a session
    ///
    /// Requests the agent to stop generating the current response.
    CancelGeneration {
        /// ID of the session to cancel generation in
        session_id: String,
    },

    /// Attempt to reconnect the connector
    ///
    /// This is useful for recovering from transient connection failures.
    /// The connector will transition to Connecting state and attempt to
    /// re-establish its connection to the agent system.
    Reconnect,

    /// Shutdown the connector gracefully
    ///
    /// The connector will clean up resources, close connections, and
    /// transition to Stopped state. The connector's task loop will exit.
    Shutdown,

    /// Respond to an agent-initiated request
    ///
    /// This command allows external systems (like the ACP Server) to inject
    /// responses for pending agent requests (e.g., permission prompts).
    /// The connector will look up the pending request by request_id and
    /// send the response to the agent via transport.
    AgentResponse {
        /// The request ID from the agent (for correlation)
        request_id: serde_json::Value,
        /// The response to send back to the agent
        response: serde_json::Value,
    },

    /// Set the session mode
    ///
    /// Change the active mode for a session. The mode determines behavior
    /// characteristics like conversation style or capabilities.
    /// The connector will send the mode change request to the agent system.
    SetSessionMode {
        /// ID of the session to update
        session_id: String,
        /// ID of the mode to switch to
        mode_id: String,
    },

    /// Set the session model
    ///
    /// Change the active model for a session. The model determines which
    /// LLM/agent backend is used for processing.
    /// The connector will send the model change request to the agent system.
    SetSessionModel {
        /// ID of the session to update
        session_id: String,
        /// ID of the model to switch to
        model_id: String,
    },

    /// Close a session (release agent resources, session remains listable).
    /// Only works when agent supports sessionCapabilities.close.
    CloseSession {
        session_id: String,
    },

    /// Set a config option value (preferred over SetSessionMode/SetSessionModel).
    /// Uses session/set_config_option per ACP spec.
    SetConfigOption {
        session_id: String,
        config_id: String,
        value: String,
    },
}

/// Trait for connector implementations
///
/// This trait defines the interface that all connectors must implement.
/// It is object-safe, allowing for dynamic dispatch via `dyn Connector`.
///
/// # Object Safety
///
/// All methods return references or simple Copy types to ensure the trait
/// can be used as a trait object. Methods that need owned values use
/// cloneable types like `mpsc::Sender` and `broadcast::Receiver`.
pub trait Connector: Send + Sync {
    /// Get the unique identifier for this connector
    fn id(&self) -> &ConnectorId;

    /// Get the type of connector (OpenCode, Acp, Mock, etc.)
    fn kind(&self) -> ConnectorKind;

    /// Get the user who owns this connector
    ///
    /// Used for authorization checks when routing commands and operations.
    fn owner(&self) -> &UserId;

    /// Get the human-readable title for this connector
    ///
    /// This is typically set during connector creation and used for display
    /// in UI components.
    fn title(&self) -> &str;

    /// Get the current state of this connector
    ///
    /// The state reflects the connector's position in its lifecycle:
    /// - Initializing: Just created, not yet connecting
    /// - Connecting: Attempting to establish connection
    /// - Ready: Connected and operational
    /// - Error: Encountered a failure (with error message)
    /// - Stopped: Shutdown or unrecoverable error
    fn state(&self) -> ConnectorState;

    /// Get a sender for sending commands to this connector
    ///
    /// Commands sent via this channel will be processed asynchronously
    /// by the connector's task loop. The sender is cloneable and can be
    /// shared across multiple clients.
    ///
    /// # Returns
    ///
    /// An mpsc sender that can be used to send `ConnectorCommand` values.
    fn command_tx(&self) -> mpsc::Sender<ConnectorCommand>;

    /// Subscribe to events from this connector
    ///
    /// Returns a broadcast receiver that will receive all events published
    /// by this connector. Each call to subscribe creates a new independent
    /// receiver.
    ///
    /// # Returns
    ///
    /// A broadcast receiver for `dirigent_protocol::Event` values.
    ///
    /// # Notes
    ///
    /// Broadcast channels have a bounded capacity. If a receiver falls too
    /// far behind (doesn't consume events fast enough), it will miss events
    /// and receive a `RecvError::Lagged` error.
    fn subscribe(&self) -> broadcast::Receiver<dirigent_protocol::Event>;

    /// Stop this connector gracefully
    ///
    /// This is a convenience method that sends a Shutdown command to the
    /// connector's command channel. The connector will clean up and transition
    /// to Stopped state asynchronously.
    fn stop(&self);

    /// Get available commands/tools for this connector
    ///
    /// Returns a list of commands that this connector exposes to external
    /// ACP clients. These commands will be forwarded when sessions are
    /// routed to this connector.
    ///
    /// # Returns
    ///
    /// A vector of command definitions with name and optional description.
    /// Returns an empty vector if the connector doesn't expose commands.
    fn get_available_commands(&self) -> Vec<crate::acp::protocol::streaming::Command> {
        // Default implementation: no commands
        Vec::new()
    }
}

/// Handle to a running connector
///
/// This struct contains all the state and channels needed to interact with
/// a connector. It implements the `Connector` trait and can be cloned to
/// create multiple handles to the same underlying connector.
///
/// # Lifecycle
///
/// A ConnectorHandle is typically created by the CoreRuntime when a connector
/// is instantiated. The handle contains:
/// - Metadata (id, kind, owner, title)
/// - Shared state protected by RwLock
/// - Command channel sender
/// - Event broadcast sender (for publishing events)
/// - Optional task handle (for graceful shutdown)
///
/// # Cloning
///
/// ConnectorHandle uses Arc internally, so cloning is cheap and creates
/// a new handle to the same connector. All clones share the same state
/// and channels.
#[derive(Clone)]
pub struct ConnectorHandle {
    /// Unique identifier for this connector
    id: ConnectorId,

    /// Type of connector (OpenCode, Acp, Mock, etc.)
    kind: ConnectorKind,

    /// User who owns this connector
    owner: UserId,

    /// Human-readable title for display
    title: String,

    /// Shared connector state
    ///
    /// Protected by RwLock to allow concurrent reads and exclusive writes.
    /// The connector's task loop updates this as state transitions occur.
    state: Arc<RwLock<ConnectorState>>,

    /// Sender for commands to the connector
    ///
    /// Commands sent via this channel are processed by the connector's
    /// async task loop.
    cmd_tx: mpsc::Sender<ConnectorCommand>,

    /// Broadcast sender for events
    ///
    /// The connector publishes events to this channel. Subscribers get
    /// receivers via the `subscribe()` method.
    events_tx: broadcast::Sender<dirigent_protocol::Event>,

    /// Join handle for the connector's background task
    ///
    /// This is None until the connector is started. Once started, this
    /// handle can be used to wait for the task to complete during shutdown.
    task_join: Option<Arc<RwLock<Option<JoinHandle<()>>>>>,

    /// Connector-specific configuration as JSON
    ///
    /// This stores the serialized configuration parameters (e.g., OpenCodeConfig)
    /// that were used to create this connector. It is preserved across restarts
    /// to allow recreation of the connector instance with the same parameters.
    config: Arc<RwLock<serde_json::Value>>,

    /// Resolved working directory for this connector
    ///
    /// This is the actual working directory path resolved from the connector
    /// configuration and global settings. It is computed once during creation
    /// and stored for quick access.
    working_directory: Arc<RwLock<Option<std::path::PathBuf>>>,

    /// T034: Optional custom icon path for this connector
    ///
    /// If set, the UI should display this custom icon instead of the default
    /// connector type emoji.
    icon_path: Option<String>,

    /// T035: Show connector type emoji as overlay on custom icon
    ///
    /// When true and icon_path is set, the connector type emoji should appear
    /// as a small overlay in the lower-right corner of the custom icon.
    show_type_overlay: bool,

    /// Structured error classification for the current error state.
    ///
    /// Shared with the connector's background task via Arc. When `ConnectorState`
    /// is `Error`, this provides a machine-readable classification. Cleared to
    /// `None` when the connector recovers.
    error_kind: Arc<RwLock<Option<ConnectorErrorKind>>>,

    /// Dynamic commands reported by the remote agent.
    ///
    /// For ACP connectors, the agent sends `available_commands_update` notifications
    /// with its current slash commands. These are stored here so that
    /// `get_available_commands()` can return them. For non-ACP connectors this
    /// stays empty and static commands are returned based on connector kind.
    available_commands: Arc<RwLock<Vec<crate::acp::protocol::streaming::Command>>>,
}

impl ConnectorHandle {
    /// Create a new ConnectorHandle
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this connector
    /// * `kind` - Type of connector (OpenCode, Acp, Mock, etc.)
    /// * `owner` - User who owns this connector
    /// * `title` - Human-readable title for display
    /// * `cmd_tx` - Sender for commands to the connector
    /// * `events_tx` - Broadcast sender for events
    /// * `config` - Connector-specific configuration as JSON
    /// * `working_directory` - Optional working directory path
    /// * `icon_path` - T034: Optional custom icon path
    /// * `show_type_overlay` - T035: Show connector type emoji as overlay
    ///
    /// # Returns
    ///
    /// A new ConnectorHandle with state initialized to `Initializing`
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        id: ConnectorId,
        kind: ConnectorKind,
        owner: UserId,
        title: String,
        cmd_tx: mpsc::Sender<ConnectorCommand>,
        events_tx: broadcast::Sender<dirigent_protocol::Event>,
        config: serde_json::Value,
        working_directory: Option<std::path::PathBuf>,
        icon_path: Option<String>,
        show_type_overlay: bool,
    ) -> Self {
        Self {
            id,
            kind,
            owner,
            title,
            state: Arc::new(RwLock::new(ConnectorState::Initializing)),
            cmd_tx,
            events_tx,
            task_join: Some(Arc::new(RwLock::new(None))),
            config: Arc::new(RwLock::new(config)),
            working_directory: Arc::new(RwLock::new(working_directory)),
            icon_path,
            show_type_overlay,
            error_kind: Arc::new(RwLock::new(None)),
            available_commands: Arc::new(RwLock::new(Vec::new())),
        }
    }

    /// Create a new ConnectorHandle with a shared state Arc
    ///
    /// This constructor allows the handle to share the same state Arc as the
    /// connector implementation, ensuring state updates from the connector's
    /// background task are visible through the handle.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this connector
    /// * `kind` - Type of connector (OpenCode, Acp, Mock, etc.)
    /// * `owner` - User who owns this connector
    /// * `title` - Human-readable title for display
    /// * `state` - Shared state Arc from the connector
    /// * `cmd_tx` - Sender for commands to the connector
    /// * `events_tx` - Broadcast sender for events
    /// * `config` - Connector-specific configuration as JSON
    /// * `working_directory` - Optional working directory path
    /// * `icon_path` - T034: Optional custom icon path
    /// * `show_type_overlay` - T035: Show connector type emoji as overlay
    ///
    /// # Returns
    ///
    /// A new ConnectorHandle sharing the provided state Arc
    #[allow(clippy::too_many_arguments)]
    pub fn new_with_state(
        id: ConnectorId,
        kind: ConnectorKind,
        owner: UserId,
        title: String,
        state: Arc<RwLock<ConnectorState>>,
        cmd_tx: mpsc::Sender<ConnectorCommand>,
        events_tx: broadcast::Sender<dirigent_protocol::Event>,
        config: serde_json::Value,
        working_directory: Option<std::path::PathBuf>,
        icon_path: Option<String>,
        show_type_overlay: bool,
    ) -> Self {
        Self {
            id,
            kind,
            owner,
            title,
            state,
            cmd_tx,
            events_tx,
            task_join: Some(Arc::new(RwLock::new(None))),
            config: Arc::new(RwLock::new(config)),
            working_directory: Arc::new(RwLock::new(working_directory)),
            icon_path,
            show_type_overlay,
            error_kind: Arc::new(RwLock::new(None)),
            available_commands: Arc::new(RwLock::new(Vec::new())),
        }
    }

    /// Get a reference to the state RwLock
    ///
    /// This is useful for connector implementations that need to update
    /// the state as the connector progresses through its lifecycle.
    pub fn state_lock(&self) -> Arc<RwLock<ConnectorState>> {
        Arc::clone(&self.state)
    }

    /// Get the current error classification for this connector.
    ///
    /// Returns `None` when the connector is healthy, or `Some(kind)` when
    /// the connector is in an error state with a classified error.
    /// Uses `try_read()` to avoid blocking, returning `None` if the lock is held.
    pub fn error_kind(&self) -> Option<ConnectorErrorKind> {
        match self.error_kind.try_read() {
            Ok(guard) => guard.clone(),
            Err(_) => None,
        }
    }

    /// Replace the error_kind Arc with one shared from a connector implementation.
    ///
    /// This allows the ConnectorHandle to observe the same error_kind state
    /// that the connector's background task updates.
    pub fn set_error_kind_arc(&mut self, error_kind: Arc<RwLock<Option<ConnectorErrorKind>>>) {
        self.error_kind = error_kind;
    }

    /// Get a reference to the events broadcast sender
    ///
    /// This is useful for connector implementations that need to publish
    /// events to subscribers.
    pub fn events_sender(&self) -> broadcast::Sender<dirigent_protocol::Event> {
        self.events_tx.clone()
    }

    /// Set the task join handle
    ///
    /// This should be called by connector implementations after spawning
    /// their background task. The handle is used during shutdown to wait
    /// for the task to complete gracefully.
    pub async fn set_task_handle(&self, handle: JoinHandle<()>) {
        if let Some(task_join) = &self.task_join {
            let mut guard = task_join.write().await;
            *guard = Some(handle);
        }
    }

    /// Get the task join handle (if set)
    ///
    /// This is useful during shutdown to wait for the connector's task
    /// to complete.
    pub async fn take_task_handle(&self) -> Option<JoinHandle<()>> {
        if let Some(task_join) = &self.task_join {
            let mut guard = task_join.write().await;
            guard.take()
        } else {
            None
        }
    }

    /// Get a reference to the config RwLock
    ///
    /// This returns a clone of the config Arc, allowing access to the
    /// connector's configuration. This is primarily used during restart
    /// to recreate the connector with the same parameters.
    pub fn config(&self) -> Arc<RwLock<serde_json::Value>> {
        Arc::clone(&self.config)
    }

    /// Get a cloned copy of the connector configuration
    ///
    /// This is a convenience method that locks the config and returns
    /// a cloned copy of the JSON value. Use this when you need to read
    /// the config without holding the lock.
    pub async fn get_config_cloned(&self) -> serde_json::Value {
        let config_lock = self.config.read().await;
        config_lock.clone()
    }

    /// Update the stored configuration
    ///
    /// This method replaces the connector's configuration with the provided value.
    /// The changes take effect immediately in the stored config but only affect
    /// connector behavior after a restart.
    ///
    /// # Arguments
    ///
    /// * `config` - The new configuration value to store
    pub async fn set_config(&self, config: serde_json::Value) {
        let mut cfg = self.config.write().await;
        *cfg = config;
    }

    /// Get the resolved working directory for this connector
    ///
    /// Returns the working directory path that was resolved during connector
    /// creation. This may be a custom path set in the connector configuration,
    /// or the default project directory if no custom path was specified.
    pub fn working_directory(&self) -> Option<std::path::PathBuf> {
        // Try to read the working directory without blocking
        if let Ok(guard) = self.working_directory.try_read() {
            guard.clone()
        } else {
            None
        }
    }

    /// T034: Get the custom icon path for this connector
    ///
    /// Returns the optional custom icon path if set. The UI should use this
    /// icon instead of the default connector type emoji when present.
    pub fn icon_path(&self) -> Option<&str> {
        self.icon_path.as_deref()
    }

    /// T035: Check if type overlay should be shown on custom icon
    ///
    /// Returns true if the connector type emoji should appear as a small
    /// overlay in the lower-right corner of the custom icon.
    pub fn show_type_overlay(&self) -> bool {
        self.show_type_overlay
    }

    /// Set the icon path for this connector
    ///
    /// Updates the custom icon path. This change is reflected immediately.
    pub fn set_icon_path(&mut self, icon_path: Option<String>) {
        self.icon_path = icon_path;
    }

    /// Set whether to show type overlay on custom icon
    ///
    /// Updates the overlay setting. This change is reflected immediately.
    pub fn set_show_type_overlay(&mut self, show_overlay: bool) {
        self.show_type_overlay = show_overlay;
    }

    /// Set the connector title
    ///
    /// Updates the display title for this connector.
    pub fn set_title(&mut self, title: String) {
        self.title = title;
    }

    /// Set the working directory
    ///
    /// Updates the working directory for this connector. This is an async operation
    /// since working_directory is protected by an RwLock.
    pub async fn set_working_directory(&self, working_directory: Option<std::path::PathBuf>) {
        let mut wd = self.working_directory.write().await;
        *wd = working_directory;
    }

    /// Replace the command sender with a new one
    ///
    /// This is an internal method used during connector restart to update
    /// the command channel after recreating the connector instance.
    ///
    /// # Warning
    ///
    /// This is a low-level operation intended only for use by CoreRuntime's
    /// restart_connector method. External callers should not use this method.
    pub async fn replace_command_tx(&self, _new_tx: mpsc::Sender<ConnectorCommand>) {
        // We can't replace cmd_tx directly since it's not behind a lock
        // Instead, we rely on the fact that restart creates a new handle
        // This method is kept for potential future use with handle mutation
        // For now, document that restart will update references via CoreRuntime
    }

    /// Update the dynamic available commands for this connector.
    ///
    /// Called when the remote agent sends an `available_commands_update`
    /// notification. The stored commands are returned by
    /// `get_available_commands()` for ACP (and any other dynamic) connectors.
    pub fn set_available_commands(
        &self,
        commands: Vec<crate::acp::protocol::streaming::Command>,
    ) {
        if let Ok(mut guard) = self.available_commands.try_write() {
            *guard = commands;
        }
    }
}

impl Connector for ConnectorHandle {
    fn id(&self) -> &ConnectorId {
        &self.id
    }

    fn kind(&self) -> ConnectorKind {
        self.kind.clone()
    }

    fn owner(&self) -> &UserId {
        &self.owner
    }

    fn title(&self) -> &str {
        &self.title
    }

    fn state(&self) -> ConnectorState {
        // Try to read the state without blocking
        // If the lock is held, we return the last known state
        // This is acceptable for status queries where eventual consistency is fine
        match self.state.try_read() {
            Ok(state_lock) => state_lock.clone(),
            Err(_) => {
                // Lock is held, return Initializing as a safe default
                // In practice, this is extremely rare and only happens during
                // state transitions, which are very fast
                ConnectorState::Initializing
            }
        }
    }

    fn command_tx(&self) -> mpsc::Sender<ConnectorCommand> {
        self.cmd_tx.clone()
    }

    fn subscribe(&self) -> broadcast::Receiver<dirigent_protocol::Event> {
        self.events_tx.subscribe()
    }

    fn stop(&self) {
        // Send shutdown command
        // We don't wait for it to be processed - this is a cooperative shutdown
        let cmd_tx = self.cmd_tx.clone();
        tokio::spawn(async move {
            let _ = cmd_tx.send(ConnectorCommand::Shutdown).await;
        });
    }

    fn get_available_commands(&self) -> Vec<crate::acp::protocol::streaming::Command> {
        // First, read any dynamically-stored commands (populated by ACP agents
        // via available_commands_update notifications).
        let dynamic = self
            .available_commands
            .try_read()
            .map(|g| g.clone())
            .unwrap_or_default();

        if !dynamic.is_empty() {
            return dynamic;
        }

        // Fall back to static commands based on connector kind
        match self.kind {
            ConnectorKind::Gateway => vec![
                crate::acp::protocol::streaming::Command {
                    name: "echo".to_string(),
                    description: Some("Enable or disable echo mode (/echo on|off)".to_string()),
                },
                crate::acp::protocol::streaming::Command {
                    name: "help".to_string(),
                    description: Some("Show available commands".to_string()),
                },
                crate::acp::protocol::streaming::Command {
                    name: "list-connectors".to_string(),
                    description: Some("Show available connectors".to_string()),
                },
                crate::acp::protocol::streaming::Command {
                    name: "select-connector".to_string(),
                    description: Some(
                        "Transfer session to a connector (/select-connector <id>)".to_string(),
                    ),
                },
                crate::acp::protocol::streaming::Command {
                    name: "claude".to_string(),
                    description: Some("Transfer session to a Claude connector".to_string()),
                },
            ],
            _ => Vec::new(),
        }
    }
}

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

    #[test]
    fn test_connector_command_clone() {
        let cmd1 = ConnectorCommand::ListSessions;
        let cmd2 = cmd1.clone();

        // Both should be ListSessions variant
        matches!(cmd1, ConnectorCommand::ListSessions);
        matches!(cmd2, ConnectorCommand::ListSessions);
    }

    #[test]
    fn test_connector_command_debug() {
        let cmd = ConnectorCommand::SendMessage {
            session_id: "test-session".to_string(),
            text: "Hello".to_string(),
        };

        let debug_str = format!("{:?}", cmd);
        assert!(debug_str.contains("SendMessage"));
        assert!(debug_str.contains("test-session"));
    }

    #[tokio::test]
    async fn test_connector_handle_creation() {
        let (cmd_tx, _cmd_rx) = mpsc::channel(100);
        let (events_tx, _events_rx) = broadcast::channel(1000);

        let handle = ConnectorHandle::new(
            "test-connector".to_string(),
            ConnectorKind::Mock,
            uuid::Uuid::nil(),
            "Test Connector".to_string(),
            cmd_tx,
            events_tx,
            serde_json::json!({}),
            None,
            None,
            false,
        );

        assert_eq!(handle.id(), "test-connector");
        assert_eq!(handle.kind(), ConnectorKind::Mock);
        assert_eq!(*handle.owner(), uuid::Uuid::nil());
        assert_eq!(handle.title(), "Test Connector");
        assert_eq!(handle.state(), ConnectorState::Initializing);
    }

    #[tokio::test]
    async fn test_connector_handle_state_update() {
        let (cmd_tx, _cmd_rx) = mpsc::channel(100);
        let (events_tx, _events_rx) = broadcast::channel(1000);

        let handle = ConnectorHandle::new(
            "test-connector".to_string(),
            ConnectorKind::Mock,
            uuid::Uuid::nil(),
            "Test Connector".to_string(),
            cmd_tx,
            events_tx,
            serde_json::json!({}),
            None,
            None,
            false,
        );

        // Update state to Ready
        {
            let state_lock = handle.state_lock();
            let mut state = state_lock.write().await;
            *state = ConnectorState::Ready;
        }

        assert_eq!(handle.state(), ConnectorState::Ready);
    }

    #[tokio::test]
    async fn test_connector_handle_subscribe() {
        let (cmd_tx, _cmd_rx) = mpsc::channel(100);
        let (events_tx, _events_rx) = broadcast::channel(1000);

        let handle = ConnectorHandle::new(
            "test-connector".to_string(),
            ConnectorKind::Mock,
            uuid::Uuid::nil(),
            "Test Connector".to_string(),
            cmd_tx,
            events_tx.clone(),
            serde_json::json!({}),
            None,
            None,
            false,
        );

        // Subscribe to events
        let mut rx1 = handle.subscribe();
        let mut rx2 = handle.subscribe();

        // Send an event
        let event = dirigent_protocol::Event::Connected;
        events_tx.send(event.clone()).ok();

        // Both receivers should get the event
        let received1 = rx1.recv().await.unwrap();
        let received2 = rx2.recv().await.unwrap();

        matches!(received1, dirigent_protocol::Event::Connected);
        matches!(received2, dirigent_protocol::Event::Connected);
    }

    #[tokio::test]
    async fn test_connector_handle_command_tx() {
        let (cmd_tx, mut cmd_rx) = mpsc::channel(100);
        let (events_tx, _events_rx) = broadcast::channel(1000);

        let handle = ConnectorHandle::new(
            "test-connector".to_string(),
            ConnectorKind::Mock,
            uuid::Uuid::nil(),
            "Test Connector".to_string(),
            cmd_tx,
            events_tx,
            serde_json::json!({}),
            None,
            None,
            false,
        );

        // Send a command
        let sender = handle.command_tx();
        sender.send(ConnectorCommand::ListSessions).await.unwrap();

        // Receive the command
        let cmd = cmd_rx.recv().await.unwrap();
        matches!(cmd, ConnectorCommand::ListSessions);
    }

    #[tokio::test]
    async fn test_connector_handle_clone() {
        let (cmd_tx, _cmd_rx) = mpsc::channel(100);
        let (events_tx, _events_rx) = broadcast::channel(1000);

        let handle1 = ConnectorHandle::new(
            "test-connector".to_string(),
            ConnectorKind::Mock,
            uuid::Uuid::nil(),
            "Test Connector".to_string(),
            cmd_tx,
            events_tx,
            serde_json::json!({}),
            None,
            None,
            false,
        );

        let handle2 = handle1.clone();

        // Both handles should refer to the same connector
        assert_eq!(handle1.id(), handle2.id());
        assert_eq!(handle1.kind(), handle2.kind());

        // Update state via handle1
        {
            let state_lock = handle1.state_lock();
            let mut state = state_lock.write().await;
            *state = ConnectorState::Ready;
        }

        // handle2 should see the update
        assert_eq!(handle2.state(), ConnectorState::Ready);
    }
}
