//! Context behavior: custom commands, isolation, and the equality escape hatch.

use std::cell::Cell;
use std::rc::Rc;

use immutability_helper::{new_context, Context, Spec, Value};

#[test]
fn context_is_constructible() {
    let ctx = Context::new();
    let result = ctx
        .update(&Value::from(1), &Spec::set(Value::from(2)))
        .unwrap();
    assert_eq!(result, Value::from(2));
}

#[test]
fn new_context_returns_fresh_context() {
    let ctx = new_context();
    let result = ctx
        .update(&Value::array(vec![]), &Spec::push(vec![Value::from(1)]))
        .unwrap();
    assert_eq!(result, Value::array(vec![Value::from(1)]));
}

#[test]
fn extend_adds_a_directive() {
    let mut ctx = Context::new();
    // $addtax: original + tax * original.
    ctx.extend("$addtax", |tax, original, _orig| {
        let tax = match tax {
            Value::Number(n) => *n,
            _ => 0.0,
        };
        let base = match original {
            Value::Number(n) => *n,
            _ => 0.0,
        };
        Ok(Value::from(base + tax * base))
    });

    let result = ctx
        .update(&Value::from(5), &Spec::custom("$addtax", Value::from(0.10)))
        .unwrap();
    assert_eq!(result, Value::Number(5.5));
}

#[test]
fn custom_command_receives_the_original_target() {
    let mut ctx = Context::new();
    let captured: Rc<Cell<bool>> = Rc::new(Cell::new(false));
    let target = Value::object(vec![("a", Value::from(1))]);
    let target_for_check = target.clone();

    let captured_clone = captured.clone();
    ctx.extend("$foobar", move |_param, original, _orig| {
        // The directive receives the original target by reference.
        captured_clone.set(original.same_value(&target_for_check));
        Ok(original.clone())
    });

    let _ = ctx
        .update(&target, &Spec::custom("$foobar", Value::Null))
        .unwrap();
    assert!(
        captured.get(),
        "directive should receive the original target"
    );
}

#[test]
fn extend_overrides_a_builtin() {
    // Registering a built-in name replaces the built-in. The custom function
    // runs in its place.
    let mut ctx = Context::new();
    ctx.extend("$push", |_param, _current, _orig| {
        Ok(Value::from("hijacked"))
    });

    let result = ctx
        .update(
            &Value::array(vec![Value::from(1)]),
            &Spec::custom("$push", Value::array(vec![Value::from(2)])),
        )
        .unwrap();
    assert_eq!(result, Value::from("hijacked"));
}

#[test]
fn extend_does_not_touch_other_contexts() {
    let mut ctx = Context::new();
    ctx.extend("$addtax", |_tax, original, _orig| Ok(original.clone()));

    // A context without the directive treats it as an unknown command and fails
    // the invalid-spec guard. A second context with the directive succeeds.
    let plain = Context::new();
    let spec = Spec::custom("$addtax", Value::from(0.10));

    assert!(plain.update(&Value::from(1), &spec).is_err());
    assert!(ctx.update(&Value::from(1), &spec).is_ok());
}

#[test]
fn nibling_directive_mixes_command_and_child() {
    // {a: {$splice: [[0, 2]]}, $merge: {b: 'you'}} on {a: [1,2,3], b: 'me'}.
    use immutability_helper::{Command, SpecEntry, SpliceArgs};

    let obj = Value::object(vec![
        (
            "a",
            Value::array(vec![Value::from(1), Value::from(2), Value::from(3)]),
        ),
        ("b", Value::from("me")),
    ]);

    let spec = Spec::object(vec![
        SpecEntry::Child {
            key: "a".into(),
            spec: Spec::splice(vec![SpliceArgs::new(0, Some(2), vec![])]),
        },
        SpecEntry::Command(Command::Merge(
            [("b".to_string(), Value::from("you"))]
                .into_iter()
                .collect(),
        )),
    ]);

    let result = Context::new().update(&obj, &spec).unwrap();
    let expected = Value::object(vec![
        ("a", Value::array(vec![Value::from(3)])),
        ("b", Value::from("you")),
    ]);
    assert_eq!(result, expected);
}

#[test]
fn is_equals_escape_hatch_collapses_deep_equal_sets() {
    // With a deep-equality comparator, setting a deeply-equal value at several
    // depths returns the original reference.
    let mut ctx = Context::new();
    ctx.set_is_equals(|a, b| a == b);

    // a = {b: {c: {d: [4, 5]}}}
    let d = Value::array(vec![Value::from(4), Value::from(5)]);
    let c = Value::object(vec![("d", d)]);
    let b = Value::object(vec![("c", c)]);
    let a = Value::object(vec![("b", b)]);

    let new_d = Value::array(vec![Value::from(4), Value::from(5)]);
    let r1 = ctx
        .update(
            &a,
            &Spec::at("b", Spec::at("c", Spec::at("d", Spec::set(new_d)))),
        )
        .unwrap();
    assert!(r1.same_value(&a));

    // Set a.b to a deeply-equal {c: {d: [4, 5]}}.
    let new_b = Value::object(vec![(
        "c",
        Value::object(vec![(
            "d",
            Value::array(vec![Value::from(4), Value::from(5)]),
        )]),
    )]);
    let r2 = ctx.update(&a, &Spec::at("b", Spec::set(new_b))).unwrap();
    assert!(r2.same_value(&a));

    // Set the whole value to a deeply-equal {b: {c: {d: [4, 5]}}}.
    let new_a = Value::object(vec![(
        "b",
        Value::object(vec![(
            "c",
            Value::object(vec![(
                "d",
                Value::array(vec![Value::from(4), Value::from(5)]),
            )]),
        )]),
    )]);
    let r3 = ctx.update(&a, &Spec::set(new_a)).unwrap();
    assert!(r3.same_value(&a));
}

#[test]
fn is_equals_escape_hatch_for_direct_apply() {
    // A direct apply that returns a deeply-equal value collapses to the input.
    let mut ctx = Context::new();
    ctx.set_is_equals(|a, b| a == b);

    let a = Value::object(vec![("b", Value::from(1))]);
    let result = ctx
        .update(
            &a,
            &Spec::apply(|_| Value::object(vec![("b", Value::from(1))])),
        )
        .unwrap();
    assert!(result.same_value(&a));
}
