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

use kameleoon_trace_logging::trace_method;

use crate::datafile::datafile::DataFile;
use crate::datafile::datafile_manager::DataFileManager;
use crate::logging::{KameleoonLogger, LogLevel};
use crate::storage::visitor_manager::VisitorManager;

pub(crate) trait HybridManager: Send + Sync {
    fn get_engine_tracking_code(&self, visitor_code: &str) -> String;
}

pub(crate) struct HybridManagerImpl<V: VisitorManager, D: DataFileManager> {
    visitor_manager: Arc<V>,
    datafile_manager: Arc<D>,
    expiration_period: Duration,
}

impl<V: VisitorManager, D: DataFileManager> HybridManagerImpl<V, D> {
    const TC_INIT: &'static str = "window.kameleoonQueue=window.kameleoonQueue||[];";

    #[trace_method(LogLevel::Debug, skip(retval, visitor_manager, datafile_manager))]
    pub fn new(visitor_manager: Arc<V>, datafile_manager: Arc<D>, expiration_period: Duration) -> Self {
        if expiration_period == Duration::ZERO {
            KameleoonLogger::error("'expiration_period' must be positive!");
        }
        let manager = Self {
            visitor_manager,
            datafile_manager,
            expiration_period,
        };
        manager
    }
}

impl<V: VisitorManager, T: DataFileManager> HybridManager for HybridManagerImpl<V, T> {
    #[trace_method(LogLevel::Debug, skip(self))]
    fn get_engine_tracking_code(&self, visitor_code: &str) -> String {
        let mut tracking_code = String::from(Self::TC_INIT);
        let expired_time = SystemTime::now() - self.expiration_period;

        let Some(visitor) = self.visitor_manager.get(visitor_code) else {
            return tracking_code;
        };

        let visitor_data = visitor.rdata();
        let assigned_variations_iter = visitor_data.assigned_variations.r().iter();

        let datafile = &*self.datafile_manager.datafile();
        for variation in assigned_variations_iter {
            if variation.assignment_time > expired_time {
                let experiment_id = variation.experiment_id;

                // Get the data file and check for JS/CSS variables
                let tracking_only = !datafile.has_experiment_js_css_variable(experiment_id);

                let code = format!(
                    "window.kameleoonQueue.push(['Experiments.assignVariation',{},{},true]);\
                    window.kameleoonQueue.push(['Experiments.trigger',{},{}]);",
                    experiment_id, variation.variation_id, experiment_id, tracking_only
                );
                tracking_code.push_str(&code);
            }
        }
        tracking_code
    }
}

#[cfg(test)]
mod tests {
    use crate::data::assigned_variation::AssignedVariation;
    use crate::data::BaseData;
    use crate::datafile::datafile_test_utils::{MockDataFile, MockDataFileManager};
    use crate::datafile::rule::RuleType;
    use crate::storage::storage_test_utils::MockVisitorManager;
    use crate::storage::visitor::Visitor;
    use crate::utils::test_utils::assert_contains_and_remove;

    use super::*;
    use arc_swap::ArcSwap;
    use mockall::predicate::*;
    use rstest::rstest;
    use std::sync::Arc;

    const VISITOR_CODE: &str = "vc";

    // Helper function to create a mock visitor with variations
    fn create_mock_visitor_with_variations(variations: Vec<AssignedVariation>) -> Visitor {
        let visitor = Visitor::new();
        visitor.add_data(variations.into_iter().map(|v| BaseData::AssignedVariation(v)), true);
        visitor
    }

    // Helper function to create an AssignedVariation
    fn create_assigned_variation(
        experiment_id: u32,
        variation_id: u32,
        assignment_time: SystemTime,
    ) -> AssignedVariation {
        AssignedVariation::new_with_time(experiment_id, variation_id, assignment_time, RuleType::Unknown)
    }

    // Helper function to create HybridManager
    // fn create_hybrid_manager_with_test_data(
    //     assignment_time_offset_123: Duration,
    //     assignment_time_offset_789: Duration,
    //     expiration_period: Duration,
    // ) -> impl HybridManager {
    //     let variations = vec![
    //         create_assigned_variation(123, 456, SystemTime::now() - assignment_time_offset_123),
    //         create_assigned_variation(789, 101, SystemTime::now() - assignment_time_offset_789),
    //     ];
    //     let visitor = create_mock_visitor_with_variations(variations);
    //     let visitor_manager = MockVisitorManager::new_with_visitor(VISITOR_CODE, visitor);

    //     let mut mock_datafile = MockDataFile::default();
    //     let number_times_123 = if assignment_time_offset_123 < expiration_period { 1 } else { 0 };
    //     let number_times_789 = if assignment_time_offset_789 < expiration_period { 1 } else { 0 };
    //     mock_datafile
    //         .expect_has_experiment_js_css_variable()
    //         .with(eq(123))
    //         .times(number_times_123)
    //         .returning(|_| false); // tracking only
    //     mock_datafile.expect_has_experiment_js_css_variable().with(eq(789)).times(number_times_789).returning(|_| true); // has JS/CSS
    //     let datafile = ArcSwap::new(Arc::new(mock_datafile));

    //     let mut datafile_manager = MockDataFileManager::new();
    //     datafile_manager.expect_datafile().times(1).returning(move || datafile.load());

    //     HybridManagerImpl::new(Arc::new(visitor_manager), Arc::new(datafile_manager), expiration_period)
    // }

    #[rstest]
    #[case(Duration::from_millis(3600 * 1000))] // 1 hour in milliseconds
    #[case(Duration::ZERO)] // zero expiration
    fn test_hybrid_manager_new(#[case] expiration_period: Duration) {
        // Arrange
        let visitor_manager = Arc::new(MockVisitorManager::new());
        let datafile_manager = Arc::new(MockDataFileManager::new());

        // Act
        let hybrid_manager = HybridManagerImpl::new(visitor_manager, datafile_manager, expiration_period);

        // Assert
        assert_eq!(hybrid_manager.expiration_period, expiration_period);
    }

    #[test]
    fn test_get_engine_tracking_code_visitor_not_found() {
        // Arrange
        let visitor_manager = MockVisitorManager::new();
        let datafile_manager = MockDataFileManager::new();
        let expiration_period = Duration::from_millis(3600 * 1000);
        let hybrid_manager =
            HybridManagerImpl::new(Arc::new(visitor_manager), Arc::new(datafile_manager), expiration_period);

        // Act
        let result = hybrid_manager.get_engine_tracking_code("nonexistent_visitor");

        // Assert
        assert_eq!(result, HybridManagerImpl::<MockVisitorManager, MockDataFileManager>::TC_INIT);
    }

    #[test]
    fn test_get_engine_tracking_code_visitor_no_variations() {
        // Arrange
        let visitor = create_mock_visitor_with_variations(vec![]);
        let visitor_manager = MockVisitorManager::new_with_visitor(VISITOR_CODE, visitor);
        let mut datafile_manager = MockDataFileManager::new();
        let datafile = ArcSwap::new(Arc::new(MockDataFile::default()));
        datafile_manager.expect_datafile().times(1).returning(move || datafile.load());
        let hybrid_manager =
            HybridManagerImpl::new(Arc::new(visitor_manager), Arc::new(datafile_manager), Duration::ZERO);

        // Act
        let result = hybrid_manager.get_engine_tracking_code(VISITOR_CODE);

        // Assert
        assert_eq!(result, HybridManagerImpl::<MockVisitorManager, MockDataFileManager>::TC_INIT);
    }

    #[rstest]
    #[case(
        // valid variations
        (1000, 1000, 5000),
        &[
            "window.kameleoonQueue=window.kameleoonQueue||[];",
            "window.kameleoonQueue.push(['Experiments.assignVariation',123,456,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',123,true]);",
            "window.kameleoonQueue.push(['Experiments.assignVariation',789,101,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',789,false]);"
        ]
    )]
    #[case(
        // expired variation #1
        (6000, 1000, 5000),
        &[
            "window.kameleoonQueue=window.kameleoonQueue||[];",
            "window.kameleoonQueue.push(['Experiments.assignVariation',789,101,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',789,false]);"
        ]
    )]
    #[case(
        // expired variation #2
        (1000, 6000, 5000),
        &[
            "window.kameleoonQueue=window.kameleoonQueue||[];",
            "window.kameleoonQueue.push(['Experiments.assignVariation',123,456,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',123,true]);"
        ]
    )]
    #[case(
        // expired all variations
        (6000, 6000, 5000),
        &["window.kameleoonQueue=window.kameleoonQueue||[];"]
    )]
    fn test_get_engine_tracking_code(#[case] args: (u64, u64, u64), #[case] expected_substrings: &[&str]) {
        // Arrange
        let (time_offset_123, time_offset_789, expiration_period) =
            (Duration::from_millis(args.0), Duration::from_millis(args.1), Duration::from_millis(args.2));

        let variations = vec![
            create_assigned_variation(123, 456, SystemTime::now() - time_offset_123),
            create_assigned_variation(789, 101, SystemTime::now() - time_offset_789),
        ];
        let visitor = create_mock_visitor_with_variations(variations);
        let visitor_manager = MockVisitorManager::new_with_visitor(VISITOR_CODE, visitor);

        let mut mock_datafile = MockDataFile::default();
        let number_times_123 = if time_offset_123 < expiration_period { 1 } else { 0 };
        let number_times_789 = if time_offset_789 < expiration_period { 1 } else { 0 };
        mock_datafile
            .expect_has_experiment_js_css_variable()
            .with(eq(123))
            .times(number_times_123)
            .returning(|_| false); // tracking only
        mock_datafile.expect_has_experiment_js_css_variable().with(eq(789)).times(number_times_789).returning(|_| true); // has JS/CSS
        let datafile = ArcSwap::new(Arc::new(mock_datafile));

        let mut datafile_manager = MockDataFileManager::new();
        datafile_manager.expect_datafile().times(1).returning(move || datafile.load());

        let hybrid_manager =
            HybridManagerImpl::new(Arc::new(visitor_manager), Arc::new(datafile_manager), expiration_period);

        // Act
        let mut tracking_code = hybrid_manager.get_engine_tracking_code(VISITOR_CODE);

        // Assert
        for expected in expected_substrings {
            tracking_code = assert_contains_and_remove(tracking_code, expected);
        }
        assert_eq!(0, tracking_code.len());
    }

    #[test]
    fn test_get_engine_tracking_code_no_datafile() {
        // Arrange
        let variations = vec![
            create_assigned_variation(123, 456, SystemTime::now()),
            create_assigned_variation(789, 101, SystemTime::now()),
        ];
        let visitor = create_mock_visitor_with_variations(variations);
        let visitor_manager = MockVisitorManager::new_with_visitor(VISITOR_CODE, visitor);

        let mut mock_datafile = MockDataFile::default();
        mock_datafile.expect_is_nullum().returning(|| true);
        mock_datafile.expect_has_experiment_js_css_variable().returning(|_| false);
        let datafile = ArcSwap::new(Arc::new(mock_datafile));

        let mut datafile_manager = MockDataFileManager::new();
        datafile_manager.expect_datafile().times(1).returning(move || datafile.load());

        let hybrid_manager =
            HybridManagerImpl::new(Arc::new(visitor_manager), Arc::new(datafile_manager), Duration::from_millis(5000));

        // Act
        let mut tracking_code = hybrid_manager.get_engine_tracking_code(VISITOR_CODE);

        let expected_substrings = [
            "window.kameleoonQueue=window.kameleoonQueue||[];",
            "window.kameleoonQueue.push(['Experiments.assignVariation',123,456,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',123,true]);",
            "window.kameleoonQueue.push(['Experiments.assignVariation',789,101,true]);\
            window.kameleoonQueue.push(['Experiments.trigger',789,true]);",
        ];

        // Assert
        for expected in expected_substrings {
            tracking_code = assert_contains_and_remove(tracking_code, expected);
        }
        assert_eq!(0, tracking_code.len());
    }

    #[test]
    fn test_tc_init_constant() {
        assert_eq!(
            HybridManagerImpl::<MockVisitorManager, MockDataFileManager>::TC_INIT,
            "window.kameleoonQueue=window.kameleoonQueue||[];"
        );
    }
}
