#![allow(dead_code)] // kept for `[[merge]]` block support — wired in a later phase

use serde_json::{Map, Value};

use super::MergeStrategy;

pub fn merge(dest: Value, src: Value, strategy: MergeStrategy) -> Value {
    match (dest, src) {
        (Value::Object(a), Value::Object(b)) => Value::Object(merge_objects(a, b, strategy)),
        (Value::Array(a), Value::Array(b)) => Value::Array(merge_arrays(a, b, strategy)),
        (dest, src) => match strategy {
            MergeStrategy::Replace => src,
            MergeStrategy::Append | MergeStrategy::Prepend => dest,
        },
    }
}

pub fn subtract(dest: Value, src: &Value) -> Value {
    match (dest, src) {
        (Value::Object(mut a), Value::Object(b)) => {
            for (key, vb) in b {
                if let Some(va) = a.remove(key) {
                    a.insert(key.clone(), subtract(va, vb));
                }
            }
            Value::Object(a)
        }
        (Value::Array(a), Value::Array(b)) => {
            Value::Array(a.into_iter().filter(|v| !b.contains(v)).collect())
        }
        (dest, _) => dest,
    }
}

fn merge_objects(
    mut a: Map<String, Value>,
    b: Map<String, Value>,
    strategy: MergeStrategy,
) -> Map<String, Value> {
    for (key, vb) in b {
        match a.remove(&key) {
            Some(va) => {
                a.insert(key, merge(va, vb, strategy));
            }
            None => {
                a.insert(key, vb);
            }
        }
    }
    a
}

fn merge_arrays(a: Vec<Value>, b: Vec<Value>, strategy: MergeStrategy) -> Vec<Value> {
    match strategy {
        MergeStrategy::Replace => b,
        MergeStrategy::Append => dedup_in_order(a.into_iter().chain(b)),
        MergeStrategy::Prepend => dedup_in_order(b.into_iter().chain(a)),
    }
}

fn dedup_in_order<I: IntoIterator<Item = Value>>(iter: I) -> Vec<Value> {
    let mut out: Vec<Value> = Vec::new();
    for v in iter {
        if !out.contains(&v) {
            out.push(v);
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::MergeStrategy::*;
    use super::*;
    use serde_json::json;

    #[test]
    fn append_array_preserves_dest_then_appends_new() {
        let out = merge(json!([1, 2, 3]), json!([3, 4, 5]), Append);
        assert_eq!(out, json!([1, 2, 3, 4, 5]));
    }

    #[test]
    fn prepend_array_puts_new_first() {
        let out = merge(json!([1, 2, 3]), json!([3, 4, 5]), Prepend);
        assert_eq!(out, json!([3, 4, 5, 1, 2]));
    }

    #[test]
    fn replace_array_discards_dest() {
        let out = merge(json!([1, 2, 3]), json!([4]), Replace);
        assert_eq!(out, json!([4]));
    }

    #[test]
    fn append_scalar_conflict_keeps_dest() {
        let out = merge(json!({"k": "old"}), json!({"k": "new"}), Append);
        assert_eq!(out, json!({"k": "old"}));
    }

    #[test]
    fn prepend_scalar_conflict_keeps_dest() {
        let out = merge(json!({"k": "old"}), json!({"k": "new"}), Prepend);
        assert_eq!(out, json!({"k": "old"}));
    }

    #[test]
    fn replace_scalar_conflict_takes_src() {
        let out = merge(json!({"k": "old"}), json!({"k": "new"}), Replace);
        assert_eq!(out, json!({"k": "new"}));
    }

    #[test]
    fn deep_merge_recurses_into_objects() {
        let dest = json!({
            "permissions": {
                "allow": ["Bash(ls:*)"],
                "deny": []
            }
        });
        let src = json!({
            "permissions": {
                "allow": ["Bash(git:*)"],
                "ask": ["Bash(rm:*)"]
            }
        });
        let out = merge(dest, src, Append);
        assert_eq!(
            out,
            json!({
                "permissions": {
                    "allow": ["Bash(ls:*)", "Bash(git:*)"],
                    "deny": [],
                    "ask": ["Bash(rm:*)"]
                }
            })
        );
    }

    #[test]
    fn new_keys_from_src_are_added_regardless_of_strategy() {
        let out = merge(json!({"a": 1}), json!({"b": 2}), Append);
        assert_eq!(out, json!({"a": 1, "b": 2}));
    }

    #[test]
    fn append_is_idempotent_on_reinstall() {
        let dest = json!({"allow": ["a", "b"]});
        let src = json!({"allow": ["b", "c"]});
        let once = merge(dest, src.clone(), Append);
        let twice = merge(once.clone(), src, Append);
        assert_eq!(once, twice);
    }

    #[test]
    fn subtract_removes_src_array_entries() {
        let out = subtract(json!([1, 2, 3, 4]), &json!([2, 4]));
        assert_eq!(out, json!([1, 3]));
    }

    #[test]
    fn subtract_recurses_into_nested_objects() {
        let dest = json!({
            "permissions": {
                "allow": ["a", "b", "c"]
            },
            "untouched": "x"
        });
        let src = json!({
            "permissions": {
                "allow": ["b"]
            }
        });
        let out = subtract(dest, &src);
        assert_eq!(
            out,
            json!({
                "permissions": {
                    "allow": ["a", "c"]
                },
                "untouched": "x"
            })
        );
    }

    #[test]
    fn subtract_leaves_scalar_conflicts_alone() {
        let out = subtract(json!({"k": "v"}), &json!({"k": "v"}));
        assert_eq!(out, json!({"k": "v"}));
    }

    #[test]
    fn install_then_subtract_round_trip() {
        let dest = json!({"allow": ["a"]});
        let src = json!({"allow": ["b", "c"]});
        let after_install = merge(dest.clone(), src.clone(), Append);
        let after_uninstall = subtract(after_install, &src);
        assert_eq!(after_uninstall, dest);
    }

    #[test]
    fn object_into_scalar_conflict_resolves_by_strategy() {
        let dest = json!({"k": "scalar"});
        let src = json!({"k": {"nested": true}});
        assert_eq!(
            merge(dest.clone(), src.clone(), Append),
            json!({"k": "scalar"})
        );
        assert_eq!(merge(dest, src, Replace), json!({"k": {"nested": true}}));
    }
}
