use std::{sync::Arc, time::SystemTime};

use crate::{
    data::keyed::Keyed,
    datafile::rule::RuleType,
    network::query_builder::{QueryBuilder, QueryParam},
    tracking::trackable::{Trackable, TrackableBase, TrackingStateCell},
};

const EVENT_TYPE: &str = "experiment";

#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq))]
pub(crate) struct AssignedVariation {
    pub(crate) experiment_id: u32,
    pub(crate) variation_id: u32,
    pub(crate) assignment_time: SystemTime,
    pub(crate) rule_type: RuleType,
    pub(crate) trackable_base: TrackableBase,
}

impl AssignedVariation {
    pub fn new(experiment_id: u32, variation_id: u32, rule_type: RuleType) -> Self {
        return AssignedVariation {
            experiment_id,
            variation_id,
            assignment_time: SystemTime::now(),
            rule_type,
            trackable_base: TrackableBase::default(),
        };
    }

    pub fn new_with_time(
        experiment_id: u32,
        variation_id: u32,
        assignment_time: SystemTime,
        rule_type: RuleType,
    ) -> Self {
        return AssignedVariation {
            trackable_base: TrackableBase::default(),
            experiment_id,
            variation_id,
            assignment_time,
            rule_type,
        };
    }
}

impl Keyed<u32> for AssignedVariation {
    fn key(&self) -> &u32 {
        return &self.experiment_id;
    }
}

impl Trackable for AssignedVariation {
    fn encode_query(&self) -> String {
        QueryBuilder::with_params(vec![
            (QueryParam::Nonce, self.trackable_base.nonce().into()),
            (QueryParam::EventType, EVENT_TYPE.into()),
            (QueryParam::ExperimentId, self.experiment_id.to_string().into()),
            (QueryParam::VariationId, self.variation_id.to_string().into()),
        ])
        .build()
    }

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

#[cfg(test)]
mod tests {
    use std::time::Duration;

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

    use super::*;

    #[test]
    fn test_event_type() {
        assert_eq!("experiment", EVENT_TYPE);
    }

    #[test]
    fn test_new() {
        // assert
        const EXP_ID: u32 = 98;
        const VAR_ID: u32 = 123;
        let rule_type = RuleType::Experimentation;
        let before = SystemTime::now();
        // act
        let av = AssignedVariation::new(EXP_ID, VAR_ID, rule_type);
        // assert
        let after = SystemTime::now();
        assert_eq!(EXP_ID, av.experiment_id);
        assert_eq!(VAR_ID, av.variation_id);
        assert_eq!(rule_type, av.rule_type);
        assert!(av.assignment_time >= before);
        assert!(av.assignment_time <= after);
    }

    #[test]
    fn test_new_with_time() {
        // assert
        const EXP_ID: u32 = 980;
        const VAR_ID: u32 = 1230;
        let time = SystemTime::UNIX_EPOCH + Duration::from_millis(1234567890);
        let rule_type = RuleType::TargetedDelivery;
        // act
        let av = AssignedVariation::new_with_time(EXP_ID, VAR_ID, time, rule_type);
        // assert
        assert_eq!(EXP_ID, av.experiment_id);
        assert_eq!(VAR_ID, av.variation_id);
        assert_eq!(time, av.assignment_time);
        assert_eq!(rule_type, av.rule_type);
    }

    #[test]
    fn test_key() {
        const EXP_ID: u32 = 567;
        let av = AssignedVariation::new(EXP_ID, 123, RuleType::Experimentation);
        assert_eq!(EXP_ID, *av.key());
    }

    #[test]
    fn test_encode_query() {
        // arrange
        const EXPERIMENT_ID: u32 = 123;
        const VARIATION_ID: u32 = 98;
        let av = AssignedVariation::new(EXPERIMENT_ID, VARIATION_ID, RuleType::Unknown);
        #[rustfmt::skip]
        let expected = format!(
            "{}={}&{}={}&{}={}",
            QueryParam::EventType.as_str(), EVENT_TYPE,
            QueryParam::ExperimentId.as_str(), EXPERIMENT_ID,
            QueryParam::VariationId.as_str(), VARIATION_ID,
        );
        // act
        let actual = av.encode_query();
        // assert
        let actual = remove_nonce_parameter(&actual);
        assert_eq!(expected, actual);
    }
}
