//! WebSocket protocol message definitions.
//!
//! This module defines the strongly typed messages exchanged between the client
//! and server over an active WebSocket connection.
//!
//! Messages are encoded using an externally tagged representation, with a
//! `"type"` discriminator and a `"data"` payload, as defined by the Tenbin
//! WebSocket protocol.

use alloy::primitives::Address;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::types::{Order, OrderStatus, QuoteRequest, WsQuoteResponse};

/// Client-to-server WebSocket protocol messages.
///
/// These messages are sent by the client during an active WebSocket session
/// to submit orders, request authentication challenges, subscribe to events,
/// and maintain connection liveness.
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data")]
pub enum UserWsClientMessage {
    /// Submit a new order to the server.
    SubmitOrder {
        /// Order to be submitted to the server.
        order: Box<Order>,
    },
    /// Request an authentication challenge for the given signer address.
    GetChallenge {
        /// Address requesting authentication challenge.
        signer: Address,
    },
    /// Authenticate and subscribe to user-level server events.
    ///
    /// The `signature` must be produced by signing the server-issued challenge
    /// for the given `signer` address.
    Subscribe {
        /// Address being authenticated and subscribed.
        signer: Address,
        /// Signature over the server-issued challenge.
        signature: String,
    },
    /// Cancel an existing order via WebSocket.
    ///
    /// The signature must be over the message: `CancelOrder/{id}/{timestamp}`
    CancelOrder {
        /// Order identifier to cancel.
        id: Uuid,
        /// Signer address (must be the order owner).
        signer: Address,
        /// Unix timestamp (must be within ±10 seconds of server time).
        timestamp: u64,
        /// EIP-191 signature over the cancel message.
        signature: String,
    },
    /// Request a price quote for a mint or redeem operation.
    ///
    /// Authentication is not required for quote requests.
    QuoteRequest {
        /// The quote request parameters.
        request: QuoteRequest,
    },
    /// Client heartbeat message used to keep the connection alive.
    Ping,
}

/// Server-to-client WebSocket protocol messages.
///
/// These messages are sent by the server either in direct response to client
/// requests or as asynchronous event notifications.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", content = "data")]
pub enum UserWsServerMessage {
    /// Challenge used during subscription auth
    Challenge {
        /// Address for which the challenge was issued.
        signer: Address,
        /// Challenge message to be signed by the client.
        message: String,
    },
    /// Confirmation that the client has successfully subscribed.
    Subscribed {
        /// Address that has been successfully subscribed.
        signer: Address,
    },
    /// Notification that an order has been accepted by the server.
    OrderAccepted {
        /// Identifier of the accepted order.
        id: Uuid,
    },
    /// Notification that an order has been cancelled.
    OrderCancelled {
        /// Identifier of the cancelled order.
        id: Uuid,
    },
    /// Notification that an existing order's status has changed.
    OrderUpdate {
        /// Identifier of the order being updated.
        id: Uuid,
        /// Address that owns the order.
        signer: Address,
        /// New status of the order.
        status: OrderStatus,
        /// Transaction hash (present when order is fulfilled).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        tx_hash: Option<String>,
    },
    /// Response to a quote request.
    ///
    /// Contains pricing information for a mint or redeem operation.
    /// The `response` field contains the quote details including status
    /// and, if successful, the pricing data.
    QuoteResponse {
        /// The quote response payload.
        response: WsQuoteResponse,
    },
    /// Error message sent by the server.
    ///
    /// Indicates a protocol-level or application-level failure.
    Error {
        /// Machine-readable error code.
        code: String,
        /// Human-readable error message.
        message: String,
    },
    /// Server heartbeat response.
    Pong,
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy::primitives::U256;
    use rust_decimal::Decimal;

    use crate::types::{QuoteSide, QuoteStatus, WsQuoteResponseData};

    #[test]
    fn quote_request_message_serialization() {
        let msg = UserWsClientMessage::QuoteRequest {
            request: QuoteRequest {
                side: QuoteSide::Mint,
                size: U256::from(100u64),
            },
        };

        let json = serde_json::to_string(&msg).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["type"], "QuoteRequest");
        assert_eq!(parsed["data"]["request"]["token"], "GOLD");
        assert_eq!(parsed["data"]["request"]["side"], "Mint");
        assert_eq!(parsed["data"]["request"]["size"], "100");
    }

    #[test]
    fn quote_request_message_roundtrip() {
        let msg = UserWsClientMessage::QuoteRequest {
            request: QuoteRequest {
                side: QuoteSide::Redeem,
                size: U256::from(500u64),
            },
        };

        let json = serde_json::to_string(&msg).unwrap();
        let decoded: UserWsClientMessage = serde_json::from_str(&json).unwrap();

        match decoded {
            UserWsClientMessage::QuoteRequest { request } => {
                assert_eq!(request.side, QuoteSide::Redeem);
                assert_eq!(request.size, U256::from(500u64));
            }
            _ => panic!("expected QuoteRequest"),
        }
    }

    #[test]
    fn quote_response_message_ok_serialization() {
        let msg = UserWsServerMessage::QuoteResponse {
            response: WsQuoteResponse {
                id: Uuid::nil(),
                status: QuoteStatus::Ok,
                data: Some(WsQuoteResponseData {
                    usd_price: Decimal::new(12345, 2),
                    usdc_collateral: U256::from(1_000_000u64),
                }),
            },
        };

        let json = serde_json::to_string(&msg).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["type"], "QuoteResponse");
        assert_eq!(parsed["data"]["response"]["status"], "Ok");
        assert!(parsed["data"]["response"]["data"].is_object());
    }

    #[test]
    fn quote_response_message_failed_serialization() {
        let msg = UserWsServerMessage::QuoteResponse {
            response: WsQuoteResponse {
                id: Uuid::nil(),
                status: QuoteStatus::Failed,
                data: None,
            },
        };

        let json = serde_json::to_string(&msg).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["type"], "QuoteResponse");
        assert_eq!(parsed["data"]["response"]["status"], "Failed");
        assert!(parsed["data"]["response"]["data"].is_null());
    }

    #[test]
    fn quote_response_message_roundtrip() {
        let msg = UserWsServerMessage::QuoteResponse {
            response: WsQuoteResponse {
                id: Uuid::nil(),
                status: QuoteStatus::Ok,
                data: Some(WsQuoteResponseData {
                    usd_price: Decimal::new(99999, 3),
                    usdc_collateral: U256::from(2_000_000u64),
                }),
            },
        };

        let json = serde_json::to_string(&msg).unwrap();
        let decoded: UserWsServerMessage = serde_json::from_str(&json).unwrap();

        match decoded {
            UserWsServerMessage::QuoteResponse { response } => {
                assert_eq!(response.status, QuoteStatus::Ok);
                let data = response.data.unwrap();
                assert_eq!(data.usd_price, Decimal::new(99999, 3));
                assert_eq!(data.usdc_collateral, U256::from(2_000_000u64));
            }
            _ => panic!("expected QuoteResponse"),
        }
    }
}
