use std::sync::Arc;

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

const EVENT_TYPE: &str = "page";

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub struct PageView {
    pub url: String,
    pub title: Option<String>,
    pub referrers: Vec<i32>,
    pub(crate) trackable_base: TrackableBase,
}

impl PageView {
    pub fn new<T: Into<String>>(url: impl Into<String>, title: Option<T>, referrers: Vec<i32>) -> Self {
        PageView {
            url: url.into(),
            title: title.map(Into::into),
            referrers,
            trackable_base: TrackableBase::default(),
        }
    }

    pub fn new_with_url(url: impl Into<String>) -> Self {
        Self::new(url, None::<String>, vec![])
    }

    pub fn new_with_opts(url: impl Into<String>, opts: PageViewOpts) -> Self {
        Self::new(url, opts.title, opts.referrers)
    }
}

impl Trackable for PageView {
    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::Href, self.url.as_str().into()),
        ]);
        if let Some(title) = &self.title {
            qb.add_ptr_param(QueryParam::Title, title);
        }
        if !self.referrers.is_empty() {
            qb.add_owned_param(QueryParam::ReferrersIndices, self.make_referrers_param());
        }
        return qb.build();
    }

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

// Trackable Helper
impl PageView {
    fn make_referrers_param(&self) -> String {
        let mut param = String::with_capacity(self.referrers.len() * 8 + 2);
        param.push('[');
        for referrer in &self.referrers {
            param.push_str(&referrer.to_string());
            param.push(',');
        }
        if param.len() > 1 {
            param.remove(param.len() - 1);
        }
        param.push(']');
        return param;
    }
}

#[derive(Debug, Default)]
#[cfg_attr(test, derive(PartialEq))]
pub struct PageViewOpts {
    title: Option<String>,
    referrers: Vec<i32>,
}

impl PageViewOpts {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn referrers(mut self, referrers: Vec<i32>) -> Self {
        self.referrers = referrers;
        self
    }
}

#[cfg(test)]
mod tests {
    use crate::utils::{test_utils::remove_nonce_parameter, url_encoder::UrlEncoder};

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

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

    #[rstest]
    #[case("http://example.com", None, Vec::new())]
    #[case("https://something.net", Some("Thing".to_owned()), vec![1, 2, 3])]
    fn test_new(#[case] url: String, #[case] title: Option<String>, #[case] referrers: Vec<i32>) {
        let pv = PageView::new(&url, title.clone(), referrers.clone());
        assert_eq!(&url, &pv.url);
        assert_eq!(title, pv.title);
        assert_eq!(referrers, pv.referrers);
    }

    #[rstest]
    fn test_new_with_url() {
        let pv = PageView::new_with_url("https://example.com".to_owned());
        assert_eq!("https://example.com", pv.url);
        assert_eq!(None, pv.title);
        assert!(pv.referrers.is_empty());
    }

    #[rstest]
    fn test_new_with_opts() {
        let pv = PageView::new_with_opts(
            "https://example.com".to_owned(),
            PageViewOpts::new().title("Example").referrers(vec![1, 2, 3]),
        );
        assert_eq!("https://example.com", pv.url);
        assert_eq!(Some("Example".to_owned()), pv.title);
        assert_eq!(vec![1, 2, 3], pv.referrers);
    }

    const URL_TO_ENCODE: &str = "https://google.com?key='aaa~aaa'&val_ue=10&aa@a-3 3 3-_.!~*'()";

    #[rstest]
    #[case(URL_TO_ENCODE, None, vec![], None)]
    #[case(URL_TO_ENCODE, Some("Title Empty Referrers".to_owned()), vec![], None)]
    #[case(URL_TO_ENCODE, Some("Title With Referrers".to_owned()), vec![10, 11], Some("[10,11]"))]
    fn test_encode_query(
        #[case] url: &str,
        #[case] title: Option<String>,
        #[case] referrers: Vec<i32>,
        #[case] expected_referrers_param: Option<&str>,
    ) {
        // arrange
        #[rustfmt::skip]
        let mut expected = format!(
            "{}={}&{}={}",
            QueryParam::EventType.as_str(), EVENT_TYPE,
            QueryParam::Href.as_str(), UrlEncoder::encode_uri_component(url),
        );
        if let Some(title) = &title {
            expected.push_str(&format!("&{}={}", QueryParam::Title.as_str(), UrlEncoder::encode_uri_component(title)));
        }
        if let Some(expected_referrers_param) = expected_referrers_param {
            let param = UrlEncoder::encode_uri_component(expected_referrers_param);
            expected.push_str(&format!("&{}={}", QueryParam::ReferrersIndices.as_str(), param));
        }
        let pv = PageView::new(url.to_owned(), title, referrers);
        // act
        let actual = pv.encode_query();
        // assert
        let actual = remove_nonce_parameter(&actual);
        assert_eq!(expected, actual);
    }

    #[rstest]
    fn test_encode_query_with_opts() {
        let pv = PageView::new_with_opts(
            URL_TO_ENCODE.to_owned(),
            PageViewOpts::new().title("Title With Referrers").referrers(vec![10, 11]),
        );
        let expected = format!(
            "{}={}&{}={}&{}={}&{}={}",
            QueryParam::EventType.as_str(),
            EVENT_TYPE,
            QueryParam::Href.as_str(),
            UrlEncoder::encode_uri_component(URL_TO_ENCODE),
            QueryParam::Title.as_str(),
            UrlEncoder::encode_uri_component("Title With Referrers"),
            QueryParam::ReferrersIndices.as_str(),
            UrlEncoder::encode_uri_component("[10,11]"),
        );
        let actual = remove_nonce_parameter(&pv.encode_query());
        assert_eq!(expected, actual);
    }
}
