use std::{
    collections::{HashMap, HashSet},
    fmt::Debug,
    sync::Arc,
};

use kameleoon_trace_logging::trace_method;

#[cfg(test)]
use crate::datafile::{
    experiment::ExperimentImpl, feature_flag::FeatureFlagImpl, me_group::MeGroupImpl, rule::RuleImpl,
    segment::SegmentImpl, variable::VariableImpl, variation::VariationImpl,
    variation_exposition::VariationByExpositionImpl,
};
use crate::{
    datafile::{
        custom_data_info::{CustomDataInfo, CustomDataInfoImpl},
        experiment::Experiment,
        feature_flag::FeatureFlag,
        me_group::MeGroup,
        rule::{Rule, RuleType},
        segment::Segment,
        settings::{REQUIRED_CONSENT_TYPE, Settings, SettingsImpl},
        variable::{Variable, VariableType},
        variation::Variation,
        variation_exposition::VariationByExposition,
    },
    error::{ErrorCode, KErr},
    logging::LogLevel,
    network::{
        dto::{ConfigurationDto, DataFileDto, ExperimentDto, FeatureFlagDto},
        url_provider::ServiceDomain,
    },
    targeting::models::SegmentModel,
    utils::KRes,
};

pub(crate) trait DataFile: Send + Sync + Debug {
    type FeatureFlag: FeatureFlag;
    type Experiment: Experiment;
    type Segment: Segment;
    type MeGroup: MeGroup;
    type Settings: Settings;
    type CustomDataInfo: CustomDataInfo;

    fn new(datafile_dto: DataFileDto, environment: Option<String>, last_modified: Option<String>) -> Self;
    fn new_nullum(environment: Option<String>) -> Self;

    fn last_modified(&self) -> Option<&str>;
    fn date_modified(&self) -> u64;
    fn settings(&self) -> &Self::Settings;
    fn custom_data_info(&self) -> &Self::CustomDataInfo;
    fn feature_flags(&self) -> &HashMap<String, Arc<Self::FeatureFlag>>;
    fn feature_flags_by_id(&self) -> &HashMap<u32, Arc<Self::FeatureFlag>>;
    fn ordered_feature_flags(&self) -> &[Arc<Self::FeatureFlag>];
    fn vars_by_id(&self) -> &HashMap<u32, Arc<dyn VariationByExposition>>;
    fn segments(&self) -> &HashMap<u32, Arc<Self::Segment>>;
    // fn rules(&self) -> &HashMap<u32, Arc<impl Rule>>;
    fn me_groups(&self) -> &HashMap<String, Self::MeGroup>;
    fn holdout(&self) -> Option<&Self::Experiment>;
    fn has_targeted_delivery_rule(&self) -> bool;

    fn get_feature_flag(&self, feature_key: &str) -> KRes<&Arc<Self::FeatureFlag>>;
    fn has_experiment_js_css_variable(&self, experiment_id: u32) -> bool;
    fn get_info_by_exp_id(&self, experiment_id: u32) -> Option<&InfoByExpId>;

    #[cfg(test)]
    fn is_nullum(&self) -> bool;
    #[cfg(test)]
    fn audience_tracking_segments(&self) -> &[Arc<Self::Segment>];
}

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct DataFileImpl<F: FeatureFlag, E: Experiment, S: Segment, M: MeGroup> {
    nullum: bool,
    environment: Option<String>,
    last_modified: Option<String>,
    date_modified: u64,
    settings: SettingsImpl,
    feature_flags: HashMap<String, Arc<F>>,
    feature_flags_by_id: HashMap<u32, Arc<F>>,
    ordered_feature_flags: Vec<Arc<F>>,
    vars_by_id: HashMap<u32, Arc<dyn VariationByExposition>>,
    custom_data_info: CustomDataInfoImpl,
    segments: HashMap<u32, Arc<S>>,
    has_targeted_delivery_rule: bool,
    holdout: Option<E>,
    experiment_with_js_css_variable: HashSet<u32>,
    audience_tracking_segments: Vec<Arc<S>>,
    me_groups: HashMap<String, M>,
    info_by_exp_id: HashMap<u32, InfoByExpId>,
}

#[cfg(test)]
pub(crate) type MockDataFileImpl = DataFileImpl<
    FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>,
    ExperimentImpl<VariationByExpositionImpl>,
    SegmentImpl,
    MeGroupImpl<FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>>,
>;

impl<F, R, E, S, M> DataFile for DataFileImpl<F, E, S, M>
where
    F: FeatureFlag<Rule = R> + 'static,
    R: Rule<Experiment = E>,
    E: Experiment + From<ExperimentDto>,
    S: Segment + From<SegmentModel>,
    M: MeGroup<FeatureFlag = F>,
{
    type FeatureFlag = F;
    type Experiment = E;
    type Segment = S;
    type MeGroup = M;
    type Settings = SettingsImpl;
    type CustomDataInfo = CustomDataInfoImpl;

    #[trace_method(LogLevel::Debug)]
    fn new(datafile_dto: DataFileDto, environment: Option<String>, last_modified: Option<String>) -> Self {
        let date_modified = datafile_dto.date_modified;
        let cdi = CustomDataInfoImpl::new(datafile_dto.custom_data);
        let ff_info = FeatureFlagInfo::process(datafile_dto.feature_flags, &cdi);
        let holdout = datafile_dto.holdout.map(E::from);
        let (segments, audience_tracking_segments) = Self::process_segments(datafile_dto.segments);
        Self {
            nullum: false,
            environment,
            last_modified,
            date_modified,
            settings: SettingsImpl::new(datafile_dto.configuration),
            custom_data_info: cdi,
            feature_flags: ff_info.feature_flags,
            feature_flags_by_id: ff_info.feature_flags_by_id,
            ordered_feature_flags: ff_info.ordered_feature_flags,
            vars_by_id: ff_info.vars_by_id,
            has_targeted_delivery_rule: ff_info.has_targeted_delivery_rule,
            holdout,
            experiment_with_js_css_variable: ff_info.experiments_with_js_css_variable,
            segments,
            audience_tracking_segments,
            me_groups: ff_info.me_groups,
            info_by_exp_id: ff_info.info_by_exp_id,
        }
    }

    #[trace_method(LogLevel::Debug)]
    fn new_nullum(environment: Option<String>) -> Self {
        let dto = DataFileDto {
            configuration: ConfigurationDto {
                real_time_update: false,
                consent_type: REQUIRED_CONSENT_TYPE.to_owned(),
                data_api_domain: ServiceDomain::DataApi.default_domain().to_owned(),
                streaming_api_domain: ServiceDomain::Streaming.default_domain().to_owned(),
            },
            feature_flags: vec![],
            custom_data: vec![],
            segments: vec![],
            date_modified: 0,
            holdout: None,
        };
        let mut datafile = Self::new(dto, environment, None);
        datafile.nullum = true;
        return datafile;
    }

    fn last_modified(&self) -> Option<&str> {
        self.last_modified.as_deref()
    }

    fn date_modified(&self) -> u64 {
        self.date_modified
    }

    fn settings(&self) -> &Self::Settings {
        &self.settings
    }

    fn custom_data_info(&self) -> &Self::CustomDataInfo {
        &self.custom_data_info
    }

    fn feature_flags(&self) -> &HashMap<String, Arc<F>> {
        &self.feature_flags
    }

    fn feature_flags_by_id(&self) -> &HashMap<u32, Arc<Self::FeatureFlag>> {
        return &self.feature_flags_by_id;
    }

    fn ordered_feature_flags(&self) -> &[Arc<Self::FeatureFlag>] {
        &self.ordered_feature_flags
    }

    fn vars_by_id(&self) -> &HashMap<u32, Arc<dyn VariationByExposition>> {
        return &self.vars_by_id;
    }

    fn segments(&self) -> &HashMap<u32, Arc<Self::Segment>> {
        &self.segments
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    fn get_feature_flag(&self, feature_key: &str) -> KRes<&Arc<F>> {
        match self.feature_flags.get(feature_key) {
            Some(feature_flag) => {
                if feature_flag.is_environment_enabled() {
                    Ok(feature_flag)
                } else {
                    let environment = self.environment.as_deref().unwrap_or("production");
                    Err(KErr::new(
                        ErrorCode::FeatureEnvironmentDisabled,
                        format!("Feature '{feature_key}' disabled for '{environment}' environment"),
                    ))
                }
            }
            None => Err(KErr::new(ErrorCode::FeatureNotFound, format!("Feature '{feature_key}' not found"))),
        }
    }

    fn has_targeted_delivery_rule(&self) -> bool {
        self.has_targeted_delivery_rule
    }

    fn has_experiment_js_css_variable(&self, experiment_id: u32) -> bool {
        self.experiment_with_js_css_variable.contains(&experiment_id)
    }

    fn holdout(&self) -> Option<&E> {
        self.holdout.as_ref()
    }

    fn me_groups(&self) -> &HashMap<String, M> {
        &self.me_groups
    }

    #[trace_method(LogLevel::Debug, skip(self))]
    fn get_info_by_exp_id(&self, experiment_id: u32) -> Option<&InfoByExpId> {
        self.info_by_exp_id.get(&experiment_id)
    }

    #[cfg(test)]
    fn is_nullum(&self) -> bool {
        return self.nullum;
    }

    #[cfg(test)]
    fn audience_tracking_segments(&self) -> &[Arc<S>] {
        &self.audience_tracking_segments
    }
}

impl<F, R, E, S, M> DataFileImpl<F, E, S, M>
where
    F: FeatureFlag<Rule = R>,
    R: Rule<Experiment = E>,
    E: Experiment + From<ExperimentDto>,
    S: Segment + From<SegmentModel>,
    M: MeGroup<FeatureFlag = F>,
{
    #[inline]
    fn process_segments(segments: Vec<SegmentModel>) -> (HashMap<u32, Arc<S>>, Vec<Arc<S>>) {
        let segments = segments.into_iter().map(S::from).map(|s| (s.id(), Arc::new(s))).collect::<HashMap<_, _>>();
        let audience_tracking_segments = segments
            .values()
            .into_iter()
            .filter_map(|s| if s.audience_tracking() { Some(s.clone()) } else { None })
            .collect();
        (segments, audience_tracking_segments)
    }
}

struct FeatureFlagInfo<F, M> {
    feature_flags: HashMap<String, Arc<F>>,
    feature_flags_by_id: HashMap<u32, Arc<F>>,
    ordered_feature_flags: Vec<Arc<F>>,
    vars_by_id: HashMap<u32, Arc<dyn VariationByExposition>>,
    has_targeted_delivery_rule: bool,
    experiments_with_js_css_variable: HashSet<u32>,
    me_groups: HashMap<String, M>,
    info_by_exp_id: HashMap<u32, InfoByExpId>,
}

impl<F, M> FeatureFlagInfo<F, M> {
    #[inline]
    fn process<'a, C>(ffs_dto: Vec<FeatureFlagDto>, cdi: &'a C) -> FeatureFlagInfo<F, M>
    where
        C: CustomDataInfo + 'static,
        F: FeatureFlag + 'static,
        M: MeGroup<FeatureFlag = F>,
    {
        let fold_state = FeatureFlagInfo {
            feature_flags: HashMap::new(),
            feature_flags_by_id: HashMap::new(),
            ordered_feature_flags: Vec::new(),
            vars_by_id: HashMap::new(),
            has_targeted_delivery_rule: false,
            experiments_with_js_css_variable: HashSet::new(),
            me_groups: HashMap::new(),
            info_by_exp_id: HashMap::new(),
        };

        let fold_state = ffs_dto.into_iter().fold(fold_state, |mut acc, dto| {
            let ff = F::new(dto, cdi);
            // Check for targeted delivery rule
            if !acc.has_targeted_delivery_rule {
                acc.has_targeted_delivery_rule = ff.rules().iter().any(|r| r.rule_type() == RuleType::TargetedDelivery);
            }

            // Check for JS/CSS variables and collect experiment IDs
            let has_js_css_variable = ff.variations().values().any(|v| {
                v.variables()
                    .values()
                    .any(|var| *var.var_type() == VariableType::Js || *var.var_type() == VariableType::Css)
            });

            if has_js_css_variable {
                ff.rules().iter().for_each(|r| {
                    acc.experiments_with_js_css_variable.insert(r.experiment().id());
                });
            }

            // Collect info by experiment ID
            ff.rules().iter().for_each(|rule| {
                let experiment_id = rule.experiment().id();

                // Get variations from the rule's experiment
                let variations: HashMap<u32, String> = rule
                    .experiment()
                    .vars_by_exp()
                    .iter()
                    .map(|v| (v.variation_id(), v.variation_key().to_string()))
                    .collect::<HashMap<_, _>>();

                let info = InfoByExpId::new(ff.id(), rule.id(), variations);
                acc.info_by_exp_id.insert(experiment_id, info);

                for var_by_exp in rule.experiment().vars_by_exp().iter() {
                    acc.vars_by_id.insert(var_by_exp.variation_id(), var_by_exp.clone());
                }
            });

            let ff = Arc::new(ff);

            // Collect ME group data
            if let Some(me_group_name) = ff.me_group_name() {
                acc.me_groups.entry(me_group_name.to_owned()).or_insert_with(|| Vec::with_capacity(2)).push(ff.clone());
            }

            // Store the feature flag
            acc.ordered_feature_flags.push(ff.clone());
            acc.feature_flags_by_id.insert(ff.id(), ff.clone());
            acc.feature_flags.insert(ff.feature_key().to_string(), ff);

            acc
        });

        let me_groups = fold_state.me_groups.into_iter().map(|(name, flags)| (name, M::new(flags))).collect();

        Self {
            feature_flags: fold_state.feature_flags,
            feature_flags_by_id: fold_state.feature_flags_by_id,
            ordered_feature_flags: fold_state.ordered_feature_flags,
            vars_by_id: fold_state.vars_by_id,
            has_targeted_delivery_rule: fold_state.has_targeted_delivery_rule,
            experiments_with_js_css_variable: fold_state.experiments_with_js_css_variable,
            me_groups,
            info_by_exp_id: fold_state.info_by_exp_id,
        }
    }
}

#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct InfoByExpId {
    feature_flag_id: u32,
    rule_id: u32,
    variations: HashMap<u32, String>,
}

impl InfoByExpId {
    fn new(feature_flag_id: u32, rule_id: u32, variations: HashMap<u32, String>) -> Self {
        Self {
            feature_flag_id,
            rule_id,
            variations,
        }
    }

    pub fn feature_flag_id(&self) -> u32 {
        self.feature_flag_id
    }

    pub fn rule_id(&self) -> u32 {
        self.rule_id
    }

    #[cfg(test)]
    pub fn variations(&self) -> &HashMap<u32, String> {
        &self.variations
    }
}

#[cfg(test)]
mod tests {

    use super::*;
    use crate::{
        datafile::{
            datafile_test_utils::{
                make_configuration_dto, make_experiment_dto, make_feature_flag_dto, make_rule_dto, make_segment_dto,
                make_variable_dto, make_variation_dto, make_variation_exposition_dto,
            },
            experiment::ExperimentImpl,
            feature_flag::FeatureFlagImpl,
            me_group::MeGroupImpl,
            rule::RuleImpl,
            segment::SegmentImpl,
            variable::VariableImpl,
            variation::VariationImpl,
            variation_exposition::VariationByExpositionImpl,
        },
        error::ErrorCode,
        network::dto::*,
        utils::test_utils::load_file,
    };
    use rstest::rstest;
    use serde_json::json;

    type TestDataFile = DataFileImpl<
        FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>,
        ExperimentImpl<VariationByExpositionImpl>,
        SegmentImpl,
        MeGroupImpl<FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>>,
    >;

    fn make_test_feature_flag_dto(
        id: u32,
        feature_key: &str,
        js_css_var: Option<&str>,
        rule_type: &str,
        me_group: Option<&str>,
    ) -> FeatureFlagDto {
        let var_type = js_css_var.unwrap_or("STRING");

        make_feature_flag_dto(
            id,
            feature_key,
            true,
            "off",
            me_group,
            None,
            vec![make_rule_dto(
                id + 100,
                rule_type,
                1.0,
                make_experiment_dto(
                    id + 1000,
                    vec![
                        make_variation_exposition_dto("variation_a", 1, 0.5),
                        make_variation_exposition_dto("variation_b", 2, 0.5),
                    ],
                ),
                None,
                Some(1),
            )],
            vec![
                make_variation_dto("variation_a", vec![make_variable_dto("var1", var_type, json!("test_value"))]),
                make_variation_dto("variation_b", vec![make_variable_dto("var2", "STRING", json!("another_value"))]),
            ],
        )
    }

    fn make_test_datafile_dto_specific(ff2_rule_type_td: bool, js_css_variable: Option<&str>) -> DataFileDto {
        let ff2_rule_type = if ff2_rule_type_td { "TARGETED_DELIVERY" } else { "EXPERIMENTATION" };
        DataFileDto {
            configuration: make_configuration_dto(false, false, "api.kameleoon.com", "events.kameleoon.com"),
            feature_flags: vec![
                make_test_feature_flag_dto(1, "feature_1", None, "EXPERIMENTATION", None),
                make_test_feature_flag_dto(2, "feature_2", js_css_variable, ff2_rule_type, Some("group_1")),
                make_test_feature_flag_dto(3, "feature_3", None, "EXPERIMENTATION", Some("group_1")),
            ],
            custom_data: vec![],
            segments: vec![make_segment_dto(1, false), make_segment_dto(2, true), make_segment_dto(3, true)],
            date_modified: 1234567890,
            holdout: Some(make_experiment_dto(9999, vec![])),
        }
    }

    fn make_test_datafile_dto() -> DataFileDto {
        make_test_datafile_dto_specific(false, None)
    }

    #[test]
    fn test_datafile_creation() {
        let dto = make_test_datafile_dto();
        let environment = Some("test".to_string());
        let last_modified = Some("2023-01-01T00:00:00Z".to_string());

        let datafile = TestDataFile::new(dto, environment.clone(), last_modified.clone());

        assert_eq!(datafile.last_modified(), Some("2023-01-01T00:00:00Z"));
        assert_eq!(datafile.date_modified(), 1234567890);
        assert_eq!(datafile.feature_flags().len(), 3);
        assert_eq!(datafile.segments().len(), 3);
        assert_eq!(datafile.audience_tracking_segments().len(), 2);
        assert_eq!(datafile.me_groups().len(), 1);
        assert!(datafile.holdout().is_some());
    }

    #[test]
    fn test_empty_feature_flags() {
        let mut dto = make_test_datafile_dto();
        dto.feature_flags = vec![];
        let datafile = TestDataFile::new(dto, None, None);

        assert_eq!(datafile.feature_flags().len(), 0);
        assert!(!datafile.has_targeted_delivery_rule());
        assert_eq!(datafile.me_groups().len(), 0);
    }

    #[test]
    fn test_get_feature_flag_success() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let result = datafile.get_feature_flag("feature_1");
        assert!(result.is_ok());

        let feature_flag = result.unwrap();
        assert_eq!(feature_flag.feature_key(), "feature_1");
        assert_eq!(feature_flag.id(), 1);
    }

    #[test]
    fn test_get_feature_flag_not_found() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, Some("test".to_string()), None);

        let result = datafile.get_feature_flag("non_existent_feature");
        assert!(result.is_err());

        let err = result.unwrap_err();
        assert_eq!(err.code(), ErrorCode::FeatureNotFound);
    }

    #[rstest]
    fn test_get_feature_flag_environment_disabled(#[values(false, true)] environment_enabled: bool) {
        // arrange
        const FF_KEY: &str = "ff1";
        let dto = DataFileDto {
            configuration: ConfigurationDto::default(),
            feature_flags: vec![FeatureFlagDto {
                id: 1,
                feature_key: FF_KEY.to_owned(),
                environment_enabled,
                variations: vec![],
                default_variation_key: "off".to_owned(),
                rules: vec![],
                mutually_exclusive_group: None,
                bucketing_custom_data_id: None,
            }],
            custom_data: vec![],
            segments: vec![],
            date_modified: 0,
            holdout: None,
        };
        let datafile = TestDataFile::new(dto, None, None);
        // act
        let result = datafile.get_feature_flag(FF_KEY);
        // assert
        if environment_enabled {
            assert_eq!(FF_KEY, result.unwrap().feature_key());
        } else {
            assert_eq!(result.unwrap_err().code(), ErrorCode::FeatureEnvironmentDisabled);
        }
    }

    #[rstest(case(false, false), case(true, true))]
    fn test_has_targeted_delivery_rule(#[case] td_rule: bool, #[case] expected: bool) {
        let dto = make_test_datafile_dto_specific(td_rule, None);
        let datafile = TestDataFile::new(dto, None, None);

        assert_eq!(datafile.has_targeted_delivery_rule(), expected);
    }

    #[rstest(case("JS"), case("CSS"))]
    fn test_has_experiment_js_css_variable(#[case] js_css_var: &str) {
        let dto = make_test_datafile_dto_specific(false, Some(js_css_var));
        let datafile = TestDataFile::new(dto, None, None);

        // feature_2 (id=2) has JS variables, so experiment id 1002 should have JS/CSS vars
        assert!(datafile.has_experiment_js_css_variable(1002));

        // feature_1 (id=1) doesn't have JS variables, so experiment id 1001 should not
        assert!(!datafile.has_experiment_js_css_variable(1001));

        // Non-existent experiment
        assert!(!datafile.has_experiment_js_css_variable(9999));
    }

    #[test]
    fn test_audience_tracking_segments() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let audience_segments = datafile.audience_tracking_segments();
        assert_eq!(audience_segments.len(), 2);

        // Check that only segments with audience_tracking=true are included
        let segment_ids: Vec<u32> = audience_segments.iter().map(|s| s.id()).collect();
        assert!(segment_ids.contains(&2));
        assert!(segment_ids.contains(&3));
        assert!(!segment_ids.contains(&1));
    }

    #[test]
    fn test_me_groups() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let me_groups = datafile.me_groups();
        assert_eq!(me_groups.len(), 1);
        assert!(me_groups.contains_key("group_1"));

        let group = me_groups.get("group_1").unwrap();
        // group_1 should contain feature_2 and feature_3
        assert_eq!(group.feature_flags().len(), 2);
        assert_eq!(group.feature_flags()[0].feature_key(), "feature_2");
        assert_eq!(group.feature_flags()[1].feature_key(), "feature_3");
    }

    #[test]
    fn test_get_info_by_exp_id() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        // Test existing experiment
        let info = datafile.get_info_by_exp_id(1001);
        assert!(info.is_some());

        let info = info.unwrap();
        assert_eq!(info.feature_flag_id(), 1);
        assert_eq!(info.rule_id(), 101);
        assert_eq!(info.variations().len(), 2);
        assert!(info.variations().contains_key(&1));
        assert!(info.variations().contains_key(&2));
        assert_eq!(info.variations().get(&1), Some(&"variation_a".to_string()));
        assert_eq!(info.variations().get(&2), Some(&"variation_b".to_string()));

        // Test non-existent experiment
        let info = datafile.get_info_by_exp_id(99999);
        assert!(info.is_none());
    }

    #[test]
    fn test_holdout() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let holdout = datafile.holdout();
        assert!(holdout.is_some());
        assert_eq!(holdout.unwrap().id(), 9999);
    }

    #[test]
    fn test_no_holdout() {
        let mut dto = make_test_datafile_dto();
        dto.holdout = None;
        let datafile = TestDataFile::new(dto, None, None);

        assert!(datafile.holdout().is_none());
    }

    #[test]
    fn test_segments_processing() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let segments = datafile.segments();
        assert_eq!(segments.len(), 3);
        assert!(segments.contains_key(&1));
        assert!(segments.contains_key(&2));
        assert!(segments.contains_key(&3));
    }

    #[test]
    fn test_feature_flags() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let feature_flags = datafile.feature_flags();
        assert_eq!(feature_flags.len(), 3);
        assert!(feature_flags.contains_key("feature_1"));
        assert!(feature_flags.contains_key("feature_2"));
        assert!(feature_flags.contains_key("feature_3"));
    }

    #[test]
    fn test_ordered_feature_flags() {
        let dto = make_test_datafile_dto();
        let datafile = TestDataFile::new(dto, None, None);

        let ordered_feature_flags = datafile.ordered_feature_flags();
        assert_eq!(ordered_feature_flags.len(), 3);
        assert!(ordered_feature_flags[0].feature_key() == "feature_1");
        assert!(ordered_feature_flags[1].feature_key() == "feature_2");
        assert!(ordered_feature_flags[2].feature_key() == "feature_3");
    }

    #[test]
    fn test_parse_datafile_from_json() {
        // Arrange
        let json = load_file("resources/datafiles/datafile_tndueuutdq.json");
        let dto: DataFileDto = serde_json::from_str(&json).expect("Failed to parse DataFileDto");

        // Act
        let datafile = TestDataFile::new(dto, None, None);

        // Assert
        assert_eq!(datafile.settings().is_streaming_mode(), false);
        assert_eq!(datafile.feature_flags().len(), 42);
        assert_eq!(datafile.date_modified(), 1755774975000);
        assert_eq!(datafile.feature_flags_by_id().len(), 42);
        assert_eq!(datafile.ordered_feature_flags()[0].feature_key(), "complextargeting");
        assert_eq!(datafile.custom_data_info().is_local_only(2), true);
        assert_eq!(datafile.segments().get(&222465).unwrap().audience_tracking(), true);
        assert!(datafile.feature_flags().values().all(|ff| {
            let ff_by_id = datafile.feature_flags_by_id().get(&ff.id());
            ff_by_id.map_or(false, |ff_by_id| Arc::ptr_eq(ff_by_id, ff))
        }));
    }

    #[rstest]
    fn test_new_nullum(#[values(None, Some("env1"))] env: Option<&str>) {
        let datafile = TestDataFile::new_nullum(env.map(|env| env.to_owned()));
        assert!(datafile.is_nullum());
        assert_eq!(env, datafile.environment.as_deref());
        assert!(datafile.last_modified().is_none());
        assert_eq!(datafile.date_modified(), 0);
        assert!(datafile.settings().consent_required());
        assert!(!datafile.settings().is_streaming_mode());
        assert_eq!(ServiceDomain::DataApi.default_domain(), datafile.settings().data_api_domain());
        assert_eq!(ServiceDomain::Streaming.default_domain(), datafile.settings().streaming_domain());
        assert!(datafile.feature_flags().is_empty());
        assert!(datafile.feature_flags_by_id().is_empty());
        assert!(datafile.ordered_feature_flags().is_empty());
        assert!(datafile.segments().is_empty());
        assert!(!datafile.has_targeted_delivery_rule());
        assert!(datafile.holdout().is_none());
        assert!(datafile.experiment_with_js_css_variable.is_empty());
        assert!(datafile.audience_tracking_segments().is_empty());
        assert!(datafile.me_groups().is_empty());
        assert!(datafile.info_by_exp_id.is_empty());
    }
}
