use kameleoon_trace_logging::trace_method;

use crate::logging::LogLevel;

macro_rules! calculate_hash {
    ($($args:expr),+ $(,)?) => {{
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        $(hasher.update($args.as_bytes());)+
        let hash = hasher.finalize();
        let value = u64::from_be_bytes(hash[..8].try_into().unwrap());
        value as f64 / u64::MAX as f64
    }};
}

// MARK: - Hasher

pub(crate) struct Hasher;

impl Hasher {
    #[trace_method(LogLevel::Debug)]
    pub fn obtain_with_me_group(code: &str, me_group: &str) -> f64 {
        calculate_hash!(code, me_group)
    }

    #[trace_method(LogLevel::Debug)]
    pub fn obtain_with_campaign_id(code: &str, campaign_id: u32) -> f64 {
        calculate_hash!(code, campaign_id.to_string())
    }

    #[trace_method(LogLevel::Debug)]
    pub fn obtain_with_campaign_id_respool_time(code: &str, campaign_id: u32, respool_time: Option<u64>) -> f64 {
        if let Some(respool_time) = respool_time {
            calculate_hash!(code, campaign_id.to_string(), respool_time.to_string())
        } else {
            calculate_hash!(code, campaign_id.to_string())
        }
    }
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use crate::utils::{hasher::Hasher, test_utils::TEST_VISITOR_CODE};
    use float_cmp::assert_approx_eq;
    use rstest::rstest;

    // Test consistency between macro and function obtain_with_me_group
    #[rstest]
    #[case("user123", "groupA")]
    #[case("", "")] // Empty strings
    #[case("a", "b")]
    #[case("same", "same")] // Duplicate inputs
    #[case("long-string-", "1234567890")] // Longer inputs
    fn test_macro_equals_function_me_group(#[case] code: &str, #[case] me_group: &str) {
        let macro_result = calculate_hash!(code, me_group);
        let function_result = Hasher::obtain_with_me_group(code, me_group);

        assert_eq!(macro_result, function_result);
    }

    // Test consistency between macro and function obtain_with_campaign_id
    #[rstest]
    #[case("user123", 10)]
    #[case("", 0)]
    #[case("a", 100)]
    #[case("same", 1000)]
    #[case("long-string-", 1234567890)]
    fn test_macro_equals_function_campaign_id(#[case] code: &str, #[case] campaign_id: u32) {
        let macro_result = calculate_hash!(code, campaign_id.to_string());
        let function_result = Hasher::obtain_with_campaign_id(code, campaign_id);

        assert_eq!(macro_result, function_result);
    }

    // Test consistency between macro and function obtain_with_campaign_id
    #[rstest]
    #[case("user123", 10, Some(10))]
    #[case("", 0, None)]
    #[case("a", 100, Some(100))]
    #[case("same", 1000, None)]
    #[case("long-string-", 1234567890, Some(1234567890))]
    fn test_macro_equals_function_campaign_id_respool_time(
        #[case] code: &str,
        #[case] campaign_id: u32,
        #[case] respool_time: Option<u64>,
    ) {
        let respool_time_str =
            if let Some(respool_time) = respool_time { respool_time.to_string() } else { "".to_owned() };
        let macro_result = calculate_hash!(code, campaign_id.to_string(), respool_time_str);
        let function_result = Hasher::obtain_with_campaign_id_respool_time(code, campaign_id, respool_time);

        assert_eq!(macro_result, function_result);
    }

    // Test different inputs produce different hashes
    #[rstest]
    #[case("a", "b", "a", "c")]
    #[case("c", "a", "b", "a")] // Different order
    #[case("x", "y", "y", "x")] // Different values
    fn test_input_sensitivity(#[case] a1: &str, #[case] b1: &str, #[case] a2: &str, #[case] b2: &str) {
        let h1 = calculate_hash!(a1, b1);
        let h2 = calculate_hash!(a2, b2);
        assert_ne!(h1, h2);
    }

    // Test pre-defined values
    #[rstest]
    #[case(TEST_VISITOR_CODE, "536", "", 0.25997919904599526)]
    #[case(TEST_VISITOR_CODE, "0", "", 0.7233544895718367)]
    #[case(TEST_VISITOR_CODE, "536", "1000", 0.47326208139881804)]
    #[case("vc1", "me1", "", 0.6415384389124803)]
    #[case("vc2", "me2", "", 0.29554487138368163)]
    fn test_predefined_values(
        #[case] code: &str,
        #[case] second_value: &str,
        #[case] third_value: &str,
        #[case] expected_hash: f64,
    ) {
        let hash = calculate_hash!(code, second_value, third_value);
        assert_approx_eq!(f64, expected_hash, hash, ulps = 3);
    }
}
