use std::sync::Arc;

use derive_builder::Builder;

use crate::{
    network::query_builder::{QueryBuilder, QueryParam},
    tracking::trackable::{Trackable, TrackableBase, TrackingStateCell},
};

const EVENT_TYPE: &str = "geolocation";

#[derive(Debug, Default, Builder)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Geolocation {
    #[builder(setter(skip))]
    pub(crate) trackable_base: TrackableBase,
    #[builder(setter(into))]
    pub country: String,
    #[builder(setter(into), default = "None")]
    pub region: Option<String>,
    #[builder(setter(into), default = "None")]
    pub city: Option<String>,
    #[builder(setter(into), default = "None")]
    pub postal_code: Option<String>,
    #[builder(setter(into), default = "None")]
    pub latitude: Option<f32>,
    #[builder(setter(into), default = "None")]
    pub longitude: Option<f32>,
}

impl Geolocation {
    pub fn new(
        country: String,
        region: Option<String>,
        city: Option<String>,
        postal_code: Option<String>,
        latitude: Option<f32>,
        longitude: Option<f32>,
    ) -> Self {
        Self {
            trackable_base: TrackableBase::default(),
            country,
            region,
            city,
            postal_code,
            latitude,
            longitude,
        }
    }
}

impl Trackable for Geolocation {
    fn encode_query(&self) -> String {
        let mut qb = QueryBuilder::with_params(vec![
            (QueryParam::Nonce, self.trackable_base.nonce().into()),
            (QueryParam::EventType, EVENT_TYPE.into()),
            (QueryParam::Country, self.country.as_str().into()),
        ]);
        if let Some(region) = &self.region {
            qb.add_ptr_param(QueryParam::Region, region);
        }
        if let Some(city) = &self.city {
            qb.add_ptr_param(QueryParam::City, city);
        }
        if let Some(postal_code) = &self.postal_code {
            qb.add_ptr_param(QueryParam::PostalCode, postal_code);
        }
        if let Some(latitude) = &self.latitude {
            qb.add_owned_param(QueryParam::Latitude, latitude.to_string());
        }
        if let Some(longitude) = &self.longitude {
            qb.add_owned_param(QueryParam::Longitude, longitude.to_string());
        }
        return qb.build();
    }

    fn state_cell(&self) -> &Arc<TrackingStateCell> {
        self.trackable_base.state_cell()
    }
}

#[cfg(test)]
mod tests {
    use std::fmt::Display;

    use crate::utils::test_utils::remove_nonce_parameter;

    use super::*;
    use rstest::rstest;

    #[rstest]
    fn test_event_type() {
        assert_eq!("geolocation", EVENT_TYPE);
    }

    #[rstest]
    fn test_build_required_only() {
        let geolocation = GeolocationBuilder::default().country("co").build().unwrap();
        assert_eq!("co", geolocation.country);
        assert_eq!(None, geolocation.region);
        assert_eq!(None, geolocation.city);
        assert_eq!(None, geolocation.postal_code);
        assert_eq!(None, geolocation.latitude);
        assert_eq!(None, geolocation.longitude);
    }

    #[rstest]
    fn test_build_all() {
        let geolocation = GeolocationBuilder::default()
            .country("co")
            .region("re".to_owned())
            .city("ci".to_owned())
            .postal_code("1234".to_owned())
            .latitude(12.34)
            .longitude(-43.21)
            .build()
            .unwrap();
        assert_eq!("co", geolocation.country);
        assert_eq!(Some("re".to_owned()), geolocation.region);
        assert_eq!(Some("ci".to_owned()), geolocation.city);
        assert_eq!(Some("1234".to_owned()), geolocation.postal_code);
        assert_eq!(Some(12.34), geolocation.latitude);
        assert_eq!(Some(-43.21), geolocation.longitude);
    }

    #[rstest]
    fn test_build_required_missing() {
        let result = GeolocationBuilder::default()
            .region("re".to_owned())
            .city("ci".to_owned())
            .postal_code("1234".to_owned())
            .latitude(12.34)
            .longitude(-43.21)
            .build();
        assert!(result.is_err());
    }

    #[rstest]
    #[case(GeolocationBuilder::default().country("co").build().unwrap())]
    #[case(GeolocationBuilder::default().country("co").region("re".to_owned()).build().unwrap())]
    #[case(GeolocationBuilder::default().country("co").city("ci".to_owned()).build().unwrap())]
    #[case(GeolocationBuilder::default().country("co").postal_code("0000".to_owned()).build().unwrap())]
    #[case(GeolocationBuilder::default().country("co").latitude(10.0).build().unwrap())]
    #[case(GeolocationBuilder::default().country("co").longitude(-15.0).build().unwrap())]
    #[case(
        GeolocationBuilder::default()
        .country("co").region("re".to_owned()).city("ci".to_owned()).postal_code("0000".to_owned())
        .latitude(10.0).longitude(-15.0).build().unwrap()
    )]
    fn test_encode_query(#[case] geolocation: Geolocation) {
        // arrange
        #[rustfmt::skip]
        let mut expected = format!(
            "{}={}&{}={}",
            QueryParam::EventType.as_str(), EVENT_TYPE,
            QueryParam::Country.as_str(), geolocation.country,
        );
        add_opt_param(&mut expected, QueryParam::Region, geolocation.region.as_deref());
        add_opt_param(&mut expected, QueryParam::City, geolocation.city.as_deref());
        add_opt_param(&mut expected, QueryParam::PostalCode, geolocation.postal_code.as_deref());
        add_opt_param(&mut expected, QueryParam::Latitude, geolocation.latitude);
        add_opt_param(&mut expected, QueryParam::Longitude, geolocation.longitude);
        // act
        let actual = geolocation.encode_query();
        // assert
        let actual = remove_nonce_parameter(&actual);
        assert_eq!(expected, actual);
    }

    fn add_opt_param<T: Display>(query: &mut String, param: QueryParam, value: Option<T>) {
        if let Some(value) = value {
            query.push_str(&format!("&{}={}", param.as_str(), value));
        }
    }
}
