//! Edge cases that the core algorithm must pin down.

use immutability_helper::{update, Spec, SpliceArgs, Value};

#[test]
fn set_undefined_adds_an_enumerable_key() {
    // update({a:1}, {b: {$set: undefined}}) => {a:1, b: undefined}, new ref.
    let original = Value::object(vec![("a", Value::from(1))]);
    let result = update(&original, &Spec::at("b", Spec::set(Value::Undefined))).unwrap();

    assert!(!result.same_value(&original));
    let Value::Object(map) = &result else {
        unreachable!()
    };
    assert_eq!(map.len(), 2);
    assert_eq!(map.get("b"), Some(&Value::Undefined));
    assert_eq!(map.get("a"), Some(&Value::from(1)));
}

#[test]
fn set_null_is_distinct_from_undefined() {
    let original = Value::object(vec![("a", Value::from(1))]);
    let with_null = update(&original, &Spec::at("b", Spec::set(Value::Null))).unwrap();
    let Value::Object(map) = &with_null else {
        unreachable!()
    };
    assert_eq!(map.get("b"), Some(&Value::Null));
    assert_ne!(map.get("b"), Some(&Value::Undefined));
}

#[test]
fn empty_spec_keeps_reference() {
    let original = Value::object(vec![("a", Value::from(1))]);
    let result = update(&original, &Spec::object(vec![])).unwrap();
    assert!(result.same_value(&original));
}

#[test]
fn unset_only_removes_own_keys() {
    // Unsetting a key that is not present is a no-op and keeps the reference.
    // This mirrors the "does not remove inherited properties" behavior: only
    // own keys are removed.
    let original = Value::object(vec![("a", Value::from(1))]);
    let result = update(&original, &Spec::unset(vec!["missing".into()])).unwrap();
    assert!(result.same_value(&original));
}

#[test]
fn array_index_update_preserves_siblings() {
    // Updating items[1] keeps the other entries intact.
    let items = Value::array(vec![
        Value::object(vec![
            ("name", Value::from("Superman")),
            ("strength", Value::from(1000)),
        ]),
        Value::object(vec![
            ("name", Value::from("Jim")),
            ("strength", Value::from(2)),
        ]),
    ]);
    let state = Value::object(vec![("items", items)]);

    let spec = Spec::at(
        "items",
        Spec::at("1", Spec::at("strength", Spec::set(Value::from(3)))),
    );
    let result = update(&state, &spec).unwrap();

    let Value::Object(top) = &result else {
        unreachable!()
    };
    let Value::Array(arr) = &top["items"] else {
        unreachable!()
    };
    let Value::Object(first) = &arr[0] else {
        unreachable!()
    };
    assert_eq!(first.get("name"), Some(&Value::from("Superman")));
    assert_eq!(first.get("strength"), Some(&Value::from(1000)));
    let Value::Object(second) = &arr[1] else {
        unreachable!()
    };
    assert_eq!(second.get("strength"), Some(&Value::from(3)));
}

#[test]
fn apply_returning_equal_new_value_is_a_new_ref() {
    // Default equality is identity, so a fresh deeply-equal object is a change.
    let original = Value::object(vec![("b", Value::from(1))]);
    let result = update(
        &original,
        &Spec::apply(|_| Value::object(vec![("b", Value::from(1))])),
    )
    .unwrap();
    assert!(!result.same_value(&original));
    assert_eq!(result, original);
}

#[test]
fn splice_leading_no_op_then_real_op() {
    let original = Value::array(vec![Value::from(1), Value::from(2), Value::from(3)]);
    let spec = Spec::splice(vec![
        SpliceArgs::empty(),
        SpliceArgs::new(0, Some(1), vec![]),
    ]);
    let result = update(&original, &spec).unwrap();
    assert_eq!(result, Value::array(vec![Value::from(2), Value::from(3)]));
}

#[test]
fn auto_array_recipe_via_custom_commands() {
    // Recreate the README $auto / $autoArray recipe with custom commands.
    // update({}, {foo: {$autoArray: {0: {$auto: {bar: {$autoArray: {$push:
    // ['x','y','z']}}}}}}}) => {foo: [{bar: ['x','y','z']}]}.
    //
    // Each recipe recurses through the same context, so the inner commands stay
    // visible. The context is shared behind an Rc so the closures can call back
    // into it.
    use std::cell::RefCell;
    use std::rc::Rc;

    use immutability_helper::Context;

    let shared: Rc<RefCell<Context>> = Rc::new(RefCell::new(Context::new()));

    let recurse = shared.clone();
    shared
        .borrow_mut()
        .extend("$auto", move |value, original, _orig| {
            let base = if matches!(original, Value::Object(_)) {
                original.clone()
            } else {
                Value::object(Vec::<(String, Value)>::new())
            };
            recurse.borrow().update_value(&base, value)
        });
    let recurse = shared.clone();
    shared
        .borrow_mut()
        .extend("$autoArray", move |value, original, _orig| {
            let base = if matches!(original, Value::Array(_)) {
                original.clone()
            } else {
                Value::array(vec![])
            };
            recurse.borrow().update_value(&base, value)
        });
    let ctx = shared.borrow();

    // Build the spec as a raw value so the custom commands parse.
    let push_spec = Value::object(vec![(
        "$push",
        Value::array(vec![Value::from("x"), Value::from("y"), Value::from("z")]),
    )]);
    let bar_auto_array = Value::object(vec![("$autoArray", push_spec)]);
    let bar_obj = Value::object(vec![("bar", bar_auto_array)]);
    let auto = Value::object(vec![("$auto", bar_obj)]);
    let index_zero = Value::object(vec![("0", auto)]);
    let foo_auto_array = Value::object(vec![("$autoArray", index_zero)]);
    let spec = Value::object(vec![("foo", foo_auto_array)]);

    let start = Value::object(Vec::<(String, Value)>::new());
    let result = ctx.update_value(&start, &spec).unwrap();

    let expected = Value::object(vec![(
        "foo",
        Value::array(vec![Value::object(vec![(
            "bar",
            Value::array(vec![Value::from("x"), Value::from("y"), Value::from("z")]),
        )])]),
    )]);
    assert_eq!(result, expected);
}
