use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;

use kanidm_proto::internal::Filter as ProtoFilter;

use crate::filter::FilterInvalid;
use crate::prelude::*;
use crate::server::ServerPhase;

#[derive(Clone, Default)]
pub struct DynGroupCache {
    insts: BTreeMap<Uuid, Filter<FilterInvalid>>,
}

pub struct DynGroup;

impl DynGroup {
    /// Determine if any dynamic groups changed as part of this operation.
    #[allow(clippy::too_many_arguments)]
    fn apply_dyngroup_change(
        qs: &mut QueryServerWriteTransaction,
        // The uuids that are affected by the dyngroup change. This is both addition
        // and removal of the uuids as members.
        affected_uuids: &mut BTreeSet<Uuid>,
        // If we should error when a dyngroup we thought should be cached is in fact,
        // not cached.
        expect: bool,
        // The identity in use.
        ident_internal: &Identity,
        // The dyn group cache
        dyn_groups: &mut DynGroupCache,
        // The list of dyn groups that were in the change set
        n_dyn_groups: &[&Entry<EntrySealed, EntryCommitted>],
    ) -> Result<(), OperationError> {
        /*
         * This triggers even if we are modifying the dyngroups account policy attributes, which
         * is allowed now. So we relax this, because systemprotection still blocks the creation
         * of dyngroups.
        if !ident.is_internal() {
            // It should be impossible to trigger this right now due to protected plugin.
            error!("It is currently an error to create a dynamic group");
            return Err(OperationError::SystemProtectedObject);
        }
        */

        if qs.get_phase() < ServerPhase::SchemaReady {
            debug!("Server is not ready to load dyngroups");
            return Ok(());
        }

        // Search all dyn groups that were involved in the operation.
        let filt = filter!(FC::Or(
            n_dyn_groups
                .iter()
                .map(|e| f_eq(Attribute::Uuid, PartialValue::Uuid(e.get_uuid())))
                .collect()
        ));
        // Load the dyn groups as a writeable set.
        let mut work_set = qs.internal_search_writeable(&filt)?;

        // Go through them all and update the groups.
        for (ref pre, ref mut nd_group) in work_set.iter_mut() {
            trace!(dyngroup_id = %nd_group.get_display_id());
            // Load the dyngroups filter
            let scope_f: ProtoFilter = nd_group
                .get_ava_single_protofilter(Attribute::DynGroupFilter)
                .cloned()
                .ok_or_else(|| {
                    error!("Missing {}", Attribute::DynGroupFilter);
                    OperationError::InvalidEntryState
                })?;

            let scope_i = Filter::from_rw(ident_internal, &scope_f, qs).map_err(|e| {
                error!("{} validation failed {:?}", Attribute::DynGroupFilter, e);
                e
            })?;

            trace!(dyngroup_filter = ?scope_i);

            let uuid = pre.get_uuid();
            // Add our uuid as affected.
            affected_uuids.insert(uuid);

            // Apply the filter and get all the uuids that are members of this dyngroup.
            let entries = qs.internal_search(scope_i.clone()).map_err(|e| {
                error!("internal search failure -> {:?}", e);
                e
            })?;

            trace!(entries_len = %entries.len());

            let members = ValueSetRefer::from_iter(entries.iter().map(|e| e.get_uuid()));
            trace!(?members);

            if let Some(uuid_iter) = members.as_ref().and_then(|a| a.as_ref_uuid_iter()) {
                affected_uuids.extend(uuid_iter);
            }

            // Mark the former members as being affected also.
            if let Some(uuid_iter) = pre.get_ava_as_refuuid(Attribute::DynMember) {
                affected_uuids.extend(uuid_iter);
            }

            if let Some(members) = members {
                // Only set something if there is actually something to do!
                nd_group.set_ava_set(&Attribute::DynMember, members);
                // push the entries to pre/cand
            } else {
                nd_group.purge_ava(Attribute::DynMember);
            }

            // Insert it to the dyngroup cache with the parsed filter for
            // fast matching in other paths.
            if dyn_groups.insts.insert(uuid, scope_i).is_none() == expect {
                error!("{} cache uuid conflict {}", Attribute::DynGroup, uuid);
                return Err(OperationError::InvalidState);
            }
        }

        if !work_set.is_empty() {
            qs.internal_apply_writable(work_set).map_err(|e| {
                error!("Failed to commit dyngroup set {:?}", e);
                e
            })?;
        }

        Ok(())
    }

    #[instrument(level = "debug", name = "dyngroup::reload", skip_all)]
    pub fn reload(qs: &mut QueryServerWriteTransaction) -> Result<(), OperationError> {
        let ident_internal = Identity::from_internal();
        // Internal search all our definitions.
        let filt = filter!(f_eq(Attribute::Class, EntryClass::DynGroup.into()));
        let entries = qs.internal_search(filt).map_err(|e| {
            error!("internal search failure -> {:?}", e);
            e
        })?;

        let mut reload_groups = BTreeMap::default();

        for nd_group in entries.into_iter() {
            let scope_f: ProtoFilter = nd_group
                .get_ava_single_protofilter(Attribute::DynGroupFilter)
                .cloned()
                .ok_or_else(|| {
                    error!("Missing {}", Attribute::DynGroupFilter);
                    OperationError::InvalidEntryState
                })?;

            let scope_i = Filter::from_rw(&ident_internal, &scope_f, qs).map_err(|e| {
                error!("dyngroup_filter validation failed {:?}", e);
                e
            })?;

            let uuid = nd_group.get_uuid();

            if reload_groups.insert(uuid, scope_i).is_some() {
                error!("dyngroup cache uuid conflict {}", uuid);
                return Err(OperationError::InvalidState);
            }
        }

        let dyn_groups = qs.get_dyngroup_cache();
        std::mem::swap(&mut reload_groups, &mut dyn_groups.insts);

        Ok(())
    }

    #[instrument(level = "debug", name = "dyngroup::post_create", skip_all)]
    pub fn post_create(
        qs: &mut QueryServerWriteTransaction,
        cand: &[Entry<EntrySealed, EntryCommitted>],
        _ident: &Identity,
    ) -> Result<BTreeSet<Uuid>, OperationError> {
        let mut affected_uuids = BTreeSet::new();

        if qs.get_phase() < ServerPhase::SchemaReady {
            debug!("Server is not ready to apply dyngroups");
            return Ok(affected_uuids);
        }

        let ident_internal = Identity::from_internal();

        let (n_dyn_groups, entries): (Vec<&Entry<_, _>>, Vec<_>) = cand.iter().partition(|entry| {
            entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
        });

        // DANGER: Why do we have to do this? During the use of qs for internal search
        // and other operations we need qs to be mut. But when we borrow dyn groups here we
        // cause multiple borrows to occur on struct members that freaks rust out. This *IS*
        // safe however because no element of the search or write process calls the dyngroup
        // cache excepting for this plugin within a single thread, meaning that stripping the
        // lifetime here is safe since we are the sole accessor.
        let dyn_groups: &mut DynGroupCache = unsafe { &mut *(qs.get_dyngroup_cache() as *mut _) };

        // For any other entries, check if they SHOULD trigger
        // a dyn group inclusion. We do this FIRST because the new
        // dyn groups will see the created entries on an internal search
        // so we don't need to reference them.

        let mut candidate_tuples = Vec::with_capacity(cand.len());

        // Apply existing dyn_groups to entries.
        trace!(?dyn_groups.insts);
        for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
            let dg_filter_valid = dg_filter
                .validate(qs.get_schema())
                .map_err(OperationError::SchemaViolation)
                .and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;

            // Did any of our modified entries match our dyn group filter?
            let matches: Vec<_> = entries
                .iter()
                .filter_map(|e| {
                    if e.entry_match_no_index(&dg_filter_valid) {
                        Some(e.get_uuid())
                    } else {
                        None
                    }
                })
                .collect();

            // If any of them did, we retrieve the dyngroup and setup to write the new
            // members to it.
            if !matches.is_empty() {
                let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
                let mut work_set = qs.internal_search_writeable(&filt)?;

                if let Some((pre, mut d_group)) = work_set.pop() {
                    matches
                        .iter()
                        .copied()
                        .for_each(|u| d_group.add_ava(Attribute::DynMember, Value::Refer(u)));

                    // The *dyn group* isn't changing, it's that a member OF the dyn group
                    // is being added. This means the dyngroup isn't part of the set that
                    // needs update to MO, only the affected members do!

                    let pre_dynmember = pre.get_ava_refer(Attribute::DynMember);
                    let post_dynmember = d_group.get_ava_refer(Attribute::DynMember);

                    match (pre_dynmember, post_dynmember) {
                        (Some(pre_m), Some(post_m)) => {
                            // Show only the *changed* uuids.
                            affected_uuids.extend(pre_m.symmetric_difference(post_m));
                        }
                        (Some(members), None) | (None, Some(members)) => {
                            // Doesn't matter what order, just that they are affected
                            affected_uuids.extend(members);
                        }
                        (None, None) => {}
                    };

                    candidate_tuples.push((pre, d_group));
                }
            }
        }

        // Write back the new changes.
        // Write this stripe if populated.
        if !candidate_tuples.is_empty() {
            qs.internal_apply_writable(candidate_tuples).map_err(|e| {
                error!("Failed to commit dyngroup set {:?}", e);
                e
            })?;
        }

        // If we created any dyn groups, populate them now.
        //    if the event is not internal, reject (for now)

        if !n_dyn_groups.is_empty() {
            trace!("considering new dyngroups");
            Self::apply_dyngroup_change(
                qs,
                &mut affected_uuids,
                false,
                &ident_internal,
                dyn_groups,
                n_dyn_groups.as_slice(),
            )?;
        }

        Ok(affected_uuids)
    }

    #[instrument(level = "debug", name = "dyngroup::post_modify", skip_all)]
    pub fn post_modify(
        qs: &mut QueryServerWriteTransaction,
        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
        cand: &[Entry<EntrySealed, EntryCommitted>],
        _ident: &Identity,
        force_cand_updates: bool,
    ) -> Result<BTreeSet<Uuid>, OperationError> {
        let mut affected_uuids = BTreeSet::new();

        if qs.get_phase() < ServerPhase::SchemaReady {
            debug!("Server is not ready to apply dyngroups");
            return Ok(affected_uuids);
        }

        let ident_internal = Identity::from_internal();

        // Probably should be filter here instead.
        let (_, pre_entries): (Vec<&Arc<Entry<_, _>>>, Vec<_>) =
            pre_cand.iter().partition(|entry| {
                entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
            });

        let (n_dyn_groups, post_entries): (Vec<&Entry<_, _>>, Vec<_>) =
            cand.iter().partition(|entry| {
                entry.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into())
            });

        // DANGER: Why do we have to do this? During the use of qs for internal search
        // and other operations we need qs to be mut. But when we borrow dyn groups here we
        // cause multiple borrows to occur on struct members that freaks rust out. This *IS*
        // safe however because no element of the search or write process calls the dyngroup
        // cache excepting for this plugin within a single thread, meaning that stripping the
        // lifetime here is safe since we are the sole accessor.
        let dyn_groups: &mut DynGroupCache = unsafe { &mut *(qs.get_dyngroup_cache() as *mut _) };

        let mut candidate_tuples = Vec::with_capacity(dyn_groups.insts.len() + cand.len());

        // If we modified a dyngroups member or filter, re-trigger it here.
        //    if the event is not internal, reject (for now)
        // We do this *first* so that we don't accidentally include/exclude anything that
        // changed in this op.

        if !n_dyn_groups.is_empty() {
            Self::apply_dyngroup_change(
                qs,
                &mut affected_uuids,
                true,
                &ident_internal,
                dyn_groups,
                n_dyn_groups.as_slice(),
            )?;
        }

        // If we modified anything else, check if a dyngroup is affected by it's change
        // if it was a member.
        trace!(?force_cand_updates, ?dyn_groups.insts);

        for (dg_uuid, dg_filter) in dyn_groups.insts.iter() {
            let dg_filter_valid = dg_filter
                .validate(qs.get_schema())
                .map_err(OperationError::SchemaViolation)
                .and_then(|f| f.resolve(&ident_internal, None, qs.get_resolve_filter_cache()))?;

            let matches: Vec<_> = pre_entries
                .iter()
                .zip(post_entries.iter())
                .filter_map(|(pre, post)| {
                    let pre_t = pre.entry_match_no_index(&dg_filter_valid);
                    let post_t = post.entry_match_no_index(&dg_filter_valid);

                    trace!(?post_t, ?force_cand_updates, ?pre_t);

                    // There are some cases where rather than the optimisation to skip
                    // asserting membership, we need to always assert that membership. Generally
                    // this occurs in replication where if a candidate was conflicted it can
                    // trigger a membership delete, but we need to ensure it's still re-added.
                    if post_t && (force_cand_updates || !pre_t) {
                        // The entry was added
                        Some(Ok(post.get_uuid()))
                    } else if pre_t && !post_t {
                        // The entry was deleted
                        Some(Err(post.get_uuid()))
                    } else {
                        None
                    }
                })
                .collect();

            trace!(?matches);

            if !matches.is_empty() {
                let filt = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*dg_uuid)));
                let mut work_set = qs.internal_search_writeable(&filt)?;

                if let Some((pre, mut d_group)) = work_set.pop() {
                    matches.iter().copied().for_each(|choice| match choice {
                        Ok(u) => d_group.add_ava(Attribute::DynMember, Value::Refer(u)),
                        Err(u) => d_group.remove_ava(Attribute::DynMember, &PartialValue::Refer(u)),
                    });

                    // The *dyn group* isn't changing, it's that a member OF the dyn group
                    // is being added. This means the dyngroup isn't part of the set that
                    // needs update to MO, only the affected members do!
                    let pre_dynmember = pre.get_ava_refer(Attribute::DynMember);
                    let post_dynmember = d_group.get_ava_refer(Attribute::DynMember);

                    match (pre_dynmember, post_dynmember) {
                        (Some(pre_m), Some(post_m)) => {
                            // Show only the *changed* uuids.
                            affected_uuids.extend(pre_m.symmetric_difference(post_m));
                        }
                        (Some(members), None) | (None, Some(members)) => {
                            // Doesn't matter what order, just that they are affected
                            affected_uuids.extend(members);
                        }
                        (None, None) => {}
                    };

                    candidate_tuples.push((pre, d_group));
                }
            }
        }

        // Write back the new changes.
        // Write this stripe if populated.
        trace!(candidate_tuples_len = %candidate_tuples.len());
        if !candidate_tuples.is_empty() {
            qs.internal_apply_writable(candidate_tuples).map_err(|e| {
                error!("Failed to commit dyngroup set {:?}", e);
                e
            })?;
        }

        trace!(?affected_uuids);

        Ok(affected_uuids)
    }

    // No post_delete handler is needed as refint takes care of this for us.

    pub fn verify(_qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
        vec![]
    }
}

#[cfg(test)]
mod tests {
    use kanidm_proto::internal::Filter as ProtoFilter;

    use crate::prelude::*;

    const UUID_TEST_GROUP: Uuid = uuid::uuid!("7bfd9931-06c2-4608-8a46-78719bb746fe");

    #[test]
    fn test_create_dyngroup_add_new_group() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_group];
        let create = vec![e_dyn];

        run_create_test!(
            Ok(()),
            preload,
            create,
            None,
            // Need to validate it did things
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");

                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_create_dyngroup_add_matching_entry() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn];
        let create = vec![e_group];

        run_create_test!(
            Ok(()),
            preload,
            create,
            None,
            // Need to validate it did things
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");

                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_create_dyngroup_add_non_matching_entry() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "no_possible_match_to_be_found".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn];
        let create = vec![e_group];

        run_create_test!(
            Ok(()),
            preload,
            create,
            None,
            // Need to validate it did things
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
            }
        );
    }

    #[test]
    fn test_create_dyngroup_add_matching_entry_and_group() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![];
        let create = vec![e_dyn, e_group];

        run_create_test!(
            Ok(()),
            preload,
            create,
            None,
            // Need to validate it did things
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");

                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
                assert!(d_group.get_ava_set(Attribute::Member).is_none());
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_existing_dyngroup_filter_into_scope() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "no_such_entry_exists".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("test_dyngroup")
            )),
            ModifyList::new_list(vec![
                Modify::Purged("dyngroup_filter".into()),
                Modify::Present(
                    Attribute::DynGroupFilter,
                    Value::JsonFilt(ProtoFilter::Eq(
                        Attribute::Name.to_string(),
                        "testgroup".to_string()
                    ))
                )
            ]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");

                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_existing_dyngroup_filter_outof_scope() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("test_dyngroup")
            )),
            ModifyList::new_list(vec![
                Modify::Purged("dyngroup_filter".into()),
                Modify::Present(
                    Attribute::DynGroupFilter,
                    Value::JsonFilt(ProtoFilter::Eq(
                        Attribute::Name.to_string(),
                        "no_such_entry_exists".to_string()
                    ))
                )
            ]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_existing_dyngroup_member_add() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("test_dyngroup")
            )),
            ModifyList::new_list(vec![Modify::Present(
                Attribute::DynMember,
                Value::Refer(UUID_ADMIN)
            )]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");
                // We assert to refer single here because we should have "removed" uuid_admin being added
                // at all.
                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_existing_dyngroup_member_remove() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("test_dyngroup")
            )),
            ModifyList::new_list(vec![Modify::Purged(Attribute::DynMember,)]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");
                // We assert to refer single here because we should have re-added the members
                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_into_matching_entry() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("not_testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("not_testgroup")
            )),
            ModifyList::new_list(vec![
                Modify::Purged(Attribute::Name,),
                Modify::Present(Attribute::Name, Value::new_iname("testgroup"))
            ]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                let members = d_group
                    .get_ava_set(Attribute::DynMember)
                    .expect("No members on dyn group");

                assert_eq!(members.to_refer_single(), Some(UUID_TEST_GROUP));
            }
        );
    }

    #[test]
    fn test_modify_dyngroup_into_non_matching_entry() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_modify_test!(
            Ok(()),
            preload,
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
            ModifyList::new_list(vec![
                Modify::Purged(Attribute::Name,),
                Modify::Present(Attribute::Name, Value::new_iname("not_testgroup"))
            ]),
            None,
            |_| {},
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
            }
        );
    }

    #[test]
    fn test_delete_dyngroup_matching_entry() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_delete_test!(
            Ok(()),
            preload,
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
            None,
            |qs: &mut QueryServerWriteTransaction| {
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("test_dyngroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                assert!(d_group.get_ava_set(Attribute::DynMember).is_none());
            }
        );
    }

    #[test]
    fn test_delete_dyngroup_group() {
        let e_dyn = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Class, EntryClass::DynGroup.to_value()),
            (Attribute::Name, Value::new_iname("test_dyngroup")),
            (
                Attribute::DynGroupFilter,
                Value::JsonFilt(ProtoFilter::Eq(
                    Attribute::Name.to_string(),
                    "testgroup".to_string()
                ))
            )
        );

        let e_group: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Group.to_value()),
            (Attribute::Name, Value::new_iname("testgroup")),
            (Attribute::Uuid, Value::Uuid(UUID_TEST_GROUP))
        );

        let preload = vec![e_dyn, e_group];

        run_delete_test!(
            Ok(()),
            preload,
            filter!(f_eq(
                Attribute::Name,
                PartialValue::new_iname("test_dyngroup")
            )),
            None,
            |qs: &mut QueryServerWriteTransaction| {
                // Note we check memberof is empty here!
                let cands = qs
                    .internal_search(filter!(f_eq(
                        Attribute::Name,
                        PartialValue::new_iname("testgroup")
                    )))
                    .expect("Internal search failure");

                let d_group = cands.first().expect("Unable to access group.");
                assert!(d_group.get_ava_set(Attribute::MemberOf).is_none());
            }
        );
    }
}