//! WebSocket client session handling.
//!
//! This module defines the WebSocket session abstraction used by the Tenbin SDK.
//! It is responsible for encoding client messages, decoding server messages,
//! and enforcing protocol-level sequencing during an active WebSocket session.
//!
//! This module does not manage connection establishment; it operates on an
//! already-connected socket.

use crate::{
    error::ClientError,
    signing::{challenge::signing_digest, signer::PayloadSigner},
    types::{QuoteRequest, WsQuoteResponse},
    ws::{
        codec::{JsonWsCodec, WsCodec},
        protocol::{UserWsClientMessage, UserWsServerMessage},
    },
};
use alloy::primitives::Address;
use futures::{Sink, SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::{Error, Message};

/// Active WebSocket session.
///
/// `WsSession` owns the WebSocket sink and stream and provides protocol-aware
/// convenience methods for authenticating, subscribing, sending requests,
/// and receiving server events.
///
/// This type is generic over the underlying sink, stream, and codec to support
/// mocking and alternate transport implementations.
#[derive(Debug)]
pub struct WsSession<S, R, C = JsonWsCodec> {
    sink: S,
    stream: R,
    codec: C,
}

impl<S, R> WsSession<S, R, JsonWsCodec> {
    /// Construct a new WebSocket session.
    ///
    /// This does not perform any protocol handshake or authentication; it simply
    /// wraps the provided sink and stream in a session abstraction.
    pub fn new(sink: S, stream: R) -> Self {
        Self {
            sink,
            stream,
            codec: JsonWsCodec,
        }
    }
}

impl<S, R, C> WsSession<S, R, C> {
    /// Construct a new WebSocket session with an explicit codec.
    ///
    /// This constructor is identical to [`WsSession::new`], but allows callers to
    /// supply a custom codec implementation instead of using the default
    /// [`JsonWsCodec`].
    ///
    /// This is primarily intended for testing, experimentation, or alternate
    /// protocol encodings.
    ///
    /// No protocol handshake, authentication, or subscription is performed by
    /// this method.
    pub fn with_codec(sink: S, stream: R, codec: C) -> Self {
        Self {
            sink,
            stream,
            codec,
        }
    }
}

impl<S, R, C: WsCodec> WsSession<S, R, C>
where
    S: Sink<Message> + Unpin,
    S::Error: Into<Error>,
    R: futures::Stream<Item = Result<Message, Error>> + Unpin,
{
    /// Send a protocol-level message to the server.
    ///
    /// This method serializes the client message using the configured codec and
    /// writes it to the WebSocket sink.
    pub async fn send(&mut self, msg: &UserWsClientMessage) -> Result<(), ClientError> {
        let json = self.codec.encode_client(msg)?;
        self.sink
            .send(Message::Text(json.into()))
            .await
            .map_err(|e| ClientError::WebSocket(e.into()))?;
        Ok(())
    }

    /// Await the next server event.
    ///
    /// This method reads the next WebSocket message, decodes it using the codec,
    /// and returns the corresponding protocol message.
    ///
    /// # Errors
    /// Returns an error if the connection is closed, the message is invalid, or
    /// decoding fails.
    pub async fn next_event(&mut self) -> Result<UserWsServerMessage, ClientError> {
        loop {
            let raw = self
                .stream
                .next()
                .await
                .ok_or_else(|| ClientError::Protocol("WebSocket closed".into()))??;
            match raw {
                Message::Text(s) => {
                    return self.codec.decode_server(&s);
                }
                Message::Binary(b) => {
                    let s = String::from_utf8(b.to_vec())
                        .map_err(|_| ClientError::Protocol("Binary frame not UTF-8".into()))?;
                    return self.codec.decode_server(&s);
                }
                Message::Ping(_) | Message::Pong(_) => {
                    continue;
                }
                Message::Close(_) => {
                    return Err(ClientError::Protocol("WebSocket closed".into()));
                }
                _ => {
                    return Err(ClientError::Protocol("Unsupported WS message".into()));
                }
            }
        }
    }

    /// Authenticate the session and subscribe to user-level events.
    ///
    /// This method performs the following steps in order:
    /// 1. Requests an authentication challenge from the server
    /// 2. Signs the challenge using the provided [`PayloadSigner`]
    /// 3. Sends the authentication response
    /// 4. Subscribes to user-level event streams
    ///
    /// # Errors
    /// Returns an error if authentication fails or the server responds with an
    /// unexpected message.
    pub async fn authenticate_and_subscribe<Signer: PayloadSigner>(
        &mut self,
        signer_addr: Address,
        signer: &Signer,
    ) -> Result<(), ClientError> {
        // Request challenge
        self.send(&UserWsClientMessage::GetChallenge {
            signer: signer_addr,
        })
        .await?;
        // Expect Challenge
        let challenge_msg = self.next_event().await?;
        let challenge = match challenge_msg {
            UserWsServerMessage::Challenge { signer: s, message } if s == signer_addr => message,
            other => {
                return Err(ClientError::Protocol(format!(
                    "Expected Challenge for signer {signer_addr:?}, got {other:?}"
                )));
            }
        };
        // Hash challenge
        let hash = signing_digest(&challenge);
        // Sign it
        let signature = signer.sign_hash(&hash).await?;
        // Send Subscribe
        self.send(&UserWsClientMessage::Subscribe {
            signer: signer_addr,
            signature: signature.to_string(),
        })
        .await?;
        // Expect Subscribed
        match self.next_event().await? {
            UserWsServerMessage::Subscribed { signer: s } if s == signer_addr => Ok(()),
            UserWsServerMessage::Error { code, message } => {
                Err(ClientError::Auth(format!("{code} {message}")))
            }
            other => Err(ClientError::Protocol(format!(
                "Expected Subscribed, got {other:?}"
            ))),
        }
    }

    /// Cancel an order via WebSocket.
    ///
    /// This method sends a `CancelOrder` message to the server. The caller must
    /// provide:
    /// - The order ID to cancel
    /// - The signer address (must match the order's signer)
    /// - A timestamp (must be within ±10 seconds of server time)
    /// - A signer to produce the cancellation signature
    ///
    /// The signature is over the message: `CancelOrder/{id}/{timestamp}`
    ///
    /// # Errors
    /// Returns an error if signing fails or the message cannot be sent.
    pub async fn cancel_order<Signer: PayloadSigner>(
        &mut self,
        order_id: uuid::Uuid,
        signer_addr: Address,
        timestamp: u64,
        signer: &Signer,
    ) -> Result<(), ClientError> {
        // Construct the message to sign
        let message = format!("CancelOrder/{}/{}", order_id, timestamp);
        let hash = signing_digest(&message);
        let signature = signer.sign_hash(&hash).await?;

        self.send(&UserWsClientMessage::CancelOrder {
            id: order_id,
            signer: signer_addr,
            timestamp,
            signature: signature.to_string(),
        })
        .await
    }

    /// Request a price quote via WebSocket.
    ///
    /// Sends a quote request and waits for the server's response. This method
    /// does not require authentication.
    ///
    /// # Arguments
    ///
    /// * `request` - The quote request parameters specifying token, side, and size
    ///
    /// # Returns
    ///
    /// Returns the quote response containing pricing information if successful.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The message cannot be sent
    /// - The server responds with an error message
    /// - The server responds with an unexpected message type
    /// - The connection is closed before receiving a response
    pub async fn request_quote(
        &mut self,
        request: QuoteRequest,
    ) -> Result<WsQuoteResponse, ClientError> {
        self.send(&UserWsClientMessage::QuoteRequest { request })
            .await?;

        match self.next_event().await? {
            UserWsServerMessage::QuoteResponse { response } => Ok(response),
            UserWsServerMessage::Error { code, message } => {
                Err(ClientError::Protocol(format!("{code}: {message}")))
            }
            other => Err(ClientError::Protocol(format!(
                "Expected QuoteResponse, got {other:?}"
            ))),
        }
    }

    /// Protocol-level ping
    pub async fn ping(&mut self) -> Result<(), ClientError> {
        self.send(&UserWsClientMessage::Ping).await
    }
}

/// Tests for WebSocket session behaviour.
///
/// These tests validate protocol sequencing, authentication flows, and
/// message decoding using mocked sinks and streams.
#[cfg(test)]
mod tests {

    use futures::SinkExt as _;
    use tokio_tungstenite::tungstenite::{Error, Message, protocol::frame::Frame};
    use uuid::Uuid;

    use alloy::primitives::U256;
    use rust_decimal::Decimal;

    use crate::{
        ClientError,
        fixtures::{FailingCodec, FailingSigner, StaticSigner, VecSink, VecStream, addr},
        transport::websocket::{TungsteniteConnector, WsConnector},
        types::{
            Order, QuoteRequest, QuoteSide, QuoteStatus, WsQuoteResponse,
            WsQuoteResponseData,
        },
        ws::{
            client::WsSession,
            protocol::{UserWsClientMessage, UserWsServerMessage},
        },
    };

    #[tokio::test]
    async fn ws_authenticate_and_subscribe_happy_path() {
        let signer = addr(1);

        let challenge = UserWsServerMessage::Challenge {
            signer,
            message: "hello".into(),
        };
        let subscribed = UserWsServerMessage::Subscribed { signer };

        let stream = VecStream::new(vec![
            Ok(Message::Text(
                serde_json::to_string(&challenge).unwrap().into(),
            )),
            Ok(Message::Text(
                serde_json::to_string(&subscribed).unwrap().into(),
            )),
        ]);

        let (sink, sent) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap();

        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 2);

        // Check the first message was GetChallenge
        if let Message::Text(s) = &sent[0] {
            let msg: UserWsClientMessage = serde_json::from_str(s).unwrap();
            matches!(msg, UserWsClientMessage::GetChallenge { signer: p } if p == signer);
        } else {
            panic!("expected text frame");
        }

        // Check the second message was Subscribe with signature
        if let Message::Text(s) = &sent[1] {
            let msg: UserWsClientMessage = serde_json::from_str(s).unwrap();
            match msg {
                UserWsClientMessage::Subscribe {
                    signer: p,
                    signature,
                } => {
                    assert_eq!(p, signer);
                    assert_eq!(
                        signature,
                        "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b"
                    );
                }
                _ => panic!("expected Subscribe"),
            }
        } else {
            panic!("expected text frame");
        }
    }

    #[tokio::test]
    async fn ws_authenticate_errors_on_wrong_message() {
        let signer = addr(1);

        // Server sends Subscribed without Challenge => protocol error
        let subscribed = UserWsServerMessage::Subscribed { signer };
        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&subscribed).unwrap().into(),
        ))]);

        let (sink, _sent) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();
        match err {
            ClientError::Protocol(_) => {}
            other => panic!("expected protocol error, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn ws_submit_order_sends_order_and_parses_orderaccepted() {
        // This demonstrates order submission over WS:
        // client sends SubmitOrder, server replies OrderAccepted.

        let id = Uuid::now_v7();
        let accepted = UserWsServerMessage::OrderAccepted { id };
        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&accepted).unwrap().into(),
        ))]);

        let (sink, sent) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        // Again, use a canonical Order fixture in your codebase.
        let order_json = r#"{
            "order_type":"Mint",
            "order_status":"Pending",
            "nonce":"1",
            "expiry":1700000000,
            "payer":"0x0000000000000000000000000000000000000001",
            "recipient":"0x0000000000000000000000000000000000000002",
            "collateral_token":"0x0000000000000000000000000000000000000003",
            "collateral_amount":"1",
            "asset_amount":"1",
            "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b",
            "controller":"0x0000000000000000000000000000000000000004"
        }"#;
        let order: Order = serde_json::from_str(order_json).unwrap();

        session
            .send(&UserWsClientMessage::SubmitOrder {
                order: Box::new(order),
            })
            .await
            .unwrap();

        let msg = session.next_event().await.unwrap();
        match msg {
            UserWsServerMessage::OrderAccepted { id: got } => assert_eq!(got, id),
            other => panic!("unexpected message: {other:?}"),
        }

        // Verify a SubmitOrder went out
        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 1);
        match &sent[0] {
            Message::Text(s) => {
                let parsed: UserWsClientMessage = serde_json::from_str(s).unwrap();
                assert!(matches!(parsed, UserWsClientMessage::SubmitOrder { .. }));
            }
            _ => panic!("expected text"),
        }
    }

    #[tokio::test]
    async fn tungstenite_connector_can_connect() {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();

        tokio::spawn(async move {
            let (stream, _) = listener.accept().await.unwrap();
            let _ = tokio_tungstenite::accept_async(stream).await.unwrap();
        });

        let url = format!("ws://{}", addr);
        let connector = TungsteniteConnector::new(url.parse().unwrap());

        let (_sink, _stream) = connector.connect().await.unwrap();
    }

    #[tokio::test]
    async fn next_event_binary_non_utf8_errors() {
        let stream = VecStream::new(vec![Ok(Message::Binary(vec![0xff, 0xfe, 0xfd].into()))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn next_event_binary_utf8_parses() {
        let msg = serde_json::json!({
            "type": "Pong"
        });

        let stream = VecStream::new(vec![Ok(Message::Binary(
            msg.to_string().into_bytes().into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let out = session.next_event().await.unwrap();
        assert!(matches!(out, UserWsServerMessage::Pong));
    }

    #[tokio::test]
    async fn next_event_close_errors() {
        let stream = VecStream::new(vec![Ok(Message::Close(None))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn next_event_unsupported_message_errors() {
        // Construct a valid but unsupported raw frame
        let frame = Frame::ping(vec![1, 2, 3]);

        let stream = VecStream::new(vec![Ok(Message::Frame(frame))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn auth_unexpected_first_message_errors() {
        let signer = addr(1);

        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::json!({
                "type": "Subscribed",
                "data": { "signer": signer }
            })
            .to_string()
            .into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn auth_challenge_wrong_signer_errors() {
        let signer = addr(1);
        let other = addr(2);

        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::json!({
                "type": "Challenge",
                "data": { "signer": other, "message": "hi" }
            })
            .to_string()
            .into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn auth_server_error_message() {
        let signer = addr(1);

        let stream = VecStream::new(vec![
            Ok(Message::Text(
                serde_json::json!({
                    "type": "Challenge",
                    "data": { "signer": signer, "message": "hi" }
                })
                .to_string()
                .into(),
            )),
            Ok(Message::Text(
                serde_json::json!({
                    "type": "Error",
                    "data": { "code": "401", "message": "nope" }
                })
                .to_string()
                .into(),
            )),
        ]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();
        assert!(matches!(err, ClientError::Auth(_)));
    }

    #[tokio::test]
    async fn next_event_stream_ends_errors() {
        let stream = VecStream::new(vec![]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn send_encode_failure_hits_error_path() {
        let stream = VecStream::new(vec![]);
        let (sink, _) = VecSink::new();

        let mut session = WsSession::with_codec(sink, stream, FailingCodec);

        let msg = UserWsClientMessage::Ping;

        let err = session.send(&msg).await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }

    #[tokio::test]
    async fn next_event_invalid_json_text() {
        let stream = VecStream::new(vec![Ok(Message::Text(
            "{this is not json}".to_string().into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }

    #[tokio::test]
    async fn auth_error_after_subscribe() {
        let signer = addr(1);

        let stream = VecStream::new(vec![
            Ok(Message::Text(
                serde_json::json!({
                    "type": "Challenge",
                    "data": { "signer": signer, "message": "hi" }
                })
                .to_string()
                .into(),
            )),
            Ok(Message::Text(
                serde_json::json!({
                    "type": "Error",
                    "data": { "code": "403", "message": "denied" }
                })
                .to_string()
                .into(),
            )),
        ]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();

        assert!(matches!(err, ClientError::Auth(_)));
    }

    #[tokio::test]
    async fn ping_sends_message() {
        let stream = VecStream::new(vec![]);
        let (sink, sent) = VecSink::new();

        let mut session = WsSession::new(sink, stream);

        session.ping().await.unwrap();

        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 1);
    }

    #[tokio::test]
    async fn ws_ping_sends_ping_message() {
        let stream = VecStream::new(vec![]);
        let (sink, sent) = VecSink::new();

        let mut session = WsSession::new(sink, stream);

        session.ping().await.unwrap();

        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 1);

        // If your WsSession::ping uses UserWsClientMessage::Ping serialized to JSON:
        // assert the emitted frame is Text containing `"type":"Ping"`
        let txt = match &sent[0] {
            tokio_tungstenite::tungstenite::Message::Text(s) => s,
            other => panic!("expected Text frame, got {other:?}"),
        };
        assert!(txt.contains(r#""type":"Ping""#));
    }

    #[tokio::test]
    async fn next_event_propagates_stream_error() {
        let stream = VecStream::new(vec![Err(Error::ConnectionClosed)]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session.next_event().await.unwrap_err();
        assert!(err.to_string().to_lowercase().contains("websocket"));
    }

    #[tokio::test]
    async fn ws_authenticate_errors_on_unexpected_message_after_subscribe() {
        let signer = addr(1);

        let stream = VecStream::new(vec![
            Ok(Message::Text(
                serde_json::json!({
                    "type": "Challenge",
                    "data": { "signer": signer, "message": "hi" }
                })
                .to_string()
                .into(),
            )),
            Ok(Message::Text(
                serde_json::json!({ "type": "Pong" }).to_string().into(),
            )),
        ]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let err = session
            .authenticate_and_subscribe(signer, &StaticSigner)
            .await
            .unwrap_err();
        assert!(err.to_string().to_lowercase().contains("protocol"));
    }

    #[tokio::test]
    async fn vecsink_close_is_covered() {
        let (mut sink, _sent) = VecSink::new();

        // This drives poll_close()
        sink.close().await.unwrap();
    }

    #[tokio::test]
    async fn next_event_skips_ping_and_returns_next_message() {
        let id = Uuid::nil();

        let stream = VecStream::new(vec![
            Ok(Message::Ping(vec![1, 2, 3].into())),
            Ok(Message::Text(
                serde_json::json!({
                    "type": "OrderAccepted",
                    "data": { "id": id }
                })
                .to_string()
                .into(),
            )),
        ]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let msg = session.next_event().await.unwrap();

        match msg {
            UserWsServerMessage::OrderAccepted { id: got } => {
                assert_eq!(got, id);
            }
            other => panic!("unexpected message: {other:?}"),
        }
    }

    #[tokio::test]
    async fn cancel_order_sends_correctly_signed_message() {
        let stream = VecStream::new(vec![]);
        let (sink, sent) = VecSink::new();

        let mut session = WsSession::new(sink, stream);

        let order_id = Uuid::nil();
        let signer_addr = addr(1);
        let timestamp = 1707000000u64;

        session
            .cancel_order(order_id, signer_addr, timestamp, &StaticSigner)
            .await
            .unwrap();

        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 1);

        // Verify the message was CancelOrder with correct fields
        if let Message::Text(s) = &sent[0] {
            let msg: UserWsClientMessage = serde_json::from_str(s).unwrap();
            match msg {
                UserWsClientMessage::CancelOrder {
                    id,
                    signer,
                    timestamp: ts,
                    signature,
                } => {
                    assert_eq!(id, order_id);
                    assert_eq!(signer, signer_addr);
                    assert_eq!(ts, timestamp);
                    assert!(!signature.is_empty());
                }
                _ => panic!("expected CancelOrder"),
            }
        } else {
            panic!("expected text frame");
        }
    }

    #[tokio::test]
    async fn cancel_order_signing_failure_returns_error() {
        let stream = VecStream::new(vec![]);
        let (sink, _) = VecSink::new();

        let mut session = WsSession::new(sink, stream);

        let err = session
            .cancel_order(Uuid::nil(), addr(1), 1707000000, &FailingSigner)
            .await
            .unwrap_err();

        assert!(matches!(err, ClientError::Auth(_)));
    }

    #[tokio::test]
    async fn request_quote_happy_path() {
        let quote_id = Uuid::now_v7();
        let response = WsQuoteResponse {
            id: quote_id,
            status: QuoteStatus::Ok,
            data: Some(WsQuoteResponseData {
                usd_price: Decimal::new(12345, 2),
                usdc_collateral: U256::from(1_000_000u64),
            }),
        };

        let server_msg = UserWsServerMessage::QuoteResponse { response };
        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&server_msg).unwrap().into(),
        ))]);

        let (sink, sent) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let request = QuoteRequest {
            side: QuoteSide::Mint,
            size: U256::from(100u64),
        };

        let result = session.request_quote(request).await.unwrap();

        assert_eq!(result.id, quote_id);
        assert_eq!(result.status, QuoteStatus::Ok);
        assert!(result.data.is_some());

        let data = result.data.unwrap();
        assert_eq!(data.usd_price, Decimal::new(12345, 2));
        assert_eq!(data.usdc_collateral, U256::from(1_000_000u64));

        // Verify the request was sent correctly
        let sent = sent.lock().unwrap();
        assert_eq!(sent.len(), 1);

        if let Message::Text(s) = &sent[0] {
            let msg: UserWsClientMessage = serde_json::from_str(s).unwrap();
            match msg {
                UserWsClientMessage::QuoteRequest { request } => {
                    assert_eq!(request.side, QuoteSide::Mint);
                    assert_eq!(request.size, U256::from(100u64));
                }
                _ => panic!("expected QuoteRequest"),
            }
        } else {
            panic!("expected text frame");
        }
    }

    #[tokio::test]
    async fn request_quote_failed_status() {
        let quote_id = Uuid::now_v7();
        let response = WsQuoteResponse {
            id: quote_id,
            status: QuoteStatus::Failed,
            data: None,
        };

        let server_msg = UserWsServerMessage::QuoteResponse { response };
        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&server_msg).unwrap().into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let request = QuoteRequest {
            side: QuoteSide::Redeem,
            size: U256::from(50u64),
        };

        let result = session.request_quote(request).await.unwrap();

        assert_eq!(result.id, quote_id);
        assert_eq!(result.status, QuoteStatus::Failed);
        assert!(result.data.is_none());
    }

    #[tokio::test]
    async fn request_quote_server_error() {
        let error_msg = UserWsServerMessage::Error {
            code: "quote_unavailable".into(),
            message: "quote service temporarily unavailable".into(),
        };

        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&error_msg).unwrap().into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let request = QuoteRequest {
            side: QuoteSide::Mint,
            size: U256::from(200u64),
        };

        let err = session.request_quote(request).await.unwrap_err();

        match err {
            ClientError::Protocol(msg) => {
                assert!(msg.contains("quote_unavailable"));
                assert!(msg.contains("quote service temporarily unavailable"));
            }
            other => panic!("expected Protocol error, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn request_quote_unexpected_message() {
        let unexpected = UserWsServerMessage::Pong;

        let stream = VecStream::new(vec![Ok(Message::Text(
            serde_json::to_string(&unexpected).unwrap().into(),
        ))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let request = QuoteRequest {
            side: QuoteSide::Mint,
            size: U256::from(1000u64),
        };

        let err = session.request_quote(request).await.unwrap_err();

        match err {
            ClientError::Protocol(msg) => {
                assert!(msg.contains("Expected QuoteResponse"));
            }
            other => panic!("expected Protocol error, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn request_quote_connection_closed() {
        let stream = VecStream::new(vec![Ok(Message::Close(None))]);

        let (sink, _) = VecSink::new();
        let mut session = WsSession::new(sink, stream);

        let request = QuoteRequest {
            side: QuoteSide::Mint,
            size: U256::from(100u64),
        };

        let err = session.request_quote(request).await.unwrap_err();

        assert!(matches!(err, ClientError::Protocol(_)));
    }
}
