//! High-level SDK client.
//!
//! This module defines the primary client type used to interact with the Tenbin
//! platform. It exposes REST functionality immediately after construction and
//! allows WebSocket sessions to be established explicitly.
//!
//! ## REST vs WebSocket
//!
//! - **REST**: Use `client.rest` for request/response operations like submitting
//!   orders, fetching quotes, and checking health.
//! - **WebSocket**: Use `client.connect_ws()` or `client.ws_subscribe()` for
//!   real-time event streaming and order updates.

use crate::{
    config::ClientConfig,
    error::ClientError,
    rest::client::RestClient,
    signing::signer::PayloadSigner,
    transport::{
        http::{HttpTransport, ReqwestTransport},
        websocket::{TungsteniteConnector, WsConnector},
    },
    ws::{client::WsSession, protocol::UserWsServerMessage},
};
use alloy::primitives::Address;

/// Unified SDK client: REST + WebSocket.
///
/// Most users should construct this via [`TenbinClient::new`] which uses:
/// - `reqwest` for HTTP
/// - `tokio-tungstenite` for WebSocket
///
/// The type is generic over transports to enable testing and custom networking.
///
/// # Example (construct)
/// ```
/// use tenbin_sdk::{ClientConfig, TenbinClient};
/// use url::Url;
///
/// let http = Url::parse("http://localhost:8080").unwrap();
/// let ws = Url::parse("ws://localhost:8080/stream").unwrap();
/// let cfg = ClientConfig::new(http, ws);
/// let client = TenbinClient::new(cfg).unwrap();
///
/// // REST surface is available on `client.rest`.
/// let _rest = &client.rest;
/// ```
///
/// # Example (connect WS)
/// ```no_run
/// use tenbin_sdk::{ClientConfig, TenbinClient};
/// use url::Url;
///
/// # async fn run() -> Result<(), tenbin_sdk::ClientError> {
/// let http = Url::parse("http://localhost:8080").unwrap();
/// let ws = Url::parse("ws://localhost:8080/stream").unwrap();
/// let cfg = ClientConfig::new(http, ws);
/// let client = TenbinClient::new(cfg)?;
///
/// let _session = client.connect_ws().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct TenbinClient<H: HttpTransport, W> {
    /// REST API client used for request/response endpoints.
    pub rest: RestClient<H>,
    /// WebSocket connector used to create streaming sessions.
    ws: W,
}

impl TenbinClient<ReqwestTransport, TungsteniteConnector> {
    /// Construct a new client using the default transports.
    ///
    /// Validates the provided configuration but performs no network I/O.
    ///
    /// # Errors
    /// Returns a configuration error if the supplied settings are invalid.
    pub fn new(config: ClientConfig) -> Result<Self, ClientError> {
        config.validate()?;
        let http = ReqwestTransport::new(config.http_base_url.clone(), config.timeout)?;
        let ws = TungsteniteConnector::new(config.ws_base_url.clone());
        Ok(Self {
            rest: RestClient::new(http),
            ws,
        })
    }
}

impl<H, W> TenbinClient<H, W>
where
    H: HttpTransport,
    W: WsConnector,
{
    /// Construct a client from explicitly provided transports.
    ///
    /// Intended for testing and advanced scenarios requiring custom transports.
    pub fn with_transports(rest: RestClient<H>, ws: W) -> Self {
        Self { rest, ws }
    }

    /// Establish a new WebSocket session.
    ///
    /// Opens a WebSocket connection and returns a session handle. The caller is
    /// responsible for authentication and subscription on the returned session.
    ///
    /// # Errors
    /// Returns an error if the WebSocket connection cannot be established.
    pub async fn connect_ws(&self) -> Result<WsSession<W::Sink, W::Stream>, ClientError> {
        let (sink, stream) = self.ws.connect().await.map_err(ClientError::WebSocket)?;
        Ok(WsSession::new(sink, stream))
    }

    /// Connect, authenticate, and subscribe to WebSocket events in one call.
    ///
    /// This convenience method combines connection establishment, challenge-response
    /// authentication, and event subscription into a single operation.
    ///
    /// # Arguments
    /// * `signer_addr` - The EVM address to authenticate as
    /// * `signer` - A [`PayloadSigner`] implementation to sign the challenge
    ///
    /// # Returns
    /// An authenticated [`WsSession`] ready to receive events via [`WsSession::next_event`].
    ///
    /// # Errors
    /// Returns an error if connection fails, authentication is rejected, or signing fails.
    ///
    /// # Example
    /// ```no_run
    /// use tenbin_sdk::{ClientConfig, TenbinClient};
    /// use tenbin_sdk::signing::signer::{PayloadSigner, SimpleSigner};
    /// use alloy::primitives::{Address, B256};
    /// use url::Url;
    ///
    /// # async fn example() -> Result<(), tenbin_sdk::ClientError> {
    /// let config = ClientConfig::new(
    ///     Url::parse("https://api.tenbin.io").unwrap(),
    ///     Url::parse("wss://api.tenbin.io/stream").unwrap(),
    /// );
    /// let client = TenbinClient::new(config)?;
    ///
    /// // Create a signer (use your own key management in production)
    /// let private_key = B256::from([0x11u8; 32]); // Example only!
    /// let signer = SimpleSigner::new(private_key);
    /// let my_address: Address = "0x1234567890123456789012345678901234567890".parse().unwrap();
    ///
    /// // Connect and subscribe
    /// let mut session = client.ws_subscribe(my_address, &signer).await?;
    ///
    /// // Now receive events
    /// loop {
    ///     let event = session.next_event().await?;
    ///     println!("Received: {:?}", event);
    /// }
    /// # }
    /// ```
    pub async fn ws_subscribe<S: PayloadSigner>(
        &self,
        signer_addr: Address,
        signer: &S,
    ) -> Result<WsSession<W::Sink, W::Stream>, ClientError> {
        let mut session = self.connect_ws().await?;
        session
            .authenticate_and_subscribe(signer_addr, signer)
            .await?;
        Ok(session)
    }

    /// Connect, authenticate, subscribe, and return the first event.
    ///
    /// Similar to [`ws_subscribe`](Self::ws_subscribe), but also waits for and
    /// returns the first server message after subscription completes.
    ///
    /// This is useful for one-shot scenarios where you want to receive a single
    /// event without managing the session lifecycle.
    ///
    /// # Arguments
    /// * `signer_addr` - The EVM address to authenticate as
    /// * `signer` - A [`PayloadSigner`] implementation to sign the challenge
    ///
    /// # Returns
    /// The first [`UserWsServerMessage`] received after subscription.
    ///
    /// # Errors
    /// Returns an error if connection, authentication, or receiving the message fails.
    pub async fn ws_subscribe_and_next<S: PayloadSigner>(
        &self,
        signer_addr: Address,
        signer: &S,
    ) -> Result<UserWsServerMessage, ClientError> {
        let mut session = self.ws_subscribe(signer_addr, signer).await?;
        session.next_event().await
    }
}

/// Tests for the high-level client.
///
/// These tests use mocked transports to validate connection, authentication,
/// and message-handling behavior without requiring a live server.
#[cfg(test)]
mod tests {
    use alloy::primitives::Address;
    use tokio_tungstenite::tungstenite::Message;
    use url::Url;
    use uuid::Uuid;

    use crate::{
        ClientConfig, ClientError, TenbinClient,
        fixtures::{FailingSigner, FixedSigner, MockHttp, MockWsConnector},
        rest::client::RestClient,
        ws::protocol::UserWsServerMessage::OrderAccepted,
    };

    #[test]
    fn client_new_constructs() {
        let http = Url::parse("http://localhost:1234").unwrap();
        let ws = Url::parse("ws://localhost:1234/stream").unwrap();

        let cfg = ClientConfig::new(http, ws);
        let client = TenbinClient::new(cfg).expect("cannot build client");

        // Exercise clone path as well
        let _ = client.clone();
    }

    #[test]
    fn client_with_transports_constructs() {
        let rest = RestClient::new(MockHttp::default());
        let ws = MockWsConnector::with_empty_stream();

        let client = TenbinClient::with_transports(rest, ws);
        let _ = client.clone();
    }

    #[tokio::test]
    async fn client_connect_ws_works() {
        let rest = RestClient::new(MockHttp::default());
        let ws = MockWsConnector::with_empty_stream();

        let client = TenbinClient::with_transports(rest, ws);

        let session = client.connect_ws().await.unwrap();
        drop(session);
    }

    #[tokio::test]
    async fn client_ws_subscribe_success() {
        let signer_addr = Address::from([1u8; 20]);

        let incoming = vec![
            Message::Text(
                serde_json::json!({
                    "type": "Challenge",
                    "data": { "signer": signer_addr, "message": "hello" }
                })
                .to_string()
                .into(),
            ),
            Message::Text(
                serde_json::json!({
                    "type": "Subscribed",
                    "data": { "signer": signer_addr }
                })
                .to_string()
                .into(),
            ),
        ];

        let ws = MockWsConnector::with_incoming(incoming);
        let rest = RestClient::new(MockHttp::default());
        let client = TenbinClient::with_transports(rest, ws);

        let signer = FixedSigner { sig: "0xsig" };

        let session = client.ws_subscribe(signer_addr, &signer).await.unwrap();
        drop(session);
    }

    #[tokio::test]
    async fn client_ws_subscribe_signing_fails() {
        let signer_addr = Address::from([2u8; 20]);

        let incoming = vec![Message::Text(
            serde_json::json!({
                "type": "Challenge",
                "data": { "signer": signer_addr, "message": "hello" }
            })
            .to_string()
            .into(),
        )];

        let ws = MockWsConnector::with_incoming(incoming);
        let rest = RestClient::new(MockHttp::default());
        let client = TenbinClient::with_transports(rest, ws);

        let signer = FailingSigner;

        let err = client.ws_subscribe(signer_addr, &signer).await.unwrap_err();
        assert!(matches!(err, ClientError::Auth(_)));
    }

    #[tokio::test]
    async fn client_ws_subscribe_and_next_returns_event() {
        let signer_addr = Address::from([3u8; 20]);
        let id = Uuid::now_v7();

        let incoming = vec![
            Message::Text(
                serde_json::json!({
                    "type": "Challenge",
                    "data": { "signer": signer_addr, "message": "hello" }
                })
                .to_string()
                .into(),
            ),
            Message::Text(
                serde_json::json!({
                    "type": "Subscribed",
                    "data": { "signer": signer_addr }
                })
                .to_string()
                .into(),
            ),
            Message::Text(
                serde_json::json!({
                    "type": "OrderAccepted",
                    "data": { "id": id }
                })
                .to_string()
                .into(),
            ),
        ];

        let ws = MockWsConnector::with_incoming(incoming);
        let rest = RestClient::new(MockHttp::default());
        let client = TenbinClient::with_transports(rest, ws);

        let signer = FixedSigner { sig: "0xsig" };

        let msg = client
            .ws_subscribe_and_next(signer_addr, &signer)
            .await
            .unwrap();

        assert!(matches!(msg, OrderAccepted { .. }));
    }
}
