//! Reference-equality and structural-sharing checks.
//!
//! These assert pointer identity with [`Value::same_value`]. A no-op update returns
//! the original reference. A real change returns a new reference but keeps
//! untouched branches shared.

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

fn obj(pairs: Vec<(&str, Value)>) -> Value {
    Value::object(pairs)
}

#[test]
fn push_empty_keeps_reference() {
    let original = Value::array(vec![Value::from("x")]);
    let result = update(&original, &Spec::push(vec![])).unwrap();
    assert!(result.same_value(&original));
}

#[test]
fn unshift_empty_keeps_reference() {
    let original = Value::array(vec![Value::from("x")]);
    let result = update(&original, &Spec::unshift(vec![])).unwrap();
    assert!(result.same_value(&original));
}

#[test]
fn splice_empty_op_keeps_reference() {
    let original = Value::array(vec![Value::from("x")]);
    let result = update(&original, &Spec::splice(vec![SpliceArgs::empty()])).unwrap();
    assert!(result.same_value(&original));
}

#[test]
fn toggle_empty_keeps_reference() {
    let original = obj(vec![("a", Value::from(false))]);
    let same = update(&original, &Spec::toggle(vec![])).unwrap();
    assert!(same.same_value(&original));

    let changed = update(&original, &Spec::toggle(vec!["a".into()])).unwrap();
    assert!(!changed.same_value(&original));
}

#[test]
fn unset_absent_key_keeps_reference() {
    let original = obj(vec![("a", Value::from(1))]);
    let same = update(&original, &Spec::unset(vec!["b".into()])).unwrap();
    assert!(same.same_value(&original));

    let changed = update(&original, &Spec::unset(vec!["a".into()])).unwrap();
    assert!(!changed.same_value(&original));
}

#[test]
fn set_same_value_keeps_reference() {
    let original = obj(vec![("a", Value::from(1))]);
    let same = update(&original, &Spec::at("a", Spec::set(Value::from(1)))).unwrap();
    assert!(same.same_value(&original));

    let changed = update(&original, &Spec::at("a", Spec::set(Value::from(2)))).unwrap();
    assert!(!changed.same_value(&original));
}

#[test]
fn merge_keeps_reference_when_unchanged() {
    // original = {a: {b: {c: true}}}
    let inner = Value::object(vec![("c", Value::from(true))]);
    let a = Value::object(vec![("b", inner.clone())]);
    let original = Value::object(vec![("a", a.clone())]);

    // Empty merge into a.
    let r1 = update(
        &original,
        &Spec::at("a", Spec::merge(Vec::<(String, Value)>::new())),
    )
    .unwrap();
    assert!(r1.same_value(&original));

    // Merge the same nested reference back.
    let r2 = update(
        &original,
        &Spec::at("a", Spec::merge(vec![("b".to_string(), inner.clone())])),
    )
    .unwrap();
    assert!(r2.same_value(&original));

    // Merge a primitive of the same value into a.b.
    let r3 = update(
        &original,
        &Spec::at(
            "a",
            Spec::at("b", Spec::merge(vec![("c".to_string(), Value::from(true))])),
        ),
    )
    .unwrap();
    assert!(r3.same_value(&original));

    // Merge a new deeply-equal object: different reference, so a real change.
    let r4 = update(
        &original,
        &Spec::at(
            "a",
            Spec::merge(vec![(
                "b".to_string(),
                Value::object(vec![("c", Value::from(true))]),
            )]),
        ),
    )
    .unwrap();
    assert!(!r4.same_value(&original));

    // Merge one new key: a real change.
    let r5 = update(
        &original,
        &Spec::at(
            "a",
            Spec::merge(vec![
                ("b".to_string(), inner.clone()),
                ("c".to_string(), Value::from(false)),
            ]),
        ),
    )
    .unwrap();
    assert!(!r5.same_value(&original));
}

#[test]
fn apply_identity_keeps_reference() {
    let inner = Value::object(vec![("b", Value::object(Vec::<(String, Value)>::new()))]);
    let original = Value::object(vec![("a", inner)]);

    let same = update(&original, &Spec::at("a", Spec::apply(|v| v.clone()))).unwrap();
    assert!(same.same_value(&original));

    let changed = update(
        &original,
        &Spec::at(
            "a",
            Spec::apply(|_| Value::object(vec![("v", Value::from(4))])),
        ),
    )
    .unwrap();
    assert!(!changed.same_value(&original));
}

#[test]
fn direct_apply_nested_identity_keeps_reference() {
    let inner = Value::object(vec![("b", Value::object(Vec::<(String, Value)>::new()))]);
    let original = Value::object(vec![("a", inner)]);

    let same = update(
        &original,
        &Spec::at("a", Spec::Apply(std::rc::Rc::new(|v: &Value| v.clone()))),
    )
    .unwrap();
    assert!(same.same_value(&original));
}

#[test]
fn deep_update_shares_untouched_branches() {
    // original = {a: {b: 1}, c: {d: {e: 1}}}
    let a = Value::object(vec![("b", Value::from(1))]);
    let d = Value::object(vec![("e", Value::from(1))]);
    let c = Value::object(vec![("d", d.clone())]);
    let original = Value::object(vec![("a", a.clone()), ("c", c.clone())]);

    // No-op set keeps the top and all children.
    let noop = update(
        &original,
        &Spec::at("a", Spec::at("b", Spec::set(Value::from(1)))),
    )
    .unwrap();
    assert!(noop.same_value(&original));
    let Value::Object(noop_map) = &noop else {
        unreachable!()
    };
    assert!(noop_map["a"].same_value(&a));

    // Change a.b. The c branch stays shared, the a branch changes.
    let changed = update(
        &original,
        &Spec::at("a", Spec::at("b", Spec::set(Value::from(2)))),
    )
    .unwrap();
    assert!(!changed.same_value(&original));

    let Value::Object(changed_map) = &changed else {
        unreachable!()
    };
    assert!(!changed_map["a"].same_value(&a));
    assert!(changed_map["c"].same_value(&c));

    let Value::Object(c_map) = &changed_map["c"] else {
        unreachable!()
    };
    assert!(c_map["d"].same_value(&d));
}

#[test]
fn combined_spec_keeps_both_branches() {
    let a = Value::object(vec![("b", Value::from(1))]);
    let d = Value::object(vec![("e", Value::from(1))]);
    let c = Value::object(vec![("d", d.clone())]);
    let original = Value::object(vec![("a", a.clone()), ("c", c.clone())]);

    // Two no-op sets in one spec keep the whole tree.
    let spec = Spec::object(vec![
        immutability_helper::SpecEntry::Child {
            key: "a".into(),
            spec: Spec::at("b", Spec::set(Value::from(1))),
        },
        immutability_helper::SpecEntry::Child {
            key: "c".into(),
            spec: Spec::at("d", Spec::at("e", Spec::set(Value::from(1)))),
        },
    ]);
    let result = update(&original, &spec).unwrap();
    assert!(result.same_value(&original));
}
