use crate::errors::PolicyEngineError;
use crate::match_utils::coerce_value_to_shape::coerce_value_to_shape;
use crate::match_utils::compare_numeric_values_with_epsilon_tolerance;
use crate::value_utils::get_value_type_string::get_value_type_string;
use serde_json::Value;

pub fn compare_value_equality_with_type_coercion(
    expected: &Value,
    actual: &Value,
) -> Result<bool, PolicyEngineError> {
    let expected_with_coercion = match coerce_value_to_shape(expected.clone(), actual) {
        Ok(v) => v,
        Err(_e) => {
            return Err(PolicyEngineError::ComparisonError {
                required_type: get_value_type_string(expected).to_string(),
                source_type: get_value_type_string(actual).to_string(),
            });
        }
    };

    match (&expected_with_coercion, actual) {
        (Value::Number(en), Value::Number(an)) => {
            compare_numeric_values_with_epsilon_tolerance::compare_numeric_equality_with_epsilon_tolerance(en, an)
        }

        _ => Ok(&expected_with_coercion == actual),
    }
}

#[cfg(test)]
mod tests {
    use super::compare_value_equality_with_type_coercion;
    use crate::errors::PolicyEngineError;
    use serde_json::{Value, json};

    #[test]
    fn null_equals_null() {
        let res =
            compare_value_equality_with_type_coercion(&Value::Null, &Value::Null).expect("ok");
        assert!(res);
    }

    #[test]
    fn comparing_incompatible_types_returns_error() {
        let err = compare_value_equality_with_type_coercion(&json!("x"), &Value::Number(1.into()))
            .unwrap_err();
        match err {
            PolicyEngineError::ComparisonError {
                required_type,
                source_type,
            } => {
                assert_eq!(required_type, "string");
                assert_eq!(source_type, "number");
            }
            _ => panic!("unexpected error type"),
        }
    }

    #[test]
    fn equal_strings_true() {
        let res = compare_value_equality_with_type_coercion(&json!("hello"), &json!("hello"))
            .expect("ok");
        assert!(res);
    }

    #[test]
    fn different_strings_false() {
        let res = compare_value_equality_with_type_coercion(&json!("hello"), &json!("world"))
            .expect("ok");
        assert!(!res);
    }

    #[test]
    fn equal_booleans_true() {
        let res =
            compare_value_equality_with_type_coercion(&json!(true), &json!(true)).expect("ok");
        assert!(res);
        let res2 =
            compare_value_equality_with_type_coercion(&json!(false), &json!(false)).expect("ok");
        assert!(res2);
    }

    #[test]
    fn different_booleans_false() {
        let res =
            compare_value_equality_with_type_coercion(&json!(true), &json!(false)).expect("ok");
        assert!(!res);
    }

    #[test]
    fn equal_integers_true() {
        let res = compare_value_equality_with_type_coercion(&json!(42), &json!(42)).expect("ok");
        assert!(res);
    }

    #[test]
    fn different_integers_false() {
        let res = compare_value_equality_with_type_coercion(&json!(41), &json!(42)).expect("ok");
        assert!(!res);
    }

    #[test]
    fn equal_floats_true() {
        let res =
            compare_value_equality_with_type_coercion(&json!(3.14), &json!(3.14)).expect("ok");
        assert!(res);
    }

    #[test]
    fn mixed_int_float_equality_true() {
        // 10 vs 10.0 should be equal numerically; with EPSILON this should be true
        let res = compare_value_equality_with_type_coercion(&json!(10), &json!(10.0)).expect("ok");
        assert!(res);
    }

    #[test]
    fn expected_numeric_string_is_coerced_to_number_shape() {
        let res = compare_value_equality_with_type_coercion(&json!("10"), &json!(10)).expect("ok");
        assert!(res);

        let res =
            compare_value_equality_with_type_coercion(&json!("3.14"), &json!(3.14)).expect("ok");
        assert!(res);

        // Exponent form
        let res =
            compare_value_equality_with_type_coercion(&json!("1e3"), &json!(1000)).expect("ok");
        assert!(res);
    }

    #[test]
    fn expected_number_is_coerced_to_string_shape() {
        // actual is string, so expected number is stringified
        let res =
            compare_value_equality_with_type_coercion(&json!(123), &json!("123")).expect("ok");
        assert!(res);

        // boolean stringification too
        let res =
            compare_value_equality_with_type_coercion(&json!(true), &json!("true")).expect("ok");
        assert!(res);

        // null stringification too
        let res =
            compare_value_equality_with_type_coercion(&Value::Null, &json!("null")).expect("ok");
        assert!(res);
    }

    #[test]
    fn expected_boolean_string_is_coerced_to_bool_shape() {
        let res =
            compare_value_equality_with_type_coercion(&json!("true"), &json!(true)).expect("ok");
        assert!(res);

        let res =
            compare_value_equality_with_type_coercion(&json!("false"), &json!(false)).expect("ok");
        assert!(res);
    }

    #[test]
    fn expected_bool_is_not_coerced_to_number_shape() {
        let err = compare_value_equality_with_type_coercion(&json!(true), &json!(1)).unwrap_err();
        match err {
            PolicyEngineError::ComparisonError {
                required_type,
                source_type,
            } => {
                assert_eq!(required_type, "boolean");
                assert_eq!(source_type, "number");
            }
            _ => panic!("unexpected error type"),
        }
    }

    #[test]
    fn expected_invalid_boolean_string_to_bool_shape_errors() {
        let err = compare_value_equality_with_type_coercion(&json!("notabool"), &json!(true))
            .unwrap_err();

        match err {
            PolicyEngineError::ComparisonError {
                required_type,
                source_type,
            } => {
                assert_eq!(required_type, "string");
                assert_eq!(source_type, "boolean");
            }
            _ => panic!("unexpected error type"),
        }
    }

    #[test]
    fn expected_invalid_numeric_string_to_number_shape_errors() {
        let err = compare_value_equality_with_type_coercion(&json!(""), &json!(0)).unwrap_err();
        match err {
            PolicyEngineError::ComparisonError {
                required_type,
                source_type,
            } => {
                assert_eq!(required_type, "string");
                assert_eq!(source_type, "number");
            }
            _ => panic!("unexpected error type"),
        }
    }
}
