use std::collections::HashMap;
use std::sync::{Arc, Weak};
use std::time::Duration;

use kameleoon_trace_logging::trace_method;
use parking_lot::{Mutex, RwLock};
use tokio::runtime::{Handle, Runtime};
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
use tokio::sync::oneshot;

use crate::config::KameleoonClientConfig;
use crate::cookies::accessor::CoreCookieAccessor;
use crate::cookies::manager::{CookieManager, CookieManagerImpl};
use crate::data::{Conversion, ConversionOpts, CustomData, KameleoonData};
use crate::datafile::datafile::{DataFile, DataFileImpl};
use crate::datafile::datafile_manager::{DataFileManager, DataFileManagerImpl};
use crate::datafile::experiment::ExperimentImpl;
use crate::datafile::feature_flag::{FeatureFlag, FeatureFlagImpl};
use crate::datafile::me_group::MeGroupImpl;
use crate::datafile::rule::RuleImpl;
use crate::datafile::segment::SegmentImpl;
use crate::datafile::streaming_service::StreamingServiceImpl;
use crate::datafile::variable::VariableImpl;
use crate::datafile::variation::VariationImpl;
use crate::datafile::variation_exposition::VariationByExpositionImpl;
use crate::error::{ErrorCode, KErr, KameleoonError};
use crate::error_log;
use crate::evaluation::evaluation_manager::{EvaluationManager, EvaluationManagerImpl};
use crate::hybrid::{HybridManager, HybridManagerImpl};
use crate::logging::LogLevel;
use crate::network::access_token_source::AccessTokenSourceImpl;
use crate::network::core_network_service::CoreNetworkServiceImpl;
use crate::network::net_provider::ReqwestNetProvider;
use crate::network::network_manager::automation_api::AutomationApiNetworkManager;
use crate::network::network_manager::NetworkManagerImpl;
use crate::network::url_provider::UrlProviderImpl;
use crate::remote_data::{RemoteDataManager, RemoteDataManagerImpl};
use crate::remote_visitor_data::{RemoteVisitorDataManager, RemoteVisitorDataManagerImpl};
use crate::storage::visitor_manager::{AddDataOptions, VisitorManager, VisitorManagerImpl};
use crate::tracking::tracking_manager::{TrackingManager, TrackingManagerImpl};
use crate::tracking::visitor_tracking_registry::VisitorTrackingRegistryImpl;
use crate::types::{RemoteVisitorDataFilter, Variation};
use crate::utils::version::SemVer;
use crate::utils::visitor_code_validator::VisitorCodeValidator;
use crate::utils::KRes;

pub type WaitInitCb = Box<dyn FnOnce(&KRes<()>) + Send + Sync>;
pub type DataFileUpdateCb = Box<dyn Fn() + Send + Sync>;

type NetworkManagerType =
    NetworkManagerImpl<CoreNetworkServiceImpl<ReqwestNetProvider>, UrlProviderImpl, Arc<AccessTokenSourceImpl>>;

type DataFileManagerType = DataFileManagerImpl<
    DataFileImpl<
        FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>,
        ExperimentImpl<VariationByExpositionImpl>,
        SegmentImpl,
        MeGroupImpl<FeatureFlagImpl<VariationImpl<VariableImpl>, RuleImpl>>,
    >,
    NetworkManagerType,
    StreamingServiceImpl<UrlProviderImpl>,
>;

type RemoteDataManagerType = RemoteDataManagerImpl<VisitorManagerImpl<DataFileManagerType>, NetworkManagerType>;

type RemoteVisitorDataManagerType =
    RemoteVisitorDataManagerImpl<VisitorManagerImpl<DataFileManagerType>, DataFileManagerType, NetworkManagerType>;

type TrackingManagerType = TrackingManagerImpl<
    VisitorTrackingRegistryImpl<VisitorManagerImpl<DataFileManagerType>>,
    DataFileManagerType,
    NetworkManagerType,
    VisitorManagerImpl<DataFileManagerType>,
>;

type CookieManagerType = CookieManagerImpl<DataFileManagerType, VisitorManagerImpl<DataFileManagerType>>;

type HybridManagerType = HybridManagerImpl<VisitorManagerImpl<DataFileManagerType>, DataFileManagerType>;

pub struct CoreKameleoonClient {
    runtime: Option<Runtime>,
    runtime_handle: Handle,
    init_callback_tx: UnboundedSender<WaitInitCb>,
    datafile_update_cb: Arc<RwLock<Option<DataFileUpdateCb>>>,
    datafile_manager: Arc<DataFileManagerType>,
    visitor_manager: Arc<VisitorManagerImpl<DataFileManagerType>>,
    evaluation_manager: Arc<EvaluationManagerImpl<VisitorManagerImpl<DataFileManagerType>>>,
    remote_data_manager: Arc<RemoteDataManagerType>,
    remote_visitor_data_manager: Arc<RemoteVisitorDataManagerType>,
    tracking_manager: Arc<TrackingManagerType>,
    cookie_manager: CookieManagerType,
    hybrid_manager: HybridManagerType,
    default_timeout: Duration,
}

impl CoreKameleoonClient {
    #[trace_method(LogLevel::Info, skip(retval))]
    pub fn new(sitecode: String, config: KameleoonClientConfig, sdk_name: String, sdk_version: String) -> KRes<Self> {
        // network
        let runtime = match tokio::runtime::Builder::new_multi_thread().worker_threads(1).enable_all().build() {
            Ok(r) => r,
            Err(err) => return Err(KameleoonError::new(ErrorCode::Internal, err.to_string())),
        };
        let runtime_handle = runtime.handle().clone();
        let sdk_ver = SemVer::parse(&sdk_version).unwrap_or(SemVer::new(0, 0, 1));
        let url_provider =
            Arc::new(UrlProviderImpl::new(sitecode, config.network_domain, sdk_name.clone(), sdk_version));
        let (network_manager, access_token_source) = {
            let mut ats = None;
            let nm = Arc::new_cyclic(|weak_nm| {
                let weak_nm = weak_nm.clone() as Weak<dyn AutomationApiNetworkManager>;
                let access_token_source = Arc::new(AccessTokenSourceImpl::new(
                    weak_nm,
                    config.client_id.clone(),
                    config.client_secret.clone(),
                    crate::network::access_token_source::RETRY_INTERVAL,
                    runtime_handle.clone(),
                ));
                ats = Some(access_token_source.clone());
                let core_network = CoreNetworkServiceImpl {
                    net_provider: ReqwestNetProvider::new(),
                };
                NetworkManagerType::new(
                    Arc::new(core_network),
                    url_provider.clone(),
                    access_token_source,
                    config.default_timeout,
                )
            });
            (nm, ats)
        };
        if let Some(ats) = access_token_source {
            ats.start_polling();
        }
        // datafile
        let (init_callback_tx, init_callback_rx) = unbounded_channel::<WaitInitCb>();
        let init_callback_rx = Mutex::new(init_callback_rx);
        let datafile_update_cb = Arc::new(RwLock::new(<Option<DataFileUpdateCb>>::None));
        let datafile_update_cb_weak = Arc::downgrade(&datafile_update_cb);
        let update_callback: Box<dyn Fn(&KRes<bool>) + Send + Sync> = Box::new(move |update_result| {
            // in case if datafile was really updated, status code == 200, but not 304 (not modified).
            if let Ok(true) = update_result {
                if let Some(df_update_cb) = datafile_update_cb_weak.upgrade() {
                    let df_update_cb = df_update_cb.read();
                    df_update_cb.as_ref().map(|cb| cb());
                }
            }
            let mut init_callback_rx = init_callback_rx.lock();
            if !init_callback_rx.is_closed() {
                // Close `init_callback_rx` if the datafile was received. This means the SDK has obtained a configuration
                // when it previously had none and was not initialized.
                if update_result.is_ok() {
                    init_callback_rx.close();
                }
                let wait_init_result = update_result.clone().map(|_| ());
                while let Ok(cb) = init_callback_rx.try_recv() {
                    cb(&wait_init_result);
                }
            }
        });
        let datafile_manager = Arc::new(DataFileManagerType::new(
            network_manager.clone(),
            StreamingServiceImpl::new(url_provider, runtime_handle.clone()),
            None,
            config.refresh_interval,
            config.environment.clone(),
            update_callback,
            runtime_handle.clone(),
        ));
        let datafile_manager_clone = datafile_manager.clone();
        runtime.spawn(async move {
            if let Err(err) = datafile_manager_clone.initialize().await {
                error_log!("Initial datafile fetch failed: {err}");
            }
        });
        // visitor data storage
        let visitor_manager = Arc::new(VisitorManagerImpl::new(datafile_manager.clone(), runtime_handle.clone()));
        visitor_manager.start_purging(config.session_duration);
        // tracking
        let tracking_manager = Arc::new(TrackingManagerImpl::new(
            VisitorTrackingRegistryImpl::new(visitor_manager.clone(), 1_000_000, 20_000),
            datafile_manager.clone(),
            network_manager.clone(),
            visitor_manager.clone(),
            config.tracking_interval,
            runtime_handle.clone(),
        ));
        tracking_manager.start_tracking();
        // and everything else
        let evaluation_manager = Arc::new(EvaluationManagerImpl::new(visitor_manager.clone(), sdk_name, sdk_ver));
        let remote_data_manager =
            Arc::new(RemoteDataManagerImpl::new(visitor_manager.clone(), network_manager.clone()));
        let remote_visitor_data_manager = Arc::new(RemoteVisitorDataManagerImpl::new(
            visitor_manager.clone(),
            datafile_manager.clone(),
            network_manager.clone(),
        ));
        let cookie_manager = CookieManagerImpl::new(
            datafile_manager.clone(),
            visitor_manager.clone(),
            config.top_level_domain.as_ref().map_or_else(|| "".to_owned(), |tld| tld.clone()),
        );
        let hybrid_manager =
            HybridManagerImpl::new(visitor_manager.clone(), datafile_manager.clone(), Duration::from_secs(5));
        Ok(Self {
            runtime: Some(runtime),
            runtime_handle,
            init_callback_tx,
            datafile_update_cb,
            datafile_manager,
            visitor_manager,
            evaluation_manager,
            remote_data_manager,
            remote_visitor_data_manager,
            tracking_manager,
            cookie_manager,
            hybrid_manager,
            default_timeout: config.default_timeout,
        })
    }
}

impl CoreKameleoonClient {
    // MARK: Initialization
    #[trace_method(LogLevel::Info, skip(self, cb))]
    pub fn wait_init(&self, timeout: Option<Duration>, cb: WaitInitCb) {
        // if SDK is already ready just call callback
        if self.is_ready() {
            cb(&Ok(()));
            return;
        }
        // need to create a new callback because original could be call on two cases: if initialization was
        // successful or it's not happened during timeout
        let cb_cell = Arc::new(Mutex::new(Some(cb)));
        let cb_cell_clone = cb_cell.clone();
        let init_cb: WaitInitCb = Box::new(move |result| {
            if let Some(cb) = cb_cell_clone.lock().take() {
                cb(result);
            }
        });
        // if new callback can't be added to channel, it means that it was closed and SDK is already initialized
        if let Err(_err) = self.init_callback_tx.send(init_cb) {
            if let Some(cb) = cb_cell.lock().take() {
                cb(&Ok(()));
            }
            return;
        }
        // run sleep task to break after timeout
        let timeout = timeout.unwrap_or(self.default_timeout);
        self.runtime_handle.spawn(async move {
            tokio::time::sleep(timeout).await;
            if let Some(cb) = cb_cell.lock().take() {
                cb(&Err(KErr::new(
                    ErrorCode::Initialization,
                    format!("KameleoonClient initialization timed out after {:?}", timeout),
                )));
            }
        });
    }

    // MARK: Ready
    #[trace_method(LogLevel::Info, skip(self))]
    pub fn is_ready(&self) -> bool {
        self.init_callback_tx.is_closed()
    }

    // MARK: Visitor
    #[trace_method(LogLevel::Info, skip(self, cookies))]
    pub fn get_visitor_code(
        &self,
        cookies: &mut impl CoreCookieAccessor,
        default_visitor_code: Option<&str>,
    ) -> KRes<String> {
        self.ensure_ready()?;
        let visitor_code = self.cookie_manager.get_or_add_visitor_code(cookies, default_visitor_code)?;
        self.cookie_manager.process_simulated_variations(cookies, &visitor_code)?;
        return Ok(visitor_code);
    }

    // MARK: Data
    #[trace_method(LogLevel::Info, skip(self))]
    pub fn add_data(&self, visitor_code: &str, data: Vec<KameleoonData>, track: bool) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        self.visitor_manager.add_data(
            visitor_code,
            data.into_iter().map(|d| d.into()),
            AddDataOptions { overwrite: true, track },
        );
        Ok(())
    }

    #[trace_method(LogLevel::Info, skip(self, cookies))]
    pub fn set_legal_consent(
        &self,
        visitor_code: &str,
        consent: bool,
        cookies: Option<&mut impl CoreCookieAccessor>,
    ) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let visitor = self.visitor_manager.get_or_create(visitor_code);
        visitor.wdata().legal_consent = consent;
        self.cookie_manager.update_visitor_code(cookies, visitor_code, consent);
        Ok(())
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn flush(&self, visitor_code: &str) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let visitor_code = visitor_code.to_owned();
        let tracking_manager = Arc::clone(&self.tracking_manager);
        self.runtime_handle.spawn(async move {
            tracking_manager.add_visitor_code(visitor_code).await;
        });
        Ok(())
    }

    #[trace_method(LogLevel::Info, skip(self, cb))]
    pub fn flush_instant(&self, visitor_code: &str, cb: impl FnOnce(KRes<()>) + Send + 'static) {
        let Some((visitor_code, cb)) = self.check_ready_vc_in_cb(visitor_code, cb) else { return };
        let tracking_manager = Arc::clone(&self.tracking_manager);
        self.runtime_handle.spawn(async move {
            cb(Ok(tracking_manager.track_visitor(visitor_code).await));
        });
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn track_conversion(
        &self,
        visitor_code: &str,
        goal_id: u32,
        revenue: f32,
        negative: bool,
        metadata: Vec<CustomData>,
    ) -> KRes<()> {
        self.add_data(
            visitor_code,
            vec![KameleoonData::Conversion(Conversion::new_with_opts(
                goal_id,
                ConversionOpts::new().revenue(revenue).negative(negative).metadata(metadata),
            ))],
            true,
        )?;
        self.flush(visitor_code)
    }

    // MARK: Evaluation
    #[trace_method(LogLevel::Info, skip(self))]
    pub fn is_feature_active(&self, visitor_code: &str, feature_key: &str, track: bool) -> KRes<bool> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let datafile = self.datafile_manager.datafile();
        let result = self.evaluation_manager.is_feature_active(datafile.as_ref(), visitor_code, feature_key, track);
        drop(datafile);
        if track && result.is_ok() {
            self.flush(visitor_code)?;
        }
        return result;
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn get_variation(&self, visitor_code: &str, feature_key: &str, track: bool) -> KRes<Variation> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let datafile = self.datafile_manager.datafile();
        let result = self.evaluation_manager.get_variation(datafile.as_ref(), visitor_code, feature_key, track);
        drop(datafile);
        if track {
            self.flush(visitor_code)?;
        }
        return result;
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn get_variations(
        &self,
        visitor_code: &str,
        only_active: bool,
        track: bool,
    ) -> KRes<HashMap<String, Variation>> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let datafile = self.datafile_manager.datafile();
        let variations = self.evaluation_manager.get_variations(datafile.as_ref(), visitor_code, only_active, track);
        drop(datafile);
        if track {
            self.flush(visitor_code)?;
        }
        Ok(variations)
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn set_forced_variation(
        &self,
        visitor_code: &str,
        experiment_id: u32,
        variation_key: Option<&str>,
        force_targeting: bool,
    ) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        if let Some(variation_key) = variation_key {
            let datafile = self.datafile_manager.datafile();
            self.evaluation_manager.set_forced_variation(
                datafile.as_ref(),
                visitor_code,
                experiment_id,
                variation_key,
                force_targeting,
            )
        } else {
            self.evaluation_manager.reset_forced_variation(visitor_code, experiment_id);
            Ok(())
        }
    }

    #[trace_method(LogLevel::Info, skip(self, cb))]
    pub fn get_remote_data(&self, key: &str, cb: impl FnOnce(KRes<Vec<u8>>) + Send + 'static) {
        if let Err(err) = self.ensure_ready() {
            cb(Err(err));
            return;
        }
        let key = key.to_owned();
        let remote_data_manager = self.remote_data_manager.clone();
        self.runtime_handle.spawn(async move {
            cb(remote_data_manager.fetch_raw(&key).await);
        });
    }

    // MARK: - Remote Data
    pub fn get_remote_visitor_data(
        &self,
        visitor_code: &str,
        filter: Option<RemoteVisitorDataFilter>,
        cb: impl FnOnce(KRes<()>) + Send + 'static,
    ) {
        let Some((visitor_code, cb)) = self.check_ready_vc_in_cb(visitor_code, cb) else { return };
        let remote_visitor_data_manager = self.remote_visitor_data_manager.clone();
        self.runtime_handle.spawn(async move {
            let result = async { remote_visitor_data_manager.fetch(&visitor_code, filter).await }.await;
            cb(result);
        });
    }

    #[trace_method(LogLevel::Info, skip(self, cb))]
    pub fn get_visitor_warehouse_audience(
        &self,
        visitor_code: &str,
        warehouse_key: Option<&str>,
        custom_data_index: u32,
        cb: impl FnOnce(KRes<()>) + Send + 'static,
    ) {
        let Some((visitor_code, cb)) = self.check_ready_vc_in_cb(visitor_code, cb) else { return };
        let warehouse_key = warehouse_key.map(|key| key.to_owned());
        let remote_data_manager = self.remote_data_manager.clone();
        self.runtime_handle.spawn(async move {
            let result = async {
                remote_data_manager
                    .fetch_visitor_warehouse_audience(&visitor_code, warehouse_key.as_deref(), custom_data_index)
                    .await
            }
            .await;
            cb(result);
        });
    }

    // MARK: Hybrid Integration
    #[trace_method(LogLevel::Info, skip(self))]
    pub fn get_engine_tracking_code(&self, visitor_code: &str) -> KRes<String> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        Ok(self.hybrid_manager.get_engine_tracking_code(visitor_code))
    }

    // MARK: Data File
    #[trace_method(LogLevel::Info, skip(self))]
    pub fn get_feature_keys(&self) -> Vec<String> {
        self.datafile_manager
            .datafile()
            .ordered_feature_flags()
            .into_iter()
            .map(|ff| ff.feature_key().to_owned())
            .collect()
    }

    #[trace_method(LogLevel::Info, skip(self, callback))]
    pub fn on_datafile_update(&self, callback: Option<DataFileUpdateCb>) {
        *self.datafile_update_cb.write() = callback;
    }

    #[trace_method(LogLevel::Info, skip(self))]
    pub fn get_types_datafile(&self) -> Arc<crate::types::DataFile> {
        self.datafile_manager.get_types_datafile()
    }

    // MARK: Core Helpers

    const NOT_INITIALIZED_ERROR_MESSAGE: &str = "KameleoonClient is not initialized";

    #[inline]
    fn ensure_ready(&self) -> KRes<()> {
        if self.is_ready() {
            Ok(())
        } else {
            Err(KErr::new(ErrorCode::Initialization, Self::NOT_INITIALIZED_ERROR_MESSAGE))
        }
    }

    #[inline]
    fn check_ready_vc_in_cb<T, F: FnOnce(KRes<T>)>(&self, visitor_code: &str, cb: F) -> Option<(String, F)> {
        if let Err(err) = self.ensure_ready() {
            cb(Err(err));
            return None;
        }
        if let Err(err) = VisitorCodeValidator::validate(visitor_code) {
            cb(Err(err));
            return None;
        }
        Some((visitor_code.to_owned(), cb))
    }

    // MARK: Rust SDK helpers

    pub async fn initialize_with_timeout(&self, timeout: Option<Duration>) -> KRes<()> {
        let (tx, rx) = oneshot::channel::<KRes<()>>();
        self.wait_init(
            timeout,
            Box::new(move |res| {
                let _ = tx.send(res.clone());
            }),
        );
        rx.await.unwrap_or_else(|_| Err(KameleoonError::from("Init callback was never called")))
    }

    pub async fn get_remote_data_async(&self, key: &str) -> KRes<String> {
        self.ensure_ready()?;
        self.remote_data_manager
            .fetch_raw(&key)
            .await
            .and_then(|b| String::from_utf8(b).map_err(|e| KErr::new(ErrorCode::Internal, e.to_string())))
    }

    pub async fn get_remote_visitor_data_async(
        &self,
        visitor_code: &str,
        filter: Option<RemoteVisitorDataFilter>,
    ) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        self.remote_visitor_data_manager.fetch(&visitor_code, filter).await
    }

    pub async fn get_visitor_warehouse_audience_async(
        &self,
        visitor_code: &str,
        warehouse_key: Option<&str>,
        custom_data_index: u32,
    ) -> KRes<()> {
        self.ensure_ready()?;
        self.remote_data_manager
            .fetch_visitor_warehouse_audience(&visitor_code, warehouse_key.as_deref(), custom_data_index)
            .await
    }

    pub async fn flush_instant_async(&self, visitor_code: &str) -> KRes<()> {
        self.ensure_ready()?;
        VisitorCodeValidator::validate(visitor_code)?;
        let visitor_code = visitor_code.to_owned();
        Ok(self.tracking_manager.track_visitor(visitor_code).await)
    }
}

impl Drop for CoreKameleoonClient {
    fn drop(&mut self) {
        if let Some(rt) = self.runtime.take() {
            std::thread::spawn(move || drop(rt)).join().ok();
        }
    }
}

// MARK: Tests

#[cfg(test)]
mod tests {
    use std::{collections::HashMap, sync::Arc, time::Duration};

    use parking_lot::RwLock;
    use rstest::*;

    use super::CoreKameleoonClient;
    use crate::{
        cookies::accessor::CoreCookieAccessor,
        core_factory::CoreKameleoonClientFactory,
        data::{Browser, BrowserKind, CustomData, Device, DeviceKind, KameleoonData},
        error::ErrorCode,
        tracking::trackable::TrackingState,
        types::JsonValue,
        utils::KRes,
    };

    fn make_kameleoon_client(sitecode: &str) -> Arc<CoreKameleoonClient> {
        const SDK_NAME: &str = "test-sdk";
        const SDK_VERSION: &str = "1.21.321";
        let config_path = format!("{}/resources/config.json", env!("CARGO_MANIFEST_DIR"));
        CoreKameleoonClientFactory::create(sitecode, Some(config_path.as_str()), None, SDK_NAME, SDK_VERSION).unwrap()
    }

    fn init_kameleoon_client(client: &CoreKameleoonClient) -> bool {
        let (tx, rx) = std::sync::mpsc::channel();
        client.wait_init(None, Box::new(move |res| tx.send(res.is_ok()).unwrap()));
        rx.recv_timeout(Duration::from_secs(5)).unwrap()
    }

    struct FakeCookies {
        values: RwLock<HashMap<String, (String, u32, Option<String>)>>,
    }

    impl FakeCookies {
        fn new() -> Self {
            Self {
                values: RwLock::new(HashMap::new()),
            }
        }
    }

    impl CoreCookieAccessor for Arc<FakeCookies> {
        fn get(&self, key: &str) -> Option<String> {
            self.values.read().get(key).map(|v| v.0.clone())
        }

        fn set(&mut self, key: &str, value: &str, max_age: u32, tld: Option<&str>) {
            self.values.write().insert(key.to_owned(), (value.to_owned(), max_age, tld.map(|tld| tld.to_owned())));
        }
    }

    #[rstest]
    #[case("og6g1fquwx")] // polling cc update
    #[case("tndueuutdq")] // streaming cc update
    fn test_init(#[case] sitecode: &str) {
        let client = make_kameleoon_client(sitecode);
        assert!(!client.is_ready());
        let ok = init_kameleoon_client(client.as_ref());
        assert!(ok);
        assert!(client.is_ready());
    }

    fn assert_not_initialized(error: crate::error::KameleoonError) {
        assert_eq!(ErrorCode::Initialization, error.code());
        assert_eq!(CoreKameleoonClient::NOT_INITIALIZED_ERROR_MESSAGE, error.message());
    }

    fn recv_callback_error<T: std::fmt::Debug + Send + 'static>(
        register: impl FnOnce(std::sync::mpsc::Sender<KRes<T>>),
    ) -> crate::error::KameleoonError {
        let (tx, rx) = std::sync::mpsc::channel();
        register(tx);
        rx.recv_timeout(Duration::from_secs(1)).unwrap().unwrap_err()
    }

    #[tokio::test]
    async fn test_all_public_sync_and_callback_methods_fail_before_init() {
        const VC: &str = "vc123";
        const FF: &str = "testff";
        let client = make_kameleoon_client("tndueuutdq");
        assert!(!client.is_ready());

        let mut cookies = Arc::new(FakeCookies::new());
        assert_not_initialized(client.get_visitor_code(&mut cookies, Some(VC)).unwrap_err());
        assert_not_initialized(client.add_data(VC, vec![], true).unwrap_err());
        assert_not_initialized(client.set_legal_consent(VC, true, None::<&mut Arc<FakeCookies>>).unwrap_err());
        assert_not_initialized(client.flush(VC).unwrap_err());
        assert_not_initialized(recv_callback_error::<()>(|tx| {
            client.flush_instant(VC, move |result| tx.send(result).unwrap())
        }));
        assert_not_initialized(client.track_conversion(VC, 456, 9999.99, true, vec![]).unwrap_err());
        assert_not_initialized(client.is_feature_active(VC, FF, false).unwrap_err());
        assert_not_initialized(client.get_variation(VC, FF, false).unwrap_err());
        assert_not_initialized(client.get_variations(VC, true, false).unwrap_err());
        assert_not_initialized(client.set_forced_variation(VC, 189891, Some("on"), true).unwrap_err());
        assert_not_initialized(recv_callback_error::<Vec<u8>>(|tx| {
            client.get_remote_data("test-key", move |result| tx.send(result).unwrap())
        }));
        assert_not_initialized(recv_callback_error::<()>(|tx| {
            client.get_remote_visitor_data(VC, None, move |result| tx.send(result).unwrap())
        }));
        assert_not_initialized(recv_callback_error::<()>(|tx| {
            client.get_visitor_warehouse_audience(VC, Some("warehouse-key"), 0, move |result| tx.send(result).unwrap())
        }));
        assert_not_initialized(client.get_engine_tracking_code(VC).unwrap_err());

        assert_not_initialized(client.get_remote_data_async("test-key").await.unwrap_err());
        assert_not_initialized(client.get_remote_visitor_data_async(VC, None).await.unwrap_err());
        assert_not_initialized(
            client.get_visitor_warehouse_audience_async(VC, Some("warehouse-key"), 0).await.unwrap_err(),
        );
        assert_not_initialized(client.flush_instant_async(VC).await.unwrap_err());
    }

    #[tokio::test]
    async fn test_run() {
        const VC: &str = "vc123";
        const FF_KEY: &str = "testff";
        const MAX_AGE_COOKIE_SECONDS: u32 = 380 * 86400;
        // init
        let client = make_kameleoon_client("tndueuutdq");
        assert!(init_kameleoon_client(client.as_ref()));
        // common data
        assert_eq!(42, client.get_feature_keys().len());
        // get visitor code
        let cookies = Arc::new(FakeCookies::new());
        let mut cookies = cookies;
        let vc = client.get_visitor_code(&mut cookies, Some(VC)).unwrap();
        assert_eq!(VC, vc);
        let cookies_values = cookies.values.read();
        assert_eq!(1, cookies_values.len());
        assert_eq!(
            &(VC.to_owned(), MAX_AGE_COOKIE_SECONDS, Some("example.com".to_owned())),
            cookies_values.get("kameleoonVisitorCode").unwrap()
        );
        // evaluation + add data + forced variation
        assert!(!client.is_feature_active(&vc, FF_KEY, false).unwrap());
        assert!(!client.get_variation(&vc, FF_KEY, false).unwrap().is_active());
        client.set_forced_variation(&vc, 189891, Some("on"), true).unwrap();
        assert!(client.is_feature_active(&vc, FF_KEY, false).unwrap());
        assert!(client.get_variation(&vc, FF_KEY, false).unwrap().is_active());
        client.set_forced_variation(&vc, 189891, None, true).unwrap();
        assert!(!client.is_feature_active(&vc, FF_KEY, false).unwrap());
        assert!(!client.get_variation(&vc, FF_KEY, false).unwrap().is_active());
        client.add_data(&vc, vec![CustomData::new_with_index(0, vec!["true".to_owned()]).into()], true).unwrap();
        assert!(client.is_feature_active(&vc, FF_KEY, false).unwrap());
        assert!(client.get_variation(&vc, FF_KEY, false).unwrap().is_active());
        assert_eq!(7, client.get_variations(&vc, true, false).unwrap().len());
        // tracking
        client.flush(&vc).unwrap();
        client.add_data(&vc, vec![KameleoonData::Device(Device::new(DeviceKind::Desktop))], true).unwrap();
        client.track_conversion(&vc, 456, 9999.99, true, vec![]).unwrap();
    }

    #[test]
    fn test_get_types_datafile() {
        let client = make_kameleoon_client("tndueuutdq");
        assert!(init_kameleoon_client(client.as_ref()));

        let datafile = client.get_types_datafile();
        let all_feature_flag_keys = datafile.feature_flags.keys().collect::<Vec<_>>();
        let feature_flag = datafile.feature_flags.get("ff_new_rules").unwrap();
        let variation = feature_flag.variations.get("on").unwrap();
        let variable_number = variation.get_variable("variable_number").map(|variable| &variable.value).unwrap();
        let rule_variation = feature_flag.rules[1].variations.get("on").unwrap();
        let rule_variation_number =
            rule_variation.get_variable("variable_number").map(|variable| &variable.value).unwrap();

        assert_eq!(42, all_feature_flag_keys.len());
        assert!(matches!(variable_number, JsonValue::Number(value) if *value == 11.0));
        assert_eq!(2, feature_flag.rules.len());
        assert_eq!(Some(866770), rule_variation.id);
        assert_eq!(variable_number, rule_variation_number);
        assert!(feature_flag.environment_enabled);
        assert_eq!("off", feature_flag.default_variation_key);
    }

    #[test]
    fn test_add_data_track() {
        // arrange
        const VISITOR_CODE: &str = "test_visitor";
        let client = make_kameleoon_client("tndueuutdq");
        assert!(init_kameleoon_client(client.as_ref()));

        let browser = Browser::new(BrowserKind::Firefox, None);
        let device = Device::new(DeviceKind::Desktop);

        // act
        client.add_data(VISITOR_CODE, vec![browser.clone().into()], true).unwrap();
        client.add_data(VISITOR_CODE, vec![device.clone().into()], false).unwrap();

        // assert

        assert_eq!(browser.trackable_base.state_cell().state(), TrackingState::Unsent, "Browser should be unsent");
        assert_eq!(device.trackable_base.state_cell().state(), TrackingState::Sent, "Device should unsent");
    }

    #[tokio::test]
    async fn test_rust_sdk_helpers() {
        // arrange
        const VC: &str = "vc123";
        let client = make_kameleoon_client("tndueuutdq");
        assert!(init_kameleoon_client(client.as_ref()));

        // act & assert
        let remote_data = client.get_remote_data_async("test 3").await.unwrap();
        assert_eq!(
            "{\"hobbies\":[\"football\",\"movies\",\"3\"],\"likesKameleoon\":true,\"age\":26.0,\"test-4\":\"no value\",\"test-3\":\"no value\"}",
            remote_data
        );

        let remote_visitor_data = client.get_remote_visitor_data_async(VC, None).await;
        assert!(remote_visitor_data.is_ok());

        let flush_instant = client.flush_instant_async(&VC).await;
        assert!(flush_instant.is_ok());
    }
}
