use super::is_audit_field;
use crate::storage::Error;
use oauth2_client::re_exports::{Deserialize, Serialize};
use pbbson::bson::Bson;
use pbbson::{Model, prost::Message};

pub fn assign(target: &mut Model, source: &Model) {
    // Assign values
    for key in source.keys() {
        if is_audit_field(key) {
            continue;
        }
        target.insert(key, source.get(key));
    }

    // Unassign removed values
    for key in target.clone().keys() {
        if is_audit_field(key) {
            continue;
        }
        if !source.contains_field(key) {
            target.insert(key, Bson::Null);
        }
    }
}

pub fn assign_message<T: Message + Clone + Default + for<'a> Deserialize<'a> + Serialize>(
    target: &mut T,
    source: &T,
) -> Result<(), Error> {
    let source_model = super::model_from_message(source)?;
    let mut target_model = super::model_from_message(target)?;
    assign(&mut target_model, &source_model);
    let new_target: T = target_model.try_into().map_err(|e| Error::internal(e.to_string()))?;
    new_target.clone_into(target);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::assign;
    use pbbson::Model;
    use pbbson::bson::{Bson, DateTime, doc};

    #[test]
    fn can_assign_non_audit_fields() {
        let source = Model::from(doc! {"name": "Joe", "age": 23_u32});
        let mut target = Model::default();
        assign(&mut target, &source);
        assert_eq!(target.get("name"), Some(&Bson::String("Joe".to_string())));
        assert_eq!(target.get("age"), Some(&Bson::Int32(23)));
    }

    #[test]
    fn cannot_clobber_audit_fields() {
        let now = DateTime::now();
        let source = Model::from(doc! {"name": "Joe"});
        let mut target = Model::from(doc! {
            "createdAt": now,
            "deletedAt": now,
            "updatedAt": now,
            "createdBy": "abc",
            "deletedBy": "def",
            "updatedBy": "ghi",
        });
        assign(&mut target, &source);
        assert_eq!(target.get("createdAt"), Some(&Bson::DateTime(now)));
        assert_eq!(target.get("deletedAt"), Some(&Bson::DateTime(now)));
        assert_eq!(target.get("updatedAt"), Some(&Bson::DateTime(now)));
        assert_eq!(target.get("createdBy"), Some(&Bson::String("abc".to_string())));
        assert_eq!(target.get("deletedBy"), Some(&Bson::String("def".to_string())));
        assert_eq!(target.get("updatedBy"), Some(&Bson::String("ghi".to_string())));
    }

    #[test]
    fn can_unset_an_optional_field() {
        let mut target = Model::from(doc! {"name": "Joe", "age": 23_u32});
        let source = Model::from(doc! {"name": "Joe"});
        assign(&mut target, &source);
        assert_eq!(target.get("name"), Some(&Bson::String("Joe".to_string())));
        assert_eq!(target.get("age"), Some(&Bson::Null));
    }
}
