// US-001: Market data endpoints stub — implemented in US-006, US-007
// US-006: get_server_time endpoint
// US-007: public market data endpoints (ping, exchange info, depth, trades, klines, tickers, etc.)

use crate::futures::models::market::{
    AggTrade, BookTickerShape, DepthResponse, ExchangeInfoResponse, FundingInfoResponse,
    FundingRateRecord, Kline, KlineInterval, MarkPriceShape, PingResponse, ServerTimeResponse,
    Ticker24hrShape, TickerPriceShape, TradeRecord,
};
use crate::rest::client::RestClient;
use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

impl RestClient {
    /// Get server time in milliseconds.
    ///
    /// Use to detect clock drift before placing orders.
    /// This is a public endpoint (no authentication required).
    pub async fn get_server_time(&self) -> Result<ApiResponse<ServerTimeResponse>, AsterDexError> {
        self.get("/fapi/v3/time", &[]).await
    }

    // -------------------------------------------------------------------------
    // US-007: Public market data endpoints
    // -------------------------------------------------------------------------

    /// Ping the REST API to check connectivity.
    ///
    /// Public endpoint — no authentication required.
    pub async fn ping(&self) -> Result<ApiResponse<PingResponse>, AsterDexError> {
        self.get("/fapi/v3/ping", &[]).await
    }

    /// Get exchange information including all trading symbols.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_exchange_info(&self) -> Result<ApiResponse<ExchangeInfoResponse>, AsterDexError> {
        self.get("/fapi/v3/exchangeInfo", &[]).await
    }

    /// Get order book depth for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_depth(
        &self,
        symbol: &str,
        limit: Option<u32>,
    ) -> Result<ApiResponse<DepthResponse>, AsterDexError> {
        let limit_str;
        let mut params = vec![("symbol", symbol)];
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/depth", &params).await
    }

    /// Get recent trades for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_trades(
        &self,
        symbol: &str,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<TradeRecord>>, AsterDexError> {
        let limit_str;
        let mut params = vec![("symbol", symbol)];
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/trades", &params).await
    }

    /// Get historical (older) trades for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_historical_trades(
        &self,
        symbol: &str,
        limit: Option<u32>,
        from_id: Option<i64>,
    ) -> Result<ApiResponse<Vec<TradeRecord>>, AsterDexError> {
        let limit_str;
        let from_id_str;
        let mut params = vec![("symbol", symbol)];
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        if let Some(id) = from_id {
            from_id_str = id.to_string();
            params.push(("fromId", &from_id_str));
        }
        self.get("/fapi/v3/historicalTrades", &params).await
    }

    /// Get compressed/aggregate trades for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_agg_trades(
        &self,
        symbol: &str,
        from_id: Option<i64>,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<AggTrade>>, AsterDexError> {
        let from_id_str;
        let start_time_str;
        let end_time_str;
        let limit_str;
        let mut params = vec![("symbol", symbol)];
        if let Some(id) = from_id {
            from_id_str = id.to_string();
            params.push(("fromId", &from_id_str));
        }
        if let Some(st) = start_time {
            start_time_str = st.to_string();
            params.push(("startTime", &start_time_str));
        }
        if let Some(et) = end_time {
            end_time_str = et.to_string();
            params.push(("endTime", &end_time_str));
        }
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/aggTrades", &params).await
    }

    /// Get kline/candlestick data for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_klines(
        &self,
        symbol: &str,
        interval: KlineInterval,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
        let start_time_str;
        let end_time_str;
        let limit_str;
        let interval_str = interval.to_str();
        let mut params = vec![("symbol", symbol), ("interval", interval_str)];
        if let Some(st) = start_time {
            start_time_str = st.to_string();
            params.push(("startTime", &start_time_str));
        }
        if let Some(et) = end_time {
            end_time_str = et.to_string();
            params.push(("endTime", &end_time_str));
        }
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/klines", &params).await
    }

    /// Get kline/candlestick data for an index price.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_index_price_klines(
        &self,
        pair: &str,
        interval: KlineInterval,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
        let start_time_str;
        let end_time_str;
        let limit_str;
        let interval_str = interval.to_str();
        let mut params = vec![("pair", pair), ("interval", interval_str)];
        if let Some(st) = start_time {
            start_time_str = st.to_string();
            params.push(("startTime", &start_time_str));
        }
        if let Some(et) = end_time {
            end_time_str = et.to_string();
            params.push(("endTime", &end_time_str));
        }
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/indexPriceKlines", &params).await
    }

    /// Get kline/candlestick data for a mark price.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_mark_price_klines(
        &self,
        symbol: &str,
        interval: KlineInterval,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<Kline>>, AsterDexError> {
        let start_time_str;
        let end_time_str;
        let limit_str;
        let interval_str = interval.to_str();
        let mut params = vec![("symbol", symbol), ("interval", interval_str)];
        if let Some(st) = start_time {
            start_time_str = st.to_string();
            params.push(("startTime", &start_time_str));
        }
        if let Some(et) = end_time {
            end_time_str = et.to_string();
            params.push(("endTime", &end_time_str));
        }
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/markPriceKlines", &params).await
    }

    /// Get mark price and funding rate for a symbol (or all symbols if None).
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_mark_price(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<MarkPriceShape>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.get("/fapi/v3/premiumIndex", &params).await
    }

    /// Get funding rate history for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_funding_rate(
        &self,
        symbol: Option<&str>,
        start_time: Option<u64>,
        end_time: Option<u64>,
        limit: Option<u32>,
    ) -> Result<ApiResponse<Vec<FundingRateRecord>>, AsterDexError> {
        let start_time_str;
        let end_time_str;
        let limit_str;
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        if let Some(st) = start_time {
            start_time_str = st.to_string();
            params.push(("startTime", &start_time_str));
        }
        if let Some(et) = end_time {
            end_time_str = et.to_string();
            params.push(("endTime", &end_time_str));
        }
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        self.get("/fapi/v3/fundingRate", &params).await
    }

    /// Get funding rate information for all symbols.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_funding_info(&self) -> Result<ApiResponse<Vec<FundingInfoResponse>>, AsterDexError> {
        self.get("/fapi/v3/fundingInfo", &[]).await
    }

    /// Get 24-hour ticker price change statistics.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_ticker_24hr(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<Ticker24hrShape>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.get("/fapi/v3/ticker/24hr", &params).await
    }

    /// Get latest price for a symbol or all symbols.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_ticker_price(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<TickerPriceShape>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.get("/fapi/v3/ticker/price", &params).await
    }

    /// Get best price/quantity on the order book for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_book_ticker(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<BookTickerShape>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.get("/fapi/v3/ticker/bookTicker", &params).await
    }

    /// Get index price references for a symbol.
    ///
    /// Public endpoint — no authentication required.
    pub async fn get_index_references(
        &self,
        symbol: Option<&str>,
    ) -> Result<ApiResponse<serde_json::Value>, AsterDexError> {
        let mut params: Vec<(&str, &str)> = vec![];
        if let Some(s) = symbol {
            params.push(("symbol", s));
        }
        self.get("/fapi/v3/indexreferences", &params).await
    }
}

// -------------------------------------------------------------------------
// US-007: Unit tests (mockito)
// -------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rest::client::RestClient;

    /// Test 1: ping returns Ok
    #[tokio::test]
    async fn ping_returns_ok() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/ping")
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body("{}")
            .create_async()
            .await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let resp = client.ping().await.unwrap();
        let _ = resp.data; // PingResponse is empty struct — just assert Ok
    }

    /// Test 2: get_depth returns bids and asks
    #[tokio::test]
    async fn get_depth_returns_bids_asks() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/depth")
            .match_query(mockito::Matcher::AllOf(vec![
                mockito::Matcher::UrlEncoded("symbol".to_string(), "BTCUSDT".to_string()),
                mockito::Matcher::UrlEncoded("limit".to_string(), "20".to_string()),
            ]))
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"lastUpdateId":12345,"bids":[["45000.00","1.5"]],"asks":[["45001.00","0.5"]]}"#)
            .create_async()
            .await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let resp = client.get_depth("BTCUSDT", Some(20)).await.unwrap();
        assert_eq!(resp.data.last_update_id, 12345);
        assert!(!resp.data.bids.is_empty());
        assert!(!resp.data.asks.is_empty());
    }

    /// Test 3: get_klines returns OHLCV data
    #[tokio::test]
    async fn get_klines_returns_ohlcv() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/klines")
            .match_query(mockito::Matcher::AllOf(vec![
                mockito::Matcher::UrlEncoded("symbol".to_string(), "BTCUSDT".to_string()),
                mockito::Matcher::UrlEncoded("interval".to_string(), "1m".to_string()),
                mockito::Matcher::UrlEncoded("limit".to_string(), "1".to_string()),
            ]))
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"[[1700000000000,"45000","46000","44000","45500","100.5",1700000059999,"4550000",500,"50.5","2275000"]]"#)
            .create_async()
            .await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let resp = client
            .get_klines("BTCUSDT", KlineInterval::OneMinute, None, None, Some(1))
            .await
            .unwrap();
        assert_eq!(resp.data[0].open_time, 1_700_000_000_000u64);
    }

    /// Test 4: get_ticker_24hr returns JSON response
    #[tokio::test]
    async fn get_ticker_24hr_returns_json() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/ticker/24hr")
            .match_query(mockito::Matcher::UrlEncoded(
                "symbol".to_string(),
                "BTCUSDT".to_string(),
            ))
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"symbol":"BTCUSDT","lastPrice":"45000.00","priceChange":"100.00","priceChangePercent":"0.22","weightedAvgPrice":"44950.00","prevClosePrice":"44900.00","lastQty":"1.0","openPrice":"44900.00","highPrice":"46000.00","lowPrice":"44500.00","volume":"5000.0","quoteVolume":"224750000.0","openTime":1699999200000,"closeTime":1700085600000,"firstId":1,"lastId":5000,"count":5000}"#)
            .create_async()
            .await;
        let client = RestClient::new_public(&server.url()).unwrap();
        let resp = client.get_ticker_24hr(Some("BTCUSDT")).await;
        assert!(resp.is_ok());
    }

    /// Test 5: public endpoint does NOT include auth params in request URL.
    ///
    /// Strategy: mock matches EXACT query "symbol=BTCUSDT" (no auth fields).
    /// If auth params were injected the mock would not match and the call would fail.
    #[tokio::test]
    async fn public_endpoint_no_auth_params() {
        let mut server = mockito::Server::new_async().await;
        let _mock = server
            .mock("GET", "/fapi/v3/depth")
            .match_query(mockito::Matcher::UrlEncoded(
                "symbol".to_string(),
                "BTCUSDT".to_string(),
            ))
            .with_status(200)
            .with_header("content-type", "application/json")
            .with_body(r#"{"lastUpdateId":1,"bids":[],"asks":[]}"#)
            .create_async()
            .await;
        let client = RestClient::new_public(&server.url()).unwrap();
        // If the client were injecting auth params (user=, signer=, nonce=, signature=)
        // the mock would NOT match (wrong query string) and the call would return an error.
        let result = client.get_depth("BTCUSDT", None).await;
        assert!(
            result.is_ok(),
            "Expected Ok but got error — auth params may have been injected: {:?}",
            result
        );
    }
}
