use std::{fmt::Debug, sync::Arc};

use crate::{
    datafile::variation_exposition::VariationByExposition,
    error::{ErrorCode, KErr},
    network::dto::{ExperimentDto, VariationByExpositionDto},
    utils::KRes,
};

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

    fn id(&self) -> u32;
    fn vars_by_exp(&self) -> &[Arc<Self::VariationByExposition>];
    fn get_variation_by_hash(&self, hash: f64) -> Option<&Arc<Self::VariationByExposition>>;
    fn get_variation_by_id(&self, variation_id: u32) -> Option<&Arc<Self::VariationByExposition>>;
    fn get_variation_by_key(&self, variation_key: &str) -> KRes<&Arc<Self::VariationByExposition>>;
}

#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct ExperimentImpl<V: VariationByExposition> {
    id: u32,
    vars_by_exp: Vec<Arc<V>>,
}

impl<V: VariationByExposition> Experiment for ExperimentImpl<V> {
    type VariationByExposition = V;

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

    fn vars_by_exp(&self) -> &[Arc<V>] {
        &self.vars_by_exp
    }

    fn get_variation_by_hash(&self, hash: f64) -> Option<&Arc<V>> {
        let mut total = 0.0;
        for var_exp in &self.vars_by_exp {
            total += var_exp.exposition();
            if total >= hash {
                return Some(var_exp);
            }
        }
        None
    }

    fn get_variation_by_id(&self, variation_id: u32) -> Option<&Arc<V>> {
        self.vars_by_exp.iter().find(|v| v.variation_id() == variation_id)
    }

    fn get_variation_by_key(&self, variation_key: &str) -> KRes<&Arc<V>> {
        for var_by_exp in &self.vars_by_exp {
            if var_by_exp.variation_key() == variation_key {
                return Ok(var_by_exp);
            }
        }
        Err(KErr::new(
            ErrorCode::FeatureVariationNotFound,
            format!("Experiment {} does not contain variation '{}'", self.id, variation_key),
        ))
    }
}

impl<V: VariationByExposition> From<ExperimentDto> for ExperimentImpl<V>
where
    V: From<VariationByExpositionDto>,
{
    fn from(dto: ExperimentDto) -> Self {
        Self {
            id: dto.id,
            vars_by_exp: dto.variation_by_exposition.into_iter().map(|dto| Arc::new(V::from(dto))).collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::datafile::{
        datafile_test_utils::{make_experiment_dto, make_variation_exposition_dto},
        variation_exposition::VariationByExpositionImpl,
    };
    use rstest::rstest;

    #[rstest]
    fn test_experiment_impl_creation() {
        // Arrange
        let experiment_dto = make_experiment_dto(
            456u32,
            vec![
                make_variation_exposition_dto("A", 1, 0.5),
                make_variation_exposition_dto("B", 2, 0.3),
                make_variation_exposition_dto("C", 3, 0.2),
            ],
        );

        // Act
        let experiment: ExperimentImpl<VariationByExpositionImpl> = ExperimentImpl::from(experiment_dto);

        // Assert
        assert_eq!(experiment.id(), 456);
        assert_eq!(experiment.vars_by_exp().len(), 3);
        assert_eq!(experiment.vars_by_exp()[0].variation_key(), "A");
        assert_eq!(experiment.vars_by_exp()[0].variation_id(), 1);
        assert_eq!(experiment.vars_by_exp()[0].exposition(), 0.5);
    }

    #[rstest]
    fn test_experiment_get_variation_by_id() {
        // Arrange
        let experiment_dto = make_experiment_dto(
            123,
            vec![make_variation_exposition_dto("A", 1, 0.5), make_variation_exposition_dto("B", 2, 0.5)],
        );
        let experiment: ExperimentImpl<VariationByExpositionImpl> = ExperimentImpl::from(experiment_dto);

        // Act
        let variation = experiment.get_variation_by_id(2);

        // Assert
        assert!(variation.is_some());
        assert_eq!(variation.unwrap().variation_key(), "B");
    }

    #[rstest]
    fn test_experiment_get_variation_by_hash() {
        // Arrange
        let experiment_dto = make_experiment_dto(
            123,
            vec![make_variation_exposition_dto("A", 1, 0.3), make_variation_exposition_dto("B", 2, 0.7)],
        );
        let experiment: ExperimentImpl<VariationByExpositionImpl> = ExperimentImpl::from(experiment_dto);

        // Act & Assert
        let variation_a = experiment.get_variation_by_hash(0.2);
        assert!(variation_a.is_some());
        assert_eq!(variation_a.unwrap().variation_key(), "A");

        let variation_b = experiment.get_variation_by_hash(0.8);
        assert!(variation_b.is_some());
        assert_eq!(variation_b.unwrap().variation_key(), "B");

        let variation_last = experiment.get_variation_by_hash(1.0);
        assert!(variation_last.is_some());
        assert_eq!(variation_last.unwrap().variation_key(), "B");
    }

    #[rstest]
    fn test_experiment_get_variation_by_key() {
        // Arrange
        let experiment_dto = make_experiment_dto(
            123,
            vec![
                make_variation_exposition_dto("VariationA", 1, 0.5),
                make_variation_exposition_dto("VariationB", 2, 0.5),
            ],
        );
        let experiment: ExperimentImpl<VariationByExpositionImpl> = ExperimentImpl::from(experiment_dto);

        // Act
        let variation = experiment.get_variation_by_key("VariationB");

        // Assert
        assert!(variation.is_ok());
        assert_eq!(variation.unwrap().variation_id(), 2);
    }

    #[rstest]
    fn test_experiment_get_variation_not_found() {
        // Arrange
        let experiment_dto = make_experiment_dto(123, vec![make_variation_exposition_dto("A", 1, 1.0)]);
        let experiment: ExperimentImpl<VariationByExpositionImpl> = ExperimentImpl::from(experiment_dto);

        // Act
        let result = experiment.get_variation_by_key("NonExistent");

        // Assert
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Experiment 123 does not contain variation 'NonExistent'"));
    }
}
