//! LDAP specific operations handling components. This is where LDAP operations
//! are sent to for processing.

use std::collections::BTreeSet;
use std::iter;
use std::str::FromStr;

use compact_jwt::JwsCompact;
use kanidm_proto::constants::*;
use kanidm_proto::internal::{ApiToken, UserAuthToken};
use ldap3_proto::simple::*;
use regex::{Regex, RegexBuilder};
use std::net::IpAddr;
use tracing::trace;
use uuid::Uuid;

use crate::event::SearchEvent;
use crate::idm::event::{LdapApplicationAuthEvent, LdapAuthEvent, LdapTokenAuthEvent};
use crate::idm::server::{IdmServer, IdmServerAuthTransaction, IdmServerTransaction};
use crate::prelude::*;

// Clippy doesn't like Bind here. But proto needs unboxed ldapmsg,
// and ldapboundtoken is moved. Really, it's not too bad, every message here is pretty sucky.
#[allow(clippy::large_enum_variant)]
pub enum LdapResponseState {
    Unbind,
    Disconnect(LdapMsg),
    Bind(LdapBoundToken, LdapMsg),
    Respond(LdapMsg),
    MultiPartResponse(Vec<LdapMsg>),
    BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LdapSession {
    // Maps through and provides anon read, but allows us to check the validity
    // of the account still.
    UnixBind(Uuid),
    UserAuthToken(UserAuthToken),
    ApiToken(ApiToken),
    ApplicationPasswordBind(Uuid, Uuid),
}

#[derive(Debug, Clone)]
pub struct LdapBoundToken {
    // Used to help ID the user doing the action, makes logging nicer.
    pub spn: String,
    pub session_id: Uuid,
    // This is the effective session permission. This is generated from either:
    // * A valid anonymous bind
    // * A valid unix pw bind
    // * A valid ApiToken
    // In a way, this is a stepping stone to an "ident" but allows us to check
    // the session is still "valid" depending on it's origin.
    pub effective_session: LdapSession,
}

pub struct LdapServer {
    rootdse: LdapSearchResultEntry,
    basedn: String,
    dnre: Regex,
    binddnre: Regex,
    max_queryable_attrs: usize,
}

#[derive(Debug)]
enum LdapBindTarget {
    Account(Uuid),
    ApiToken,
    Application(String, Uuid),
}

impl LdapServer {
    pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> {
        // let ct = duration_from_epoch_now();
        let mut idms_prox_read = idms.proxy_read().await?;
        // This is the rootdse path.
        // get the domain_info item
        let domain_entry = idms_prox_read
            .qs_read
            .internal_search_uuid(UUID_DOMAIN_INFO)?;

        // Get the maximum number of queryable attributes from the domain entry
        let max_queryable_attrs = domain_entry
            .get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs)
            .map(|u| u as usize)
            .unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES);

        let basedn = domain_entry
            .get_ava_single_iutf8(Attribute::DomainLdapBasedn)
            .map(|s| s.to_string())
            .or_else(|| {
                domain_entry
                    .get_ava_single_iname(Attribute::DomainName)
                    .map(ldap_domain_to_dc)
            })
            .ok_or(OperationError::InvalidEntryState)?;

        // It is necessary to swap greed to avoid the first group "<attr>=<val>" matching the
        // next group "app=<app>", son one can use "app=app1,dc=test,dc=net" as search base:
        // Greedy (app=app1,dc=test,dc=net):
        //     Match 1      - app=app1,dc=test,dc=net
        //     Group 1      - app=app1,
        //     Group <attr> - app
        //     Group <val>  - app1
        //     Group 6      - dc=test,dc=net
        // Ungreedy (app=app1,dc=test,dc=net):
        //     Match 1      - app=app1,dc=test,dc=net
        //     Group 4      - app=app1,
        //     Group <app>  - app1
        //     Group 6      - dc=test,dc=net
        let dnre = RegexBuilder::new(
            format!("^((?P<attr>[^=,]+)=(?P<val>[^=,]+),)?(app=(?P<app>[^=,]+),)?({basedn})$")
                .as_str(),
        )
        .swap_greed(true)
        .build()
        .map_err(|_| OperationError::InvalidEntryState)?;

        let binddnre = Regex::new(
            format!("^((([^=,]+)=)?(?P<val>[^=,]+))(,app=(?P<app>[^=,]+))?(,{basedn})?$").as_str(),
        )
        .map_err(|_| OperationError::InvalidEntryState)?;

        let rootdse = LdapSearchResultEntry {
            dn: "".to_string(),
            attributes: vec![
                LdapPartialAttribute {
                    atype: ATTR_OBJECTCLASS.to_string(),
                    vals: vec!["top".as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "vendorname".to_string(),
                    vals: vec!["Kanidm Project".as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "vendorversion".to_string(),
                    vals: vec![env!("CARGO_PKG_VERSION").as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "supportedldapversion".to_string(),
                    vals: vec!["3".as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "supportedextension".to_string(),
                    vals: vec!["1.3.6.1.4.1.4203.1.11.3".as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "supportedfeatures".to_string(),
                    vals: vec!["1.3.6.1.4.1.4203.1.5.1".as_bytes().to_vec()],
                },
                LdapPartialAttribute {
                    atype: "defaultnamingcontext".to_string(),
                    vals: vec![basedn.as_bytes().to_vec()],
                },
            ],
        };

        Ok(LdapServer {
            rootdse,
            basedn,
            dnre,
            binddnre,
            max_queryable_attrs,
        })
    }

    #[instrument(level = "debug", skip_all)]
    async fn do_search(
        &self,
        idms: &IdmServer,
        sr: &SearchRequest,
        uat: &LdapBoundToken,
        source: Source,
        // eventid: &Uuid,
    ) -> Result<Vec<LdapMsg>, OperationError> {
        admin_info!("Attempt LDAP Search for {}", uat.spn);
        // If the request is "", Base, Present(Attribute::ObjectClass.into()), [], then we want the rootdse.
        if sr.base.is_empty() && sr.scope == LdapSearchScope::Base {
            admin_info!("LDAP Search success - RootDSE");
            Ok(vec![
                sr.gen_result_entry(self.rootdse.clone()),
                sr.gen_success(),
            ])
        } else {
            // We want something else apparently. Need to do some more work ...
            // Parse the operation and make sure it's sane before we start the txn.

            // This scoping returns an extra filter component.

            let (opt_attr, opt_value) = match self.dnre.captures(sr.base.as_str()) {
                Some(caps) => (
                    caps.name("attr").map(|v| v.as_str().to_string()),
                    caps.name("val").map(|v| v.as_str().to_string()),
                ),
                None => {
                    request_error!("LDAP Search failure - invalid basedn");
                    return Err(OperationError::InvalidRequestState);
                }
            };

            let req_dn = match (opt_attr, opt_value) {
                (Some(a), Some(v)) => Some((a, v)),
                (None, None) => None,
                _ => {
                    request_error!("LDAP Search failure - invalid rdn");
                    return Err(OperationError::InvalidRequestState);
                }
            };

            trace!(rdn = ?req_dn);

            // Map the Some(a,v) to ...?

            let ext_filter = match (&sr.scope, req_dn) {
                // OneLevel and Child searches are **very** similar for us because child
                // is a "subtree search excluding base". Because we don't have a tree structure at
                // all, this is the same as a one level (all children of base excluding base).
                (LdapSearchScope::Children, Some(_r)) | (LdapSearchScope::OneLevel, Some(_r)) => {
                    return Ok(vec![sr.gen_success()])
                }
                (LdapSearchScope::Children, None) | (LdapSearchScope::OneLevel, None) => {
                    // exclude domain_info
                    Some(LdapFilter::Not(Box::new(LdapFilter::Equality(
                        Attribute::Uuid.to_string(),
                        STR_UUID_DOMAIN_INFO.to_string(),
                    ))))
                }
                // because we request a specific DN, these are the same since we want the same
                // entry.
                (LdapSearchScope::Base, Some((a, v)))
                | (LdapSearchScope::Subtree, Some((a, v))) => Some(LdapFilter::Equality(a, v)),
                (LdapSearchScope::Base, None) => {
                    // domain_info
                    Some(LdapFilter::Equality(
                        Attribute::Uuid.to_string(),
                        STR_UUID_DOMAIN_INFO.to_string(),
                    ))
                }
                (LdapSearchScope::Subtree, None) => {
                    // No filter changes needed.
                    None
                }
            };

            let mut no_attrs = false;
            let mut all_attrs = false;
            let mut all_op_attrs = false;

            let attrs_len = sr.attrs.len();
            if sr.attrs.is_empty() {
                // If [], then "all" attrs
                all_attrs = true;
            } else if attrs_len < self.max_queryable_attrs {
                sr.attrs.iter().for_each(|a| {
                    if a == "*" {
                        all_attrs = true;
                    } else if a == "+" {
                        // This forces the BE to get all the attrs so we can
                        // map all vattrs.
                        all_attrs = true;
                        all_op_attrs = true;
                    } else if a == "1.1" {
                        /*
                         *  ref https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.8
                         *
                         *  A list containing only the OID "1.1" indicates that no
                         *  attributes are to be returned. If "1.1" is provided with other
                         *  attributeSelector values, the "1.1" attributeSelector is
                         *  ignored. This OID was chosen because it does not (and can not)
                         *  correspond to any attribute in use.
                         */
                        if sr.attrs.len() == 1 {
                            no_attrs = true;
                        }
                    }
                })
            } else {
                admin_error!(
                    "Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}",
                    self.max_queryable_attrs, attrs_len
                );
                return Err(OperationError::ResourceLimit);
            }

            // We need to retain this to know what the client requested.
            let (k_attrs, l_attrs) = if no_attrs {
                // Request no attributes and no mapped attributes.
                (None, Vec::with_capacity(0))
            } else if all_op_attrs {
                // We need all attrs, and we do a full v_attr map.
                (None, ldap_all_vattrs())
            } else if all_attrs {
                // We are already getting all attrs, but if there are any virtual attrs
                // we need them in our request as well.
                let req_attrs: Vec<String> = sr
                    .attrs
                    .iter()
                    .filter_map(|a| {
                        let a_lower = a.to_lowercase();

                        if ldap_vattr_map(&a_lower).is_some() {
                            Some(a_lower)
                        } else {
                            None
                        }
                    })
                    .collect();

                (None, req_attrs)
            } else {
                // What the client requested, in LDAP forms.
                let req_attrs: Vec<String> = sr
                    .attrs
                    .iter()
                    .filter_map(|a| {
                        if a == "*" || a == "+" || a == "1.1" {
                            None
                        } else {
                            Some(a.to_lowercase())
                        }
                    })
                    .collect();
                // This is what the client requested, but mapped to kanidm forms.
                // NOTE: All req_attrs are lowercase at this point.
                let mapped_attrs: BTreeSet<_> = req_attrs
                    .iter()
                    .map(|a| Attribute::from(ldap_vattr_map(a).unwrap_or(a)))
                    .collect();

                (Some(mapped_attrs), req_attrs)
            };

            admin_info!(attr = ?l_attrs, "LDAP Search Request LDAP Attrs");
            admin_info!(attr = ?k_attrs, "LDAP Search Request Mapped Attrs");

            let ct = duration_from_epoch_now();
            let mut idm_read = idms.proxy_read().await?;
            // Now start the txn - we need it for resolving filter components.

            // join the filter, with ext_filter
            let lfilter = match ext_filter {
                Some(ext) => LdapFilter::And(vec![
                    sr.filter.clone(),
                    ext,
                    LdapFilter::Not(Box::new(LdapFilter::Or(vec![
                        LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
                        LdapFilter::Equality(
                            Attribute::Class.to_string(),
                            "attributetype".to_string(),
                        ),
                        LdapFilter::Equality(
                            Attribute::Class.to_string(),
                            "access_control_profile".to_string(),
                        ),
                    ]))),
                ]),
                None => LdapFilter::And(vec![
                    sr.filter.clone(),
                    LdapFilter::Not(Box::new(LdapFilter::Or(vec![
                        LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
                        LdapFilter::Equality(
                            Attribute::Class.to_string(),
                            "attributetype".to_string(),
                        ),
                        LdapFilter::Equality(
                            Attribute::Class.to_string(),
                            "access_control_profile".to_string(),
                        ),
                    ]))),
                ]),
            };

            admin_info!(filter = ?lfilter, "LDAP Search Filter");

            // Build the event, with the permissions from effective_session
            //
            // ! Remember, searchEvent wraps to ignore hidden for us.
            let ident = idm_read
                .validate_ldap_session(&uat.effective_session, source, ct)
                .map_err(|e| {
                    admin_error!("Invalid identity: {:?}", e);
                    e
                })?;
            let se = SearchEvent::new_ext_impersonate_uuid(
                &mut idm_read.qs_read,
                ident,
                &lfilter,
                k_attrs,
            )
            .map_err(|e| {
                admin_error!("failed to create search event -> {:?}", e);
                e
            })?;

            let res = idm_read.qs_read.search_ext(&se).map_err(|e| {
                admin_error!("search failure {:?}", e);
                e
            })?;

            // These have already been fully reduced (access controls applied),
            // so we can just transform the values and open palm slam them into
            // the result structure.
            let lres: Result<Vec<_>, _> = res
                .into_iter()
                .map(|e| {
                    e.to_ldap(
                        &mut idm_read.qs_read,
                        self.basedn.as_str(),
                        all_attrs,
                        &l_attrs,
                    )
                    // if okay, wrap in a ldap msg.
                    .map(|r| sr.gen_result_entry(r))
                })
                .chain(iter::once(Ok(sr.gen_success())))
                .collect();

            let lres = lres.map_err(|e| {
                admin_error!("entry resolve failure {:?}", e);
                e
            })?;

            admin_info!(
                nentries = %lres.len(),
                "LDAP Search Success -> number of entries"
            );

            Ok(lres)
        }
    }

    async fn do_bind(
        &self,
        idms: &IdmServer,
        dn: &str,
        pw: &str,
    ) -> Result<Option<LdapBoundToken>, OperationError> {
        security_info!(
            "Attempt LDAP Bind for {}",
            if dn.is_empty() { "(empty dn)" } else { dn }
        );
        let ct = duration_from_epoch_now();

        let mut idm_auth = idms.auth().await?;
        let target = self.bind_target_from_bind_dn(&mut idm_auth, dn, pw).await?;

        let result = match target {
            LdapBindTarget::Account(uuid) => {
                let lae = LdapAuthEvent::from_parts(uuid, pw.to_string())?;
                idm_auth.auth_ldap(&lae, ct).await?
            }
            LdapBindTarget::ApiToken => {
                let jwsc = JwsCompact::from_str(pw).map_err(|err| {
                    error!(?err, "Invalid JwsCompact supplied as authentication token.");
                    OperationError::NotAuthenticated
                })?;

                let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
                idm_auth.token_auth_ldap(&lae, ct).await?
            }
            LdapBindTarget::Application(ref app_name, usr_uuid) => {
                let lae =
                    LdapApplicationAuthEvent::new(app_name.as_str(), usr_uuid, pw.to_string())?;
                idm_auth.application_auth_ldap(&lae, ct).await?
            }
        };

        idm_auth.commit()?;

        if result.is_some() {
            security_info!(
                "✅ LDAP Bind success for {} -> {:?}",
                if dn.is_empty() { "(empty dn)" } else { dn },
                target
            );
        } else {
            security_info!(
                "❌ LDAP Bind failure for {} -> {:?}",
                if dn.is_empty() { "(empty dn)" } else { dn },
                target
            );
        }

        Ok(result)
    }

    #[instrument(level = "debug", skip_all)]
    async fn do_compare(
        &self,
        idms: &IdmServer,
        cr: &CompareRequest,
        uat: &LdapBoundToken,
        source: Source,
    ) -> Result<Vec<LdapMsg>, OperationError> {
        admin_info!("Attempt LDAP CompareRequest for {}", uat.spn);

        let (opt_attr, opt_value) = match self.dnre.captures(cr.entry.as_str()) {
            Some(caps) => (
                caps.name("attr").map(|v| v.as_str().to_string()),
                caps.name("val").map(|v| v.as_str().to_string()),
            ),
            None => {
                request_error!("LDAP Search failure - invalid basedn");
                return Err(OperationError::InvalidRequestState);
            }
        };

        let ext_filter = match (opt_attr, opt_value) {
            (Some(a), Some(v)) => LdapFilter::Equality(a, v),
            _ => {
                request_error!("LDAP Search failure - invalid rdn");
                return Err(OperationError::InvalidRequestState);
            }
        };

        let ct = duration_from_epoch_now();
        let mut idm_read = idms.proxy_read().await?;
        // Now start the txn - we need it for resolving filter components.

        // join the filter, with ext_filter
        let lfilter = LdapFilter::And(vec![
            ext_filter.clone(),
            LdapFilter::Equality(cr.atype.clone(), cr.val.clone()),
            LdapFilter::Not(Box::new(LdapFilter::Or(vec![
                LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
                LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
                LdapFilter::Equality(
                    Attribute::Class.to_string(),
                    "access_control_profile".to_string(),
                ),
            ]))),
        ]);

        admin_info!(filter = ?lfilter, "LDAP Compare Filter");

        // Build the event, with the permissions from effective_session
        let ident = idm_read
            .validate_ldap_session(&uat.effective_session, source, ct)
            .map_err(|e| {
                admin_error!("Invalid identity: {:?}", e);
                e
            })?;

        let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
        let filter_orig = f
            .validate(idm_read.qs_read.get_schema())
            .map_err(OperationError::SchemaViolation)?;
        let filter = filter_orig.clone().into_ignore_hidden();

        let ee = ExistsEvent {
            ident: ident.clone(),
            filter,
            filter_orig,
        };

        let res = idm_read.qs_read.exists(&ee).map_err(|e| {
            admin_error!("call to exists failure {:?}", e);
            e
        })?;

        if res {
            admin_info!("LDAP Compare -> True");
            return Ok(vec![cr.gen_compare_true()]);
        }

        // we need to check if the entry exists at all (without the ava).
        let lfilter = LdapFilter::And(vec![
            ext_filter,
            LdapFilter::Not(Box::new(LdapFilter::Or(vec![
                LdapFilter::Equality(Attribute::Class.to_string(), "classtype".to_string()),
                LdapFilter::Equality(Attribute::Class.to_string(), "attributetype".to_string()),
                LdapFilter::Equality(
                    Attribute::Class.to_string(),
                    "access_control_profile".to_string(),
                ),
            ]))),
        ]);
        let f = Filter::from_ldap_ro(&ident, &lfilter, &mut idm_read.qs_read)?;
        let filter_orig = f
            .validate(idm_read.qs_read.get_schema())
            .map_err(OperationError::SchemaViolation)?;
        let filter = filter_orig.clone().into_ignore_hidden();
        let ee = ExistsEvent {
            ident,
            filter,
            filter_orig,
        };

        let res = idm_read.qs_read.exists(&ee).map_err(|e| {
            admin_error!("call to exists failure {:?}", e);
            e
        })?;

        if res {
            admin_info!("LDAP Compare -> False");
            return Ok(vec![cr.gen_compare_false()]);
        }

        Ok(vec![
            cr.gen_error(LdapResultCode::NoSuchObject, "".to_string())
        ])
    }

    pub async fn do_op(
        &self,
        idms: &IdmServer,
        server_op: ServerOps,
        uat: Option<LdapBoundToken>,
        ip_addr: IpAddr,
        eventid: Uuid,
    ) -> Result<LdapResponseState, OperationError> {
        let source = Source::Ldaps(ip_addr);

        match server_op {
            ServerOps::SimpleBind(sbr) => self
                .do_bind(idms, sbr.dn.as_str(), sbr.pw.as_str())
                .await
                .map(|r| match r {
                    Some(lbt) => LdapResponseState::Bind(lbt, sbr.gen_success()),
                    None => LdapResponseState::Respond(sbr.gen_invalid_cred()),
                })
                .or_else(|e| {
                    let (rc, msg) = operationerr_to_ldapresultcode(e);
                    Ok(LdapResponseState::Respond(sbr.gen_error(rc, msg)))
                }),
            ServerOps::Search(sr) => match uat {
                Some(u) => self
                    .do_search(idms, &sr, &u, source)
                    .await
                    .map(LdapResponseState::MultiPartResponse)
                    .or_else(|e| {
                        let (rc, msg) = operationerr_to_ldapresultcode(e);
                        Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
                    }),
                None => {
                    // Search can occur without a bind, so bind first.
                    // This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
                    let lbt = match self.do_bind(idms, "", "").await {
                        Ok(Some(lbt)) => lbt,
                        Ok(None) => {
                            return Ok(LdapResponseState::Respond(
                                sr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
                            ))
                        }
                        Err(e) => {
                            let (rc, msg) = operationerr_to_ldapresultcode(e);
                            return Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)));
                        }
                    };
                    // If okay, do the search.
                    self.do_search(idms, &sr, &lbt, Source::Internal)
                        .await
                        .map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
                        .or_else(|e| {
                            let (rc, msg) = operationerr_to_ldapresultcode(e);
                            Ok(LdapResponseState::Respond(sr.gen_error(rc, msg)))
                        })
                }
            },
            ServerOps::Unbind(_) => {
                // No need to notify on unbind (per rfc4511)
                Ok(LdapResponseState::Unbind)
            }
            ServerOps::Compare(cr) => match uat {
                Some(u) => self
                    .do_compare(idms, &cr, &u, source)
                    .await
                    .map(LdapResponseState::MultiPartResponse)
                    .or_else(|e| {
                        let (rc, msg) = operationerr_to_ldapresultcode(e);
                        Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
                    }),
                None => {
                    // Compare can occur without a bind, so bind first.
                    // This is per section 4 of RFC 4513 (https://www.rfc-editor.org/rfc/rfc4513#section-4).
                    let lbt = match self.do_bind(idms, "", "").await {
                        Ok(Some(lbt)) => lbt,
                        Ok(None) => {
                            return Ok(LdapResponseState::Respond(
                                cr.gen_error(LdapResultCode::InvalidCredentials, "".to_string()),
                            ))
                        }
                        Err(e) => {
                            let (rc, msg) = operationerr_to_ldapresultcode(e);
                            return Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)));
                        }
                    };
                    // If okay, do the compare.
                    self.do_compare(idms, &cr, &lbt, Source::Internal)
                        .await
                        .map(|r| LdapResponseState::BindMultiPartResponse(lbt, r))
                        .or_else(|e| {
                            let (rc, msg) = operationerr_to_ldapresultcode(e);
                            Ok(LdapResponseState::Respond(cr.gen_error(rc, msg)))
                        })
                }
            },
            ServerOps::Whoami(wr) => match uat {
                Some(u) => Ok(LdapResponseState::Respond(
                    wr.gen_success(format!("u: {}", u.spn).as_str()),
                )),
                None => Ok(LdapResponseState::Respond(
                    wr.gen_operror(format!("Unbound Connection {eventid}").as_str()),
                )),
            },
        } // end match server op
    }

    async fn bind_target_from_bind_dn(
        &self,
        idm_auth: &mut IdmServerAuthTransaction<'_>,
        dn: &str,
        pw: &str,
    ) -> Result<LdapBindTarget, OperationError> {
        if dn.is_empty() {
            if pw.is_empty() {
                return Ok(LdapBindTarget::Account(UUID_ANONYMOUS));
            } else {
                // This is the path to access api-token logins.
                return Ok(LdapBindTarget::ApiToken);
            }
        } else if dn == "dn=token" {
            // Is the passed dn requesting token auth?
            // We use dn= here since these are attr=value, and dn is a phantom so it will
            // never be present or match a real value. We also make it an ava so that clients
            // that over-zealously validate dn syntax are happy.
            return Ok(LdapBindTarget::ApiToken);
        }

        if let Some(captures) = self.binddnre.captures(dn) {
            if let Some(usr) = captures.name("val") {
                let usr = usr.as_str();

                if usr.is_empty() {
                    error!("Failed to parse user name from bind DN, it is empty (capture group is {:#?})", captures.name("val"));
                    return Err(OperationError::NoMatchingEntries);
                }

                let usr_uuid = idm_auth.qs_read.name_to_uuid(usr).map_err(|e| {
                    error!(err = ?e, ?usr, "Error resolving rdn to target");
                    e
                })?;

                if let Some(app) = captures.name("app") {
                    let app = app.as_str();

                    if app.is_empty() {
                        error!("Failed to parse application name from bind DN, it is empty (capture group is {:#?})", captures.name("app"));
                        return Err(OperationError::NoMatchingEntries);
                    }

                    return Ok(LdapBindTarget::Application(app.to_string(), usr_uuid));
                }

                return Ok(LdapBindTarget::Account(usr_uuid));
            }
        }

        error!(
            "Failed to parse bind DN, no captures. Bind DN was {:?})",
            dn
        );
        Err(OperationError::NoMatchingEntries)
    }
}

fn ldap_domain_to_dc(input: &str) -> String {
    let mut output: String = String::new();
    input.split('.').for_each(|dc| {
        output.push_str("dc=");
        output.push_str(dc);
        #[allow(clippy::single_char_pattern, clippy::single_char_add_str)]
        output.push_str(",");
    });
    // Remove the last ','
    output.pop();
    output
}

fn operationerr_to_ldapresultcode(e: OperationError) -> (LdapResultCode, String) {
    match e {
        OperationError::InvalidRequestState => {
            (LdapResultCode::ConstraintViolation, "".to_string())
        }
        OperationError::InvalidAttributeName(s) | OperationError::InvalidAttribute(s) => {
            (LdapResultCode::InvalidAttributeSyntax, s)
        }
        OperationError::SchemaViolation(se) => {
            (LdapResultCode::UnwillingToPerform, format!("{se:?}"))
        }
        e => (LdapResultCode::Other, format!("{e:?}")),
    }
}

#[inline]
pub(crate) fn ldap_all_vattrs() -> Vec<String> {
    vec![
        ATTR_CN.to_string(),
        ATTR_EMAIL.to_string(),
        ATTR_LDAP_EMAIL_ADDRESS.to_string(),
        LDAP_ATTR_DN.to_string(),
        LDAP_ATTR_EMAIL_ALTERNATIVE.to_string(),
        LDAP_ATTR_EMAIL_PRIMARY.to_string(),
        LDAP_ATTR_ENTRYDN.to_string(),
        LDAP_ATTR_ENTRYUUID.to_string(),
        LDAP_ATTR_KEYS.to_string(),
        LDAP_ATTR_MAIL_ALTERNATIVE.to_string(),
        LDAP_ATTR_MAIL_PRIMARY.to_string(),
        ATTR_OBJECTCLASS.to_string(),
        ATTR_LDAP_SSHPUBLICKEY.to_string(),
        ATTR_UIDNUMBER.to_string(),
        ATTR_UID.to_string(),
        ATTR_GECOS.to_string(),
    ]
}

#[inline]
pub(crate) fn ldap_vattr_map(input: &str) -> Option<&str> {
    // ⚠️  WARNING ⚠️
    // If you modify this list you MUST add these values to
    // corresponding phantom attributes in the schema to prevent
    // incorrect future or duplicate usage.
    //
    //   LDAP NAME     KANI ATTR SOURCE NAME
    match input {
        // EntryDN and DN have special handling in to_ldap in Entry. However, we
        // need to map them to "name" so that if the user has requested dn/entrydn
        // only, then we still requested at least one attribute from the backend
        // allowing the access control tests to take place. Otherwise no entries
        // would be returned.
        ATTR_CN | ATTR_UID | LDAP_ATTR_ENTRYDN | LDAP_ATTR_DN => Some(ATTR_NAME),
        ATTR_GECOS => Some(ATTR_DISPLAYNAME),
        ATTR_EMAIL => Some(ATTR_MAIL),
        ATTR_LDAP_EMAIL_ADDRESS => Some(ATTR_MAIL),
        LDAP_ATTR_EMAIL_ALTERNATIVE => Some(ATTR_MAIL),
        LDAP_ATTR_EMAIL_PRIMARY => Some(ATTR_MAIL),
        LDAP_ATTR_ENTRYUUID => Some(ATTR_UUID),
        LDAP_ATTR_KEYS => Some(ATTR_SSH_PUBLICKEY),
        LDAP_ATTR_MAIL_ALTERNATIVE => Some(ATTR_MAIL),
        LDAP_ATTR_MAIL_PRIMARY => Some(ATTR_MAIL),
        ATTR_OBJECTCLASS => Some(ATTR_CLASS),
        ATTR_LDAP_SSHPUBLICKEY => Some(ATTR_SSH_PUBLICKEY), // no-underscore -> underscore
        ATTR_UIDNUMBER => Some(ATTR_GIDNUMBER),             // yes this is intentional
        _ => None,
    }
}

#[inline]
pub(crate) fn ldap_attr_filter_map(input: &str) -> Attribute {
    let a_lower = input.to_lowercase();
    Attribute::from(ldap_vattr_map(&a_lower).unwrap_or(a_lower.as_str()))
}

#[cfg(test)]
mod tests {
    use crate::prelude::*;

    use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
    use hashbrown::HashSet;
    use kanidm_proto::internal::ApiToken;
    use ldap3_proto::proto::{
        LdapFilter, LdapMsg, LdapOp, LdapResultCode, LdapSearchScope, LdapSubstringFilter,
    };
    use ldap3_proto::simple::*;

    use super::{LdapServer, LdapSession};
    use crate::idm::application::GenerateApplicationPasswordEvent;
    use crate::idm::event::{LdapApplicationAuthEvent, UnixPasswordChangeEvent};
    use crate::idm::serviceaccount::GenerateApiTokenEvent;

    const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";

    #[idm_test]
    async fn test_ldap_simple_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        // make the admin a valid posix account
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Name, PartialValue::new_iname("admin"))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::Class, EntryClass::PosixAccount.into()),
                Modify::Present(Attribute::GidNumber, Value::new_uint32(2001)),
            ]),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());

        let pce = UnixPasswordChangeEvent::new_internal(UUID_ADMIN, TEST_PASSWORD);

        assert!(idms_prox_write.set_unix_account_password(&pce).is_ok());
        assert!(idms_prox_write.commit().is_ok()); // Committing all configs

        // default UNIX_PW bind (default is set to true)
        // Hence allows all unix binds
        let admin_t = ldaps
            .do_bind(idms, "admin", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, "admin@example.com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));

        // Setting UNIX_PW_BIND flag to false:
        // Hence all of the below authentication will fail (asserts are still satisfied)
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let disallow_unix_pw_flag = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
            ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(false)),
        );
        assert!(idms_prox_write
            .qs_write
            .modify(&disallow_unix_pw_flag)
            .is_ok());
        assert!(idms_prox_write.commit().is_ok());
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );
        assert!(
            ldaps.do_bind(idms, "", "test").await.unwrap_err() == OperationError::NotAuthenticated
        );
        let admin_t = ldaps.do_bind(idms, "admin", TEST_PASSWORD).await.unwrap();
        assert!(admin_t.is_none());

        // Setting UNIX_PW_BIND flag to true :
        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        let allow_unix_pw_flag = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
            ModifyList::new_purge_and_set(Attribute::LdapAllowUnixPwBind, Value::Bool(true)),
        );
        assert!(idms_prox_write.qs_write.modify(&allow_unix_pw_flag).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        // Now test the admin and various DN's
        let admin_t = ldaps
            .do_bind(idms, "admin", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, "admin@example.com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, "name=admin,dc=example,dc=com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(
                idms,
                "spn=admin@example.com,dc=example,dc=com",
                TEST_PASSWORD,
            )
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(
                idms,
                format!("uuid={STR_UUID_ADMIN},dc=example,dc=com").as_str(),
                TEST_PASSWORD,
            )
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));

        let admin_t = ldaps
            .do_bind(idms, "name=admin", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, "spn=admin@example.com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(
                idms,
                format!("uuid={STR_UUID_ADMIN}").as_str(),
                TEST_PASSWORD,
            )
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));

        let admin_t = ldaps
            .do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(idms, "admin@example.com,dc=example,dc=com", TEST_PASSWORD)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));
        let admin_t = ldaps
            .do_bind(
                idms,
                format!("{STR_UUID_ADMIN},dc=example,dc=com").as_str(),
                TEST_PASSWORD,
            )
            .await
            .unwrap()
            .unwrap();
        assert_eq!(admin_t.effective_session, LdapSession::UnixBind(UUID_ADMIN));

        // Bad password, check last to prevent softlocking of the admin account.
        assert!(ldaps
            .do_bind(idms, "admin", "test")
            .await
            .unwrap()
            .is_none());

        // Non-existent and invalid DNs
        assert!(ldaps
            .do_bind(
                idms,
                "spn=admin@example.com,dc=clownshoes,dc=example,dc=com",
                TEST_PASSWORD
            )
            .await
            .is_err());
        assert!(ldaps
            .do_bind(
                idms,
                "spn=claire@example.com,dc=example,dc=com",
                TEST_PASSWORD
            )
            .await
            .is_err());
        assert!(ldaps
            .do_bind(idms, ",dc=example,dc=com", TEST_PASSWORD)
            .await
            .is_err());
        assert!(ldaps
            .do_bind(idms, "dc=example,dc=com", TEST_PASSWORD)
            .await
            .is_err());

        assert!(ldaps.do_bind(idms, "claire", "test").await.is_err());
    }

    #[idm_test]
    async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let testdn = format!("app=app1,{0}", ldaps.basedn);
        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
        assert!(captures.name("app").is_some());
        assert!(captures.name("attr").is_none());
        assert!(captures.name("val").is_none());

        let testdn = format!("uid=foo,app=app1,{0}", ldaps.basedn);
        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
        assert!(captures.name("app").is_some());
        assert!(captures.name("attr").is_some());
        assert!(captures.name("val").is_some());

        let testdn = format!("uid=foo,{0}", ldaps.basedn);
        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
        assert!(captures.name("app").is_none());
        assert!(captures.name("attr").is_some());
        assert!(captures.name("val").is_some());
    }

    #[idm_test]
    async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let grp_uuid = Uuid::new_v4();
        let app_uuid = Uuid::new_v4();
        let app_name = "testapp1";

        // Setup person, group and application
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname("testgroup1")),
                (Attribute::Uuid, Value::Uuid(grp_uuid))
            );

            let e3 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname(app_name)),
                (Attribute::Uuid, Value::Uuid(app_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1, e2, e3])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // Searches under application base DN must show same content
        let sr = SearchRequest {
            msgid: 1,
            base: format!("app={app_name},dc=example,dc=com"),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec!["*".to_string()],
        };

        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec!["*".to_string()],
        };

        let r2 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();
        assert!(!r1.is_empty());
        assert_eq!(r1.len(), r2.len());
    }

    #[idm_test]
    async fn test_ldap_spn_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let usr_name = "panko";

        // Setup person, group and application
        {
            let e1: Entry<EntryInit, EntryNew> = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname(usr_name)),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::DisplayName, Value::new_utf8s(usr_name))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // Searching a malformed spn shouldn't cause the query to fail
        let sr = SearchRequest {
            msgid: 1,
            base: format!("dc=example,dc=com"),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Or(vec![
                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
            ]),
            attrs: vec!["*".to_string()],
        };

        let result = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .map(|r| {
                r.into_iter()
                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
                    .collect::<Vec<_>>()
            })
            .unwrap();

        assert!(!result.is_empty());

        let sr = SearchRequest {
            msgid: 1,
            base: format!("dc=example,dc=com"),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::And(vec![
                LdapFilter::Equality(Attribute::Name.to_string(), usr_name.to_string()),
                LdapFilter::Equality(Attribute::Spn.to_string(), usr_name.to_string()),
            ]),
            attrs: vec!["*".to_string()],
        };

        let empty_result = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .map(|r| {
                r.into_iter()
                    .filter(|r| matches!(r.op, LdapOp::SearchResultEntry(_)))
                    .collect::<Vec<_>>()
            })
            .unwrap();

        assert!(empty_result.is_empty());
    }

    #[idm_test]
    async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let grp_uuid = Uuid::new_v4();
        let app_uuid = Uuid::new_v4();

        // Setup person, group and application
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname("testgroup1")),
                (Attribute::Uuid, Value::Uuid(grp_uuid))
            );

            let e3 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname("testapp1")),
                (Attribute::Uuid, Value::Uuid(app_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1, e2, e3])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // No session, user not member of linked group
        let res = ldaps
            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());

        {
            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(idms_prox_write
                .qs_write
                .internal_modify_uuid(grp_uuid, &ml)
                .is_ok());
            assert!(idms_prox_write.commit().is_ok());
        }

        // No session, user does not have app password for testapp1
        let res = ldaps
            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());

        let pass1: String;
        let pass2: String;
        let pass3: String;
        {
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app_uuid,
                "apppwd1".to_string(),
            );
            pass1 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");

            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app_uuid,
                "apppwd2".to_string(),
            );
            pass2 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");

            assert!(idms_prox_write.commit().is_ok());

            // Application password overwritten on duplicated label
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app_uuid,
                "apppwd2".to_string(),
            );
            pass3 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");
            assert!(idms_prox_write.commit().is_ok());
        }

        // Got session, app password valid
        let res = ldaps
            .do_bind(
                idms,
                "spn=testperson1,app=testapp1,dc=example,dc=com",
                pass1.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_some());

        // No session, app password overwritten
        let res = ldaps
            .do_bind(
                idms,
                "spn=testperson1,app=testapp1,dc=example,dc=com",
                pass2.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());

        // Got session, app password overwritten
        let res = ldaps
            .do_bind(
                idms,
                "spn=testperson1,app=testapp1,dc=example,dc=com",
                pass3.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_some());

        // No session, invalid app password
        let res = ldaps
            .do_bind(
                idms,
                "spn=testperson1,app=testapp1,dc=example,dc=com",
                "FOO",
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());
    }

    #[idm_test]
    async fn test_ldap_application_linked_group(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let usr_name = "testuser1";

        let grp1_uuid = Uuid::new_v4();
        let grp1_name = "testgroup1";
        let grp2_uuid = Uuid::new_v4();
        let grp2_name = "testgroup2";

        let app1_uuid = Uuid::new_v4();
        let app1_name = "testapp1";
        let app2_uuid = Uuid::new_v4();
        let app2_name = "testapp2";

        // Setup person, groups and applications
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname(usr_name)),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::Description, Value::new_utf8s(usr_name)),
                (Attribute::DisplayName, Value::new_utf8s(usr_name))
            );

            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname(grp1_name)),
                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
                (Attribute::Member, Value::Refer(usr_uuid))
            );

            let e3 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname(grp2_name)),
                (Attribute::Uuid, Value::Uuid(grp2_uuid))
            );

            let e4 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname(app1_name)),
                (Attribute::Uuid, Value::Uuid(app1_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
            );

            let e5 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname(app2_name)),
                (Attribute::Uuid, Value::Uuid(app2_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp2_uuid))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1, e2, e3, e4, e5])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        let pass_app1: String;
        let pass_app2: String;
        {
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app1_uuid,
                "label".to_string(),
            );
            pass_app1 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");

            // It is possible to generate an application password even if the
            // user is not member of the linked group
            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app2_uuid,
                "label".to_string(),
            );
            pass_app2 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");

            assert!(idms_prox_write.commit().is_ok());
        }

        // Got session, app password valid
        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
                pass_app1.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_some());

        // No session, not member
        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
                pass_app2.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());

        // Add user to grp2
        {
            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(idms_prox_write
                .qs_write
                .internal_modify_uuid(grp2_uuid, &ml)
                .is_ok());
            assert!(idms_prox_write.commit().is_ok());
        }

        // Got session, app password valid
        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
                pass_app2.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_some());

        // No session, wrong app
        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
                pass_app2.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_none());

        // Bind error, app not exists
        {
            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
                Attribute::Uuid,
                PartialValue::Uuid(app2_uuid)
            )));
            assert!(idms_prox_write.qs_write.delete(&de).is_ok());
            assert!(idms_prox_write.commit().is_ok());
        }

        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
                pass_app2.as_str(),
            )
            .await;
        assert!(res.is_err());
    }

    // For testing the timeouts
    // We need times on this scale
    //    not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
    const TEST_CURRENT_TIME: u64 = 6000;
    const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
    const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
    const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
    const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;

    async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));

        let me = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
            ModifyList::new_list(vec![
                Modify::Present(Attribute::AccountExpire, v_expire),
                Modify::Present(Attribute::AccountValidFrom, v_valid_from),
            ]),
        );
        assert!(idms_write.qs_write.modify(&me).is_ok());
        idms_write.commit().expect("Must not fail");
    }

    #[idm_test]
    async fn test_ldap_application_valid_from_expire(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let usr_name = "testuser1";

        let grp1_uuid = Uuid::new_v4();
        let grp1_name = "testgroup1";

        let app1_uuid = Uuid::new_v4();
        let app1_name = "testapp1";

        let pass_app1: String;

        // Setup person, group, application and app password
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname(usr_name)),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::Description, Value::new_utf8s(usr_name)),
                (Attribute::DisplayName, Value::new_utf8s(usr_name))
            );

            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname(grp1_name)),
                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
                (Attribute::Member, Value::Refer(usr_uuid))
            );

            let e3 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname(app1_name)),
                (Attribute::Uuid, Value::Uuid(app1_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1, e2, e3])
                .and_then(|_| server_txn.commit())
                .is_ok());

            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

            let ev = GenerateApplicationPasswordEvent::new_internal(
                usr_uuid,
                app1_uuid,
                "label".to_string(),
            );
            pass_app1 = idms_prox_write
                .generate_application_password(&ev)
                .expect("Failed to generate application password");

            assert!(idms_prox_write.commit().is_ok());
        }

        // Got session, app password valid
        let res = ldaps
            .do_bind(
                idms,
                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
                pass_app1.as_str(),
            )
            .await;
        assert!(res.is_ok());
        assert!(res.unwrap().is_some());

        // Any account that is not yet valid / expired can't auth.
        // Set the valid bounds high/low
        // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
        set_account_valid_time(idms, usr_uuid).await;

        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
        let time = Duration::from_secs(TEST_CURRENT_TIME);
        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);

        let mut idms_auth = idms.auth().await.unwrap();
        let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
            .expect("Failed to build auth event");

        let r1 = idms_auth
            .application_auth_ldap(&lae, time_low)
            .await
            .expect_err("Authentication succeeded");
        assert_eq!(r1, OperationError::SessionExpired);

        let r1 = idms_auth
            .application_auth_ldap(&lae, time)
            .await
            .expect("Failed auth");
        assert!(r1.is_some());

        let r1 = idms_auth
            .application_auth_ldap(&lae, time_high)
            .await
            .expect_err("Authentication succeeded");
        assert_eq!(r1, OperationError::SessionExpired);
    }

    macro_rules! assert_entry_contains {
        (
            $entry:expr,
            $dn:expr,
            $($item:expr),*
        ) => {{
            assert_eq!($entry.dn, $dn);
            // Build a set from the attrs.
            let mut attrs = HashSet::new();
            for a in $entry.attributes.iter() {
                for v in a.vals.iter() {
                    attrs.insert((a.atype.as_str(), v.as_slice()));
                }
            };
            info!(?attrs);
            $(
                warn!("{}", $item.0);
                assert!(attrs.contains(&(
                    $item.0.as_ref(), $item.1.as_bytes()
                )));
            )*

        }};
    }

    #[idm_test]
    async fn test_ldap_virtual_attribute_generation(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let ssh_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst";

        // Setup a user we want to check.
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::PosixAccount.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (
                    Attribute::Uuid,
                    Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
                ),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
                (Attribute::GidNumber, Value::new_uint32(12345)),
                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh")),
                (
                    Attribute::SshPublicKey,
                    Value::new_sshkey_str("test", ssh_ed25519).expect("Invalid ssh key")
                )
            );

            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            let ce = CreateEvent::new_internal(vec![e1]);
            assert!(server_txn
                .qs_write
                .create(&ce)
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login.
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // Check that when we request *, we get default list.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec!["*".to_string()],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Class, EntryClass::Object.to_string()),
                    (Attribute::Class, EntryClass::Person.to_string()),
                    (Attribute::Class, EntryClass::Account.to_string()),
                    (Attribute::Class, EntryClass::PosixAccount.to_string()),
                    (Attribute::DisplayName, "testperson1"),
                    (Attribute::Name, "testperson1"),
                    (Attribute::GidNumber, "12345"),
                    (Attribute::LoginShell, "/bin/zsh"),
                    (Attribute::SshPublicKey, ssh_ed25519),
                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
                );
            }
            _ => panic!("Oh no"),
        };

        // Check that when we request +, we get all attrs and the vattrs
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec!["+".to_string()],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::ObjectClass, EntryClass::Object.as_ref()),
                    (Attribute::ObjectClass, EntryClass::Person.as_ref()),
                    (Attribute::ObjectClass, EntryClass::Account.as_ref()),
                    (Attribute::ObjectClass, EntryClass::PosixAccount.as_ref()),
                    (Attribute::DisplayName, "testperson1"),
                    (Attribute::Name, "testperson1"),
                    (Attribute::GidNumber, "12345"),
                    (Attribute::LoginShell, "/bin/zsh"),
                    (Attribute::SshPublicKey, ssh_ed25519),
                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
                    (
                        Attribute::EntryDn,
                        "spn=testperson1@example.com,dc=example,dc=com"
                    ),
                    (Attribute::UidNumber, "12345"),
                    (Attribute::Cn, "testperson1"),
                    (Attribute::LdapKeys, ssh_ed25519)
                );
            }
            _ => panic!("Oh no"),
        };

        // Check that when we request an attr by name, we get all of them correctly.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec![
                LDAP_ATTR_NAME.to_string(),
                Attribute::EntryDn.to_string(),
                ATTR_LDAP_KEYS.to_string(),
                Attribute::UidNumber.to_string(),
            ],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Name, "testperson1"),
                    (
                        Attribute::EntryDn,
                        "spn=testperson1@example.com,dc=example,dc=com"
                    ),
                    (Attribute::UidNumber, "12345"),
                    (Attribute::LdapKeys, ssh_ed25519)
                );
            }
            _ => panic!("Oh no"),
        };
    }

    #[idm_test]
    async fn test_ldap_token_privilege_granting(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        // Setup the ldap server
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        // Prebuild the search req we'll be using this test.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec![
                LDAP_ATTR_NAME,
                LDAP_ATTR_MAIL,
                LDAP_ATTR_MAIL_PRIMARY,
                LDAP_ATTR_MAIL_ALTERNATIVE,
                LDAP_ATTR_EMAIL_PRIMARY,
                LDAP_ATTR_EMAIL_ALTERNATIVE,
            ]
            .into_iter()
            .map(|s| s.to_string())
            .collect(),
        };

        let sa_uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");

        // Configure the user account that will have the tokens issued.
        // Should be a SERVICE account.
        let apitoken = {
            // Create a service account,

            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Uuid, Value::Uuid(sa_uuid)),
                (Attribute::Name, Value::new_iname("service_permission_test")),
                (
                    Attribute::DisplayName,
                    Value::new_utf8s("service_permission_test")
                )
            );

            // Setup a person with an email
            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::PosixAccount.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (
                    Attribute::Mail,
                    Value::EmailAddress("testperson1@example.com".to_string(), true)
                ),
                (
                    Attribute::Mail,
                    Value::EmailAddress("testperson1.alternative@example.com".to_string(), false)
                ),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1")),
                (Attribute::GidNumber, Value::new_uint32(12345)),
                (Attribute::LoginShell, Value::new_iutf8("/bin/zsh"))
            );

            // Setup an access control for the service account to view mail attrs.

            let ct = duration_from_epoch_now();

            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            let ce = CreateEvent::new_internal(vec![e1, e2]);
            assert!(server_txn.qs_write.create(&ce).is_ok());

            // idm_people_read_priv
            let me = ModifyEvent::new_internal_invalid(
                filter!(f_eq(
                    Attribute::Name,
                    PartialValue::new_iname("idm_people_pii_read")
                )),
                ModifyList::new_list(vec![Modify::Present(
                    Attribute::Member,
                    Value::Refer(sa_uuid),
                )]),
            );
            assert!(server_txn.qs_write.modify(&me).is_ok());

            // Issue a token
            // make it purpose = ldap <- currently purpose isn't supported,
            // it's an idea for future.
            let gte = GenerateApiTokenEvent::new_internal(sa_uuid, "TestToken", None);

            let apitoken = server_txn
                .service_account_generate_api_token(&gte, ct)
                .expect("Failed to create new apitoken");

            assert!(server_txn.commit().is_ok());

            apitoken
        };

        // assert the token fails on non-ldap events token-xchg <- currently
        // we don't have purpose so this isn't tested.

        // Bind with anonymous, search and show mail attr isn't accessible.
        let anon_lbt = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_lbt.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        let r1 = ldaps
            .do_search(idms, &sr, &anon_lbt, Source::Internal)
            .await
            .unwrap();
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Name, "testperson1")
                );
            }
            _ => panic!("Oh no"),
        };

        // Inspect the token to get its uuid out.
        let jws_verifier = JwsDangerReleaseWithoutVerify::default();

        let apitoken_inner = jws_verifier
            .verify(&apitoken)
            .unwrap()
            .from_json::<ApiToken>()
            .unwrap();

        // Bind using the token as a DN
        let sa_lbt = ldaps
            .do_bind(idms, "dn=token", &apitoken.to_string())
            .await
            .unwrap()
            .unwrap();
        assert_eq!(
            sa_lbt.effective_session,
            LdapSession::ApiToken(apitoken_inner.clone())
        );

        // Bind using the token as a pw
        let sa_lbt = ldaps
            .do_bind(idms, "", &apitoken.to_string())
            .await
            .unwrap()
            .unwrap();
        assert_eq!(
            sa_lbt.effective_session,
            LdapSession::ApiToken(apitoken_inner)
        );

        // Search and retrieve mail that's now accessible.
        let r1 = ldaps
            .do_search(idms, &sr, &sa_lbt, Source::Internal)
            .await
            .unwrap();
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Name, "testperson1"),
                    (Attribute::Mail, "testperson1@example.com"),
                    (Attribute::Mail, "testperson1.alternative@example.com"),
                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
                    (
                        LDAP_ATTR_MAIL_ALTERNATIVE,
                        "testperson1.alternative@example.com"
                    ),
                    (LDAP_ATTR_EMAIL_PRIMARY, "testperson1@example.com"),
                    (
                        LDAP_ATTR_EMAIL_ALTERNATIVE,
                        "testperson1.alternative@example.com"
                    )
                );
            }
            _ => panic!("Oh no"),
        };

        // ======= test with a substring search

        let sr = SearchRequest {
            msgid: 2,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::And(vec![
                LdapFilter::Equality(Attribute::Class.to_string(), "posixAccount".to_string()),
                LdapFilter::Substring(
                    LDAP_ATTR_MAIL.to_string(),
                    LdapSubstringFilter {
                        initial: None,
                        any: vec![],
                        final_: Some("@example.com".to_string()),
                    },
                ),
            ]),
            attrs: vec![
                LDAP_ATTR_NAME,
                LDAP_ATTR_MAIL,
                LDAP_ATTR_MAIL_PRIMARY,
                LDAP_ATTR_MAIL_ALTERNATIVE,
            ]
            .into_iter()
            .map(|s| s.to_string())
            .collect(),
        };

        let r1 = ldaps
            .do_search(idms, &sr, &sa_lbt, Source::Internal)
            .await
            .unwrap();

        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Name, "testperson1"),
                    (Attribute::Mail, "testperson1@example.com"),
                    (Attribute::Mail, "testperson1.alternative@example.com"),
                    (LDAP_ATTR_MAIL_PRIMARY, "testperson1@example.com"),
                    (
                        LDAP_ATTR_MAIL_ALTERNATIVE,
                        "testperson1.alternative@example.com"
                    )
                );
            }
            _ => panic!("Oh no"),
        };
    }

    #[idm_test]
    async fn test_ldap_virtual_attribute_with_all_attr_search(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");

        // Setup a user we want to check.
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(acct_uuid)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login.
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // Check that when we request a virtual attr by name *and* all_attrs we get all the requested values.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec![
                "*".to_string(),
                // Already being returned
                LDAP_ATTR_NAME.to_string(),
                // This is a virtual attribute
                Attribute::EntryUuid.to_string(),
            ],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Name, "testperson1"),
                    (Attribute::DisplayName, "testperson1"),
                    (Attribute::Uuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930"),
                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
                );
            }
            _ => panic!("Oh no"),
        };
    }

    // Test behaviour of the 1.1 attribute.
    #[idm_test]
    async fn test_ldap_one_dot_one_attribute(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");

        // Setup a user we want to check.
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(acct_uuid)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login.
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // If we request only 1.1, we get no attributes.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec!["1.1".to_string()],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_eq!(
                    lsre.dn.as_str(),
                    "spn=testperson1@example.com,dc=example,dc=com"
                );
                assert!(lsre.attributes.is_empty());
            }
            _ => panic!("Oh no"),
        };

        // If we request 1.1 and another attr, 1.1 is IGNORED.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec![
                "1.1".to_string(),
                // This should be present.
                Attribute::EntryUuid.to_string(),
            ],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
                );
            }
            _ => panic!("Oh no"),
        };
    }

    #[idm_test]
    async fn test_ldap_rootdse_basedn_change(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        let sr = SearchRequest {
            msgid: 1,
            base: "".to_string(),
            scope: LdapSearchScope::Base,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec!["*".to_string()],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        trace!(?r1);

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "",
                    (Attribute::ObjectClass, "top"),
                    ("vendorname", "Kanidm Project"),
                    ("supportedldapversion", "3"),
                    ("defaultnamingcontext", "dc=example,dc=com")
                );
            }
            _ => panic!("Oh no"),
        };

        drop(ldaps);

        // Change the domain basedn

        let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
        // make the admin a valid posix account
        let me_posix = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
            ModifyList::new_purge_and_set(
                Attribute::DomainLdapBasedn,
                Value::new_iutf8("o=kanidmproject"),
            ),
        );
        assert!(idms_prox_write.qs_write.modify(&me_posix).is_ok());

        assert!(idms_prox_write.commit().is_ok());

        // Now re-test
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        let sr = SearchRequest {
            msgid: 1,
            base: "".to_string(),
            scope: LdapSearchScope::Base,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec!["*".to_string()],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        trace!(?r1);

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "",
                    (Attribute::ObjectClass, "top"),
                    ("vendorname", "Kanidm Project"),
                    ("supportedldapversion", "3"),
                    ("defaultnamingcontext", "o=kanidmproject")
                );
            }
            _ => panic!("Oh no"),
        };
    }

    #[idm_test]
    async fn test_ldap_sssd_compat(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");

        // Setup a user we want to check.
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::PosixAccount.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(acct_uuid)),
                (Attribute::GidNumber, Value::Uint32(12345)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login.
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        // SSSD tries to just search for silly attrs all the time. We ignore them.
        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::And(vec![
                LdapFilter::Equality(Attribute::Class.to_string(), "sudohost".to_string()),
                LdapFilter::Substring(
                    Attribute::SudoHost.to_string(),
                    LdapSubstringFilter {
                        initial: Some("a".to_string()),
                        any: vec!["x".to_string()],
                        final_: Some("z".to_string()),
                    },
                ),
            ]),
            attrs: vec![
                "*".to_string(),
                // Already being returned
                LDAP_ATTR_NAME.to_string(),
                // This is a virtual attribute
                Attribute::EntryUuid.to_string(),
            ],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        // Empty results and ldap proto success msg.
        assert_eq!(r1.len(), 1);

        // Second search

        let sr = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Equality(Attribute::Name.to_string(), "testperson1".to_string()),
            attrs: vec![
                "uid".to_string(),
                "uidNumber".to_string(),
                "gidNumber".to_string(),
                "gecos".to_string(),
                "cn".to_string(),
                "entryuuid".to_string(),
            ],
        };
        let r1 = ldaps
            .do_search(idms, &sr, &anon_t, Source::Internal)
            .await
            .unwrap();

        trace!(?r1);

        // The result, and the ldap proto success msg.
        assert_eq!(r1.len(), 2);
        match &r1[0].op {
            LdapOp::SearchResultEntry(lsre) => {
                assert_entry_contains!(
                    lsre,
                    "spn=testperson1@example.com,dc=example,dc=com",
                    (Attribute::Uid, "testperson1"),
                    (Attribute::Cn, "testperson1"),
                    (Attribute::Gecos, "testperson1"),
                    (Attribute::UidNumber, "12345"),
                    (Attribute::GidNumber, "12345"),
                    (Attribute::EntryUuid, "cc8e95b4-c24f-4d68-ba54-8bed76f63930")
                );
            }
            _ => panic!("Oh no"),
        };
    }

    #[idm_test]
    async fn test_ldap_compare_request(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        // Setup a user we want to check.
        {
            let acct_uuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");

            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::PosixAccount.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(acct_uuid)),
                (Attribute::GidNumber, Value::Uint32(12345)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login.
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        #[track_caller]
        fn assert_compare_result(r: &[LdapMsg], code: &LdapResultCode) {
            assert_eq!(r.len(), 1);
            match &r[0].op {
                LdapOp::CompareResult(lcr) => {
                    assert_eq!(&lcr.code, code);
                }
                _ => panic!("Oh no"),
            };
        }

        let cr = CompareRequest {
            msgid: 1,
            entry: "name=testperson1,dc=example,dc=com".to_string(),
            atype: Attribute::Name.to_string(),
            val: "testperson1".to_string(),
        };

        assert_compare_result(
            &ldaps
                .do_compare(idms, &cr, &anon_t, Source::Internal)
                .await
                .unwrap(),
            &LdapResultCode::CompareTrue,
        );

        let cr = CompareRequest {
            msgid: 1,
            entry: "name=testperson1,dc=example,dc=com".to_string(),
            atype: Attribute::GidNumber.to_string(),
            val: "12345".to_string(),
        };

        assert_compare_result(
            &ldaps
                .do_compare(idms, &cr, &anon_t, Source::Internal)
                .await
                .unwrap(),
            &LdapResultCode::CompareTrue,
        );

        let cr = CompareRequest {
            msgid: 1,
            entry: "name=testperson1,dc=example,dc=com".to_string(),
            atype: Attribute::Name.to_string(),
            val: "other".to_string(),
        };
        assert_compare_result(
            &ldaps
                .do_compare(idms, &cr, &anon_t, Source::Internal)
                .await
                .unwrap(),
            &LdapResultCode::CompareFalse,
        );

        let cr = CompareRequest {
            msgid: 1,
            entry: "name=other,dc=example,dc=com".to_string(),
            atype: Attribute::Name.to_string(),
            val: "other".to_string(),
        };
        assert_compare_result(
            &ldaps
                .do_compare(idms, &cr, &anon_t, Source::Internal)
                .await
                .unwrap(),
            &LdapResultCode::NoSuchObject,
        );

        let cr = CompareRequest {
            msgid: 1,
            entry: "invalidentry".to_string(),
            atype: Attribute::Name.to_string(),
            val: "other".to_string(),
        };
        assert!(&ldaps
            .do_compare(idms, &cr, &anon_t, Source::Internal)
            .await
            .is_err());

        let cr = CompareRequest {
            msgid: 1,
            entry: "name=other,dc=example,dc=com".to_string(),
            atype: "invalid".to_string(),
            val: "other".to_string(),
        };
        assert_eq!(
            &ldaps
                .do_compare(idms, &cr, &anon_t, Source::Internal)
                .await
                .unwrap_err(),
            &OperationError::InvalidAttributeName("invalid".to_string()),
        );
    }

    #[idm_test]
    async fn test_ldap_maximum_queryable_attributes(
        idms: &IdmServer,
        _idms_delayed: &IdmServerDelayed,
    ) {
        // Set the max queryable attrs to 2

        let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();

        let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
            ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
        );
        assert!(server_txn
            .qs_write
            .modify(&set_ldap_maximum_queryable_attrs)
            .and_then(|_| server_txn.commit())
            .is_ok());

        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");

        let usr_uuid = Uuid::new_v4();
        let grp_uuid = Uuid::new_v4();
        let app_uuid = Uuid::new_v4();
        let app_name = "testapp1";

        // Setup person, group and application
        {
            let e1 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::Person.to_value()),
                (Attribute::Name, Value::new_iname("testperson1")),
                (Attribute::Uuid, Value::Uuid(usr_uuid)),
                (Attribute::Description, Value::new_utf8s("testperson1")),
                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
            );

            let e2 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Group.to_value()),
                (Attribute::Name, Value::new_iname("testgroup1")),
                (Attribute::Uuid, Value::Uuid(grp_uuid))
            );

            let e3 = entry_init!(
                (Attribute::Class, EntryClass::Object.to_value()),
                (Attribute::Class, EntryClass::Account.to_value()),
                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
                (Attribute::Class, EntryClass::Application.to_value()),
                (Attribute::DisplayName, Value::new_utf8s("Application")),
                (Attribute::Name, Value::new_iname(app_name)),
                (Attribute::Uuid, Value::Uuid(app_uuid)),
                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
            );

            let ct = duration_from_epoch_now();
            let mut server_txn = idms.proxy_write(ct).await.unwrap();
            assert!(server_txn
                .qs_write
                .internal_create(vec![e1, e2, e3])
                .and_then(|_| server_txn.commit())
                .is_ok());
        }

        // Setup the anonymous login
        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
        assert_eq!(
            anon_t.effective_session,
            LdapSession::UnixBind(UUID_ANONYMOUS)
        );

        let invalid_search = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec![
                "objectClass".to_string(),
                "cn".to_string(),
                "givenName".to_string(),
            ],
        };

        let valid_search = SearchRequest {
            msgid: 1,
            base: "dc=example,dc=com".to_string(),
            scope: LdapSearchScope::Subtree,
            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
            attrs: vec!["objectClass: person".to_string()],
        };

        let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
            .do_search(idms, &invalid_search, &anon_t, Source::Internal)
            .await;

        let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
            .do_search(idms, &valid_search, &anon_t, Source::Internal)
            .await;

        assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
        assert!(valid_res.is_ok());
    }
}