use crate::errors::PolicyEngineError;
use crate::match_utils::coerce_value_to_number::coerce_value_to_number;
use crate::value_utils::get_value_type_string::get_value_type_string;
use serde_json::Value;

pub fn compare_numeric_with_coercion<F: Fn(f64, f64) -> bool>(
    expected: &Value,
    actual: &Value,
    cmp: F,
) -> Result<bool, PolicyEngineError> {
    let Ok(expected_coerced) = coerce_value_to_number(expected.clone()) else {
        return Err(PolicyEngineError::ComparisonError {
            required_type: "number".into(),
            source_type: get_value_type_string(expected).into(),
        });
    };

    let Ok(actual_coerced) = coerce_value_to_number(actual.clone()) else {
        return Err(PolicyEngineError::ComparisonError {
            required_type: "number".into(),
            source_type: get_value_type_string(actual).into(),
        });
    };

    match (expected_coerced.as_f64(), actual_coerced.as_f64()) {
        (Some(expected_number), Some(actual_number)) => Ok(cmp(actual_number, expected_number)),

        // This shouldn't happen because coerce_value_to_number should have already rejected non-numbers
        (None, _) => Err(PolicyEngineError::ComparisonError {
            required_type: "number".into(),
            source_type: get_value_type_string(expected).into(),
        }),

        (_, None) => Err(PolicyEngineError::ComparisonError {
            required_type: "number".into(),
            source_type: get_value_type_string(actual).into(),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::compare_numeric_with_coercion;
    use serde_json::json;

    #[test]
    fn greater_than_happy_path() {
        let expected = json!(10);
        let actual = json!(15);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).expect("ok");
        assert!(res);
    }

    #[test]
    fn less_than_happy_path() {
        let expected = json!(10);
        let actual = json!(5);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a < e).expect("ok");
        assert!(res);
    }

    #[test]
    fn equality_happy_path_integers() {
        let expected = json!(42);
        let actual = json!(42);
        let res =
            compare_numeric_with_coercion(&expected, &actual, |a, e| (a - e).abs() < f64::EPSILON)
                .expect("ok");
        assert!(res);
    }

    #[test]
    fn equality_happy_path_floats() {
        let expected = json!(3.14);
        let actual = json!(3.14);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| (a - e).abs() < 1e-12)
            .expect("ok");
        assert!(res);
    }

    #[test]
    fn comparator_receives_actual_then_expected() {
        let expected = json!(2);
        let actual = json!(8);
        // The comparator checks ordering explicitly
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| {
            // Ensure we received (actual, expected) as documented by implementation
            assert_eq!(a, 8.0);
            assert_eq!(e, 2.0);
            a % e == 0.0
        })
        .expect("ok");
        assert!(res);
    }

    #[test]
    fn error_when_expected_is_non_number() {
        let expected = json!("ten");
        let actual = json!(10);
        let err = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).unwrap_err();
        // We don't assert the exact message; just that an error is returned
        let _ = err; // type check
    }

    #[test]
    fn error_when_actual_is_non_number() {
        let expected = json!(10);
        let actual = json!("ten");
        let err = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).unwrap_err();
        let _ = err;
    }

    #[test]
    fn handles_mixed_integer_and_float() {
        let expected = json!(10.5);
        let actual = json!(11);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).expect("ok");
        assert!(res);
    }

    #[test]
    fn numeric_string_expected_is_coerced() {
        let expected = json!("10");
        let actual = json!(15);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).expect("ok");
        assert!(res);
    }

    #[test]
    fn numeric_string_actual_is_coerced() {
        let expected = json!(10);
        let actual = json!("15");
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).expect("ok");
        assert!(res);
    }

    #[test]
    fn numeric_string_both_sides_are_coerced() {
        let expected = json!("10");
        let actual = json!("15");
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| a > e).expect("ok");
        assert!(res);
    }

    #[test]
    fn numeric_string_float_is_coerced() {
        let expected = json!("3.14");
        let actual = json!(3.14);
        let res = compare_numeric_with_coercion(&expected, &actual, |a, e| (a - e).abs() < 1e-12)
            .expect("ok");
        assert!(res);
    }
}
