//! REST API client.
//!
//! This module implements the Tenbin REST interface using an injected
//! `HttpTransport`. Each method performs a single HTTP request and validates
//! the HTTP status code before deserializing the response body.
//!
//! ## Endpoint Categories
//!
//! ### Unauthenticated Endpoints
//! - `RestClient::health_check` - Service health status
//! - `RestClient::get_deployment` - Contract addresses and configuration
//! - `RestClient::submit_order` - Submit a new order (order is self-authenticating via signature)
//! - `RestClient::get_order` - Fetch a specific order by ID
//! - `RestClient::get_quote` - Request a price quote
//!
//! ### Authenticated Endpoints
//! These endpoints require signature-based authentication via `AuthCredentials`:
//! - `RestClient::get_pending_orders` - List pending orders for the signer
//! - `RestClient::cancel_order` - Cancel a pending order
//!
//! ## Authentication
//!
//! For authenticated endpoints, use [`crate::signing::rest_auth::generate_auth_credentials`]
//! to create the required credentials. The authentication message format is
//! `{METHOD}{PATH}{TIMESTAMP}` signed using EIP-191.
//!
//! Non-success HTTP statuses are surfaced as `ClientError::Protocol`.

use crate::{
    error::ClientError,
    transport::http::{AuthCredentials, HttpTransport},
    types::{DeploymentInfo, Order, QuoteRequest, QuoteResponse},
};
use http::StatusCode;
use uuid::Uuid;

/// Minimal response payload returned by order submission.
///
/// This is intentionally private and only used to parse the returned `id`.
#[derive(serde::Deserialize)]
struct OrderIdResponse {
    /// Order identifier returned by the server.
    id: Uuid,
}

/// REST client for Tenbin request/response endpoints.
///
/// The client is generic over an `HttpTransport` implementation to support
/// mocking and alternative transport stacks.
#[derive(Clone)]
pub struct RestClient<T: HttpTransport> {
    /// Underlying HTTP transport used to issue requests.
    transport: T,
}

/// REST endpoint implementations.
impl<T: HttpTransport> RestClient<T> {
    /// Construct a new REST client using the provided transport.
    pub fn new(transport: T) -> Self {
        Self { transport }
    }

    /// Submit a new order to the platform.
    ///
    /// The order must include a valid EIP-712 signature from the payer. Use
    /// [`crate::signing::order::signing_digest`] to compute the digest to sign.
    ///
    /// A successful submission is indicated by either `200 OK` or `201 CREATED`.
    /// The returned value is the server-assigned order ID.
    ///
    /// # Arguments
    /// * `order` - The signed order to submit
    ///
    /// # Returns
    /// The UUID assigned to the order by the server.
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] if the HTTP status is not `200 OK` or `201 CREATED`
    /// - Returns [`ClientError::Protocol`] with 400 if the order is invalid
    /// - Returns [`ClientError::Serde`] if the response body cannot be deserialized
    ///
    /// # Example
    /// ```ignore
    /// use tenbin_sdk::types::{Order, OrderType};
    /// use tenbin_sdk::signing::order::signing_digest;
    ///
    /// // Build your order (obtain domain_separator from get_deployment())
    /// let mut order = Order { /* ... */ };
    ///
    /// // Compute the signing digest
    /// let digest = signing_digest(&order, deployment.domain_separator);
    ///
    /// // Sign it with your signer
    /// let signature = signer.sign_hash(&digest).await?;
    /// order.signature = Some(signature);
    ///
    /// // Submit
    /// let order_id = client.rest.submit_order(&order).await?;
    /// println!("Order submitted with ID: {}", order_id);
    /// ```
    pub async fn submit_order(&self, order: &Order) -> Result<Uuid, ClientError> {
        let resp = self.transport.post_json("/order", order).await?;
        if resp.status != StatusCode::OK && resp.status != StatusCode::CREATED {
            return Err(ClientError::Protocol(format!(
                "POST /order failed: {}",
                resp.status
            )));
        }
        Ok(serde_json::from_slice::<OrderIdResponse>(&resp.body)?.id)
    }

    /// Check service health.
    ///
    /// This endpoint verifies database and RPC provider connectivity.
    ///
    /// # Returns
    /// - `Ok(true)` if the service is healthy (200 OK)
    /// - `Ok(false)` if the health check failed (503) or timed out (408)
    /// - `Err` for other unexpected errors
    pub async fn health_check(&self) -> Result<bool, ClientError> {
        let resp = self.transport.get("/healthz").await?;
        match resp.status {
            StatusCode::OK => Ok(true),
            StatusCode::SERVICE_UNAVAILABLE | StatusCode::REQUEST_TIMEOUT => Ok(false),
            status => Err(ClientError::Protocol(format!(
                "GET /healthz unexpected status: {}",
                status
            ))),
        }
    }

    /// Get contract deployment information.
    ///
    /// Returns addresses for key contracts in the deployment including
    /// the controller, collateral token, and asset token.
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] if the HTTP status is not `200 OK`.
    /// - Returns [`ClientError::Serde`] if the response body cannot be deserialized.
    pub async fn get_deployment(&self) -> Result<DeploymentInfo, ClientError> {
        let resp = self.transport.get("/deployment").await?;
        if resp.status != StatusCode::OK {
            return Err(ClientError::Protocol(format!(
                "GET /deployment failed: {}",
                resp.status
            )));
        }
        Ok(serde_json::from_slice(&resp.body)?)
    }

    /// Fetch a single order by ID.
    ///
    /// This endpoint does not require authentication.
    ///
    /// # Arguments
    /// * `id` - The order UUID to fetch
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] with 404 if order not found
    /// - Returns [`ClientError::Serde`] if the response body cannot be deserialized
    ///
    /// # Example
    /// ```ignore
    /// let order = client.rest.get_order(order_id).await?;
    /// println!("Order status: {:?}", order.order_status);
    /// ```
    pub async fn get_order(&self, id: Uuid) -> Result<Order, ClientError> {
        let path = format!("/order/{id}");
        let resp = self.transport.get(&path).await?;
        if resp.status != StatusCode::OK {
            return Err(ClientError::Protocol(format!(
                "GET /order/{id} failed: {}",
                resp.status
            )));
        }
        Ok(serde_json::from_slice(&resp.body)?)
    }

    /// Fetch all pending orders for the authenticated user.
    ///
    /// Returns orders in the `Pending` state owned by the authenticated signer.
    /// Requires signature-based authentication.
    ///
    /// # Arguments
    /// * `auth` - Authentication credentials (see [`crate::signing::rest_auth::generate_auth_credentials`])
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] with 401 if authentication fails
    /// - Returns [`ClientError::Serde`] if the response body cannot be deserialized
    ///
    /// # Example
    /// ```ignore
    /// use tenbin_sdk::signing::rest_auth::generate_auth_credentials;
    /// use std::time::{SystemTime, UNIX_EPOCH};
    ///
    /// let timestamp = SystemTime::now()
    ///     .duration_since(UNIX_EPOCH)
    ///     .unwrap()
    ///     .as_secs();
    ///
    /// let auth = generate_auth_credentials(
    ///     "GET",
    ///     "/orders/pending",
    ///     my_address,
    ///     timestamp,
    ///     &my_signer,
    /// ).await?;
    ///
    /// let orders = client.rest.get_pending_orders(&auth).await?;
    /// for order in orders {
    ///     println!("Pending order: {:?}", order.id);
    /// }
    /// ```
    pub async fn get_pending_orders(
        &self,
        auth: &AuthCredentials,
    ) -> Result<Vec<Order>, ClientError> {
        let resp = self
            .transport
            .get_authenticated("/orders/pending", auth)
            .await?;
        if resp.status != StatusCode::OK {
            return Err(ClientError::Protocol(format!(
                "GET /orders/pending failed: {}",
                resp.status
            )));
        }
        Ok(serde_json::from_slice(&resp.body)?)
    }

    /// Cancel a pending order.
    ///
    /// Cancels an order that is currently in the `Pending` state. Only the order
    /// owner (the signer) can cancel their own orders.
    ///
    /// # Arguments
    /// * `id` - The order UUID to cancel
    /// * `auth` - Authentication credentials (see [`crate::signing::rest_auth::generate_auth_credentials`])
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] with 401 if authentication fails
    /// - Returns [`ClientError::Protocol`] with 404 if order not found
    /// - Returns [`ClientError::Protocol`] with 409 if order is not in Pending state
    ///
    /// # Example
    /// ```ignore
    /// use tenbin_sdk::signing::rest_auth::generate_auth_credentials;
    /// use std::time::{SystemTime, UNIX_EPOCH};
    ///
    /// let timestamp = SystemTime::now()
    ///     .duration_since(UNIX_EPOCH)
    ///     .unwrap()
    ///     .as_secs();
    ///
    /// let auth = generate_auth_credentials(
    ///     "DELETE",
    ///     &format!("/order/{}", order_id),
    ///     my_address,
    ///     timestamp,
    ///     &my_signer,
    /// ).await?;
    ///
    /// client.rest.cancel_order(order_id, &auth).await?;
    /// println!("Order cancelled successfully");
    /// ```
    pub async fn cancel_order(&self, id: Uuid, auth: &AuthCredentials) -> Result<(), ClientError> {
        let path = format!("/order/{id}");
        let resp = self.transport.delete(&path, auth).await?;
        if resp.status != StatusCode::NO_CONTENT {
            return Err(ClientError::Protocol(format!(
                "DELETE /order/{id} failed: {}",
                resp.status
            )));
        }
        Ok(())
    }

    /// Request a price quote for a mint or redeem operation.
    ///
    /// Quotes are indicative and may change between request and order submission.
    ///
    /// # Arguments
    /// * `request` - The quote request specifying token, side, and size
    ///
    /// # Returns
    /// A [`QuoteResponse`] containing the quote ID, status, and pricing information.
    ///
    /// # Errors
    /// - Returns [`ClientError::Protocol`] if the HTTP status is not `200 OK`
    /// - Returns [`ClientError::Serde`] if the response body cannot be deserialized
    ///
    /// # Example
    /// ```ignore
    /// use tenbin_sdk::types::{QuoteRequest, QuoteSide, QuoteStatus};
    /// use alloy::primitives::U256;
    ///
    /// let request = QuoteRequest {
    ///     side: QuoteSide::Mint,
    ///     size: U256::from(1_000_000u64), // 1M units
    /// };
    ///
    /// let quote = client.rest.get_quote(&request).await?;
    /// if quote.status == QuoteStatus::Ok {
    ///     println!("USD Price: {:?}", quote.usd_price);
    ///     println!("USDC Collateral: {:?}", quote.usdc_collateral);
    /// }
    /// ```
    pub async fn get_quote(&self, request: &QuoteRequest) -> Result<QuoteResponse, ClientError> {
        let resp = self.transport.post_json("/quote", request).await?;
        if resp.status != StatusCode::OK {
            return Err(ClientError::Protocol(format!(
                "POST /quote failed: {}",
                resp.status
            )));
        }
        Ok(serde_json::from_slice(&resp.body)?)
    }
}

/// Tests for REST client behaviour.
///
/// These tests validate:
/// - status-code validation (`Protocol` errors)
/// - JSON parsing failures (`Serde` errors)
/// - successful parsing of returned order ids using mocked transports.
#[cfg(test)]
mod tests {
    use alloy::primitives::{Address, U256};
    use bytes::Bytes;
    use http::StatusCode;
    use uuid::Uuid;

    use crate::{
        ClientError, TenbinClient,
        fixtures::{self, MockHttp, MockWsConnector},
        rest::client::RestClient,
        transport::http::AuthCredentials,
        types::{Order, QuoteRequest, QuoteSide, QuoteStatus},
    };

    #[tokio::test]
    async fn submit_order_parses_uuid() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        let id = Uuid::now_v7();
        mock.push(StatusCode::CREATED, format!(r#"{{"id":"{id}"}}"#));

        // Use a minimal dummy Order by deserializing valid JSON (avoids constructing Address/U256 manually here).
        // In your real repo, you’ll likely have a test factory or `Order::dummy()` behind cfg(test).
        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();

        let got = client.submit_order(&order).await.unwrap();
        assert_eq!(got, id);
    }

    #[test]
    fn tenbin_sdk_constructs_with_transports() {
        let http = MockHttp::default();
        let rest = RestClient::new(http.clone());

        let ws = MockWsConnector::default();

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

        // Just asserting construction + cloneability already exercises code paths
        let _ = client.clone();
    }

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

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

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

    #[test]
    fn rest_client_new_constructs() {
        let http = MockHttp::default();
        let client = RestClient::new(http);

        // Move + drop paths
        let _ = client;
    }

    #[tokio::test]
    async fn submit_order_unexpected_status_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::BAD_REQUEST, Bytes::from_static(b"bad"));

        let order = fixtures::valid_order();
        let err = client.submit_order(&order).await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn submit_order_empty_body_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::CREATED, Bytes::new());

        let order = fixtures::valid_order();
        let err = client.submit_order(&order).await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }

    // health_check tests
    #[tokio::test]
    async fn health_check_returns_true_on_200() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, Bytes::new());

        let result = client.health_check().await.unwrap();
        assert!(result);
    }

    #[tokio::test]
    async fn health_check_returns_false_on_503() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::SERVICE_UNAVAILABLE, Bytes::new());

        let result = client.health_check().await.unwrap();
        assert!(!result);
    }

    #[tokio::test]
    async fn health_check_returns_false_on_408() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::REQUEST_TIMEOUT, Bytes::new());

        let result = client.health_check().await.unwrap();
        assert!(!result);
    }

    #[tokio::test]
    async fn health_check_returns_error_on_unexpected_status() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::INTERNAL_SERVER_ERROR, Bytes::new());

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

    // get_deployment tests
    #[tokio::test]
    async fn get_deployment_success() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        let json = r#"{
            "controller": "0x0000000000000000000000000000000000000001",
            "domain_separator": "0x0000000000000000000000000000000000000000000000000000000000000002",
            "collateral": "0x0000000000000000000000000000000000000003",
            "token": "0x0000000000000000000000000000000000000004"
        }"#;
        mock.push(StatusCode::OK, json);

        let info = client.get_deployment().await.unwrap();
        let expected: Address = "0x0000000000000000000000000000000000000001"
            .parse()
            .unwrap();
        assert_eq!(info.controller, expected);
    }

    #[tokio::test]
    async fn get_deployment_non_200_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::INTERNAL_SERVER_ERROR, Bytes::new());

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

    #[tokio::test]
    async fn get_deployment_invalid_json_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, Bytes::from_static(b"{not-json"));

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

    // get_order tests
    #[tokio::test]
    async fn get_order_success() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        let order_json = serde_json::to_string(&fixtures::valid_order()).unwrap();
        mock.push(StatusCode::OK, order_json);

        let order = client.get_order(Uuid::nil()).await.unwrap();
        assert_eq!(order.order_type, crate::types::OrderType::Mint);
    }

    #[tokio::test]
    async fn get_order_non_200_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::NOT_FOUND, Bytes::new());

        let err = client.get_order(Uuid::nil()).await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn get_order_invalid_json_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, Bytes::from_static(b"{not-json"));

        let err = client.get_order(Uuid::nil()).await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }

    // get_pending_orders tests
    #[tokio::test]
    async fn get_pending_orders_success() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, "[]");

        let auth = AuthCredentials {
            address: Address::ZERO,
            timestamp: 1707000000,
            signature: "0xsig".to_string(),
        };

        let orders = client.get_pending_orders(&auth).await.unwrap();
        assert!(orders.is_empty());
    }

    #[tokio::test]
    async fn get_pending_orders_non_200_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::UNAUTHORIZED, Bytes::new());

        let auth = AuthCredentials {
            address: Address::ZERO,
            timestamp: 1707000000,
            signature: "0xsig".to_string(),
        };

        let err = client.get_pending_orders(&auth).await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn get_pending_orders_invalid_json_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, Bytes::from_static(b"[not-json"));

        let auth = AuthCredentials {
            address: Address::ZERO,
            timestamp: 1707000000,
            signature: "0xsig".to_string(),
        };

        let err = client.get_pending_orders(&auth).await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }

    // cancel_order tests
    #[tokio::test]
    async fn cancel_order_success() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::NO_CONTENT, Bytes::new());

        let auth = AuthCredentials {
            address: Address::ZERO,
            timestamp: 1707000000,
            signature: "0xsig".to_string(),
        };

        client.cancel_order(Uuid::nil(), &auth).await.unwrap();
    }

    #[tokio::test]
    async fn cancel_order_non_204_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::NOT_FOUND, Bytes::new());

        let auth = AuthCredentials {
            address: Address::ZERO,
            timestamp: 1707000000,
            signature: "0xsig".to_string(),
        };

        let err = client.cancel_order(Uuid::nil(), &auth).await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    // get_quote tests
    #[tokio::test]
    async fn get_quote_success() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        let id = Uuid::now_v7();
        let json = format!(
            r#"{{"id":"{}","status":"Ok","usd_price":"1.5","usdc_collateral":"1000"}}"#,
            id
        );
        mock.push(StatusCode::OK, json);

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

        let response = client.get_quote(&request).await.unwrap();
        assert_eq!(response.id, id);
        assert_eq!(response.status, QuoteStatus::Ok);
    }

    #[tokio::test]
    async fn get_quote_non_200_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::BAD_REQUEST, Bytes::new());

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

        let err = client.get_quote(&request).await.unwrap_err();
        assert!(matches!(err, ClientError::Protocol(_)));
    }

    #[tokio::test]
    async fn get_quote_invalid_json_returns_error() {
        let mock = MockHttp::default();
        let client = RestClient::new(mock.clone());

        mock.push(StatusCode::OK, Bytes::from_static(b"{not-json"));

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

        let err = client.get_quote(&request).await.unwrap_err();
        assert!(matches!(err, ClientError::Serde(_)));
    }
}
