use crate::Error;
use crate::common::query::QueryWriter;
use crate::common::validate::{validate_required_symbol, validate_required_symbols};
use crate::transport::pagination::PaginatedRequest;

use super::{Adjustment, AuctionFeed, Currency, DataFeed, Sort, Tape, TickType, TimeFrame};

#[derive(Clone, Debug, Default)]
pub struct BarsRequest {
    pub symbols: Vec<String>,
    pub timeframe: TimeFrame,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub adjustment: Option<Adjustment>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct BarsSingleRequest {
    pub symbol: String,
    pub timeframe: TimeFrame,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub adjustment: Option<Adjustment>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct AuctionsRequest {
    pub symbols: Vec<String>,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub asof: Option<String>,
    pub feed: Option<AuctionFeed>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
    pub sort: Option<Sort>,
}

#[derive(Clone, Debug, Default)]
pub struct AuctionsSingleRequest {
    pub symbol: String,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub asof: Option<String>,
    pub feed: Option<AuctionFeed>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
    pub sort: Option<Sort>,
}

#[derive(Clone, Debug, Default)]
pub struct QuotesRequest {
    pub symbols: Vec<String>,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct QuotesSingleRequest {
    pub symbol: String,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct TradesRequest {
    pub symbols: Vec<String>,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct TradesSingleRequest {
    pub symbol: String,
    pub start: Option<String>,
    pub end: Option<String>,
    pub limit: Option<u32>,
    pub feed: Option<DataFeed>,
    pub sort: Option<Sort>,
    pub asof: Option<String>,
    pub currency: Option<Currency>,
    pub page_token: Option<String>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestBarsRequest {
    pub symbols: Vec<String>,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestBarRequest {
    pub symbol: String,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestQuotesRequest {
    pub symbols: Vec<String>,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestQuoteRequest {
    pub symbol: String,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestTradesRequest {
    pub symbols: Vec<String>,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct LatestTradeRequest {
    pub symbol: String,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct SnapshotsRequest {
    pub symbols: Vec<String>,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct SnapshotRequest {
    pub symbol: String,
    pub feed: Option<DataFeed>,
    pub currency: Option<Currency>,
}

#[derive(Clone, Debug, Default)]
pub struct ConditionCodesRequest {
    pub ticktype: TickType,
    pub tape: Tape,
}

impl BarsRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_csv("symbols", self.symbols);
        query.push_opt("timeframe", Some(self.timeframe));
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("adjustment", self.adjustment);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl BarsSingleRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_opt("timeframe", Some(self.timeframe));
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("adjustment", self.adjustment);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl AuctionsRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_csv("symbols", self.symbols);
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl AuctionsSingleRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl QuotesRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_csv("symbols", self.symbols);
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl QuotesSingleRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl TradesRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_csv("symbols", self.symbols);
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl TradesSingleRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")?;
        validate_limit(self.limit, 1, 10_000)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_opt("start", self.start);
        query.push_opt("end", self.end);
        query.push_opt("limit", self.limit);
        query.push_opt("feed", self.feed);
        query.push_opt("currency", self.currency);
        query.push_opt("page_token", self.page_token);
        query.push_opt("sort", self.sort);
        query.push_opt("asof", self.asof);
        query.finish()
    }
}

impl LatestBarsRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_batch_query(self.symbols, self.feed, self.currency)
    }
}

impl LatestBarRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_single_query(self.feed, self.currency)
    }
}

impl LatestQuotesRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_batch_query(self.symbols, self.feed, self.currency)
    }
}

impl LatestQuoteRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_single_query(self.feed, self.currency)
    }
}

impl LatestTradesRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_batch_query(self.symbols, self.feed, self.currency)
    }
}

impl LatestTradeRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_single_query(self.feed, self.currency)
    }
}

impl SnapshotsRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbols(&self.symbols)
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_batch_query(self.symbols, self.feed, self.currency)
    }
}

impl SnapshotRequest {
    pub(crate) fn validate(&self) -> Result<(), Error> {
        validate_required_symbol(&self.symbol, "symbol")
    }

    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        latest_single_query(self.feed, self.currency)
    }
}

impl ConditionCodesRequest {
    pub(crate) fn to_query(self) -> Vec<(String, String)> {
        let mut query = QueryWriter::default();
        query.push_opt("tape", Some(self.tape));
        query.finish()
    }
}

impl PaginatedRequest for BarsSingleRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for QuotesSingleRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for AuctionsSingleRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

fn latest_batch_query(
    symbols: Vec<String>,
    feed: Option<DataFeed>,
    currency: Option<Currency>,
) -> Vec<(String, String)> {
    let mut query = QueryWriter::default();
    query.push_csv("symbols", symbols);
    query.push_opt("feed", feed);
    query.push_opt("currency", currency);
    query.finish()
}

fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
    if let Some(limit) = limit {
        if !(min..=max).contains(&limit) {
            return Err(Error::InvalidRequest(format!(
                "limit must be between {min} and {max}"
            )));
        }
    }

    Ok(())
}

fn latest_single_query(
    feed: Option<DataFeed>,
    currency: Option<Currency>,
) -> Vec<(String, String)> {
    let mut query = QueryWriter::default();
    query.push_opt("feed", feed);
    query.push_opt("currency", currency);
    query.finish()
}

impl PaginatedRequest for TradesSingleRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for BarsRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for AuctionsRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for QuotesRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

impl PaginatedRequest for TradesRequest {
    fn with_page_token(&self, page_token: Option<String>) -> Self {
        let mut next = self.clone();
        next.page_token = page_token;
        next
    }
}

#[cfg(test)]
mod tests {
    use crate::Error;

    use super::*;

    #[test]
    fn stocks_data_feed_serializes_to_official_strings() {
        assert_eq!(DataFeed::DelayedSip.to_string(), "delayed_sip");
        assert_eq!(DataFeed::Iex.to_string(), "iex");
        assert_eq!(DataFeed::Otc.to_string(), "otc");
        assert_eq!(DataFeed::Sip.to_string(), "sip");
        assert_eq!(DataFeed::Boats.to_string(), "boats");
        assert_eq!(DataFeed::Overnight.to_string(), "overnight");
    }

    #[test]
    fn stocks_adjustment_serializes_to_official_strings() {
        assert_eq!(Adjustment::raw().to_string(), "raw");
        assert_eq!(Adjustment::split().to_string(), "split");
        assert_eq!(Adjustment::dividend().to_string(), "dividend");
        assert_eq!(Adjustment::spin_off().to_string(), "spin-off");
        assert_eq!(Adjustment::all().to_string(), "all");
        assert_eq!(
            Adjustment::from("split,dividend").to_string(),
            "split,dividend"
        );
    }

    #[test]
    fn stocks_timeframe_serializes_to_official_strings() {
        assert_eq!(TimeFrame::from("1Min").to_string(), "1Min");
        assert_eq!(TimeFrame::from("5Min").to_string(), "5Min");
        assert_eq!(TimeFrame::from("1Day").to_string(), "1Day");
        assert_eq!(TimeFrame::from("1Week").to_string(), "1Week");
        assert_eq!(TimeFrame::from("3Month").to_string(), "3Month");
    }

    #[test]
    fn bars_request_serializes_official_query_words() {
        let request = BarsRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            timeframe: TimeFrame::from("1Day"),
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(50),
            adjustment: Some(Adjustment::from("split,dividend")),
            feed: Some(DataFeed::Boats),
            sort: Some(Sort::Desc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-123".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("timeframe".to_string(), "1Day".to_string()),
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "50".to_string()),
                ("adjustment".to_string(), "split,dividend".to_string()),
                ("feed".to_string(), "boats".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-123".to_string()),
                ("sort".to_string(), "desc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn bars_single_request_serializes_official_query_words() {
        let request = BarsSingleRequest {
            symbol: "AAPL".into(),
            timeframe: TimeFrame::from("1Day"),
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(50),
            adjustment: Some(Adjustment::from("split,dividend")),
            feed: Some(DataFeed::Boats),
            sort: Some(Sort::Desc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-123".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("timeframe".to_string(), "1Day".to_string()),
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "50".to_string()),
                ("adjustment".to_string(), "split,dividend".to_string()),
                ("feed".to_string(), "boats".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-123".to_string()),
                ("sort".to_string(), "desc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn stocks_auction_feed_serializes_to_official_strings() {
        assert_eq!(AuctionFeed::Sip.to_string(), "sip");
    }

    #[test]
    fn auctions_request_serializes_official_query_words() {
        let request = AuctionsRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(10),
            asof: Some("2024-03-04".into()),
            feed: Some(AuctionFeed::Sip),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-auctions".into()),
            sort: Some(Sort::Asc),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "10".to_string()),
                ("feed".to_string(), "sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-auctions".to_string()),
                ("sort".to_string(), "asc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn auctions_single_request_serializes_official_query_words() {
        let request = AuctionsSingleRequest {
            symbol: "AAPL".into(),
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(10),
            asof: Some("2024-03-04".into()),
            feed: Some(AuctionFeed::Sip),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-auctions-single".into()),
            sort: Some(Sort::Desc),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "10".to_string()),
                ("feed".to_string(), "sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-auctions-single".to_string()),
                ("sort".to_string(), "desc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn quotes_request_serializes_official_query_words() {
        let request = QuotesRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(25),
            feed: Some(DataFeed::Iex),
            sort: Some(Sort::Asc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-456".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "25".to_string()),
                ("feed".to_string(), "iex".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-456".to_string()),
                ("sort".to_string(), "asc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn quotes_single_request_serializes_official_query_words() {
        let request = QuotesSingleRequest {
            symbol: "AAPL".into(),
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(25),
            feed: Some(DataFeed::Iex),
            sort: Some(Sort::Asc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-456".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "25".to_string()),
                ("feed".to_string(), "iex".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-456".to_string()),
                ("sort".to_string(), "asc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn trades_request_serializes_official_query_words() {
        let request = TradesRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(10),
            feed: Some(DataFeed::Sip),
            sort: Some(Sort::Desc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-789".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "10".to_string()),
                ("feed".to_string(), "sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-789".to_string()),
                ("sort".to_string(), "desc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn trades_single_request_serializes_official_query_words() {
        let request = TradesSingleRequest {
            symbol: "AAPL".into(),
            start: Some("2024-03-01T00:00:00Z".into()),
            end: Some("2024-03-05T00:00:00Z".into()),
            limit: Some(10),
            feed: Some(DataFeed::Sip),
            sort: Some(Sort::Desc),
            asof: Some("2024-03-04".into()),
            currency: Some(Currency::from("USD")),
            page_token: Some("page-789".into()),
        };

        assert_eq!(
            request.to_query(),
            vec![
                ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
                ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
                ("limit".to_string(), "10".to_string()),
                ("feed".to_string(), "sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
                ("page_token".to_string(), "page-789".to_string()),
                ("sort".to_string(), "desc".to_string()),
                ("asof".to_string(), "2024-03-04".to_string()),
            ]
        );
    }

    #[test]
    fn latest_batch_requests_serialize_official_query_words() {
        let bars = LatestBarsRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            feed: Some(DataFeed::DelayedSip),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            bars.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("feed".to_string(), "delayed_sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );

        let trades = LatestTradesRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            feed: Some(DataFeed::Iex),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            trades.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("feed".to_string(), "iex".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );
    }

    #[test]
    fn latest_single_requests_serialize_official_query_words() {
        let bar = LatestBarRequest {
            symbol: "AAPL".into(),
            feed: Some(DataFeed::Sip),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            bar.to_query(),
            vec![
                ("feed".to_string(), "sip".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );

        let trade = LatestTradeRequest {
            symbol: "AAPL".into(),
            feed: Some(DataFeed::Boats),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            trade.to_query(),
            vec![
                ("feed".to_string(), "boats".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );
    }

    #[test]
    fn snapshot_requests_serialize_official_query_words() {
        let batch = SnapshotsRequest {
            symbols: vec!["AAPL".into(), "MSFT".into()],
            feed: Some(DataFeed::Overnight),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            batch.to_query(),
            vec![
                ("symbols".to_string(), "AAPL,MSFT".to_string()),
                ("feed".to_string(), "overnight".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );

        let single = SnapshotRequest {
            symbol: "AAPL".into(),
            feed: Some(DataFeed::Otc),
            currency: Some(Currency::from("USD")),
        };

        assert_eq!(
            single.to_query(),
            vec![
                ("feed".to_string(), "otc".to_string()),
                ("currency".to_string(), "USD".to_string()),
            ]
        );
    }

    #[test]
    fn stocks_ticktype_and_tape_serialize_to_official_strings() {
        assert_eq!(TickType::Trade.as_str(), "trade");
        assert_eq!(TickType::Quote.as_str(), "quote");
        assert_eq!(Tape::A.as_str(), "A");
        assert_eq!(Tape::B.as_str(), "B");
        assert_eq!(Tape::C.as_str(), "C");
    }

    #[test]
    fn metadata_request_serializes_official_query_words() {
        let request = ConditionCodesRequest {
            ticktype: TickType::Trade,
            tape: Tape::A,
        };

        assert_eq!(
            request.to_query(),
            vec![("tape".to_string(), "A".to_string()),]
        );
    }

    #[test]
    fn batch_requests_reject_empty_symbols_for_required_symbol_endpoints() {
        let errors = [
            BarsRequest::default()
                .validate()
                .expect_err("bars symbols must be required"),
            AuctionsRequest::default()
                .validate()
                .expect_err("auctions symbols must be required"),
            QuotesRequest::default()
                .validate()
                .expect_err("quotes symbols must be required"),
            TradesRequest::default()
                .validate()
                .expect_err("trades symbols must be required"),
            LatestBarsRequest::default()
                .validate()
                .expect_err("latest bars symbols must be required"),
            LatestQuotesRequest::default()
                .validate()
                .expect_err("latest quotes symbols must be required"),
            LatestTradesRequest::default()
                .validate()
                .expect_err("latest trades symbols must be required"),
            SnapshotsRequest::default()
                .validate()
                .expect_err("snapshots symbols must be required"),
        ];

        for error in errors {
            assert!(matches!(
                error,
                Error::InvalidRequest(message)
                    if message.contains("symbols") && message.contains("empty")
            ));
        }
    }

    #[test]
    fn latest_single_requests_reject_blank_symbols() {
        let errors = [
            LatestQuoteRequest::default()
                .validate()
                .expect_err("latest quote symbol must be required"),
            LatestQuoteRequest {
                symbol: "   ".into(),
                ..LatestQuoteRequest::default()
            }
            .validate()
            .expect_err("latest quote symbol must reject whitespace-only input"),
            LatestTradeRequest::default()
                .validate()
                .expect_err("latest trade symbol must be required"),
            LatestTradeRequest {
                symbol: "   ".into(),
                ..LatestTradeRequest::default()
            }
            .validate()
            .expect_err("latest trade symbol must reject whitespace-only input"),
            SnapshotRequest::default()
                .validate()
                .expect_err("snapshot symbol must be required"),
            SnapshotRequest {
                symbol: "   ".into(),
                ..SnapshotRequest::default()
            }
            .validate()
            .expect_err("snapshot symbol must reject whitespace-only input"),
        ];

        for error in errors {
            assert!(matches!(
                error,
                Error::InvalidRequest(message)
                    if message.contains("symbol") && message.contains("invalid")
            ));
        }
    }

    #[test]
    fn historical_requests_reject_limits_outside_documented_range() {
        let errors = [
            BarsRequest {
                symbols: vec!["AAPL".into()],
                limit: Some(0),
                ..BarsRequest::default()
            }
            .validate()
            .expect_err("bars limit below one must fail"),
            BarsSingleRequest {
                symbol: "AAPL".into(),
                limit: Some(10_001),
                ..BarsSingleRequest::default()
            }
            .validate()
            .expect_err("single bars limit above ten thousand must fail"),
            AuctionsRequest {
                symbols: vec!["AAPL".into()],
                limit: Some(0),
                ..AuctionsRequest::default()
            }
            .validate()
            .expect_err("auctions limit below one must fail"),
            AuctionsSingleRequest {
                symbol: "AAPL".into(),
                limit: Some(10_001),
                ..AuctionsSingleRequest::default()
            }
            .validate()
            .expect_err("single auctions limit above ten thousand must fail"),
            QuotesRequest {
                symbols: vec!["AAPL".into()],
                limit: Some(0),
                ..QuotesRequest::default()
            }
            .validate()
            .expect_err("quotes limit below one must fail"),
            QuotesSingleRequest {
                symbol: "AAPL".into(),
                limit: Some(10_001),
                ..QuotesSingleRequest::default()
            }
            .validate()
            .expect_err("single quotes limit above ten thousand must fail"),
            TradesRequest {
                symbols: vec!["AAPL".into()],
                limit: Some(0),
                ..TradesRequest::default()
            }
            .validate()
            .expect_err("trades limit below one must fail"),
            TradesSingleRequest {
                symbol: "AAPL".into(),
                limit: Some(10_001),
                ..TradesSingleRequest::default()
            }
            .validate()
            .expect_err("single trades limit above ten thousand must fail"),
        ];

        for error in errors {
            assert!(matches!(
                error,
                Error::InvalidRequest(message)
                    if message.contains("limit") && message.contains("10000")
            ));
        }
    }
}
