use crate::match_utils::coerce_value_to_number;
use serde_json::Value;

#[derive(Debug, thiserror::Error)]
pub enum CoerceError {
    #[error("cannot coerce {from:?} to {to}")]
    CannotCoerce { from: Value, to: &'static str },
}

pub fn coerce_value_to_shape(a: Value, b: &Value) -> Result<Value, CoerceError> {
    match b {
        // b is null. Attempt to coerce a to null.
        Value::Null => match a {
            Value::Null => Ok(Value::Null),
            _ => Err(CoerceError::CannotCoerce {
                from: a,
                to: "null",
            }),
        },

        // b is a bool. Attempt to coerce a to a bool.
        Value::Bool(_) => match a {
            Value::Bool(x) => Ok(Value::Bool(x)),

            Value::String(s) => match s.as_str() {
                "true" => Ok(Value::Bool(true)),
                "false" => Ok(Value::Bool(false)),
                _ => Err(CoerceError::CannotCoerce {
                    from: Value::String(s),
                    to: "bool",
                }),
            },

            other => Err(CoerceError::CannotCoerce {
                from: other,
                to: "bool",
            }),
        },

        // b is a number. Attempt to coerce a to a number.
        Value::Number(_) => coerce_value_to_number::coerce_value_to_number(a),

        // b is a string. Attempt to coerce a to a string.
        Value::String(_) => match a {
            Value::String(s) => Ok(Value::String(s)),
            other => Ok(Value::String(other.to_string())),
        },

        Value::Array(_) => match a {
            Value::Array(arr) => Ok(Value::Array(arr)),
            other => Err(CoerceError::CannotCoerce {
                from: other,
                to: "array",
            }),
        },

        Value::Object(_) => match a {
            Value::Object(map) => Ok(Value::Object(map)),
            other => Err(CoerceError::CannotCoerce {
                from: other,
                to: "object",
            }),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::{CoerceError, coerce_value_to_shape};
    use serde_json::{Value, json};

    fn assert_cannot_coerce(err: CoerceError, to: &'static str) {
        match err {
            CoerceError::CannotCoerce {
                from: _,
                to: got_to,
            } => {
                assert_eq!(got_to, to);
            }
        }
    }

    #[test]
    fn coerce_null_shape() {
        assert_eq!(
            coerce_value_to_shape(Value::Null, &Value::Null).unwrap(),
            Value::Null
        );

        let err = coerce_value_to_shape(json!("x"), &Value::Null).unwrap_err();
        assert_cannot_coerce(err, "null");
    }

    #[test]
    fn coerce_bool_shape() {
        assert_eq!(
            coerce_value_to_shape(json!(true), &json!(false)).unwrap(),
            json!(true)
        );

        assert_eq!(
            coerce_value_to_shape(json!("true"), &json!(false)).unwrap(),
            json!(true)
        );
        assert_eq!(
            coerce_value_to_shape(json!("false"), &json!(true)).unwrap(),
            json!(false)
        );

        let err = coerce_value_to_shape(json!("notabool"), &json!(true)).unwrap_err();
        assert_cannot_coerce(err, "bool");

        let err = coerce_value_to_shape(json!(1), &json!(true)).unwrap_err();
        assert_cannot_coerce(err, "bool");
    }

    #[test]
    fn coerce_number_shape() {
        assert_eq!(
            coerce_value_to_shape(json!(123), &json!(0)).unwrap(),
            json!(123)
        );
        assert_eq!(
            coerce_value_to_shape(json!("3.14"), &json!(0)).unwrap(),
            json!(3.14)
        );

        let err = coerce_value_to_shape(json!(""), &json!(0)).unwrap_err();
        assert_cannot_coerce(err, "number");

        let err = coerce_value_to_shape(json!({"a": 1}), &json!(0)).unwrap_err();
        assert_cannot_coerce(err, "number");
    }

    #[test]
    fn coerce_string_shape() {
        assert_eq!(
            coerce_value_to_shape(json!("hi"), &json!("")).unwrap(),
            json!("hi")
        );

        // Non-string values should be stringified.
        assert_eq!(
            coerce_value_to_shape(json!(123), &json!("")).unwrap(),
            json!("123")
        );
        assert_eq!(
            coerce_value_to_shape(json!(true), &json!("")).unwrap(),
            json!("true")
        );
        assert_eq!(
            coerce_value_to_shape(Value::Null, &json!("")).unwrap(),
            json!("null")
        );
    }

    #[test]
    fn coerce_array_shape() {
        assert_eq!(
            coerce_value_to_shape(json!([1, 2, 3]), &json!([])).unwrap(),
            json!([1, 2, 3])
        );

        let err = coerce_value_to_shape(json!({"x": 1}), &json!([])).unwrap_err();
        assert_cannot_coerce(err, "array");
    }

    #[test]
    fn coerce_object_shape() {
        assert_eq!(
            coerce_value_to_shape(json!({"x": 1}), &json!({})).unwrap(),
            json!({"x": 1})
        );

        let err = coerce_value_to_shape(json!([1, 2, 3]), &json!({})).unwrap_err();
        assert_cannot_coerce(err, "object");
    }
}
