use serde_yaml::{Mapping, Value};

use crate::query::document::{Update, UpdateOperator};

pub fn apply(update: &Update, doc: &mut Mapping) {
    for op in &update.operators {
        match op {
            UpdateOperator::Set { path, value } => set_path(doc, &path.0, value.clone()),
            UpdateOperator::Unset { path } => {
                unset_path(doc, &path.0);
            }
        }
    }
}

fn set_path(doc: &mut Mapping, segments: &[String], value: Value) {
    if segments.is_empty() {
        return;
    }
    if segments.len() == 1 {
        doc.insert(Value::String(segments[0].clone()), value);
        return;
    }
    let head_key = Value::String(segments[0].clone());
    let needs_init = !matches!(doc.get(&head_key), Some(Value::Mapping(_)));
    if needs_init {
        doc.insert(head_key.clone(), Value::Mapping(Mapping::new()));
    }
    let inner = match doc.get_mut(&head_key).unwrap() {
        Value::Mapping(m) => m,
        _ => unreachable!(),
    };
    set_path(inner, &segments[1..], value)
}

fn unset_path(doc: &mut Mapping, segments: &[String]) {
    if segments.is_empty() {
        return;
    }
    if segments.len() == 1 {
        doc.remove(Value::String(segments[0].clone()));
        return;
    }
    let head_key = Value::String(segments[0].clone());
    let inner = match doc.get_mut(&head_key) {
        Some(Value::Mapping(m)) => m,
        _ => return,
    };
    unset_path(inner, &segments[1..]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::query::document::FieldPath;

    fn build_set(path: &[&str], value: Value) -> UpdateOperator {
        UpdateOperator::Set {
            path: FieldPath(path.iter().map(|s| s.to_string()).collect()),
            value,
        }
    }

    fn build_unset(path: &[&str]) -> UpdateOperator {
        UpdateOperator::Unset {
            path: FieldPath(path.iter().map(|s| s.to_string()).collect()),
        }
    }

    fn doc(pairs: Vec<(&str, Value)>) -> Mapping {
        let mut m = Mapping::new();
        for (k, v) in pairs {
            m.insert(Value::String(k.to_string()), v);
        }
        m
    }

    fn nested(pairs: Vec<(&str, Value)>) -> Value {
        Value::Mapping(doc(pairs))
    }

    fn key(s: &str) -> Value {
        Value::String(s.into())
    }

    #[test]
    fn set_top_level_field() {
        let mut d = doc(vec![("status", "draft".into())]);
        let u = Update {
            operators: vec![build_set(&["reviewed"], Value::Bool(true))],
        };
        apply(&u, &mut d);
        assert_eq!(d.get(key("reviewed")), Some(&Value::Bool(true)));
    }

    #[test]
    fn set_replaces_existing_field() {
        let mut d = doc(vec![("status", "draft".into())]);
        let u = Update {
            operators: vec![build_set(
                &["status"],
                Value::String("published".to_string()),
            )],
        };
        apply(&u, &mut d);
        assert_eq!(
            d.get(key("status")),
            Some(&Value::String("published".into()))
        );
    }

    #[test]
    fn set_dotted_path_auto_creates_intermediates() {
        let mut d = Mapping::new();
        let u = Update {
            operators: vec![build_set(&["a", "b", "c"], Value::Number(1.into()))],
        };
        apply(&u, &mut d);
        let a = d.get(key("a")).expect("a present").as_mapping().unwrap();
        let b = a.get(key("b")).expect("b present").as_mapping().unwrap();
        assert_eq!(b.get(key("c")), Some(&Value::Number(1.into())));
    }

    #[test]
    fn set_dotted_path_extends_existing_mapping() {
        let mut d = doc(vec![("a", nested(vec![("x", 1i64.into())]))]);
        let u = Update {
            operators: vec![build_set(&["a", "y"], Value::Number(2.into()))],
        };
        apply(&u, &mut d);
        let a = d.get(key("a")).unwrap().as_mapping().unwrap();
        assert_eq!(a.get(key("x")), Some(&Value::Number(1.into())));
        assert_eq!(a.get(key("y")), Some(&Value::Number(2.into())));
    }

    #[test]
    fn set_through_scalar_replaces_with_mapping() {
        let mut d = doc(vec![("a", "scalar".into())]);
        let u = Update {
            operators: vec![build_set(&["a", "b"], Value::Number(1.into()))],
        };
        apply(&u, &mut d);
        let a = d.get(key("a")).expect("a present").as_mapping().unwrap();
        assert_eq!(a.get(key("b")), Some(&Value::Number(1.into())));
    }

    #[test]
    fn unset_existing_field() {
        let mut d = doc(vec![("status", "draft".into()), ("reviewed", true.into())]);
        let u = Update {
            operators: vec![build_unset(&["reviewed"])],
        };
        apply(&u, &mut d);
        assert!(!d.contains_key(key("reviewed")));
    }

    #[test]
    fn unset_missing_is_noop() {
        let mut d = doc(vec![("status", "draft".into())]);
        let u = Update {
            operators: vec![build_unset(&["never_existed"])],
        };
        apply(&u, &mut d);
        assert_eq!(d.get(key("status")), Some(&Value::String("draft".into())));
    }

    #[test]
    fn unset_through_non_mapping_is_noop() {
        let mut d = doc(vec![("a", "scalar".into())]);
        let u = Update {
            operators: vec![build_unset(&["a", "b"])],
        };
        apply(&u, &mut d);
        assert_eq!(d.get(key("a")), Some(&Value::String("scalar".into())));
    }

    #[test]
    fn multiple_operators_apply_in_order() {
        let mut d = doc(vec![("a", 1i64.into()), ("b", 2i64.into())]);
        let u = Update {
            operators: vec![
                build_set(&["c"], Value::Number(3.into())),
                build_unset(&["a"]),
            ],
        };
        apply(&u, &mut d);
        assert!(!d.contains_key(key("a")));
        assert_eq!(d.get(key("c")), Some(&Value::Number(3.into())));
    }
}
