use crate::idm::server::IdmServerProxyReadTransaction;
use crate::prelude::*;
use kanidm_proto::internal::AppLink;

impl<'a> IdmServerProxyReadTransaction<'a> {
    pub fn list_applinks(&mut self, ident: &Identity) -> Result<Vec<AppLink>, OperationError> {
        // From the member-of of the ident.
        let Some(ident_mo) = ident.get_memberof() else {
            debug!("Ident has no memberof, no applinks are present");
            return Ok(Vec::with_capacity(0));
        };

        // Formerly we did an internal search here, but we no longer need to since we have
        // the access control module setup so that we can search for and see rs that we
        // have access to.
        //
        // We do this weird looking f_executed/f_intent shenanigans to actually search
        // on what we have access to, but we apply access as though we did a search on
        // class=oauth2_resource_server instead, and we still apply access here.
        let f_executed = filter!(f_or(
            ident_mo
                .iter()
                .copied()
                .map(|uuid| { f_eq(Attribute::OAuth2RsScopeMap, PartialValue::Refer(uuid)) })
                .collect()
        ));
        let f_intent = filter!(f_eq(
            Attribute::Class,
            EntryClass::OAuth2ResourceServer.into()
        ));

        // _ext reduces the entries based on access.
        let oauth2_related = self
            .qs_read
            .impersonate_search_ext(f_executed, f_intent, ident)?;
        trace!(?oauth2_related);

        // Aggregate results to a Vec of AppLink
        let apps = oauth2_related
            .iter()
            .filter_map(|entry| {
                let display_name = entry
                    .get_ava_single_utf8(Attribute::DisplayName)
                    .map(str::to_string)?;

                let redirect_url = entry
                    .get_ava_single_url(Attribute::OAuth2RsOriginLanding)
                    .cloned()?;

                let name = entry
                    .get_ava_single_iname(Attribute::Name)
                    .map(str::to_string)?;

                let has_image = entry.get_ava_single_image(Attribute::Image).is_some();

                Some(AppLink::Oauth2 {
                    name,
                    display_name,
                    redirect_url,
                    has_image,
                })
            })
            .collect::<Vec<_>>();

        debug!("returned {} related apps", apps.len());
        trace!(?apps);

        Ok(apps)
    }
}

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

    #[idm_test]
    async fn test_idm_applinks_list(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
        let ct = duration_from_epoch_now();
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();

        // Create an RS, the user and a group..
        let usr_uuid = Uuid::new_v4();
        let grp_uuid = Uuid::new_v4();

        let e_rs: Entry<EntryInit, EntryNew> = entry_init!(
            (Attribute::Class, EntryClass::Object.to_value()),
            (Attribute::Class, EntryClass::Account.to_value()),
            (
                Attribute::Class,
                EntryClass::OAuth2ResourceServer.to_value()
            ),
            (
                Attribute::Class,
                EntryClass::OAuth2ResourceServerBasic.to_value()
            ),
            (Attribute::Name, Value::new_iname("test_resource_server")),
            (
                Attribute::DisplayName,
                Value::new_utf8s("test_resource_server")
            ),
            (
                Attribute::OAuth2RsOrigin,
                Value::new_url_s("https://demo.example.com").unwrap()
            ),
            (
                Attribute::OAuth2RsOriginLanding,
                Value::new_url_s("https://demo.example.com/landing").unwrap()
            ),
            // System admins
            (
                Attribute::OAuth2RsScopeMap,
                Value::new_oauthscopemap(
                    grp_uuid,
                    btreeset![kanidm_proto::constants::OAUTH2_SCOPE_READ.to_string()]
                )
                .expect("invalid oauthscope")
            )
        );

        let e_usr = 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("testaccount")),
            (Attribute::Uuid, Value::Uuid(usr_uuid)),
            (Attribute::Description, Value::new_utf8s("testaccount")),
            (Attribute::DisplayName, Value::new_utf8s("Test Account"))
        );

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

        let ce = CreateEvent::new_internal(vec![e_rs, e_grp, e_usr]);
        assert!(idms_prox_write.qs_write.create(&ce).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        // Now do an applink query, they will not be there.
        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        let ident = idms_prox_read
            .qs_read
            .internal_search_uuid(usr_uuid)
            .map(Identity::from_impersonate_entry_readonly)
            .expect("Failed to impersonate identity");

        let apps = idms_prox_read
            .list_applinks(&ident)
            .expect("Failed to access related apps");

        assert!(apps.is_empty());
        drop(idms_prox_read);

        // Add them to the group.
        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
        let me_inv_m = ModifyEvent::new_internal_invalid(
            filter!(f_eq(Attribute::Uuid, PartialValue::Refer(grp_uuid))),
            ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid)),
        );
        assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
        assert!(idms_prox_write.commit().is_ok());

        let mut idms_prox_read = idms.proxy_read().await.unwrap();

        let ident = idms_prox_read
            .qs_read
            .internal_search_uuid(usr_uuid)
            .map(Identity::from_impersonate_entry_readonly)
            .expect("Failed to impersonate identity");

        let apps = idms_prox_read
            .list_applinks(&ident)
            .expect("Failed to access related apps");

        let app = apps.first().expect("No apps return!");

        assert!(match app {
            AppLink::Oauth2 {
                name,
                display_name,
                redirect_url,
                has_image,
            } => {
                name == "test_resource_server"
                    && display_name == "test_resource_server"
                    && redirect_url
                        == &Url::parse("https://demo.example.com/landing")
                            .expect("Failed to parse URL")
                    && !has_image
            } // _ => false,
        })
    }
}
