use std::{any::Any, fs, path::PathBuf, time::Duration};

use regex::Regex;

use crate::data::assigned_variation::AssignedVariation;
use crate::data::personalization::Personalization;
use crate::data::visitor_visits::{Visit, VisitorVisits};
use crate::datafile::datafile::{DataFile, DataFileImpl};
use crate::datafile::experiment::ExperimentImpl;
use crate::datafile::feature_flag::FeatureFlagImpl;
use crate::datafile::me_group::MeGroupImpl;
use crate::datafile::rule::{RuleImpl, RuleType};
use crate::datafile::segment::SegmentImpl;
use crate::datafile::variable::VariableImpl;
use crate::datafile::variation::VariationImpl;
use crate::datafile::variation_exposition::VariationByExpositionImpl;
use crate::network::dto::DataFileDto;
use crate::targeting::{targeting_tree::TargetingTree, TargetingData};
use crate::utils::{string_generator::generate_string, time_utils::millis_to_time};

pub(crate) const TEST_VISITOR_CODE: &str = "222D7B50-A0C7-435B-B628-E86670DDBDB9";

#[macro_export]
#[doc(hidden)]
macro_rules! assert_delta {
    ($expected:expr, $actual:expr, $delta:expr) => {
        assert!(
            if $expected >= $actual { $expected - $actual } else { $actual - $expected } <= $delta,
            "Expected {:?} ± {:?}, got {:?}",
            $expected,
            $delta,
            $actual
        )
    };
}

// MARK: - Downcast
pub trait DowncastTestExt<'a> {
    fn as_any(&'a self) -> &'a dyn Any;
}

impl<'a, T: Any> DowncastTestExt<'a> for T {
    fn as_any(&'a self) -> &'a dyn Any {
        self
    }
}

pub fn downcast_opt_ref<'a, T: Any>(value: &'a dyn DowncastTestExt<'a>) -> Option<&'a T> {
    value.as_any().downcast_ref::<T>()
}

pub fn downcast_ref<'a, T: Any>(value: &'a dyn DowncastTestExt<'a>) -> &'a T {
    value.as_any().downcast_ref::<T>().unwrap()
}

pub fn generate_test_string(len: usize) -> String {
    const CHARSET: &str = "abcdefghijklmnopqrstuvwxyz0123456789";
    generate_string(CHARSET, len)
}

pub fn load_file(file_name: &str) -> String {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push(file_name);
    fs::read_to_string(path).expect("Failed to read JSON test file")
}

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

pub(crate) fn load_datafile(file_path: &str) -> TestDataFile {
    let sdf = std::fs::read_to_string(file_path).unwrap();
    let dto: DataFileDto = serde_json::from_str(&sdf).unwrap();
    TestDataFile::new(dto, None, None)
}

pub(crate) fn load_targeting_tree(file_path: &str) -> TargetingTree {
    let stree = std::fs::read_to_string(file_path).unwrap();
    let model: crate::targeting::models::SegmentModel = serde_json::from_str(&stree).unwrap();
    return TargetingTree::from_json(model.condition_data).unwrap();
}

pub(crate) fn assert_targeting(
    targeting_tree_file_path: &str,
    data: &TargetingData<impl DataFile>,
    expect_targeted: bool,
) {
    let tree = load_targeting_tree(targeting_tree_file_path);
    let targeted = tree.check(data);
    assert_eq!(expect_targeted, targeted);
}

pub(crate) fn new_visitor_visits() -> VisitorVisits {
    VisitorVisits::new(
        10,
        vec![
            Visit::new(millis_to_time(500), millis_to_time(500)),
            Visit::new(millis_to_time(100), millis_to_time(100)),
        ],
        millis_to_time(900),
        Duration::ZERO,
    )
}

pub(crate) fn new_var(exp_id: u32, var_id: u32, assignment_ts: u64) -> AssignedVariation {
    return AssignedVariation::new_with_time(exp_id, var_id, millis_to_time(assignment_ts), RuleType::Unknown);
}

pub(crate) fn new_perso(id: u32, variation_id: u32, assignment_ts: u64) -> Personalization {
    return Personalization::new(id, variation_id, millis_to_time(assignment_ts));
}

pub fn assert_contains_and_remove(s: String, snippet: &str) -> String {
    assert!(s.contains(snippet), "Expected snippet not found");
    s.replace(snippet, "")
}

pub fn remove_nonce_parameter(url: &str) -> String {
    let regex = Regex::new(r"nonce=[0-9A-F]+&|&?nonce=[0-9A-F]+").unwrap();
    assert_eq!(1, regex.find_iter(url).take(2).count());
    regex.replace(url, "").into_owned()
}

pub fn soft_remove_nonce_parameter(url: &str) -> String {
    let regex = Regex::new(r"nonce=[0-9A-F]+&|&?nonce=[0-9A-F]+").unwrap();
    regex.replace_all(url, "").into_owned()
}
