//! HTTP transport abstractions.
//!
//! This module defines the HTTP transport layer used by the Tenbin SDK.
//! It provides a minimal abstraction over HTTP request/response handling,
//! allowing different HTTP backends to be injected for production use
//! and testing.
//!
//! The transport layer is responsible only for issuing HTTP requests and
//! returning raw responses; higher-level protocol handling is implemented
//! elsewhere.

use alloy::primitives::Address;
use async_trait::async_trait;
use bytes::Bytes;
use http::StatusCode;

use crate::ClientError;

/// Authentication credentials for REST endpoints requiring signature-based auth.
///
/// These credentials are sent as HTTP headers on authenticated requests.
/// Use [`crate::signing::rest_auth::generate_auth_credentials`] to create
/// credentials from a signer.
///
/// # HTTP Headers
///
/// | Header | Value |
/// |--------|-------|
/// | `X-Auth-Address` | Hex-encoded EVM address |
/// | `X-Auth-Timestamp` | Unix timestamp in seconds |
/// | `X-Auth-Signature` | EIP-191 signature |
///
/// # 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?;
/// ```
#[derive(Debug, Clone)]
pub struct AuthCredentials {
    /// Signer's EVM wallet address (sent as `X-Auth-Address`)
    pub address: Address,
    /// Unix timestamp in seconds (sent as `X-Auth-Timestamp`).
    /// Must be within server's timestamp tolerance window.
    pub timestamp: u64,
    /// EIP-191 signature over the message `{METHOD}{PATH}{TIMESTAMP}`
    /// (sent as `X-Auth-Signature`)
    pub signature: String,
}

/// Raw HTTP response returned by an [`HttpTransport`].
///
/// Contains the HTTP status code and response body without any protocol-level
/// interpretation. Higher-level clients are responsible for validating status
/// codes and deserializing the body.
#[derive(Debug, Clone)]
pub struct HttpResponse {
    /// HTTP status code returned by the server.
    pub status: StatusCode,
    /// Raw response body bytes.
    pub body: Bytes,
}

/// HTTP transport abstraction.
///
/// Implementations of this trait are responsible for performing HTTP requests
/// and returning raw responses. The SDK relies on this trait to decouple
/// higher-level logic from any specific HTTP client implementation.
#[async_trait]
pub trait HttpTransport: Send + Sync + Clone + 'static {
    /// Perform an HTTP GET request.
    ///
    /// The request path is resolved relative to the transport's base URL.
    async fn get(&self, path: &str) -> Result<HttpResponse, ClientError>;

    /// Perform an authenticated HTTP GET request.
    ///
    /// Includes authentication headers (`X-Auth-Address`, `X-Auth-Timestamp`,
    /// `X-Auth-Signature`) in the request.
    async fn get_authenticated(
        &self,
        path: &str,
        auth: &AuthCredentials,
    ) -> Result<HttpResponse, ClientError>;

    /// Perform an HTTP POST request with a JSON body.
    ///
    /// The request body is serialized as JSON before being sent.
    async fn post_json<T: serde::Serialize + Send + Sync>(
        &self,
        path: &str,
        body: &T,
    ) -> Result<HttpResponse, ClientError>;

    /// Perform an authenticated HTTP DELETE request.
    ///
    /// Includes authentication headers (`X-Auth-Address`, `X-Auth-Timestamp`,
    /// `X-Auth-Signature`) in the request.
    async fn delete(&self, path: &str, auth: &AuthCredentials)
    -> Result<HttpResponse, ClientError>;
}

/// `reqwest`-based HTTP transport implementation.
///
/// This is the default HTTP transport used by the SDK in production.
#[derive(Clone)]
pub struct ReqwestTransport {
    /// Underlying `reqwest` HTTP client.
    client: reqwest::Client,
    /// Base URL used to resolve request paths.
    base_url: url::Url,
}

impl ReqwestTransport {
    /// Construct a new `ReqwestTransport`.
    ///
    /// Ensures the base URL path ends with `/` so that `Url::join` resolves
    /// relative paths correctly (e.g. `https://host/tgld/` + `quote` →
    /// `https://host/tgld/quote` instead of `https://host/quote`).
    ///
    /// This initializes the underlying HTTP client but performs no network I/O.
    pub fn new(mut base_url: url::Url, timeout: std::time::Duration) -> Result<Self, ClientError> {
        if !base_url.path().ends_with('/') {
            base_url.set_path(&format!("{}/", base_url.path()));
        }
        let client = reqwest::Client::builder()
            .timeout(timeout)
            .build()
            .map_err(|e| ClientError::Transport(e.to_string()))?;
        Ok(Self { client, base_url })
    }

    /// Resolve a path against the base URL, stripping any leading `/` so
    /// `Url::join` treats it as relative.
    fn resolve_url(&self, path: &str) -> Result<url::Url, ClientError> {
        let relative = path.strip_prefix('/').unwrap_or(path);
        self.base_url
            .join(relative)
            .map_err(|e| ClientError::Config(format!("invalid URL path '{}': {}", path, e)))
    }
}

/// HTTP transport implementation backed by `reqwest`.
#[async_trait]
impl HttpTransport for ReqwestTransport {
    async fn get(&self, path: &str) -> Result<HttpResponse, ClientError> {
        let url = self.resolve_url(path)?;
        let resp = self.client.get(url).send().await?;
        let status = StatusCode::from_u16(resp.status().as_u16())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        let body = resp.bytes().await?;
        Ok(HttpResponse { status, body })
    }

    async fn get_authenticated(
        &self,
        path: &str,
        auth: &AuthCredentials,
    ) -> Result<HttpResponse, ClientError> {
        let url = self.resolve_url(path)?;
        let resp = self
            .client
            .get(url)
            .header("X-Auth-Address", format!("{:?}", auth.address))
            .header("X-Auth-Timestamp", auth.timestamp.to_string())
            .header("X-Auth-Signature", &auth.signature)
            .send()
            .await?;
        let status = StatusCode::from_u16(resp.status().as_u16())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        let body = resp.bytes().await?;
        Ok(HttpResponse { status, body })
    }

    async fn post_json<T: serde::Serialize + Send + Sync>(
        &self,
        path: &str,
        body: &T,
    ) -> Result<HttpResponse, ClientError> {
        let url = self.resolve_url(path)?;
        let resp = self.client.post(url).json(body).send().await?;
        let status = StatusCode::from_u16(resp.status().as_u16())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        let body = resp.bytes().await?;
        Ok(HttpResponse { status, body })
    }

    async fn delete(
        &self,
        path: &str,
        auth: &AuthCredentials,
    ) -> Result<HttpResponse, ClientError> {
        let url = self.resolve_url(path)?;
        let resp = self
            .client
            .delete(url)
            .header("X-Auth-Address", format!("{:?}", auth.address))
            .header("X-Auth-Timestamp", auth.timestamp.to_string())
            .header("X-Auth-Signature", &auth.signature)
            .send()
            .await?;
        let status = StatusCode::from_u16(resp.status().as_u16())
            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
        let body = resp.bytes().await?;
        Ok(HttpResponse { status, body })
    }
}

/// Tests for HTTP transport behavior.
///
/// These tests validate correct request routing, status handling,
/// and response body propagation using a mock HTTP server.
#[cfg(test)]
mod tests {
    use std::time::Duration;

    use alloy::primitives::Address;
    use httpmock::Method::{DELETE, GET};
    use serde::{Deserialize, Serialize};

    use super::AuthCredentials;
    use crate::transport::http::{HttpTransport, ReqwestTransport};

    #[derive(Serialize, Deserialize)]
    struct TestPayload {
        value: u64,
    }

    #[tokio::test]
    async fn reqwest_transport_get_returns_body_and_status() {
        let server = httpmock::MockServer::start();

        server.mock(|when, then| {
            when.method(GET).path("/ping");
            then.status(200).body("pong");
        });

        let transport =
            ReqwestTransport::new(server.base_url().parse().unwrap(), Duration::from_secs(1))
                .expect("cannot build transport");

        let resp = transport.get("/ping").await.unwrap();

        assert_eq!(resp.status, http::StatusCode::OK);
        assert_eq!(resp.body, bytes::Bytes::from("pong"));
    }

    #[tokio::test]
    async fn reqwest_transport_get_with_non_base_url_returns_config_error() {
        let base_url = url::Url::parse("mailto:test@example.com").unwrap();

        let transport = ReqwestTransport::new(base_url, Duration::from_secs(1)).unwrap();

        let err = transport.get("/ping").await.unwrap_err();

        match err {
            crate::ClientError::Config(msg) => {
                assert!(msg.contains("invalid URL path"));
            }
            other => panic!("unexpected error: {:?}", other),
        }
    }

    #[tokio::test]
    async fn reqwest_transport_post_json_returns_body_and_status() {
        let server = httpmock::MockServer::start();

        server.mock(|when, then| {
            when.method(httpmock::Method::POST)
                .path("/submit")
                .json_body_obj(&TestPayload { value: 42 });
            then.status(201).body("created");
        });

        let transport =
            ReqwestTransport::new(server.base_url().parse().unwrap(), Duration::from_secs(1))
                .unwrap();

        let resp = transport
            .post_json("/submit", &TestPayload { value: 42 })
            .await
            .unwrap();

        assert_eq!(resp.status, http::StatusCode::CREATED);
        assert_eq!(resp.body, bytes::Bytes::from("created"));
    }

    #[tokio::test]
    async fn reqwest_transport_post_with_non_base_url_returns_config_error() {
        let base_url = url::Url::parse("mailto:test@example.com").unwrap();

        let transport = ReqwestTransport::new(base_url, Duration::from_secs(1)).unwrap();

        let payload = TestPayload { value: 1 };

        let err = transport.post_json("/submit", &payload).await.unwrap_err();

        match err {
            crate::ClientError::Config(msg) => {
                assert!(msg.contains("invalid URL path"));
            }
            other => panic!("unexpected error: {:?}", other),
        }
    }

    #[tokio::test]
    async fn reqwest_transport_get_propagates_non_200_status() {
        let server = httpmock::MockServer::start();

        server.mock(|when, then| {
            when.method(GET).path("/error");
            then.status(418).body("teapot");
        });

        let transport =
            ReqwestTransport::new(server.base_url().parse().unwrap(), Duration::from_secs(1))
                .unwrap();

        let resp = transport.get("/error").await.unwrap();

        assert_eq!(resp.status, http::StatusCode::IM_A_TEAPOT);
        assert_eq!(resp.body, bytes::Bytes::from("teapot"));
    }

    #[tokio::test]
    async fn reqwest_transport_get_authenticated_sends_headers() {
        let server = httpmock::MockServer::start();

        server.mock(|when, then| {
            when.method(GET)
                .path("/auth")
                .header_exists("X-Auth-Address")
                .header_exists("X-Auth-Timestamp")
                .header_exists("X-Auth-Signature");
            then.status(200).body("authed");
        });

        let transport =
            ReqwestTransport::new(server.base_url().parse().unwrap(), Duration::from_secs(1))
                .unwrap();

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

        let resp = transport.get_authenticated("/auth", &auth).await.unwrap();

        assert_eq!(resp.status, http::StatusCode::OK);
        assert_eq!(resp.body, bytes::Bytes::from("authed"));
    }

    #[tokio::test]
    async fn reqwest_transport_get_authenticated_with_invalid_path_returns_config_error() {
        let base_url = url::Url::parse("mailto:test@example.com").unwrap();
        let transport = ReqwestTransport::new(base_url, Duration::from_secs(1)).unwrap();

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

        let err = transport
            .get_authenticated("/auth", &auth)
            .await
            .unwrap_err();

        match err {
            crate::ClientError::Config(msg) => {
                assert!(msg.contains("invalid URL path"));
            }
            other => panic!("unexpected error: {:?}", other),
        }
    }

    #[tokio::test]
    async fn reqwest_transport_delete_sends_headers() {
        let server = httpmock::MockServer::start();

        server.mock(|when, then| {
            when.method(DELETE)
                .path("/resource")
                .header_exists("X-Auth-Address")
                .header_exists("X-Auth-Timestamp")
                .header_exists("X-Auth-Signature");
            then.status(204);
        });

        let transport =
            ReqwestTransport::new(server.base_url().parse().unwrap(), Duration::from_secs(1))
                .unwrap();

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

        let resp = transport.delete("/resource", &auth).await.unwrap();

        assert_eq!(resp.status, http::StatusCode::NO_CONTENT);
    }

    #[tokio::test]
    async fn reqwest_transport_delete_with_invalid_path_returns_config_error() {
        let base_url = url::Url::parse("mailto:test@example.com").unwrap();
        let transport = ReqwestTransport::new(base_url, Duration::from_secs(1)).unwrap();

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

        let err = transport.delete("/resource", &auth).await.unwrap_err();

        match err {
            crate::ClientError::Config(msg) => {
                assert!(msg.contains("invalid URL path"));
            }
            other => panic!("unexpected error: {:?}", other),
        }
    }
}
