use std::fmt;

use crate::utils::url_encoder::UrlEncoder;

// MARK: - QueryParam
// Query parameter names as enum for type safety
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryParam {
    BodyUa,
    BrowserIndex,
    BrowserVersion,
    Cbs,
    City,
    ClientId,
    ClientSecret,
    Conversion,
    Country,
    CurrentVisit,
    CustomData,
    DeviceType,
    Environment,
    EventType,
    Experiment,
    ExperimentId,
    Geolocation,
    GoalId,
    GrantType,
    Href,
    Index,
    Kcs,
    Key,
    Latitude,
    Longitude,
    MappingIdentifier,
    MappingValue,
    MaxNumberPreviousVisits,
    Metadata,
    Negative,
    Nonce,
    Os,
    OsIndex,
    Overwrite,
    Page,
    Personalization,
    PostalCode,
    ReferrersIndices,
    Region,
    Revenue,
    SdkName,
    SdkVersion,
    SiteCode,
    StaticData,
    TimeSincePreviousVisit,
    Title,
    Ts,
    UserAgent,
    ValuesCountMap,
    VariationId,
    Version,
    VisitorCode,
    VisitNumber,
}

impl QueryParam {
    pub const fn as_str(&self) -> &'static str {
        match self {
            QueryParam::BodyUa => "bodyUa",
            QueryParam::BrowserIndex => "browserIndex",
            QueryParam::BrowserVersion => "browserVersion",
            QueryParam::Cbs => "cbs",
            QueryParam::City => "city",
            QueryParam::ClientId => "client_id",
            QueryParam::ClientSecret => "client_secret",
            QueryParam::Conversion => "conversion",
            QueryParam::Country => "country",
            QueryParam::CurrentVisit => "currentVisit",
            QueryParam::CustomData => "customData",
            QueryParam::DeviceType => "deviceType",
            QueryParam::Environment => "environment",
            QueryParam::EventType => "eventType",
            QueryParam::Experiment => "experiment",
            QueryParam::ExperimentId => "id",
            QueryParam::Geolocation => "geolocation",
            QueryParam::GoalId => "goalId",
            QueryParam::GrantType => "grant_type",
            QueryParam::Href => "href",
            QueryParam::Index => "index",
            QueryParam::Kcs => "kcs",
            QueryParam::Key => "key",
            QueryParam::Latitude => "latitude",
            QueryParam::Longitude => "longitude",
            QueryParam::MappingIdentifier => "mappingIdentifier",
            QueryParam::MappingValue => "mappingValue",
            QueryParam::MaxNumberPreviousVisits => "maxNumberPreviousVisits",
            QueryParam::Metadata => "metadata",
            QueryParam::Negative => "negative",
            QueryParam::Nonce => "nonce",
            QueryParam::Os => "os",
            QueryParam::OsIndex => "osIndex",
            QueryParam::Overwrite => "overwrite",
            QueryParam::Page => "page",
            QueryParam::Personalization => "personalization",
            QueryParam::PostalCode => "postalCode",
            QueryParam::ReferrersIndices => "referrersIndices",
            QueryParam::Region => "region",
            QueryParam::Revenue => "revenue",
            QueryParam::SdkName => "sdkName",
            QueryParam::SdkVersion => "sdkVersion",
            QueryParam::SiteCode => "siteCode",
            QueryParam::StaticData => "staticData",
            QueryParam::TimeSincePreviousVisit => "timeSincePreviousVisit",
            QueryParam::Title => "title",
            QueryParam::Ts => "ts",
            QueryParam::UserAgent => "userAgent",
            QueryParam::ValuesCountMap => "valuesCountMap",
            QueryParam::VariationId => "variationId",
            QueryParam::Version => "version",
            QueryParam::VisitorCode => "visitorCode",
            QueryParam::VisitNumber => "visitNumber",
        }
    }

    // The method
    pub const fn skip_encoding(&self) -> bool {
        match self {
            // List of parameters that should skip encoding
            QueryParam::BodyUa => true,
            QueryParam::BrowserIndex => true,
            QueryParam::BrowserVersion => true,
            QueryParam::Cbs => true,
            QueryParam::ClientId => true,
            QueryParam::ClientSecret => true,
            QueryParam::Conversion => true,
            QueryParam::CurrentVisit => true,
            QueryParam::CustomData => true,
            QueryParam::DeviceType => true,
            QueryParam::EventType => true,
            QueryParam::Experiment => true,
            QueryParam::ExperimentId => true,
            QueryParam::Geolocation => true,
            QueryParam::GoalId => true,
            QueryParam::GrantType => true,
            QueryParam::Index => true,
            QueryParam::Kcs => true,
            QueryParam::Latitude => true,
            QueryParam::Longitude => true,
            QueryParam::MappingIdentifier => true,
            QueryParam::MaxNumberPreviousVisits => true,
            QueryParam::Negative => true,
            QueryParam::Nonce => true,
            QueryParam::Os => true,
            QueryParam::OsIndex => true,
            QueryParam::Overwrite => true,
            QueryParam::Page => true,
            QueryParam::Personalization => true,
            QueryParam::Revenue => true,
            QueryParam::SdkName => true,
            QueryParam::SdkVersion => true,
            QueryParam::SiteCode => true,
            QueryParam::StaticData => true,
            QueryParam::TimeSincePreviousVisit => true,
            QueryParam::Ts => true,
            QueryParam::VariationId => true,
            QueryParam::Version => true,
            QueryParam::VisitNumber => true,
            _ => false,
        }
    }
}

// MARK: - QueryBuilder

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum QPValue<'a> {
    Owned(String),
    Ptr(&'a str),
}

impl<'a> QPValue<'a> {
    pub fn as_str(&self) -> &str {
        match self {
            QPValue::Owned(s) => s.as_str(),
            QPValue::Ptr(s) => s,
        }
    }
}

impl<'a> From<&'a str> for QPValue<'a> {
    fn from(value: &'a str) -> Self {
        return QPValue::Ptr(value);
    }
}

impl<'a> From<String> for QPValue<'a> {
    fn from(value: String) -> Self {
        return QPValue::Owned(value);
    }
}

// MARK: - QueryBuilder
// Query builder for constructing URL parameters
#[derive(Debug)]
pub struct QueryBuilder<'a> {
    params: Vec<(QueryParam, QPValue<'a>)>,
}

impl<'a> QueryBuilder<'a> {
    pub fn new() -> Self {
        Self { params: Vec::new() }
    }

    pub fn with_params(params: Vec<(QueryParam, QPValue<'a>)>) -> Self {
        Self { params }
    }

    pub fn add_param(&mut self, param: QueryParam, value: QPValue<'a>) -> &mut Self {
        self.params.push((param, value));
        self
    }

    pub fn add_owned_param(&mut self, param: QueryParam, value: String) -> &mut Self {
        self.add_param(param, QPValue::Owned(value))
    }

    pub fn add_ptr_param(&mut self, param: QueryParam, value: &'a str) -> &mut Self {
        self.add_param(param, QPValue::Ptr(value))
    }

    pub fn build(&self) -> String {
        if self.params.is_empty() {
            return String::new();
        }

        self.params
            .iter()
            .map(|(param, value)| {
                let value = if param.skip_encoding() {
                    value.as_str()
                } else {
                    &UrlEncoder::encode_uri_component(value.as_str())
                };
                format!("{}={}", param.as_str(), value)
            })
            .collect::<Vec<_>>()
            .join("&")
    }
}

impl<'a> fmt::Display for QueryBuilder<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.build())
    }
}

// MARK: - Tests

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

    use super::*;

    // MARK: - QueryParam

    #[rstest]
    fn test_query_param_not_skip_encoding() {
        assert!(!QueryParam::City.skip_encoding());
        assert!(!QueryParam::Country.skip_encoding());
        assert!(!QueryParam::Environment.skip_encoding());
        assert!(!QueryParam::Href.skip_encoding());
        assert!(!QueryParam::Key.skip_encoding());
        assert!(!QueryParam::MappingValue.skip_encoding());
        assert!(!QueryParam::Metadata.skip_encoding());
        assert!(!QueryParam::PostalCode.skip_encoding());
        assert!(!QueryParam::ReferrersIndices.skip_encoding());
        assert!(!QueryParam::Region.skip_encoding());
        assert!(!QueryParam::Title.skip_encoding());
        assert!(!QueryParam::UserAgent.skip_encoding());
        assert!(!QueryParam::ValuesCountMap.skip_encoding());
        assert!(!QueryParam::VisitorCode.skip_encoding());
    }

    // MARK: - QueryBuilder

    #[rstest]
    fn test_query_builder_new() {
        let builder = QueryBuilder::new();
        assert!(builder.params.is_empty());
    }

    #[rstest]
    fn test_query_builder_with_params() {
        let params = vec![(QueryParam::Environment, QPValue::Ptr("prod")), (QueryParam::Ts, QPValue::Ptr("12345"))];
        let builder = QueryBuilder::with_params(params);
        assert_eq!(
            vec![(QueryParam::Environment, QPValue::Ptr("prod")), (QueryParam::Ts, QPValue::Ptr("12345"))],
            builder.params
        );
    }

    #[rstest]
    fn test_query_builder_add_param() {
        let mut builder = QueryBuilder::new();
        builder.add_ptr_param(QueryParam::Environment, "prod").add_ptr_param(QueryParam::Ts, "12345");
        assert_eq!(
            vec![(QueryParam::Environment, QPValue::Ptr("prod")), (QueryParam::Ts, QPValue::Ptr("12345"))],
            builder.params
        );
    }

    #[rstest]
    #[case::single_param(
        vec![(QueryParam::Environment, "prod")],
        "environment=prod"
    )]
    #[case::multiple_params(
        vec![
            (QueryParam::Environment, "prod"),
            (QueryParam::Ts, "12345")
        ],
        "environment=prod&ts=12345"
    )]
    #[case::with_encoding(
        vec![
            (QueryParam::City, "New York"),
            (QueryParam::Country, "US")
        ],
        "city=New%20York&country=US"
    )]
    fn test_query_builder_build_cases(#[case] params: Vec<(QueryParam, &'static str)>, #[case] expected: &'static str) {
        let mut builder = QueryBuilder::new();
        for (param, value) in params {
            builder.add_ptr_param(param, value);
        }
        assert_eq!(expected, builder.build());
    }

    #[rstest]
    #[case::standard_encoding(
        vec![
            (QueryParam::City, "New York"),
            (QueryParam::Country, "US")
        ],
        "city=New%20York&country=US"
    )]
    #[case::special_chars(
        vec![
            (QueryParam::CustomData, "你好"),
            (QueryParam::Title, "世界")
        ],
        "customData=你好&title=%E4%B8%96%E7%95%8C"
    )]
    fn test_query_builder_encoding_cases(
        #[case] params: Vec<(QueryParam, &'static str)>,
        #[case] expected: &'static str,
    ) {
        let mut builder = QueryBuilder::new();
        for (param, value) in params {
            builder.add_ptr_param(param, value);
        }
        assert_eq!(expected, builder.build());
    }

    #[rstest]
    fn test_query_builder_display() {
        let mut builder = QueryBuilder::new();
        builder.add_ptr_param(QueryParam::Environment, "prod");
        assert_eq!("environment=prod", format!("{}", builder));
        assert_eq!(builder.build(), format!("{}", builder));
    }

    #[rstest]
    fn test_query_builder_add_owned_param() {
        let mut builder = QueryBuilder::new();
        builder.add_owned_param(QueryParam::Environment, "prod".to_owned());
        assert_eq!("environment=prod", builder.to_string());
    }
}
