use std::fmt::Debug;

use crate::{
    datafile::{
        experiment::{Experiment, ExperimentImpl},
        variation_exposition::VariationByExpositionImpl,
    },
    network::dto::RuleDto,
};

#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum RuleType {
    Unknown,
    Experimentation,
    TargetedDelivery,
}

impl From<&String> for RuleType {
    fn from(s: &String) -> Self {
        match s.to_ascii_uppercase().as_str() {
            "EXPERIMENTATION" => RuleType::Experimentation,
            "TARGETED_DELIVERY" => RuleType::TargetedDelivery,
            _ => RuleType::Unknown,
        }
    }
}

pub(crate) trait Rule: Send + Sync + Debug {
    type Experiment: Experiment;

    fn new(rule_dto: RuleDto) -> Self;
    fn id(&self) -> u32;
    fn exposition(&self) -> f64;
    fn experiment(&self) -> &Self::Experiment;
    fn respool_time(&self) -> Option<u64>;
    fn segment_id(&self) -> Option<u32>;
    fn rule_type(&self) -> RuleType;
}

#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct RuleImpl {
    id: u32,
    rule_type: RuleType,
    exposition: f64,
    experiment: ExperimentImpl<VariationByExpositionImpl>,
    respool_time: Option<u64>,
    segment_id: Option<u32>,
}

impl Rule for RuleImpl {
    type Experiment = ExperimentImpl<VariationByExpositionImpl>;

    fn new(rule_dto: RuleDto) -> Self {
        let rule_type = RuleType::from(&rule_dto.rule_type);
        let experiment = ExperimentImpl::from(rule_dto.experiment);
        Self {
            id: rule_dto.id,
            rule_type,
            exposition: rule_dto.exposition,
            experiment,
            respool_time: rule_dto.respool_time,
            segment_id: rule_dto.segment_id,
        }
    }

    fn id(&self) -> u32 {
        self.id
    }

    fn exposition(&self) -> f64 {
        self.exposition
    }

    fn experiment(&self) -> &Self::Experiment {
        &self.experiment
    }

    fn respool_time(&self) -> Option<u64> {
        self.respool_time
    }

    fn segment_id(&self) -> Option<u32> {
        self.segment_id
    }

    fn rule_type(&self) -> RuleType {
        self.rule_type
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use crate::datafile::datafile_test_utils::make_experiment_dto;
    use crate::network::dto::VariationByExpositionDto;
    use crate::{
        datafile::datafile_test_utils::{make_rule_dto, make_variation_exposition_dto},
        utils::test_utils::downcast_ref,
    };
    use rstest::rstest;

    #[rstest]
    #[case(RuleType::Experimentation, "EXPERIMENTATION")]
    #[case(RuleType::TargetedDelivery, "TARGETED_DELIVERY")]
    #[case(RuleType::Unknown, "UNKNOWN")]
    #[case(RuleType::Unknown, "invalid")]
    fn test_rule_type_from_string(#[case] expected: RuleType, #[case] input: &str) {
        // Act
        let rule_type = RuleType::from(&input.to_owned());

        // Assert
        match (expected, rule_type) {
            (RuleType::Experimentation, RuleType::Experimentation) => {}
            (RuleType::TargetedDelivery, RuleType::TargetedDelivery) => {}
            (RuleType::Unknown, RuleType::Unknown) => {}
            _ => panic!("Rule types don't match"),
        }
    }

    #[rstest(
        case("EXPERIMENTATION", vec![]),
        case("EXPERIMENTATION", vec![make_variation_exposition_dto("A", 1, 0.5)]),
        case("EXPERIMENTATION", vec![
            make_variation_exposition_dto("A", 1, 0.5),
            make_variation_exposition_dto("B", 2, 0.5)
        ]),
        case("TARGETED_DELIVERY", vec![]),
        case("UNKNOWN", vec![]),
    )]
    fn test_rule_impl_creation(#[case] rule_type: &str, #[case] variations: Vec<VariationByExpositionDto>) {
        // Arrange
        let id: u32 = getrandom::u32().unwrap();
        // let order: u32 = rng.random();
        let exposition = getrandom::u32().unwrap() as f64;
        let experiment_id = getrandom::u32().unwrap();
        let respool_time = getrandom::u64().unwrap();
        let segment_id = getrandom::u32().unwrap();
        let rule_dto = make_rule_dto(
            id,
            // order,
            rule_type,
            exposition,
            make_experiment_dto(experiment_id, variations.clone()),
            Some(respool_time),
            Some(segment_id),
        );

        // Act
        let rule = Box::leak(Box::new(RuleImpl::new(rule_dto)));

        // Assert
        assert_eq!(rule.id(), id);
        assert_eq!(rule.exposition(), exposition);
        assert_eq!(rule.experiment().id(), experiment_id);
        assert_eq!(rule.respool_time(), Some(respool_time));
        assert_eq!(rule.segment_id(), Some(segment_id));
        assert_eq!(
            &ExperimentImpl::<VariationByExpositionImpl>::from(make_experiment_dto(experiment_id, variations)),
            downcast_ref::<ExperimentImpl<VariationByExpositionImpl>>(rule.experiment())
        );
    }
}
