// file: kb_lib/src/ws_client.rs

//! Generic asynchronous WebSocket transport client.
//!
//! Version `0.3.x` keeps the transport and JSON-RPC helpers introduced earlier
//! and adds:
//! - a registry of pending JSON-RPC requests
//! - a registry of active subscriptions
//! - automatic routing of notifications to known subscriptions
//! - automatic unsubscribe attempts before disconnect

use futures_util::SinkExt;
use futures_util::StreamExt;

/// Outgoing transport-level WebSocket message.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WsOutgoingMessage {
    /// UTF-8 text message.
    Text(std::string::String),
    /// Binary message.
    Binary(std::vec::Vec<u8>),
    /// Ping message.
    Ping(std::vec::Vec<u8>),
    /// Pong message.
    Pong(std::vec::Vec<u8>),
    /// Close handshake initiation.
    Close,
}

/// Active subscription metadata tracked by the client runtime.
#[derive(Clone, Debug, PartialEq)]
pub struct WsSubscriptionInfo {
    /// Local request identifier that created the subscription.
    pub request_id: u64,
    /// Remote subscription identifier returned by the server.
    pub subscription_id: u64,
    /// Subscribe method name.
    pub subscribe_method: std::string::String,
    /// Unsubscribe method name paired with the subscription.
    pub unsubscribe_method: std::string::String,
    /// Expected notification method name.
    pub notification_method: std::string::String,
    /// Original subscribe request parameters.
    pub params: std::vec::Vec<serde_json::Value>,
}

/// Incoming WebSocket transport event emitted by [`crate::WsClient`].
#[derive(Clone, Debug, PartialEq)]
pub enum WsEvent {
    /// Connection established successfully.
    Connected {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Resolved endpoint URL actually used for the connection attempt.
        endpoint_url: std::string::String,
    },
    /// Text message received.
    TextMessage {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Received text payload.
        text: std::string::String,
    },
    /// Parsed JSON-RPC text message received.
    JsonRpcMessage {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Parsed JSON-RPC message.
        message: crate::JsonRpcWsIncomingMessage,
    },
    /// JSON-RPC parsing failed for a text message that looked like JSON.
    JsonRpcParseError {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Original text payload.
        text: std::string::String,
        /// Parse error.
        error: crate::Error,
    },
    /// A subscribe response created a tracked active subscription.
    SubscriptionRegistered {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Registered subscription metadata.
        subscription: WsSubscriptionInfo,
    },
    /// A notification was matched to a tracked active subscription.
    SubscriptionNotification {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Matched subscription metadata.
        subscription: WsSubscriptionInfo,
        /// Received notification payload.
        notification: crate::JsonRpcWsNotification,
        /// Indicates whether the notification method matches the expected one.
        method_matches_registry: bool,
    },
    /// A notification could not be matched to any tracked active subscription.
    JsonRpcNotificationWithoutSubscription {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Received notification payload.
        notification: crate::JsonRpcWsNotification,
    },
    /// An unsubscribe response removed a tracked subscription.
    SubscriptionUnregistered {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Removed subscription identifier.
        subscription_id: u64,
        /// Unsubscribe method used by the request.
        unsubscribe_method: std::string::String,
        /// Indicates whether the subscription was active before removal.
        was_active: bool,
    },
    /// Binary message received.
    BinaryMessage {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Received binary payload.
        data: std::vec::Vec<u8>,
    },
    /// Ping frame received.
    Ping {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Ping payload.
        data: std::vec::Vec<u8>,
    },
    /// Pong frame received.
    Pong {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Pong payload.
        data: std::vec::Vec<u8>,
    },
    /// Close frame received.
    CloseReceived {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Optional close code.
        code: std::option::Option<u16>,
        /// Optional textual reason.
        reason: std::option::Option<std::string::String>,
    },
    /// Connection lifecycle reached the disconnected state.
    Disconnected {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
    },
    /// Transport-level error.
    Error {
        /// Stable endpoint name from configuration.
        endpoint_name: std::string::String,
        /// Captured error.
        error: crate::Error,
    },
}

#[derive(Debug)]
struct WsClientRuntime {
    generation: u64,
    writer_tx: std::option::Option<tokio::sync::mpsc::Sender<WsWriteCommand>>,
    shutdown_tx: std::option::Option<tokio::sync::watch::Sender<bool>>,
    completion_notify: std::option::Option<std::sync::Arc<tokio::sync::Notify>>,
    read_abort_handle: std::option::Option<tokio::task::AbortHandle>,
    write_abort_handle: std::option::Option<tokio::task::AbortHandle>,
    supervisor_abort_handle: std::option::Option<tokio::task::AbortHandle>,
}

impl WsClientRuntime {
    fn new() -> Self {
        return Self {
            generation: 0,
            writer_tx: None,
            shutdown_tx: None,
            completion_notify: None,
            read_abort_handle: None,
            write_abort_handle: None,
            supervisor_abort_handle: None,
        };
    }
}

#[derive(Debug)]
struct WsClientRegistry {
    pending_requests: std::collections::BTreeMap<u64, WsPendingJsonRpcRequest>,
    active_subscriptions: std::collections::BTreeMap<u64, WsSubscriptionInfo>,
}

impl WsClientRegistry {
    fn new() -> Self {
        return Self {
            pending_requests: std::collections::BTreeMap::new(),
            active_subscriptions: std::collections::BTreeMap::new(),
        };
    }
}

#[derive(Clone, Debug, PartialEq)]
struct WsPendingJsonRpcRequest {
    request_id: u64,
    method: std::string::String,
    kind: WsPendingJsonRpcRequestKind,
}

#[derive(Clone, Debug, PartialEq)]
enum WsPendingJsonRpcRequestKind {
    Generic,
    Subscribe {
        notification_method: std::string::String,
        unsubscribe_method: std::string::String,
        params: std::vec::Vec<serde_json::Value>,
    },
    Unsubscribe {
        subscription_id: u64,
        unsubscribe_method: std::string::String,
    },
}

#[derive(Clone, Debug)]
enum WsWriteCommand {
    Send(WsOutgoingMessage),
}

/// Generic asynchronous WebSocket client.
#[derive(Clone, Debug)]
pub struct WsClient {
    endpoint: crate::WsEndpointConfig,
    resolved_url: std::string::String,
    next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
    state: std::sync::Arc<tokio::sync::RwLock<crate::ConnectionState>>,
    event_tx: tokio::sync::broadcast::Sender<WsEvent>,
    runtime: std::sync::Arc<tokio::sync::Mutex<WsClientRuntime>>,
    registry: std::sync::Arc<tokio::sync::Mutex<WsClientRegistry>>,
    detection_notification_forwarder: std::sync::Arc<
        tokio::sync::Mutex<
            std::option::Option<tokio::sync::mpsc::Sender<crate::WsDetectionNotificationEnvelope>>,
        >,
    >,
    transaction_resolution_notification_forwarder: std::sync::Arc<
        tokio::sync::Mutex<
            std::option::Option<tokio::sync::mpsc::Sender<crate::WsTransactionResolutionEnvelope>>,
        >,
    >,
}

impl WsClient {
    /// Creates a new WebSocket client bound to an endpoint configuration.
    pub fn new(endpoint: crate::WsEndpointConfig) -> Result<Self, crate::Error> {
        if endpoint.name.trim().is_empty() {
            return Err(crate::Error::Config(
                "ws client endpoint name must not be empty".to_string(),
            ));
        }
        let resolved_url_result = endpoint.resolved_url();
        let resolved_url = match resolved_url_result {
            Ok(resolved_url) => resolved_url,
            Err(error) => return Err(error),
        };
        let (event_tx, _) = tokio::sync::broadcast::channel(endpoint.event_channel_capacity);
        return Ok(Self {
            endpoint,
            resolved_url,
            next_request_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
            state: std::sync::Arc::new(tokio::sync::RwLock::new(
                crate::ConnectionState::Disconnected,
            )),
            event_tx,
            runtime: std::sync::Arc::new(tokio::sync::Mutex::new(WsClientRuntime::new())),
            registry: std::sync::Arc::new(tokio::sync::Mutex::new(WsClientRegistry::new())),
            detection_notification_forwarder: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
            transaction_resolution_notification_forwarder: std::sync::Arc::new(
                tokio::sync::Mutex::new(None),
            ),
        });
    }

    /// Returns the endpoint name of this client.
    pub fn endpoint_name(&self) -> &str {
        return &self.endpoint.name;
    }

    /// Returns the resolved endpoint URL of this client.
    pub fn endpoint_url(&self) -> &str {
        return &self.resolved_url;
    }

    /// Returns the endpoint configuration of this client.
    pub fn endpoint_config(&self) -> &crate::WsEndpointConfig {
        return &self.endpoint;
    }

    /// Returns a new broadcast receiver subscribed to transport events.
    pub fn subscribe_events(&self) -> tokio::sync::broadcast::Receiver<WsEvent> {
        return self.event_tx.subscribe();
    }

    /// Sets the optional detection notification forwarder.
    pub async fn set_detection_notification_forwarder(
        &self,
        sender: tokio::sync::mpsc::Sender<crate::WsDetectionNotificationEnvelope>,
    ) {
        let mut guard = self.detection_notification_forwarder.lock().await;
        *guard = Some(sender);
    }

    /// Clears the optional detection notification forwarder.
    pub async fn clear_detection_notification_forwarder(&self) {
        let mut guard = self.detection_notification_forwarder.lock().await;
        *guard = None;
    }

    /// Returns the next request identifier and increments the internal counter.
    pub fn next_request_id(&self) -> u64 {
        return self.next_request_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    }

    /// Returns the current connection state.
    pub async fn connection_state(&self) -> crate::ConnectionState {
        let state_guard = self.state.read().await;
        return *state_guard;
    }

    /// Returns the number of tracked pending JSON-RPC requests.
    pub async fn pending_request_count(&self) -> usize {
        let registry_guard = self.registry.lock().await;
        return registry_guard.pending_requests.len();
    }

    /// Returns the number of tracked active subscriptions.
    pub async fn active_subscription_count(&self) -> usize {
        let registry_guard = self.registry.lock().await;
        return registry_guard.active_subscriptions.len();
    }

    /// Returns a snapshot of the tracked active subscriptions.
    pub async fn active_subscriptions(&self) -> std::vec::Vec<WsSubscriptionInfo> {
        let registry_guard = self.registry.lock().await;
        return registry_guard.active_subscriptions.values().cloned().collect();
    }

    /// Connects the client to its remote WebSocket endpoint.
    pub async fn connect(&self) -> Result<(), crate::Error> {
        if !self.endpoint.enabled {
            return Err(crate::Error::InvalidState(format!(
                "ws endpoint '{}' is disabled in configuration",
                self.endpoint.name
            )));
        }
        let current_state = self.connection_state().await;
        if current_state != crate::ConnectionState::Disconnected {
            return Err(crate::Error::InvalidState(format!(
                "ws client '{}' cannot connect from state {:?}",
                self.endpoint.name, current_state
            )));
        }
        {
            let mut state_guard = self.state.write().await;
            *state_guard = crate::ConnectionState::Connecting;
        }
        tracing::trace!(
            endpoint_name = %self.endpoint.name,
            endpoint_url = %self.resolved_url,
            "connecting websocket client"
        );
        let connect_timeout = std::time::Duration::from_millis(self.endpoint.connect_timeout_ms);
        let connect_future = tokio_tungstenite::connect_async(self.resolved_url.clone());
        let timeout_result = tokio::time::timeout(connect_timeout, connect_future).await;
        let connect_result = match timeout_result {
            Ok(connect_result) => connect_result,
            Err(_) => {
                let error = crate::Error::Ws(format!(
                    "timeout while connecting websocket endpoint '{}'",
                    self.endpoint.name
                ));
                self.emit_event(WsEvent::Error {
                    endpoint_name: self.endpoint.name.clone(),
                    error: error.clone(),
                });
                let mut state_guard = self.state.write().await;
                *state_guard = crate::ConnectionState::Disconnected;
                return Err(error);
            },
        };
        let (ws_stream, _response) = match connect_result {
            Ok(parts) => parts,
            Err(error) => {
                let error = crate::Error::Ws(format!(
                    "cannot connect websocket endpoint '{}': {error}",
                    self.endpoint.name
                ));
                self.emit_event(WsEvent::Error {
                    endpoint_name: self.endpoint.name.clone(),
                    error: error.clone(),
                });
                let mut state_guard = self.state.write().await;
                *state_guard = crate::ConnectionState::Disconnected;
                return Err(error);
            },
        };
        let (write_half, read_half) = ws_stream.split();
        let (writer_tx, writer_rx) =
            tokio::sync::mpsc::channel(self.endpoint.write_channel_capacity);
        let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
        let completion_notify = std::sync::Arc::new(tokio::sync::Notify::new());
        let read_client = self.clone();
        let read_shutdown_tx = shutdown_tx.clone();
        let read_handle = tokio::spawn(async move {
            read_client.run_read_loop(read_half, read_shutdown_tx).await;
        });
        let read_abort_handle = read_handle.abort_handle();
        let write_client = self.clone();
        let write_handle = tokio::spawn(async move {
            write_client.run_write_loop(write_half, writer_rx, shutdown_rx).await;
        });
        let write_abort_handle = write_handle.abort_handle();
        let generation = {
            let mut runtime_guard = self.runtime.lock().await;
            if runtime_guard.writer_tx.is_some() {
                let mut state_guard = self.state.write().await;
                *state_guard = crate::ConnectionState::Disconnected;
                return Err(crate::Error::InvalidState(format!(
                    "ws client '{}' runtime is already active",
                    self.endpoint.name
                )));
            }
            runtime_guard.generation = runtime_guard.generation.saturating_add(1);
            runtime_guard.writer_tx = Some(writer_tx.clone());
            runtime_guard.shutdown_tx = Some(shutdown_tx.clone());
            runtime_guard.completion_notify = Some(completion_notify.clone());
            runtime_guard.read_abort_handle = Some(read_abort_handle);
            runtime_guard.write_abort_handle = Some(write_abort_handle);
            runtime_guard.generation
        };
        let supervisor_client = self.clone();
        let supervisor_notify = completion_notify.clone();
        let supervisor_handle = tokio::spawn(async move {
            supervisor_client
                .run_supervisor(generation, read_handle, write_handle, supervisor_notify)
                .await;
        });
        let supervisor_abort_handle = supervisor_handle.abort_handle();
        {
            let mut runtime_guard = self.runtime.lock().await;
            if runtime_guard.generation == generation {
                runtime_guard.supervisor_abort_handle = Some(supervisor_abort_handle);
            }
        }
        {
            let mut state_guard = self.state.write().await;
            *state_guard = crate::ConnectionState::Connected;
        }
        self.emit_event(WsEvent::Connected {
            endpoint_name: self.endpoint.name.clone(),
            endpoint_url: self.resolved_url.clone(),
        });
        tracing::trace!(
            endpoint_name = %self.endpoint.name,
            endpoint_url = %self.resolved_url,
            "websocket client connected"
        );
        return Ok(());
    }

    /// Sends a transport-level WebSocket message.
    pub async fn send_message(&self, message: WsOutgoingMessage) -> Result<(), crate::Error> {
        let writer_tx_option = {
            let runtime_guard = self.runtime.lock().await;
            runtime_guard.writer_tx.clone()
        };
        let writer_tx = match writer_tx_option {
            Some(writer_tx) => writer_tx,
            None => {
                return Err(crate::Error::NotConnected(format!(
                    "ws client '{}' is not connected",
                    self.endpoint.name
                )));
            },
        };
        let queue_timeout = std::time::Duration::from_millis(self.endpoint.request_timeout_ms);
        let send_future = writer_tx.send(WsWriteCommand::Send(message));
        let timeout_result = tokio::time::timeout(queue_timeout, send_future).await;
        match timeout_result {
            Ok(send_result) => match send_result {
                Ok(()) => return Ok(()),
                Err(error) => {
                    return Err(crate::Error::Ws(format!(
                        "cannot queue outgoing websocket message for endpoint '{}': {error}",
                        self.endpoint.name
                    )));
                },
            },
            Err(_) => {
                return Err(crate::Error::Ws(format!(
                    "timeout while queueing outgoing websocket message for endpoint '{}'",
                    self.endpoint.name
                )));
            },
        }
    }

    /// Sends a UTF-8 text message.
    pub async fn send_text(&self, text: std::string::String) -> Result<(), crate::Error> {
        return self.send_message(WsOutgoingMessage::Text(text)).await;
    }

    /// Sends a binary message.
    pub async fn send_binary(&self, data: std::vec::Vec<u8>) -> Result<(), crate::Error> {
        return self.send_message(WsOutgoingMessage::Binary(data)).await;
    }

    /// Sends a ping message.
    pub async fn send_ping(&self, data: std::vec::Vec<u8>) -> Result<(), crate::Error> {
        return self.send_message(WsOutgoingMessage::Ping(data)).await;
    }

    /// Sends a pong message.
    pub async fn send_pong(&self, data: std::vec::Vec<u8>) -> Result<(), crate::Error> {
        return self.send_message(WsOutgoingMessage::Pong(data)).await;
    }

    /// Serializes and sends a JSON value as a text message.
    pub async fn send_json_value(&self, value: &serde_json::Value) -> Result<(), crate::Error> {
        let serialization_result = serde_json::to_string(value);
        let text = match serialization_result {
            Ok(text) => text,
            Err(error) => {
                return Err(crate::Error::Json(format!(
                    "cannot serialize websocket json payload for endpoint '{}': {error}",
                    self.endpoint.name
                )));
            },
        };
        return self.send_text(text).await;
    }

    /// Sends a prebuilt JSON-RPC request object and tracks it when the request id is numeric.
    pub async fn send_json_rpc_request_object(
        &self,
        request: &crate::JsonRpcWsRequest,
    ) -> Result<(), crate::Error> {
        let value_result = request.to_value();
        let value = match value_result {
            Ok(value) => value,
            Err(error) => return Err(error),
        };
        let tracked_request = build_pending_json_rpc_request(request);
        if let Some(tracked_request) = &tracked_request {
            let mut registry_guard = self.registry.lock().await;
            registry_guard
                .pending_requests
                .insert(tracked_request.request_id, tracked_request.clone());
        }
        let send_result = self.send_json_value(&value).await;
        if let Err(error) = send_result {
            if let Some(tracked_request) = tracked_request {
                let mut registry_guard = self.registry.lock().await;
                registry_guard.pending_requests.remove(&tracked_request.request_id);
            }
            return Err(error);
        }
        return Ok(());
    }

    /// Builds and sends a JSON-RPC request with a generated numeric identifier.
    ///
    /// Returns the generated request identifier.
    pub async fn send_json_rpc_request(
        &self,
        method: std::string::String,
        params: std::vec::Vec<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let request_id = self.next_request_id();
        let request = crate::JsonRpcWsRequest::new_with_u64_id(request_id, method, params);
        let send_result = self.send_json_rpc_request_object(&request).await;
        match send_result {
            Ok(()) => return Ok(request_id),
            Err(error) => return Err(error),
        }
    }

    /// Subscribes to account change notifications using raw JSON values.
    pub async fn account_subscribe_raw(
        &self,
        pubkey: std::string::String,
        config: std::option::Option<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let params = build_single_key_optional_config_params(pubkey, config);
        return self.send_json_rpc_request("accountSubscribe".to_string(), params).await;
    }

    /// Unsubscribes from an account change subscription using raw JSON values.
    pub async fn account_unsubscribe_raw(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "accountUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to block notifications using raw JSON values.
    ///
    /// The Solana RPC documentation marks this subscription as unstable.
    pub async fn block_subscribe_raw(
        &self,
        filter: serde_json::Value,
        config: std::option::Option<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let params = build_first_value_optional_config_params(filter, config);
        return self.send_json_rpc_request("blockSubscribe".to_string(), params).await;
    }

    /// Unsubscribes from a block notification subscription using raw JSON values.
    pub async fn block_unsubscribe_raw(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "blockUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to transaction log notifications using raw JSON values.
    pub async fn logs_subscribe_raw(
        &self,
        filter: serde_json::Value,
        config: std::option::Option<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let params = build_first_value_optional_config_params(filter, config);
        return self.send_json_rpc_request("logsSubscribe".to_string(), params).await;
    }

    /// Unsubscribes from a logs subscription using raw JSON values.
    pub async fn logs_unsubscribe_raw(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "logsUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to program-owned account notifications using raw JSON values.
    pub async fn program_subscribe_raw(
        &self,
        program_id: std::string::String,
        config: std::option::Option<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let params = build_single_key_optional_config_params(program_id, config);
        return self.send_json_rpc_request("programSubscribe".to_string(), params).await;
    }

    /// Unsubscribes from a program subscription using raw JSON values.
    pub async fn program_unsubscribe_raw(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "programUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to root notifications.
    pub async fn root_subscribe(&self) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request("rootSubscribe".to_string(), std::vec::Vec::new())
            .await;
    }

    /// Unsubscribes from a root subscription.
    pub async fn root_unsubscribe(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "rootUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to one transaction signature status using raw JSON values.
    pub async fn signature_subscribe_raw(
        &self,
        signature: std::string::String,
        config: std::option::Option<serde_json::Value>,
    ) -> Result<u64, crate::Error> {
        let params = build_single_key_optional_config_params(signature, config);
        return self.send_json_rpc_request("signatureSubscribe".to_string(), params).await;
    }

    /// Unsubscribes from a signature subscription using raw JSON values.
    pub async fn signature_unsubscribe_raw(
        &self,
        subscription_id: u64,
    ) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "signatureUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to slot notifications.
    pub async fn slot_subscribe(&self) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request("slotSubscribe".to_string(), std::vec::Vec::new())
            .await;
    }

    /// Unsubscribes from a slot subscription.
    pub async fn slot_unsubscribe(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "slotUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to slot lifecycle update notifications.
    pub async fn slots_updates_subscribe(&self) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request("slotsUpdatesSubscribe".to_string(), std::vec::Vec::new())
            .await;
    }

    /// Unsubscribes from a slot lifecycle update subscription.
    pub async fn slots_updates_unsubscribe(
        &self,
        subscription_id: u64,
    ) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "slotsUpdatesUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Subscribes to vote notifications.
    pub async fn vote_subscribe(&self) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request("voteSubscribe".to_string(), std::vec::Vec::new())
            .await;
    }

    /// Unsubscribes from a vote subscription.
    pub async fn vote_unsubscribe(&self, subscription_id: u64) -> Result<u64, crate::Error> {
        return self
            .send_json_rpc_request(
                "voteUnsubscribe".to_string(),
                vec![serde_json::Value::from(subscription_id)],
            )
            .await;
    }

    /// Initiates the close handshake.
    pub async fn send_close(&self) -> Result<(), crate::Error> {
        return self.send_message(WsOutgoingMessage::Close).await;
    }

    /// Disconnects the client from its remote endpoint.
    ///
    /// Before closing the transport, this method attempts to unsubscribe all
    /// currently tracked active subscriptions and waits up to
    /// `unsubscribe_timeout_ms` for their removal.
    pub async fn disconnect(&self) -> Result<(), crate::Error> {
        let current_state = self.connection_state().await;
        if current_state == crate::ConnectionState::Disconnected {
            return Ok(());
        }
        if current_state == crate::ConnectionState::Connecting {
            return Err(crate::Error::InvalidState(format!(
                "ws client '{}' cannot disconnect while still connecting",
                self.endpoint.name
            )));
        }
        {
            let mut state_guard = self.state.write().await;
            *state_guard = crate::ConnectionState::Disconnecting;
        }
        tracing::trace!(
            endpoint_name = %self.endpoint.name,
            "disconnecting websocket client"
        );
        let auto_unsubscribe_result = self.unsubscribe_all_active_subscriptions().await;
        if let Err(error) = auto_unsubscribe_result {
            tracing::warn!(
                endpoint_name = %self.endpoint.name,
                "automatic unsubscribe phase failed before disconnect: {}",
                error
            );
        }
        let (
            generation,
            writer_tx_option,
            shutdown_tx_option,
            completion_notify_option,
            read_abort_handle_option,
            write_abort_handle_option,
            supervisor_abort_handle_option,
        ) = {
            let runtime_guard = self.runtime.lock().await;
            (
                runtime_guard.generation,
                runtime_guard.writer_tx.clone(),
                runtime_guard.shutdown_tx.clone(),
                runtime_guard.completion_notify.clone(),
                runtime_guard.read_abort_handle.clone(),
                runtime_guard.write_abort_handle.clone(),
                runtime_guard.supervisor_abort_handle.clone(),
            )
        };
        if let Some(writer_tx) = writer_tx_option {
            let close_timeout = std::time::Duration::from_millis(self.endpoint.request_timeout_ms);
            let close_send_future = writer_tx.send(WsWriteCommand::Send(WsOutgoingMessage::Close));
            let close_send_timeout_result =
                tokio::time::timeout(close_timeout, close_send_future).await;
            match close_send_timeout_result {
                Ok(close_send_result) => {
                    if let Err(error) = close_send_result {
                        tracing::warn!(
                            endpoint_name = %self.endpoint.name,
                            "cannot queue close frame during disconnect: {error}"
                        );
                    }
                },
                Err(_) => {
                    tracing::warn!(
                        endpoint_name = %self.endpoint.name,
                        "timeout while queueing close frame during disconnect"
                    );
                },
            }
        }
        if let Some(shutdown_tx) = shutdown_tx_option {
            let send_result = shutdown_tx.send(true);
            if let Err(error) = send_result {
                tracing::trace!(
                    endpoint_name = %self.endpoint.name,
                    "shutdown signal could not be delivered because receiver is already gone: {error}"
                );
            }
        }
        let completion_notify = match completion_notify_option {
            Some(completion_notify) => completion_notify,
            None => {
                let state_changed = self.clear_runtime_after_generation(generation).await;
                if state_changed {
                    self.emit_event(WsEvent::Disconnected {
                        endpoint_name: self.endpoint.name.clone(),
                    });
                }
                return Ok(());
            },
        };
        let completion_timeout = std::time::Duration::from_millis(self.endpoint.request_timeout_ms);
        let notified_future = completion_notify.notified();
        let completion_result = tokio::time::timeout(completion_timeout, notified_future).await;
        match completion_result {
            Ok(()) => return Ok(()),
            Err(_) => {
                tracing::warn!(
                    endpoint_name = %self.endpoint.name,
                    "disconnect timeout reached, aborting websocket tasks"
                );
                if let Some(read_abort_handle) = read_abort_handle_option {
                    read_abort_handle.abort();
                }
                if let Some(write_abort_handle) = write_abort_handle_option {
                    write_abort_handle.abort();
                }
                if let Some(supervisor_abort_handle) = supervisor_abort_handle_option {
                    supervisor_abort_handle.abort();
                }
                let state_changed = self.clear_runtime_after_generation(generation).await;
                if state_changed {
                    self.emit_event(WsEvent::Disconnected {
                        endpoint_name: self.endpoint.name.clone(),
                    });
                }
                return Ok(());
            },
        }
    }

    async fn unsubscribe_all_active_subscriptions(&self) -> Result<usize, crate::Error> {
        let subscriptions = self.active_subscriptions().await;
        if subscriptions.is_empty() {
            return Ok(0);
        }
        tracing::trace!(
            endpoint_name = %self.endpoint.name,
            subscription_count = subscriptions.len(),
            "sending automatic unsubscribe requests before disconnect"
        );
        for subscription in &subscriptions {
            let unsubscribe_params = vec![serde_json::Value::from(subscription.subscription_id)];
            let send_result = self
                .send_json_rpc_request(subscription.unsubscribe_method.clone(), unsubscribe_params)
                .await;
            if let Err(error) = send_result {
                tracing::warn!(
                    endpoint_name = %self.endpoint.name,
                    subscription_id = subscription.subscription_id,
                    unsubscribe_method = %subscription.unsubscribe_method,
                    "cannot send automatic unsubscribe request: {}",
                    error
                );
            }
        }
        let started_at = std::time::Instant::now();
        let wait_timeout = std::time::Duration::from_millis(self.endpoint.unsubscribe_timeout_ms);
        loop {
            let active_count = self.active_subscription_count().await;
            if active_count == 0 {
                break;
            }
            if started_at.elapsed() >= wait_timeout {
                tracing::warn!(
                    endpoint_name = %self.endpoint.name,
                    remaining_active_subscriptions = active_count,
                    "automatic unsubscribe wait timeout reached"
                );
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
        }
        return Ok(subscriptions.len());
    }

    async fn run_supervisor(
        &self,
        generation: u64,
        read_handle: tokio::task::JoinHandle<()>,
        write_handle: tokio::task::JoinHandle<()>,
        completion_notify: std::sync::Arc<tokio::sync::Notify>,
    ) {
        let read_join_result = read_handle.await;
        if let Err(error) = read_join_result {
            let error = crate::Error::Ws(format!(
                "read task for endpoint '{}' ended with join error: {error}",
                self.endpoint.name
            ));
            self.emit_event(WsEvent::Error {
                endpoint_name: self.endpoint.name.clone(),
                error,
            });
        }
        let write_join_result = write_handle.await;
        if let Err(error) = write_join_result {
            let error = crate::Error::Ws(format!(
                "write task for endpoint '{}' ended with join error: {error}",
                self.endpoint.name
            ));
            self.emit_event(WsEvent::Error {
                endpoint_name: self.endpoint.name.clone(),
                error,
            });
        }
        let state_changed = self.clear_runtime_after_generation(generation).await;
        completion_notify.notify_waiters();
        if state_changed {
            self.emit_event(WsEvent::Disconnected {
                endpoint_name: self.endpoint.name.clone(),
            });
        }
    }

    async fn run_read_loop<S>(
        &self,
        mut read_half: futures_util::stream::SplitStream<tokio_tungstenite::WebSocketStream<S>>,
        shutdown_tx: tokio::sync::watch::Sender<bool>,
    ) where
        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + std::marker::Send + 'static,
    {
        loop {
            let message_option = read_half.next().await;
            let message_result = match message_option {
                Some(message_result) => message_result,
                None => {
                    let _ = shutdown_tx.send(true);
                    break;
                },
            };
            let message = match message_result {
                Ok(message) => message,
                Err(error) => {
                    if !is_normal_close_error(&error) {
                        self.emit_event(WsEvent::Error {
                            endpoint_name: self.endpoint.name.clone(),
                            error: crate::Error::Ws(format!(
                                "read error on endpoint '{}': {error}",
                                self.endpoint.name
                            )),
                        });
                    }
                    let _ = shutdown_tx.send(true);
                    break;
                },
            };
            match message {
                tokio_tungstenite::tungstenite::Message::Text(text) => {
                    let text_string = text.to_string();
                    self.emit_event(WsEvent::TextMessage {
                        endpoint_name: self.endpoint.name.clone(),
                        text: text_string.clone(),
                    });
                    if crate::is_probable_json_rpc_object_text(&text_string) {
                        let parse_result = crate::parse_json_rpc_ws_incoming_text(&text_string);
                        match parse_result {
                            Ok(parsed_message) => {
                                self.emit_event(WsEvent::JsonRpcMessage {
                                    endpoint_name: self.endpoint.name.clone(),
                                    message: parsed_message.clone(),
                                });
                                self.handle_incoming_json_rpc_message(&parsed_message).await;
                            },
                            Err(error) => {
                                self.emit_event(WsEvent::JsonRpcParseError {
                                    endpoint_name: self.endpoint.name.clone(),
                                    text: text_string.clone(),
                                    error,
                                });
                            },
                        }
                    }
                },
                tokio_tungstenite::tungstenite::Message::Binary(data) => {
                    self.emit_event(WsEvent::BinaryMessage {
                        endpoint_name: self.endpoint.name.clone(),
                        data: data.to_vec(),
                    });
                },
                tokio_tungstenite::tungstenite::Message::Ping(data) => {
                    self.emit_event(WsEvent::Ping {
                        endpoint_name: self.endpoint.name.clone(),
                        data: data.to_vec(),
                    });
                },
                tokio_tungstenite::tungstenite::Message::Pong(data) => {
                    self.emit_event(WsEvent::Pong {
                        endpoint_name: self.endpoint.name.clone(),
                        data: data.to_vec(),
                    });
                },
                tokio_tungstenite::tungstenite::Message::Close(frame_option) => {
                    let mut code_option = None;
                    let mut reason_option = None;
                    if let Some(frame) = frame_option {
                        let code: u16 = frame.code.into();
                        code_option = Some(code);
                        if !frame.reason.is_empty() {
                            reason_option = Some(frame.reason.to_string());
                        }
                    }
                    self.emit_event(WsEvent::CloseReceived {
                        endpoint_name: self.endpoint.name.clone(),
                        code: code_option,
                        reason: reason_option,
                    });
                    let _ = shutdown_tx.send(true);
                    break;
                },
                tokio_tungstenite::tungstenite::Message::Frame(_frame) => {
                    tracing::trace!(
                        endpoint_name = %self.endpoint.name,
                        "ignoring internal tungstenite frame variant"
                    );
                },
            }
        }
    }

    async fn handle_incoming_json_rpc_message(&self, message: &crate::JsonRpcWsIncomingMessage) {
        match message {
            crate::JsonRpcWsIncomingMessage::SuccessResponse(response) => {
                let request_id_option = json_value_to_u64(&response.id);
                let request_id = match request_id_option {
                    Some(request_id) => request_id,
                    None => return,
                };
                let pending_request_option = {
                    let mut registry_guard = self.registry.lock().await;
                    registry_guard.pending_requests.remove(&request_id)
                };
                let pending_request = match pending_request_option {
                    Some(pending_request) => pending_request,
                    None => return,
                };
                match pending_request.kind {
                    WsPendingJsonRpcRequestKind::Generic => {},
                    WsPendingJsonRpcRequestKind::Subscribe {
                        notification_method,
                        unsubscribe_method,
                        params,
                    } => {
                        let subscription_id_option = response.result.as_u64();
                        let subscription_id = match subscription_id_option {
                            Some(subscription_id) => subscription_id,
                            None => return,
                        };
                        let subscription = WsSubscriptionInfo {
                            request_id,
                            subscription_id,
                            subscribe_method: pending_request.method,
                            unsubscribe_method,
                            notification_method,
                            params,
                        };
                        {
                            let mut registry_guard = self.registry.lock().await;
                            registry_guard
                                .active_subscriptions
                                .insert(subscription_id, subscription.clone());
                        }
                        self.emit_event(WsEvent::SubscriptionRegistered {
                            endpoint_name: self.endpoint.name.clone(),
                            subscription,
                        });
                    },
                    WsPendingJsonRpcRequestKind::Unsubscribe {
                        subscription_id,
                        unsubscribe_method,
                    } => {
                        let result_bool = response.result.as_bool();
                        if result_bool != Some(true) {
                            return;
                        }
                        let removed_subscription_option = {
                            let mut registry_guard = self.registry.lock().await;
                            registry_guard.active_subscriptions.remove(&subscription_id)
                        };
                        self.emit_event(WsEvent::SubscriptionUnregistered {
                            endpoint_name: self.endpoint.name.clone(),
                            subscription_id,
                            unsubscribe_method,
                            was_active: removed_subscription_option.is_some(),
                        });
                    },
                }
            },
            crate::JsonRpcWsIncomingMessage::ErrorResponse(response) => {
                let request_id_option = json_value_to_u64(&response.id);
                let request_id = match request_id_option {
                    Some(request_id) => request_id,
                    None => return,
                };
                let mut registry_guard = self.registry.lock().await;
                registry_guard.pending_requests.remove(&request_id);
            },
            crate::JsonRpcWsIncomingMessage::Notification(notification) => {
                let subscription_id = notification.params.subscription;
                let matched_subscription_option = {
                    let registry_guard = self.registry.lock().await;
                    registry_guard.active_subscriptions.get(&subscription_id).cloned()
                };
                let resolution_subscription = matched_subscription_option.clone();
                match matched_subscription_option {
                    Some(subscription) => {
                        let method_matches_registry =
                            subscription.notification_method == notification.method;
                        self.emit_event(WsEvent::SubscriptionNotification {
                            endpoint_name: self.endpoint.name.clone(),
                            subscription,
                            notification: notification.clone(),
                            method_matches_registry,
                        });
                    },
                    None => {
                        self.emit_event(WsEvent::JsonRpcNotificationWithoutSubscription {
                            endpoint_name: self.endpoint.name.clone(),
                            notification: notification.clone(),
                        });
                    },
                }
                forward_detection_notification_if_configured(
                    &self.detection_notification_forwarder,
                    self.endpoint.name.as_str(),
                    notification,
                )
                .await;
                forward_transaction_resolution_notification_if_configured(
                    &self.transaction_resolution_notification_forwarder,
                    self.endpoint.name.as_str(),
                    notification,
                    resolution_subscription,
                )
                .await;
            },
        }
    }

    async fn run_write_loop<S>(
        &self,
        mut write_half: futures_util::stream::SplitSink<
            tokio_tungstenite::WebSocketStream<S>,
            tokio_tungstenite::tungstenite::Message,
        >,
        mut writer_rx: tokio::sync::mpsc::Receiver<WsWriteCommand>,
        mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
    ) where
        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + std::marker::Send + 'static,
    {
        loop {
            tokio::select! {
                shutdown_result = shutdown_rx.changed() => {
                    match shutdown_result {
                        Ok(()) => {
                            if *shutdown_rx.borrow() {
                                let close_result = write_half.send(
                                    tokio_tungstenite::tungstenite::Message::Close(None)
                                ).await;
                                match close_result {
                                    Ok(()) => {},
                                    Err(error) => {
                                        if !is_normal_close_error(&error) {
                                            self.emit_event(WsEvent::Error {
                                                endpoint_name: self.endpoint.name.clone(),
                                                error: crate::Error::Ws(format!(
                                                    "write close error on endpoint '{}': {error}",
                                                    self.endpoint.name
                                                )),
                                            });
                                        }
                                    },
                                }
                                let flush_result = write_half.flush().await;
                                if let Err(error) = flush_result {
                                    if !is_normal_close_error(&error) {
                                        self.emit_event(WsEvent::Error {
                                            endpoint_name: self.endpoint.name.clone(),
                                            error: crate::Error::Ws(format!(
                                                "write flush error on endpoint '{}': {error}",
                                                self.endpoint.name
                                            )),
                                        });
                                    }
                                }
                                let close_sink_result = write_half.close().await;
                                if let Err(error) = close_sink_result {
                                    if !is_normal_close_error(&error) {
                                        self.emit_event(WsEvent::Error {
                                            endpoint_name: self.endpoint.name.clone(),
                                            error: crate::Error::Ws(format!(
                                                "sink close error on endpoint '{}': {error}",
                                                self.endpoint.name
                                            )),
                                        });
                                    }
                                }
                                break;
                            }
                        },
                        Err(_error) => {
                            break;
                        },
                    }
                },
                command_option = writer_rx.recv() => {
                    let command = match command_option {
                        Some(command) => command,
                        None => {
                            let flush_result = write_half.flush().await;
                            if let Err(error) = flush_result {
                                if !is_normal_close_error(&error) {
                                    self.emit_event(WsEvent::Error {
                                        endpoint_name: self.endpoint.name.clone(),
                                        error: crate::Error::Ws(format!(
                                            "write flush error on endpoint '{}': {error}",
                                            self.endpoint.name
                                        )),
                                    });
                                }
                            }
                            let close_sink_result = write_half.close().await;
                            if let Err(error) = close_sink_result {
                                if !is_normal_close_error(&error) {
                                    self.emit_event(WsEvent::Error {
                                        endpoint_name: self.endpoint.name.clone(),
                                        error: crate::Error::Ws(format!(
                                            "sink close error on endpoint '{}': {error}",
                                            self.endpoint.name
                                        )),
                                    });
                                }
                            }
                            break;
                        },
                    };
                    match command {
                        WsWriteCommand::Send(message) => {
                            let tungstenite_message = convert_outgoing_message(message.clone());
                            let send_result = write_half.send(tungstenite_message).await;
                            match send_result {
                                Ok(()) => {
                                    if message == WsOutgoingMessage::Close {
                                        let flush_result = write_half.flush().await;
                                        if let Err(error) = flush_result {
                                            if !is_normal_close_error(&error) {
                                                self.emit_event(WsEvent::Error {
                                                    endpoint_name: self.endpoint.name.clone(),
                                                    error: crate::Error::Ws(format!(
                                                        "write flush error on endpoint '{}': {error}",
                                                        self.endpoint.name
                                                    )),
                                                });
                                            }
                                        }
                                        break;
                                    }
                                },
                                Err(error) => {
                                    if !is_normal_close_error(&error) {
                                        self.emit_event(WsEvent::Error {
                                            endpoint_name: self.endpoint.name.clone(),
                                            error: crate::Error::Ws(format!(
                                                "write error on endpoint '{}': {error}",
                                                self.endpoint.name
                                            )),
                                        });
                                    }
                                    break;
                                },
                            }
                        },
                    }
                },
            }
        }
    }

    async fn clear_runtime_after_generation(&self, generation: u64) -> bool {
        let should_clear = {
            let mut runtime_guard = self.runtime.lock().await;
            if runtime_guard.generation != generation {
                false
            } else {
                runtime_guard.writer_tx = None;
                runtime_guard.shutdown_tx = None;
                runtime_guard.completion_notify = None;
                runtime_guard.read_abort_handle = None;
                runtime_guard.write_abort_handle = None;
                runtime_guard.supervisor_abort_handle = None;
                true
            }
        };
        if !should_clear {
            return false;
        }
        {
            let mut registry_guard = self.registry.lock().await;
            registry_guard.pending_requests.clear();
            registry_guard.active_subscriptions.clear();
        }
        let mut state_guard = self.state.write().await;
        if *state_guard == crate::ConnectionState::Disconnected {
            return false;
        }
        *state_guard = crate::ConnectionState::Disconnected;
        return true;
    }

    fn emit_event(&self, event: WsEvent) {
        let send_result = self.event_tx.send(event);
        if let Err(error) = send_result {
            tracing::trace!(
                endpoint_name = %self.endpoint.name,
                "websocket event dropped because no receiver is currently subscribed: {error}"
            );
        }
    }

    /// Sets the optional transaction-resolution notification forwarder.
    pub async fn set_transaction_resolution_notification_forwarder(
        &self,
        sender: tokio::sync::mpsc::Sender<crate::WsTransactionResolutionEnvelope>,
    ) {
        let mut guard = self.transaction_resolution_notification_forwarder.lock().await;
        *guard = Some(sender);
    }

    /// Clears the optional transaction-resolution notification forwarder.
    pub async fn clear_transaction_resolution_notification_forwarder(&self) {
        let mut guard = self.transaction_resolution_notification_forwarder.lock().await;
        *guard = None;
    }
}

fn convert_outgoing_message(message: WsOutgoingMessage) -> tokio_tungstenite::tungstenite::Message {
    match message {
        WsOutgoingMessage::Text(text) => {
            return tokio_tungstenite::tungstenite::Message::Text(text.into());
        },
        WsOutgoingMessage::Binary(data) => {
            return tokio_tungstenite::tungstenite::Message::Binary(data.into());
        },
        WsOutgoingMessage::Ping(data) => {
            return tokio_tungstenite::tungstenite::Message::Ping(data.into());
        },
        WsOutgoingMessage::Pong(data) => {
            return tokio_tungstenite::tungstenite::Message::Pong(data.into());
        },
        WsOutgoingMessage::Close => return tokio_tungstenite::tungstenite::Message::Close(None),
    }
}

fn is_normal_close_error(error: &tokio_tungstenite::tungstenite::Error) -> bool {
    match error {
        tokio_tungstenite::tungstenite::Error::ConnectionClosed => return true,
        tokio_tungstenite::tungstenite::Error::AlreadyClosed => return true,
        _ => return false,
    }
}

fn json_value_to_u64(json_value_to_u64: &serde_json::Value) -> std::option::Option<u64> {
    return json_value_to_u64.as_u64();
}

fn is_subscribe_method(method: &str) -> bool {
    return method.ends_with("Subscribe");
}

fn is_unsubscribe_method(method: &str) -> bool {
    return method.ends_with("Unsubscribe");
}

fn infer_unsubscribe_method_from_subscribe(
    subscribe_method: &str,
) -> std::option::Option<std::string::String> {
    if !is_subscribe_method(subscribe_method) {
        return None;
    }
    let base = subscribe_method.trim_end_matches("Subscribe");
    return Some(format!("{base}Unsubscribe"));
}

fn infer_notification_method_from_subscribe(
    subscribe_method: &str,
) -> std::option::Option<std::string::String> {
    if !is_subscribe_method(subscribe_method) {
        return None;
    }
    let base = subscribe_method.trim_end_matches("Subscribe");
    return Some(format!("{base}Notification"));
}

fn build_pending_json_rpc_request(
    request: &crate::JsonRpcWsRequest,
) -> std::option::Option<WsPendingJsonRpcRequest> {
    let request_id_option = json_value_to_u64(&request.id);
    let request_id = match request_id_option {
        Some(request_id) => request_id,
        None => return None,
    };
    if is_subscribe_method(&request.method) {
        let notification_method_option = infer_notification_method_from_subscribe(&request.method);
        let notification_method = match notification_method_option {
            Some(notification_method) => notification_method,
            None => return None,
        };
        let unsubscribe_method_option = infer_unsubscribe_method_from_subscribe(&request.method);
        let unsubscribe_method = match unsubscribe_method_option {
            Some(unsubscribe_method) => unsubscribe_method,
            None => return None,
        };
        return Some(WsPendingJsonRpcRequest {
            request_id,
            method: request.method.clone(),
            kind: WsPendingJsonRpcRequestKind::Subscribe {
                notification_method,
                unsubscribe_method,
                params: request.params.clone(),
            },
        });
    }
    if is_unsubscribe_method(&request.method) {
        let first_param_option = request.params.first();
        let first_param = match first_param_option {
            Some(first_param) => first_param,
            None => return None,
        };
        let subscription_id_option = first_param.as_u64();
        let subscription_id = match subscription_id_option {
            Some(subscription_id) => subscription_id,
            None => return None,
        };
        return Some(WsPendingJsonRpcRequest {
            request_id,
            method: request.method.clone(),
            kind: WsPendingJsonRpcRequestKind::Unsubscribe {
                subscription_id,
                unsubscribe_method: request.method.clone(),
            },
        });
    }
    return Some(WsPendingJsonRpcRequest {
        request_id,
        method: request.method.clone(),
        kind: WsPendingJsonRpcRequestKind::Generic,
    });
}

fn build_single_key_optional_config_params(
    key: std::string::String,
    config: std::option::Option<serde_json::Value>,
) -> std::vec::Vec<serde_json::Value> {
    let mut params = vec![serde_json::Value::String(key)];
    if let Some(config) = config {
        params.push(config);
    }
    return params;
}

fn build_first_value_optional_config_params(
    first: serde_json::Value,
    config: std::option::Option<serde_json::Value>,
) -> std::vec::Vec<serde_json::Value> {
    let mut params = vec![first];
    if let Some(config) = config {
        params.push(config);
    }
    return params;
}

async fn forward_detection_notification_if_configured(
    forwarder: &std::sync::Arc<
        tokio::sync::Mutex<
            std::option::Option<tokio::sync::mpsc::Sender<crate::WsDetectionNotificationEnvelope>>,
        >,
    >,
    endpoint_name: &str,
    notification: &crate::JsonRpcWsNotification,
) {
    let sender_option = {
        let guard = forwarder.lock().await;
        guard.clone()
    };
    let sender = match sender_option {
        Some(sender) => sender,
        None => return,
    };
    let envelope = crate::WsDetectionNotificationEnvelope::new(
        Some(endpoint_name.to_string()),
        notification.clone(),
    );
    let send_result = sender.try_send(envelope);
    match send_result {
        Ok(()) => {},
        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
            tracing::warn!(
                target: "ws_client",
                "detection notification relay queue is full endpoint_name={}",
                endpoint_name
            );
        },
        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
            tracing::warn!(
                target: "ws_client",
                "detection notification relay queue is closed endpoint_name={}",
                endpoint_name
            );
        },
    }
}

async fn forward_transaction_resolution_notification_if_configured(
    forwarder: &std::sync::Arc<
        tokio::sync::Mutex<
            std::option::Option<tokio::sync::mpsc::Sender<crate::WsTransactionResolutionEnvelope>>,
        >,
    >,
    endpoint_name: &str,
    notification: &crate::JsonRpcWsNotification,
    subscription: std::option::Option<crate::WsSubscriptionInfo>,
) {
    let sender_option = {
        let guard = forwarder.lock().await;
        guard.clone()
    };
    let sender = match sender_option {
        Some(sender) => sender,
        None => return,
    };
    let envelope = crate::WsTransactionResolutionEnvelope::new(
        Some(endpoint_name.to_string()),
        notification.clone(),
        subscription,
    );
    let send_result = sender.try_send(envelope);
    match send_result {
        Ok(()) => {},
        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
            tracing::warn!(
                target: "ws_client",
                "transaction resolution relay queue is full endpoint_name={}",
                endpoint_name
            );
        },
        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
            tracing::warn!(
                target: "ws_client",
                "transaction resolution relay queue is closed endpoint_name={}",
                endpoint_name
            );
        },
    }
}

#[cfg(test)]
mod tests {
    use futures_util::SinkExt;
    use futures_util::StreamExt;

    #[derive(Debug)]
    struct TestWsServer {
        url: std::string::String,
        shutdown_tx: std::option::Option<tokio::sync::oneshot::Sender<()>>,
        observed_methods: std::sync::Arc<tokio::sync::Mutex<std::vec::Vec<std::string::String>>>,
    }

    impl TestWsServer {
        async fn spawn_echo_server() -> Self {
            let observed_methods =
                std::sync::Arc::new(tokio::sync::Mutex::new(std::vec::Vec::new()));
            let bind_result = tokio::net::TcpListener::bind("127.0.0.1:0").await;
            let listener = bind_result.expect("listener bind must succeed");
            let local_addr = listener.local_addr().expect("local addr must be available");
            let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
            tokio::spawn(async move {
                loop {
                    tokio::select! {
                        _ = &mut shutdown_rx => {
                            break;
                        },
                        accept_result = listener.accept() => {
                            let (stream, _peer_addr) = accept_result.expect("accept must succeed");
                            tokio::spawn(async move {
                                let accept_ws_result = tokio_tungstenite::accept_async(stream).await;
                                let mut ws_stream = accept_ws_result.expect("websocket accept must succeed");
                                loop {
                                    let message_option = ws_stream.next().await;
                                    let message_result = match message_option {
                                        Some(message_result) => message_result,
                                        None => {
                                            break;
                                        },
                                    };
                                    let message = message_result.expect("message read must succeed");
                                    match message {
                                        tokio_tungstenite::tungstenite::Message::Text(text) => {
                                            ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Text(text)
                                            ).await.expect("text echo must succeed");
                                        },
                                        tokio_tungstenite::tungstenite::Message::Binary(data) => {
                                            ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Binary(data)
                                            ).await.expect("binary echo must succeed");
                                        },
                                        tokio_tungstenite::tungstenite::Message::Ping(data) => {
                                            ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Pong(data)
                                            ).await.expect("pong reply must succeed");
                                        },
                                        tokio_tungstenite::tungstenite::Message::Pong(data) => {
                                            ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Pong(data)
                                            ).await.expect("pong echo must succeed");
                                        },
                                        tokio_tungstenite::tungstenite::Message::Close(frame) => {
                                            let _ = ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Close(frame)
                                            ).await;
                                            break;
                                        },
                                        tokio_tungstenite::tungstenite::Message::Frame(_frame) => {},
                                    }
                                }
                            });
                        },
                    }
                }
            });
            return Self {
                url: format!("ws://{}", local_addr),
                shutdown_tx: Some(shutdown_tx),
                observed_methods,
            };
        }

        async fn spawn_json_rpc_server() -> Self {
            let observed_methods =
                std::sync::Arc::new(tokio::sync::Mutex::new(std::vec::Vec::new()));
            let bind_result = tokio::net::TcpListener::bind("127.0.0.1:0").await;
            let listener = bind_result.expect("listener bind must succeed");
            let local_addr = listener.local_addr().expect("local addr must be available");
            let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
            let observed_methods_for_server = observed_methods.clone();
            tokio::spawn(async move {
                loop {
                    tokio::select! {
                        _ = &mut shutdown_rx => {
                            break;
                        },
                        accept_result = listener.accept() => {
                            let (stream, _peer_addr) = accept_result.expect("accept must succeed");
                            let observed_methods_for_connection = observed_methods_for_server.clone();
                            tokio::spawn(async move {
                                let accept_ws_result = tokio_tungstenite::accept_async(stream).await;
                                let mut ws_stream = accept_ws_result.expect("websocket accept must succeed");
                                loop {
                                    let message_option = ws_stream.next().await;
                                    let message_result = match message_option {
                                        Some(message_result) => message_result,
                                        None => {
                                            break;
                                        },
                                    };
                                    let message = message_result.expect("message read must succeed");
                                    match message {
                                        tokio_tungstenite::tungstenite::Message::Text(text) => {
                                            let value: serde_json::Value = serde_json::from_str(text.as_ref())
                                                .expect("request json must parse");
                                            let method = value["method"].as_str().expect("method must be a string").to_string();
                                            let id = value["id"].clone();
                                            {
                                                let mut observed_methods_guard =
                                                    observed_methods_for_connection.lock().await;
                                                observed_methods_guard.push(method.clone());
                                            }
                                            if method == "slotSubscribe" {
                                                let response = serde_json::json!({
                                                    "jsonrpc": "2.0",
                                                    "result": 77,
                                                    "id": id
                                                });
                                                ws_stream.send(
                                                    tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())
                                                ).await.expect("success response send must succeed");
                                                let notification = serde_json::json!({
                                                    "jsonrpc": "2.0",
                                                    "method": "slotNotification",
                                                    "params": {
                                                        "result": {
                                                            "parent": 10,
                                                            "root": 11,
                                                            "slot": 12
                                                        },
                                                        "subscription": 77
                                                    }
                                                });
                                                ws_stream.send(
                                                    tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())
                                                ).await.expect("notification send must succeed");
                                            } else if method == "slotUnsubscribe" {
                                                let response = serde_json::json!({
                                                    "jsonrpc": "2.0",
                                                    "result": true,
                                                    "id": id
                                                });
                                                ws_stream.send(
                                                    tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())
                                                ).await.expect("unsubscribe response send must succeed");
                                            } else {
                                                let response = serde_json::json!({
                                                    "jsonrpc": "2.0",
                                                    "error": {
                                                        "code": -32601,
                                                        "message": "Method not found"
                                                    },
                                                    "id": id
                                                });
                                                ws_stream.send(
                                                    tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())
                                                ).await.expect("error response send must succeed");
                                            }
                                        },
                                        tokio_tungstenite::tungstenite::Message::Close(frame) => {
                                            let _ = ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Close(frame)
                                            ).await;
                                            break;
                                        },
                                        tokio_tungstenite::tungstenite::Message::Binary(_data) => {},
                                        tokio_tungstenite::tungstenite::Message::Ping(data) => {
                                            ws_stream.send(
                                                tokio_tungstenite::tungstenite::Message::Pong(data)
                                            ).await.expect("pong reply must succeed");
                                        },
                                        tokio_tungstenite::tungstenite::Message::Pong(_data) => {},
                                        tokio_tungstenite::tungstenite::Message::Frame(_frame) => {},
                                    }
                                }
                            });
                        },
                    }
                }
            });
            return Self {
                url: format!("ws://{}", local_addr),
                shutdown_tx: Some(shutdown_tx),
                observed_methods,
            };
        }

        async fn observed_methods_snapshot(&self) -> std::vec::Vec<std::string::String> {
            let observed_methods_guard = self.observed_methods.lock().await;
            return observed_methods_guard.clone();
        }
        async fn shutdown(mut self) {
            if let Some(shutdown_tx) = self.shutdown_tx.take() {
                let _ = shutdown_tx.send(());
            }
        }
    }

    fn make_ws_endpoint(url: std::string::String) -> crate::WsEndpointConfig {
        return crate::WsEndpointConfig {
            name: "test_ws".to_string(),
            enabled: true,
            provider: "test".to_string(),
            url,
            api_key_env_var: None,
            roles: vec!["test".to_string()],
            max_subscriptions: 16,
            connect_timeout_ms: 2000,
            request_timeout_ms: 2000,
            unsubscribe_timeout_ms: 1000,
            write_channel_capacity: 32,
            event_channel_capacity: 64,
            auto_reconnect: false,
        };
    }

    async fn recv_event(
        receiver: &mut tokio::sync::broadcast::Receiver<crate::WsEvent>,
    ) -> crate::WsEvent {
        let timeout_result =
            tokio::time::timeout(std::time::Duration::from_secs(2), receiver.recv()).await;
        let recv_result = timeout_result.expect("event receive timeout must not occur");
        return recv_result.expect("event receive must succeed");
    }

    #[tokio::test]
    async fn next_request_id_is_shared_between_clones() {
        let endpoint = make_ws_endpoint("ws://127.0.0.1:65535".to_string());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let cloned = client.clone();
        assert_eq!(client.next_request_id(), 1);
        assert_eq!(cloned.next_request_id(), 2);
        assert_eq!(client.next_request_id(), 3);
    }

    #[tokio::test]
    async fn connect_send_text_and_disconnect() {
        let server = TestWsServer::spawn_echo_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let connected_event = recv_event(&mut receiver).await;
        match connected_event {
            crate::WsEvent::Connected { endpoint_name, endpoint_url } => {
                assert_eq!(endpoint_name, "test_ws");
                assert_eq!(endpoint_url, server.url);
            },
            other => {
                panic!("unexpected connected event: {other:?}");
            },
        }
        client.send_text("hello".to_string()).await.expect("text send must succeed");
        let text_event = recv_event(&mut receiver).await;
        match text_event {
            crate::WsEvent::TextMessage { endpoint_name, text } => {
                assert_eq!(endpoint_name, "test_ws");
                assert_eq!(text, "hello");
            },
            other => {
                panic!("unexpected text event: {other:?}");
            },
        }
        client.disconnect().await.expect("disconnect must succeed");
        assert_eq!(client.connection_state().await, crate::ConnectionState::Disconnected);
        let mut disconnected_seen = false;
        for _ in 0..4 {
            let event = recv_event(&mut receiver).await;
            if let crate::WsEvent::Disconnected { endpoint_name } = event {
                assert_eq!(endpoint_name, "test_ws");
                disconnected_seen = true;
                break;
            }
        }
        assert!(disconnected_seen, "disconnected event must be observed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn connect_twice_returns_invalid_state() {
        let server = TestWsServer::spawn_echo_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        client.connect().await.expect("first connect must succeed");
        let second_connect_result = client.connect().await;
        assert!(second_connect_result.is_err());
        let error = second_connect_result.expect_err("second connect must fail");
        match error {
            crate::Error::InvalidState(message) => {
                assert!(message.contains("cannot connect"));
            },
            other => {
                panic!("unexpected error variant: {other:?}");
            },
        }
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn send_ping_receives_pong_event() {
        let server = TestWsServer::spawn_echo_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        client.send_ping(vec![1, 2, 3, 4]).await.expect("ping send must succeed");
        let event = recv_event(&mut receiver).await;
        match event {
            crate::WsEvent::Pong { endpoint_name, data } => {
                assert_eq!(endpoint_name, "test_ws");
                assert_eq!(data, vec![1, 2, 3, 4]);
            },
            other => {
                panic!("unexpected event: {other:?}");
            },
        }
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn subscribe_registers_subscription_and_routes_notification() {
        let server = TestWsServer::spawn_json_rpc_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        let request_id = client
            .send_json_rpc_request("slotSubscribe".to_string(), std::vec::Vec::new())
            .await
            .expect("json-rpc send must succeed");
        assert_eq!(request_id, 1);

        let mut subscription_registered_seen = false;
        let mut subscription_notification_seen = false;

        for _ in 0..8 {
            let event = recv_event(&mut receiver).await;
            match event {
                crate::WsEvent::SubscriptionRegistered { endpoint_name, subscription } => {
                    assert_eq!(endpoint_name, "test_ws");
                    assert_eq!(subscription.request_id, 1);
                    assert_eq!(subscription.subscription_id, 77);
                    assert_eq!(subscription.subscribe_method, "slotSubscribe");
                    assert_eq!(subscription.unsubscribe_method, "slotUnsubscribe");
                    assert_eq!(subscription.notification_method, "slotNotification");
                    subscription_registered_seen = true;
                },
                crate::WsEvent::SubscriptionNotification {
                    endpoint_name,
                    subscription,
                    notification,
                    method_matches_registry,
                } => {
                    assert_eq!(endpoint_name, "test_ws");
                    assert_eq!(subscription.subscription_id, 77);
                    assert!(method_matches_registry);
                    assert_eq!(notification.method, "slotNotification");
                    assert_eq!(notification.params.subscription, 77);
                    assert_eq!(notification.params.result["slot"], serde_json::Value::from(12u64));
                    subscription_notification_seen = true;
                },
                crate::WsEvent::TextMessage { .. } => {},
                crate::WsEvent::JsonRpcMessage { .. } => {},
                other => {
                    panic!("unexpected event: {other:?}");
                },
            }
            if subscription_registered_seen && subscription_notification_seen {
                break;
            }
        }
        assert!(subscription_registered_seen, "subscription must be registered");
        assert!(subscription_notification_seen, "subscription notification must be routed");
        assert_eq!(client.active_subscription_count().await, 1);
        assert_eq!(client.pending_request_count().await, 0);
        client.disconnect().await.expect("disconnect must succeed");
        let observed_methods = server.observed_methods_snapshot().await;
        assert!(observed_methods.iter().any(|method| return method == "slotSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "slotUnsubscribe"));
        server.shutdown().await;
    }

    #[tokio::test]
    async fn unknown_json_rpc_method_emits_error_and_clears_pending_request() {
        let server = TestWsServer::spawn_json_rpc_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        let request_id = client
            .send_json_rpc_request("unknownMethod".to_string(), std::vec::Vec::new())
            .await
            .expect("json-rpc send must succeed");
        assert_eq!(request_id, 1);
        let mut error_seen = false;
        for _ in 0..6 {
            let event = recv_event(&mut receiver).await;
            match event {
                crate::WsEvent::JsonRpcMessage { message, .. } => match message {
                    crate::JsonRpcWsIncomingMessage::ErrorResponse(response) => {
                        assert_eq!(response.id, serde_json::Value::from(1u64));
                        assert_eq!(response.error.code, -32601);
                        assert_eq!(response.error.message, "Method not found");
                        error_seen = true;
                        break;
                    },
                    crate::JsonRpcWsIncomingMessage::SuccessResponse(other) => {
                        panic!("unexpected success response: {other:?}");
                    },
                    crate::JsonRpcWsIncomingMessage::Notification(other) => {
                        panic!("unexpected notification: {other:?}");
                    },
                },
                crate::WsEvent::TextMessage { .. } => {},
                other => {
                    panic!("unexpected event: {other:?}");
                },
            }
        }
        assert!(error_seen, "json-rpc error response must be observed");
        assert_eq!(client.pending_request_count().await, 0);
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn helper_methods_send_expected_subscribe_method_names() {
        let server = TestWsServer::spawn_json_rpc_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        let _ = client
            .account_subscribe_raw(
                "CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12".to_string(),
                Some(serde_json::json!({"encoding": "jsonParsed"})),
            )
            .await
            .expect("account_subscribe must succeed");
        let _ = client
            .block_subscribe_raw(
                serde_json::json!("all"),
                Some(serde_json::json!({"commitment": "confirmed"})),
            )
            .await
            .expect("block_subscribe must succeed");
        let _ = client
            .logs_subscribe_raw(
                serde_json::json!("all"),
                Some(serde_json::json!({"commitment": "finalized"})),
            )
            .await
            .expect("logs_subscribe must succeed");
        let _ = client
            .program_subscribe_raw(
                crate::SYSTEM_PROGRAM_ID.to_string(),
                Some(serde_json::json!({"encoding": "base64"})),
            )
            .await
            .expect("program_subscribe must succeed");

        let _ = client.root_subscribe().await.expect("root_subscribe must succeed");
        let _ = client
            .signature_subscribe_raw(
                "2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b".to_string(),
                Some(serde_json::json!({"commitment": "confirmed"})),
            )
            .await
            .expect("signature_subscribe must succeed");
        let _ = client.slot_subscribe().await.expect("slot_subscribe must succeed");
        let _ = client
            .slots_updates_subscribe()
            .await
            .expect("slots_updates_subscribe must succeed");
        let _ = client.vote_subscribe().await.expect("vote_subscribe must succeed");
        tokio::time::sleep(std::time::Duration::from_millis(150)).await;
        let observed_methods = server.observed_methods_snapshot().await;
        assert!(observed_methods.iter().any(|method| return method == "accountSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "blockSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "logsSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "programSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "rootSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "signatureSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "slotSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "slotsUpdatesSubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "voteSubscribe"));
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn helper_methods_send_expected_unsubscribe_method_names() {
        let server = TestWsServer::spawn_json_rpc_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        let _ = client
            .account_unsubscribe_raw(10)
            .await
            .expect("account_unsubscribe must succeed");
        let _ = client.block_unsubscribe_raw(11).await.expect("block_unsubscribe must succeed");
        let _ = client.logs_unsubscribe_raw(12).await.expect("logs_unsubscribe must succeed");
        let _ = client
            .program_unsubscribe_raw(13)
            .await
            .expect("program_unsubscribe must succeed");
        let _ = client.root_unsubscribe(14).await.expect("root_unsubscribe must succeed");
        let _ = client
            .signature_unsubscribe_raw(15)
            .await
            .expect("signature_unsubscribe must succeed");
        let _ = client.slot_unsubscribe(16).await.expect("slot_unsubscribe must succeed");
        let _ = client
            .slots_updates_unsubscribe(17)
            .await
            .expect("slots_updates_unsubscribe must succeed");
        let _ = client.vote_unsubscribe(18).await.expect("vote_unsubscribe must succeed");
        tokio::time::sleep(std::time::Duration::from_millis(150)).await;
        let observed_methods = server.observed_methods_snapshot().await;
        assert!(observed_methods.iter().any(|method| return method == "accountUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "blockUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "logsUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "programUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "rootUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "signatureUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "slotUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "slotsUpdatesUnsubscribe"));
        assert!(observed_methods.iter().any(|method| return method == "voteUnsubscribe"));
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }

    #[tokio::test]
    async fn notification_is_forwarded_to_detection_relay() {
        let server = TestWsServer::spawn_json_rpc_server().await;
        let endpoint = make_ws_endpoint(server.url.clone());
        let client = crate::WsClient::new(endpoint).expect("client creation must succeed");
        let (relay_tx, mut relay_rx) = crate::WsDetectionRelay::channel(8);
        client.set_detection_notification_forwarder(relay_tx).await;
        let mut receiver = client.subscribe_events();
        client.connect().await.expect("connect must succeed");
        let _ = recv_event(&mut receiver).await;
        let request_id = client
            .send_json_rpc_request("slotSubscribe".to_string(), std::vec::Vec::new())
            .await
            .expect("json-rpc send must succeed");
        assert_eq!(request_id, 1);
        let envelope = tokio::time::timeout(std::time::Duration::from_secs(2), relay_rx.recv())
            .await
            .expect("relay receive timeout")
            .expect("relay must receive one envelope");
        assert_eq!(envelope.endpoint_name.as_deref(), Some("test_ws"));
        assert_eq!(envelope.notification.method, "slotNotification");
        assert_eq!(envelope.notification.params.subscription, 77);
        assert_eq!(envelope.notification.params.result["slot"], serde_json::Value::from(12u64));
        client.disconnect().await.expect("disconnect must succeed");
        server.shutdown().await;
    }
}
