#![deny(warnings)]
use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_hsm_crypto::{
    provider::{BoxedDynTpm, SoftTpm, Tpm},
    AuthValue,
};
use kanidm_proto::config::ServerRole;
use kanidm_proto::constants::ATTR_ACCOUNT_EXPIRE;
use kanidmd_core::config::{Configuration, IntegrationTestConfig};
use kanidmd_core::create_server_core;
use kanidmd_testkit::{is_free_port, PORT_ALLOC};
use sparkle_resolver_common::db::{Cache, Db};
use sparkle_resolver_common::idprovider::interface::Id;
use sparkle_resolver_common::idprovider::kanidm::KanidmProvider;
use sparkle_resolver_common::idprovider::system::SystemProvider;
use sparkle_resolver_common::resolver::Resolver;
use sparkle_unix_common::constants::{
    DEFAULT_CACHE_TIMEOUT, DEFAULT_CACHE_TIMEOUT_JITTER_MS, DEFAULT_GID_ATTR_MAP,
    DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL,
    DEFAULT_UID_ATTR_MAP,
};
use sparkle_unix_common::unix_config::{GroupMap, KanidmConfig};
use sparkle_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use time::OffsetDateTime;
use tokio::sync::mpsc;
use tokio::task;
use tracing::debug;

const ADMIN_TEST_USER: &str = "admin";
const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
const IDM_ADMIN_TEST_USER: &str = "idm_admin";
const IDM_ADMIN_TEST_PASSWORD: &str = "integration test idm_admin password";
const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
const TESTACCOUNT1_PASSWORD_INC: &str = "never going to work";
const ACCOUNT_EXPIRE: &str = "1970-01-01T00:00:00+00:00";

type Fixture = Box<dyn FnOnce(KanidmClient) -> Pin<Box<dyn Future<Output = ()>>>>;

fn fixture<T>(f: fn(KanidmClient) -> T) -> Fixture
where
    T: Future<Output = ()> + 'static,
{
    Box::new(move |n| Box::pin(f(n)))
}

async fn setup_test(fix_fn: Fixture) -> (Resolver, mpsc::Receiver<Id>, KanidmClient) {
    sketching::test_init();

    let mut counter = 0;
    let port = loop {
        let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
        if is_free_port(possible_port) {
            break possible_port;
        }
        counter += 1;
        #[allow(clippy::assertions_on_constants)]
        if counter >= 5 {
            eprintln!("Unable to allocate port!");
            debug_assert!(false);
        }
    };

    let int_config = Box::new(IntegrationTestConfig {
        admin_user: ADMIN_TEST_USER.to_string(),
        admin_password: ADMIN_TEST_PASSWORD.to_string(),
        idm_admin_user: IDM_ADMIN_TEST_USER.to_string(),
        idm_admin_password: IDM_ADMIN_TEST_PASSWORD.to_string(),
    });

    // Setup the config ...
    let mut config = Configuration::new_for_test();
    config.address = vec![format!("127.0.0.1:{port}")];
    config.integration_test_config = Some(int_config);
    config.role = ServerRole::WriteReplicaNoUI;
    config.threads = 1;

    create_server_core(config, false)
        .await
        .expect("failed to start server core");
    // We have to yield now to guarantee that the elements are setup.
    task::yield_now().await;

    // Setup the client, and the address we selected.
    let addr = format!("http://127.0.0.1:{port}");

    // Run fixtures
    let adminclient = KanidmClientBuilder::new()
        .address(addr.clone())
        .enable_native_ca_roots(false)
        .no_proxy()
        .build()
        .expect("Failed to build sync client");

    let res = adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await;
    debug!("auth_simple_password res: {res:?}");
    assert!(res.is_ok());

    // Setup a service-account that can access the unix parts.
    adminclient
        .idm_service_account_create("unixd_service", "Unixd Service Account", "idm_admins")
        .await
        .expect("Unable to create service account");

    adminclient
        .idm_group_add_members("idm_unix_authentication_read", &["unixd_service"])
        .await
        .expect("Unable to add service account to unixd read group");

    let service_api_token = adminclient
        .idm_service_account_generate_api_token("unixd_service", "accesstoken", None, false, false)
        .await
        .expect("Unable to create service account api token");

    // Now we can disable the anonymous account.
    adminclient
        .idm_service_account_set_attr("anonymous", ATTR_ACCOUNT_EXPIRE, &[ACCOUNT_EXPIRE])
        .await
        .expect("Failed to disable the anonymous account");

    // Finally execute the test fixtures
    fix_fn(adminclient).await;

    let client = KanidmClientBuilder::new()
        .address(addr.clone())
        .enable_native_ca_roots(false)
        .no_proxy()
        .build()
        .expect("Failed to build async admin client");

    let rsclient = KanidmClientBuilder::new()
        .address(addr)
        .enable_native_ca_roots(false)
        .no_proxy()
        .build()
        .expect("Failed to build client");

    let db = Db::new(
        "", // The sqlite db path, this is in memory.
    )
    .expect("Failed to setup DB");

    let mut dbtxn = db.write().await;
    dbtxn.migrate().expect("Unable to migrate cache db");

    let mut hsm = BoxedDynTpm::new(SoftTpm::default());

    let auth_value = AuthValue::ephemeral().unwrap();

    let loadable_machine_key = hsm.root_storage_key_create(&auth_value).unwrap();
    let machine_key = hsm
        .root_storage_key_load(&auth_value, &loadable_machine_key)
        .unwrap();

    let system_provider = SystemProvider::new().unwrap();

    let idprovider = KanidmProvider::new(
        rsclient,
        &KanidmConfig {
            conn_timeout: 1,
            request_timeout: 1,
            pam_allowed_login_groups: vec!["allowed_group".to_string()],
            map_group: vec![GroupMap {
                local: "extensible_group".to_string(),
                with: "testgroup1".to_string(),
            }],
            service_account_token: Some(service_api_token),
        },
        SystemTime::now(),
        &mut (&mut dbtxn).into(),
        &mut hsm,
        &machine_key,
    )
    .await
    .unwrap();

    drop(machine_key);

    dbtxn.commit().expect("Unable to commit dbtxn");

    let (cachelayer, async_refresh_rx) = Resolver::new(
        db,
        Arc::new(system_provider),
        vec![Arc::new(idprovider)],
        hsm,
        DEFAULT_CACHE_TIMEOUT,
        DEFAULT_SHELL.to_string(),
        DEFAULT_HOME_PREFIX.into(),
        DEFAULT_HOME_ATTR,
        DEFAULT_HOME_ALIAS,
        DEFAULT_UID_ATTR_MAP,
        DEFAULT_GID_ATTR_MAP,
    )
    .await
    .expect("Failed to build cache layer.");

    // test_fn(cachelayer, client);
    (cachelayer, async_refresh_rx, client)
    // We DO NOT need teardown, as sqlite is in mem
    // let the tables hit the floor
}

/// This is the test fixture. It sets up the following:
/// - creates a test account (testaccount1)
/// - extends the test account with posix attrs
/// - adds a ssh public key to the test account
/// - sets a posix password for the test account
/// - creates a test group (testgroup1) and adds the test account to the test group
/// - extends testgroup1 with posix attrs
/// - creates two more groups with unix perms (allowed_group, masked_group)
async fn test_fixture(rsclient: KanidmClient) {
    // Create a new account
    rsclient
        .idm_person_account_create("testaccount1", "Posix Demo Account")
        .await
        .unwrap();

    // Extend the account with posix attrs.
    rsclient
        .idm_person_account_unix_extend("testaccount1", Some(20000), None)
        .await
        .unwrap();
    // Assign an ssh public key.
    rsclient
        .idm_person_account_post_ssh_pubkey("testaccount1", "tk",
        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst")
        .await
        .unwrap();
    // Set a posix password
    rsclient
        .idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_A)
        .await
        .unwrap();

    // Setup a group
    rsclient.idm_group_create("testgroup1", None).await.unwrap();
    rsclient
        .idm_group_add_members("testgroup1", &["testaccount1"])
        .await
        .unwrap();
    rsclient
        .idm_group_unix_extend("testgroup1", Some(20001))
        .await
        .unwrap();

    // Setup the allowed group
    rsclient
        .idm_group_create("allowed_group", None)
        .await
        .unwrap();
    rsclient
        .idm_group_unix_extend("allowed_group", Some(20002))
        .await
        .unwrap();

    // Setup a group that is masked by nxset, but allowed in overrides
    rsclient
        .idm_group_create("masked_group", None)
        .await
        .unwrap();
    rsclient
        .idm_group_unix_extend("masked_group", Some(20003))
        .await
        .unwrap();
}

#[tokio::test]
async fn test_cache_sshkey() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;
    // Force offline. Show we have no keys.
    cachelayer.mark_offline().await;

    let sk = cachelayer
        .get_sshkeys("testaccount1")
        .await
        .expect("Failed to get from cache.");
    assert!(sk.is_empty());

    // Bring ourselves online.
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    let sk = cachelayer
        .get_sshkeys("testaccount1")
        .await
        .expect("Failed to get from cache.");
    assert_eq!(sk.len(), 1);

    // Go offline, and get from cache.
    cachelayer.mark_offline().await;
    let sk = cachelayer
        .get_sshkeys("testaccount1")
        .await
        .expect("Failed to get from cache.");
    assert_eq!(sk.len(), 1);
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    let current_time = SystemTime::now();
    // We know that async refreshes occur in the last 1/3rd of the entries
    // expiry. However we have to account for jitter. This means we need the expiry minus jitter
    // and a bit
    let refresh_time = SystemTime::now()
        + (Duration::from_secs(DEFAULT_CACHE_TIMEOUT)
            - (Duration::from_millis(DEFAULT_CACHE_TIMEOUT_JITTER_MS) + Duration::from_secs(1)));

    // Force offline. Show we have no account
    cachelayer.mark_offline().await;

    let ut = cachelayer
        .get_nssaccount_name_time("testaccount1", current_time)
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_none());

    // go online
    cachelayer.mark_next_check_now(current_time).await;
    assert!(cachelayer.test_connection().await);

    // get the account
    let ut = cachelayer
        .get_nssaccount_name_time("testaccount1", current_time)
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    // #392: Check that a `shell=None` is set to `default_shell`.
    assert_eq!(ut.unwrap().shell, *DEFAULT_SHELL);

    // go offline
    cachelayer.mark_offline().await;

    // can still get account
    let ut = cachelayer
        .get_nssaccount_name_time("testaccount1", current_time)
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    // Finally, check we have "all accounts" in the list.
    let us = cachelayer
        .get_nssaccounts()
        .await
        .expect("failed to list all accounts");
    assert_eq!(us.len(), 1);

    // Set the time to when an async refresh should occur
    let ut = cachelayer
        .get_nssaccount_name_time("testaccount1", refresh_time)
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());
    // Check that it's now in the queue to refresh.
    assert_eq!(async_refresh_rx.len(), 1);
}

#[tokio::test]
async fn test_cache_group() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;
    // Force offline. Show we have no groups.
    cachelayer.mark_offline().await;
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());

    // go online. Get the group
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());

    // go offline. still works
    cachelayer.mark_offline().await;
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());
    // And check we have no members in the group. Members are an artifact of
    // user lookups!
    assert!(gt.unwrap().members.is_empty());

    // clear cache, go online
    assert!(cachelayer.invalidate().await.is_ok());
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // get an account with the group
    // DO NOT get the group yet.
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    // go offline.
    cachelayer.mark_offline().await;

    // show we have the group despite no direct calls
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());
    // And check we have members in the group, since we came from a userlook up
    assert_eq!(gt.unwrap().members.len(), 1);

    // Finally, check we have "all groups" in the list.
    let gs = cachelayer
        .get_nssgroups()
        .await
        .expect("failed to list all groups");
    assert_eq!(gs.len(), 2);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_group_delete() {
    let (cachelayer, async_refresh_rx, adminclient) = setup_test(fixture(test_fixture)).await;
    // get the group
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());

    // delete it.
    adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await
        .expect("failed to auth as admin");
    adminclient
        .idm_group_delete("testgroup1")
        .await
        .expect("failed to delete");

    // invalidate cache
    assert!(cachelayer.invalidate().await.is_ok());

    // "get it"
    // should be empty.
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account_delete() {
    let (cachelayer, async_refresh_rx, adminclient) = setup_test(fixture(test_fixture)).await;
    // get the account
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    // delete it.
    adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await
        .expect("failed to auth as admin");
    adminclient
        .idm_person_account_delete("testaccount1")
        .await
        .expect("failed to delete");

    // invalidate cache
    assert!(cachelayer.invalidate().await.is_ok());

    // "get it"
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    // should be empty.
    assert!(ut.is_none());

    // The group should be removed too.
    let gt = cachelayer
        .get_nssgroup_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account_password() {
    #[allow(clippy::disallowed_methods)]
    let current_time = OffsetDateTime::now_utc();
    let (cachelayer, async_refresh_rx, adminclient) = setup_test(fixture(test_fixture)).await;
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    // Test authentication failure.
    let a1 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    // We have to wait due to softlocking.
    tokio::time::sleep(Duration::from_secs(1)).await;

    // Test authentication success.
    let a2 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
        .await
        .expect("failed to authenticate");
    assert_eq!(a2, Some(true));

    // change pw
    adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await
        .expect("failed to auth as admin");
    adminclient
        .idm_person_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("Failed to change password");

    // test auth (old pw) fail
    let a3 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
        .await
        .expect("failed to authenticate");
    assert_eq!(a3, Some(false));

    // We have to wait due to softlocking.
    tokio::time::sleep(Duration::from_secs(1)).await;

    // test auth (new pw) success
    let a4 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert_eq!(a4, Some(true));

    // Go offline.
    cachelayer.mark_offline().await;

    // Test auth success
    let a5 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert_eq!(a5, Some(true));

    // No softlock during offline.

    // Test auth failure.
    let a6 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_INC)
        .await
        .expect("failed to authenticate");
    assert_eq!(a6, Some(false));

    // clear cache
    cachelayer
        .clear_cache()
        .await
        .expect("failed to clear cache");

    // test auth good (fail)
    let a7 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert!(a7.is_none());

    // go online
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // test auth success
    let a8 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert_eq!(a8, Some(true));
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account_pam_allowed() {
    let (cachelayer, async_refresh_rx, adminclient) = setup_test(fixture(test_fixture)).await;
    cachelayer.mark_next_check_now(SystemTime::now()).await;

    // Should fail
    let a1 = cachelayer
        .pam_account_allowed("testaccount1")
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await
        .expect("failed to auth as admin");
    adminclient
        .idm_group_add_members("allowed_group", &["testaccount1"])
        .await
        .unwrap();

    // Invalidate cache to force a refresh
    assert!(cachelayer.invalidate().await.is_ok());

    // Should pass
    let a2 = cachelayer
        .pam_account_allowed("testaccount1")
        .await
        .expect("failed to authenticate");
    assert_eq!(a2, Some(true));
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account_pam_nonexist() {
    #[allow(clippy::disallowed_methods)]
    let current_time = OffsetDateTime::now_utc();
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;
    cachelayer.mark_next_check_now(SystemTime::now()).await;

    let a1 = cachelayer
        .pam_account_allowed("NO_SUCH_ACCOUNT")
        .await
        .expect("failed to authenticate");
    assert!(a1.is_none());

    let a2 = cachelayer
        .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert!(a2.is_none());

    cachelayer.mark_offline().await;

    let a1 = cachelayer
        .pam_account_allowed("NO_SUCH_ACCOUNT")
        .await
        .expect("failed to authenticate");
    assert!(a1.is_none());

    let a2 = cachelayer
        .pam_account_authenticate("NO_SUCH_ACCOUNT", current_time, TESTACCOUNT1_PASSWORD_B)
        .await
        .expect("failed to authenticate");
    assert!(a2.is_none());
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_account_expiry() {
    #[allow(clippy::disallowed_methods)]
    let current_time = OffsetDateTime::now_utc();
    let (cachelayer, async_refresh_rx, adminclient) = setup_test(fixture(test_fixture)).await;
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // We need one good auth first to prime the cache with a hash.
    let a1 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(true));
    // Invalidate to make sure we go online next checks.
    assert!(cachelayer.invalidate().await.is_ok());

    // expire the account
    adminclient
        .auth_simple_password("admin", ADMIN_TEST_PASSWORD)
        .await
        .expect("failed to auth as admin");
    adminclient
        .idm_person_account_set_attr("testaccount1", ATTR_ACCOUNT_EXPIRE, &[ACCOUNT_EXPIRE])
        .await
        .unwrap();
    // auth will fail
    let a2 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
        .await
        .expect("failed to authenticate");
    assert_eq!(a2, Some(false));

    // ssh keys should be empty
    let sk = cachelayer
        .get_sshkeys("testaccount1")
        .await
        .expect("Failed to get from cache.");
    assert!(sk.is_empty());

    // Pam account allowed should be denied.
    let a3 = cachelayer
        .pam_account_allowed("testaccount1")
        .await
        .expect("failed to authenticate");
    assert_eq!(a3, Some(false));

    // go offline
    cachelayer.mark_offline().await;

    // Now, check again. Since this uses the cached pw and we are offline, this
    // will now succeed.
    let a4 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, TESTACCOUNT1_PASSWORD_A)
        .await
        .expect("failed to authenticate");
    assert_eq!(a4, Some(true));

    // ssh keys should be empty
    let sk = cachelayer
        .get_sshkeys("testaccount1")
        .await
        .expect("Failed to get from cache.");
    assert!(sk.is_empty());

    // Pam account allowed should be denied.
    let a5 = cachelayer
        .pam_account_allowed("testaccount1")
        .await
        .expect("failed to authenticate");
    assert_eq!(a5, Some(false));
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_nxcache() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);
    // Is it in the nxcache?

    assert!(cachelayer
        .check_nxcache(&Id::Name("oracle".to_string()))
        .await
        .is_none());
    assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_none());
    assert!(cachelayer
        .check_nxcache(&Id::Name("oracle_group".to_string()))
        .await
        .is_none());
    assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_none());

    // Look for the acc id + nss id
    let ut = cachelayer
        .get_nssaccount_name("oracle")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_none());
    let ut = cachelayer
        .get_nssaccount_gid(2000)
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_none());

    let gt = cachelayer
        .get_nssgroup_name("oracle_group")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());
    let gt = cachelayer
        .get_nssgroup_gid(3000)
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());

    // Should all now be nxed
    assert!(
        cachelayer
            .check_nxcache(&Id::Name("oracle".to_string()))
            .await
            .is_some(),
        "'oracle' Wasn't in the nxcache!"
    );
    assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_some());
    assert!(cachelayer
        .check_nxcache(&Id::Name("oracle_group".to_string()))
        .await
        .is_some());
    assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_some());

    // invalidate cache
    assert!(cachelayer.invalidate().await.is_ok());

    // Both should NOT be in nxcache now.
    assert!(cachelayer
        .check_nxcache(&Id::Name("oracle".to_string()))
        .await
        .is_none());
    assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_none());
    assert!(cachelayer
        .check_nxcache(&Id::Name("oracle_group".to_string()))
        .await
        .is_none());
    assert!(cachelayer.check_nxcache(&Id::Gid(3000)).await.is_none());
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_nxset_account() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    // Important! This is what sets up that testaccount1 won't be resolved
    // because it's in the "local" user set.
    cachelayer
        .reload_system_identities(
            vec![EtcUser {
                name: "testaccount1".to_string(),
                uid: 30000,
                gid: 30000,
                password: Default::default(),
                gecos: Default::default(),
                homedir: Default::default(),
                shell: Default::default(),
            }],
            vec![],
            vec![],
        )
        .await;

    // go online
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // get the account
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");

    let ut = ut.unwrap();
    // Assert the user is the system version.
    assert_eq!(ut.uid, 30000);

    // go offline
    cachelayer.mark_offline().await;

    // still not present, was not cached.
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");

    let ut = ut.unwrap();
    // Assert the user is the system version.
    assert_eq!(ut.uid, 30000);

    // Finally, check it's the system version in all accounts.
    let us = cachelayer
        .get_nssaccounts()
        .await
        .expect("failed to list all accounts");

    let us: Vec<_> = us
        .into_iter()
        .filter(|nss_user| nss_user.name == "testaccount1")
        .collect();

    assert_eq!(us.len(), 1);
    assert_eq!(us[0].gid, 30000);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_nxset_group() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    // Important! This is what sets up that testgroup1 won't be resolved
    // because it's in the "local" group set.
    cachelayer
        .reload_system_identities(
            vec![],
            vec![],
            vec![EtcGroup {
                name: "testgroup1".to_string(),
                // Important! We set the GID to differ from what kanidm stores so we can
                // tell we got the system version.
                gid: 30001,
                password: Default::default(),
                members: Default::default(),
            }],
        )
        .await;

    // go online. Get the group
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");

    // We get the group, it's the system version. Check the gid.
    let gt = gt.unwrap();
    assert_eq!(gt.gid, 30001);

    // go offline. still works
    cachelayer.mark_offline().await;
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 30001);

    // clear cache, go online
    assert!(cachelayer.invalidate().await.is_ok());
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // get a kanidm account with the kanidm equivalent group
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    // go offline.
    cachelayer.mark_offline().await;

    // show that the group we have is still the system version, and lacks our
    // member.
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 30001);
    assert!(gt.members.is_empty());

    // Finally, check we only have the system group version in the list.
    let gs = cachelayer
        .get_nssgroups()
        .await
        .expect("failed to list all groups");

    let gs: Vec<_> = gs
        .into_iter()
        .filter(|nss_group| nss_group.name == "testgroup1")
        .collect();

    debug!("{gs:?}");
    assert_eq!(gs.len(), 1);
    assert_eq!(gs[0].gid, 30001);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
async fn test_cache_authenticate_system_account() {
    const SECURE_PASSWORD: &str = "a";

    let current_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(365);
    let expire_time = OffsetDateTime::UNIX_EPOCH + time::Duration::days(380);
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    let yescrypted_username = "yescrypted_account".to_string();

    // Important! This is what sets up that testaccount1 won't be resolved
    // because it's in the "local" user set.
    cachelayer
        .reload_system_identities(
            vec![
            EtcUser {
                name: "testaccount1".to_string(),
                uid: 30000,
                gid: 30000,
                password: Default::default(),
                gecos: Default::default(),
                homedir: Default::default(),
                shell: Default::default(),
            },
            EtcUser {
                name: "testaccount2".to_string(),
                uid: 30001,
                gid: 30001,
                password: Default::default(),
                gecos: Default::default(),
                homedir: Default::default(),
                shell: Default::default(),
            },
            EtcUser {
                name: yescrypted_username.clone(),
                uid: 30002,
                gid: 30002,
                password: Default::default(),
                gecos: Default::default(),
                homedir: Default::default(),
                shell: Default::default(),
            }
            ],
            vec![
                EtcShadow {
                    name: "testaccount1".to_string(),
                    // The very secure password, "a".
                    password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
                    epoch_change_seconds: None,
                    days_min_password_age: 0,
                    days_max_password_age: Some(1),
                    days_warning_period: 1,
                    days_inactivity_period: None,
                    epoch_expire_seconds: Some(time::OffsetDateTime::UNIX_EPOCH + time::Duration::days(380)),
                    flag_reserved: None
                },
                EtcShadow {
                    name: "testaccount2".to_string(),
                    // The very secure password, "a".
                    password: CryptPw::Sha512("$6$5.bXZTIXuVv.xI3.$sAubscCJPwnBWwaLt2JR33lo539UyiDku.aH5WVSX0Tct9nGL2ePMEmrqT3POEdBlgNQ12HJBwskewGu2dpF//".to_string()),
                    epoch_change_seconds: Some(time::OffsetDateTime::UNIX_EPOCH + time::Duration::days(364)),
                    days_min_password_age: 0,
                    days_max_password_age: Some(2),
                    days_warning_period: 1,
                    days_inactivity_period: None,
                    epoch_expire_seconds: Some(time::OffsetDateTime::UNIX_EPOCH + time::Duration::days(380)),
                    flag_reserved: None
                },
                EtcShadow {
                    name: yescrypted_username.clone(),
                    // The very secure password, "a".
                    password: CryptPw::YesCrypt("$y$j9T$LdJMENpBABJJ3hIHjB1Bi.$GFxnbKnR8WaEdBMGMctf6JGMs56hU5dYcy6UrKGWr62".to_string()),
                    epoch_change_seconds: Some(time::OffsetDateTime::UNIX_EPOCH + time::Duration::days(364)),
                    days_min_password_age: 0,
                    days_max_password_age: Some(2),
                    days_warning_period: 1,
                    days_inactivity_period: None,
                    epoch_expire_seconds: Some(time::OffsetDateTime::UNIX_EPOCH + time::Duration::days(380)),
                    flag_reserved: None
                },
            ],
            vec![],
        )
        .await;

    // get the accounts to assert they exist,
    let _ = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    let _ = cachelayer
        .get_nssaccount_name("testaccount2")
        .await
        .expect("Failed to get from cache");
    let _ = cachelayer
        .get_nssaccount_name(&yescrypted_username)
        .await
        .expect("Failed to get from cache");

    // Non exist name
    let a1 = cachelayer
        .pam_account_authenticate("testaccount69", current_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, None);

    // Check wrong pw.
    let a1 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, "wrong password")
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    let a1 = cachelayer
        .pam_account_authenticate("testaccount2", current_time, "wrong password")
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    let a1 = cachelayer
        .pam_account_authenticate(&yescrypted_username, current_time, "wrong password")
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    // Check correct pw (both accounts)
    let a1 = cachelayer
        .pam_account_authenticate("testaccount1", current_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(true));

    let a1 = cachelayer
        .pam_account_authenticate("testaccount2", current_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(true));

    let a1 = cachelayer
        .pam_account_authenticate(&yescrypted_username, current_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(true), "yescrypted password failed to authenticate");

    // Check expired time (both accounts)
    let a1 = cachelayer
        .pam_account_authenticate("testaccount1", expire_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    let a1 = cachelayer
        .pam_account_authenticate("testaccount2", expire_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    let a1 = cachelayer
        .pam_account_authenticate(&yescrypted_username, expire_time, SECURE_PASSWORD)
        .await
        .expect("failed to authenticate");
    assert_eq!(a1, Some(false));

    // due to how posix auth works, session and authorisation are simpler, and should
    // always just return "true".
    let a1 = cachelayer
        .pam_account_allowed("testaccount1")
        .await
        .expect("failed to authorise");
    assert_eq!(a1, Some(true));

    let a1 = cachelayer
        .pam_account_allowed("testaccount2")
        .await
        .expect("failed to authorise");
    assert_eq!(a1, Some(true));

    let a1 = cachelayer
        .pam_account_allowed(&yescrypted_username)
        .await
        .expect("failed to authorise");
    assert_eq!(a1, Some(true));

    // Should we make home dirs?
    let a1 = cachelayer
        .pam_account_beginsession("testaccount1")
        .await
        .expect("failed to begin session");
    assert_eq!(a1, None);

    let a1 = cachelayer
        .pam_account_beginsession("testaccount2")
        .await
        .expect("failed to begin session");
    assert_eq!(a1, None);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

/// Issue 1830. If cache items expire where we have an account and a group, and we
/// refresh the group *first*, the group appears to drop it's members. This is because
/// sqlite "INSERT OR REPLACE INTO" triggers a delete cascade of the foreign key elements
/// which then makes the group appear empty.
///
/// We can reproduce this by retrieving an account + group, wait for expiry, then retrieve
/// only the group.
#[tokio::test]
async fn test_cache_group_fk_deferred() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // Get the account then the group.
    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());
    assert_eq!(gt.unwrap().members.len(), 1);

    // Invalidate all items.
    cachelayer.mark_offline().await;
    assert!(cachelayer.invalidate().await.is_ok());
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    // Get the *group*. It *should* still have it's members.
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_some());
    // And check we have members in the group, since we came from a userlook up
    assert_eq!(gt.unwrap().members.len(), 1);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}

#[tokio::test]
/// Test group extension. Groups extension is not the same as "overriding". Extension
/// only allows the *members* of a remote group to supplement the members of the local
/// group. This prevents a remote group changing the gidnumber of the local group and
/// causing breakages.
async fn test_cache_extend_group_members() {
    let (cachelayer, async_refresh_rx, _adminclient) = setup_test(fixture(test_fixture)).await;

    cachelayer
        .reload_system_identities(
            vec![EtcUser {
                name: "local_account".to_string(),
                uid: 30000,
                gid: 30000,
                password: Default::default(),
                gecos: Default::default(),
                homedir: Default::default(),
                shell: Default::default(),
            }],
            vec![],
            vec![EtcGroup {
                // This group is configured to allow extension from
                // the group "testgroup1"
                name: "extensible_group".to_string(),
                gid: 30001,
                password: Default::default(),
                // We have the local account as a member, it should NOT be stomped.
                members: vec!["local_account".to_string()],
            }],
        )
        .await;

    // Force offline. Show we have no groups.
    cachelayer.mark_offline().await;
    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");
    assert!(gt.is_none());

    // While offline, extensible_group has only local_account as a member.
    let gt = cachelayer
        .get_nssgroup_name("extensible_group")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 30001);
    assert_eq!(gt.members.as_slice(), &["local_account".to_string()]);

    // Go online. Group now exists, extensible_group has group members.
    // Need to resolve test-account first so that the membership is linked.
    cachelayer.mark_next_check_now(SystemTime::now()).await;
    assert!(cachelayer.test_connection().await);

    let ut = cachelayer
        .get_nssaccount_name("testaccount1")
        .await
        .expect("Failed to get from cache");
    assert!(ut.is_some());

    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 20001);
    assert_eq!(
        gt.members.as_slice(),
        &["testaccount1@idm.example.com".to_string()]
    );

    let gt = cachelayer
        .get_nssgroup_name("extensible_group")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    // Even though it's extended, still needs to be the local uid/gid
    assert_eq!(gt.gid, 30001);
    assert_eq!(
        gt.members.as_slice(),
        &[
            "local_account".to_string(),
            "testaccount1@idm.example.com".to_string()
        ]
    );

    let groups = cachelayer
        .get_nssgroups()
        .await
        .expect("Failed to get from cache");

    assert!(groups.iter().any(|group| {
        group.name == "extensible_group"
            && group.members.as_slice()
                == [
                    "local_account".to_string(),
                    "testaccount1@idm.example.com".to_string(),
                ]
    }));

    // Go offline. Group cached, extensible_group has members.
    cachelayer.mark_offline().await;

    let gt = cachelayer
        .get_nssgroup_name("testgroup1")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 20001);
    assert_eq!(
        gt.members.as_slice(),
        &["testaccount1@idm.example.com".to_string()]
    );

    let gt = cachelayer
        .get_nssgroup_name("extensible_group")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    // Even though it's extended, still needs to be the local uid/gid
    assert_eq!(gt.gid, 30001);
    assert_eq!(
        gt.members.as_slice(),
        &[
            "local_account".to_string(),
            "testaccount1@idm.example.com".to_string()
        ]
    );

    // clear cache
    cachelayer
        .clear_cache()
        .await
        .expect("failed to clear cache");

    // No longer has testaccount.
    let gt = cachelayer
        .get_nssgroup_name("extensible_group")
        .await
        .expect("Failed to get from cache");

    let gt = gt.unwrap();
    assert_eq!(gt.gid, 30001);
    assert_eq!(gt.members.as_slice(), &["local_account".to_string()]);
    // Assert no outstanding async requests
    assert!(async_refresh_rx.is_empty());
}
