/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use ahash::AHashSet;
use directory::{
    backend::{
        internal::{
            lookup::DirectoryStore,
            manage::{self, ChangedPrincipals, ManageDirectory, UpdatePrincipal},
            PrincipalField, PrincipalUpdate, PrincipalValue,
        },
        RcptType,
    },
    Principal, QueryBy, Type,
};
use jmap_proto::types::collection::Collection;
use mail_send::Credentials;
use store::{
    roaring::RoaringBitmap,
    write::{BatchBuilder, BitmapClass, ValueClass},
    BitmapKey, Store, ValueKey,
};

use crate::directory::{DirectoryTest, IntoTestPrincipal, TestPrincipal};

#[tokio::test]
async fn internal_directory() {
    let config = DirectoryTest::new(None).await;

    for (store_id, store) in config.stores.stores {
        println!("Testing internal directory with store {:?}", store_id);
        store.destroy().await;

        // A principal without name should fail
        assert_eq!(
            store
                .create_principal(Principal::default(), None, None)
                .await,
            Err(manage::err_missing(PrincipalField::Name))
        );

        // Basic account creation
        let john_id = store
            .create_principal(
                TestPrincipal {
                    name: "john".to_string(),
                    description: Some("John Doe".to_string()),
                    secrets: vec!["secret".to_string(), "secret2".to_string()],
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap()
            .id;

        // Two accounts with the same name should fail
        assert_eq!(
            store
                .create_principal(
                    TestPrincipal {
                        name: "john".to_string(),
                        ..Default::default()
                    }
                    .into(),
                    None,
                    None
                )
                .await,
            Err(manage::err_exists(PrincipalField::Name, "john".to_string()))
        );

        // An account using a non-existent domain should fail
        assert_eq!(
            store
                .create_principal(
                    TestPrincipal {
                        name: "jane".to_string(),
                        emails: vec!["jane@example.org".to_string()],
                        ..Default::default()
                    }
                    .into(),
                    None,
                    None
                )
                .await,
            Err(manage::not_found("example.org".to_string()))
        );

        // Create a domain name
        store
            .create_principal(
                TestPrincipal {
                    name: "example.org".to_string(),
                    typ: Type::Domain,
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap();
        assert!(store.is_local_domain("example.org").await.unwrap());
        assert!(!store.is_local_domain("otherdomain.org").await.unwrap());

        // Add an email address
        assert!(store
            .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                PrincipalUpdate::add_item(
                    PrincipalField::Emails,
                    PrincipalValue::String("john@example.org".to_string()),
                )
            ]))
            .await
            .is_ok());
        assert_eq!(
            store.rcpt("john@example.org").await.unwrap(),
            RcptType::Mailbox
        );
        assert_eq!(
            store.email_to_id("john@example.org").await.unwrap(),
            Some(john_id)
        );

        // Using non-existent domain should fail
        assert_eq!(
            store
                .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                    PrincipalUpdate::add_item(
                        PrincipalField::Emails,
                        PrincipalValue::String("john@otherdomain.org".to_string()),
                    )
                ]))
                .await,
            Err(manage::not_found("otherdomain.org".to_string()))
        );

        // Create an account with an email address
        let jane_id = store
            .create_principal(
                TestPrincipal {
                    name: "jane".to_string(),
                    description: Some("Jane Doe".to_string()),
                    secrets: vec!["my_secret".to_string(), "my_secret2".to_string()],
                    emails: vec!["jane@example.org".to_string()],
                    quota: 123,
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap()
            .id;

        assert_eq!(
            store.rcpt("jane@example.org").await.unwrap(),
            RcptType::Mailbox
        );
        assert_eq!(
            store.rcpt("jane@otherdomain.org").await.unwrap(),
            RcptType::Invalid
        );
        assert_eq!(
            store.email_to_id("jane@example.org").await.unwrap(),
            Some(jane_id)
        );
        assert_eq!(store.vrfy("jane").await.unwrap(), vec!["jane@example.org"]);
        assert_eq!(
            store
                .query(
                    QueryBy::Credentials(&Credentials::new(
                        "jane".to_string(),
                        "my_secret".to_string()
                    )),
                    true
                )
                .await
                .unwrap()
                .map(|p| p.into_test()),
            Some(TestPrincipal {
                id: jane_id,
                name: "jane".to_string(),
                description: Some("Jane Doe".to_string()),
                emails: vec!["jane@example.org".to_string()],
                secrets: vec!["my_secret".to_string(), "my_secret2".to_string()],
                quota: 123,
                ..Default::default()
            })
        );
        assert_eq!(
            store
                .query(
                    QueryBy::Credentials(&Credentials::new(
                        "jane".to_string(),
                        "wrong_password".to_string()
                    )),
                    true
                )
                .await
                .unwrap(),
            None
        );

        // Duplicate email address should fail
        assert_eq!(
            store
                .create_principal(
                    TestPrincipal {
                        name: "janeth".to_string(),
                        description: Some("Janeth Doe".to_string()),
                        emails: vec!["jane@example.org".to_string()],
                        ..Default::default()
                    }
                    .into(),
                    None,
                    None
                )
                .await,
            Err(manage::err_exists(
                PrincipalField::Emails,
                "jane@example.org".to_string()
            ))
        );

        // Create a mailing list
        let list_id = store
            .create_principal(
                TestPrincipal {
                    name: "list".to_string(),
                    typ: Type::List,
                    emails: vec!["list@example.org".to_string()],
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap()
            .id;
        assert!(store
            .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![
                PrincipalUpdate::set(
                    PrincipalField::Members,
                    PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]),
                ),
                PrincipalUpdate::set(
                    PrincipalField::ExternalMembers,
                    PrincipalValue::StringList(vec![
                        "mike@other.org".to_string(),
                        "lucy@foobar.net".to_string()
                    ]),
                )
            ]))
            .await
            .is_ok());

        assert_list_members(
            &store,
            "list@example.org",
            [
                "john@example.org",
                "mike@other.org",
                "lucy@foobar.net",
                "jane@example.org",
            ],
        )
        .await;

        assert_eq!(
            store
                .query(QueryBy::Name("list"), true)
                .await
                .unwrap()
                .unwrap()
                .into_test(),
            TestPrincipal {
                name: "list".to_string(),
                id: list_id,
                typ: Type::List,
                emails: vec!["list@example.org".to_string()],
                ..Default::default()
            }
        );
        assert_eq!(
            store
                .expn("list@example.org")
                .await
                .unwrap()
                .into_iter()
                .collect::<AHashSet<_>>(),
            [
                "john@example.org",
                "mike@other.org",
                "lucy@foobar.net",
                "jane@example.org"
            ]
            .into_iter()
            .map(|s| s.to_string())
            .collect::<AHashSet<_>>()
        );

        // Create groups
        store
            .create_principal(
                TestPrincipal {
                    name: "sales".to_string(),
                    description: Some("Sales Team".to_string()),
                    typ: Type::Group,
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap();
        store
            .create_principal(
                TestPrincipal {
                    name: "support".to_string(),
                    description: Some("Support Team".to_string()),
                    typ: Type::Group,
                    ..Default::default()
                }
                .into(),
                None,
                None,
            )
            .await
            .unwrap();

        // Add John to the Sales and Support groups
        assert!(store
            .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                PrincipalUpdate::add_item(
                    PrincipalField::MemberOf,
                    PrincipalValue::String("sales".to_string()),
                ),
                PrincipalUpdate::add_item(
                    PrincipalField::MemberOf,
                    PrincipalValue::String("support".to_string()),
                )
            ]))
            .await
            .is_ok());
        let mut principal = store
            .query(QueryBy::Name("john"), true)
            .await
            .unwrap()
            .unwrap();
        store.map_field_ids(&mut principal, &[]).await.unwrap();
        assert_eq!(
            principal.into_test().into_sorted(),
            TestPrincipal {
                id: john_id,
                name: "john".to_string(),
                description: Some("John Doe".to_string()),
                secrets: vec!["secret".to_string(), "secret2".to_string()],
                emails: vec!["john@example.org".to_string()],
                member_of: vec!["sales".to_string(), "support".to_string()],
                lists: vec!["list".to_string()],
                ..Default::default()
            }
        );

        // Adding a non-existent user should fail
        assert_eq!(
            store
                .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                    PrincipalUpdate::add_item(
                        PrincipalField::MemberOf,
                        PrincipalValue::String("accounting".to_string()),
                    )
                ]))
                .await,
            Err(manage::not_found("accounting".to_string()))
        );

        // Remove a member from a group
        assert!(store
            .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                PrincipalUpdate::remove_item(
                    PrincipalField::MemberOf,
                    PrincipalValue::String("support".to_string()),
                )
            ]))
            .await
            .is_ok());
        let mut principal = store
            .query(QueryBy::Name("john"), true)
            .await
            .unwrap()
            .unwrap();
        store.map_field_ids(&mut principal, &[]).await.unwrap();
        assert_eq!(
            principal.into_test().into_sorted(),
            TestPrincipal {
                id: john_id,
                name: "john".to_string(),
                description: Some("John Doe".to_string()),
                secrets: vec!["secret".to_string(), "secret2".to_string()],
                emails: vec!["john@example.org".to_string()],
                member_of: vec!["sales".to_string()],
                lists: vec!["list".to_string()],
                ..Default::default()
            }
        );

        // Update multiple fields
        assert!(store
            .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![
                PrincipalUpdate::set(
                    PrincipalField::Name,
                    PrincipalValue::String("john.doe".to_string())
                ),
                PrincipalUpdate::set(
                    PrincipalField::Description,
                    PrincipalValue::String("Johnny Doe".to_string())
                ),
                PrincipalUpdate::set(
                    PrincipalField::Secrets,
                    PrincipalValue::StringList(vec!["12345".to_string()])
                ),
                PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)),
                PrincipalUpdate::remove_item(
                    PrincipalField::Emails,
                    PrincipalValue::String("john@example.org".to_string()),
                ),
                PrincipalUpdate::add_item(
                    PrincipalField::Emails,
                    PrincipalValue::String("john.doe@example.org".to_string()),
                )
            ]))
            .await
            .is_ok());

        let mut principal = store
            .query(QueryBy::Name("john.doe"), true)
            .await
            .unwrap()
            .unwrap();
        store.map_field_ids(&mut principal, &[]).await.unwrap();
        assert_eq!(
            principal.into_test().into_sorted(),
            TestPrincipal {
                id: john_id,
                name: "john.doe".to_string(),
                description: Some("Johnny Doe".to_string()),
                secrets: vec!["12345".to_string()],
                emails: vec!["john.doe@example.org".to_string()],
                quota: 1024,
                typ: Type::Individual,
                member_of: vec!["sales".to_string()],
                lists: vec!["list".to_string()],
                ..Default::default()
            }
        );
        assert_eq!(store.get_principal_id("john").await.unwrap(), None);
        assert_eq!(
            store.rcpt("john@example.org").await.unwrap(),
            RcptType::Invalid
        );
        assert_eq!(
            store.rcpt("john.doe@example.org").await.unwrap(),
            RcptType::Mailbox
        );

        // Remove a member from a mailing list and then add it back
        assert!(store
            .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![
                PrincipalUpdate::remove_item(
                    PrincipalField::Members,
                    PrincipalValue::String("john.doe".to_string()),
                )
            ]))
            .await
            .is_ok());
        assert_list_members(
            &store,
            "list@example.org",
            ["jane@example.org", "mike@other.org", "lucy@foobar.net"],
        )
        .await;
        assert!(store
            .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![
                PrincipalUpdate::add_item(
                    PrincipalField::Members,
                    PrincipalValue::String("john.doe".to_string()),
                )
            ]))
            .await
            .is_ok());
        assert_list_members(
            &store,
            "list@example.org",
            [
                "john.doe@example.org",
                "jane@example.org",
                "mike@other.org",
                "lucy@foobar.net",
            ],
        )
        .await;

        // Field validation
        assert_eq!(
            store
                .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![
                    PrincipalUpdate::set(
                        PrincipalField::Name,
                        PrincipalValue::String("jane".to_string())
                    ),
                ]))
                .await,
            Err(manage::err_exists(PrincipalField::Name, "jane".to_string()))
        );
        assert_eq!(
            store
                .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![
                    PrincipalUpdate::add_item(
                        PrincipalField::Emails,
                        PrincipalValue::String("jane@example.org".to_string())
                    ),
                ]))
                .await,
            Err(manage::err_exists(
                PrincipalField::Emails,
                "jane@example.org".to_string()
            ))
        );

        // List accounts
        assert_eq!(
            store
                .list_principals(
                    None,
                    None,
                    &[Type::Individual, Type::Group, Type::List],
                    &[],
                    0,
                    0
                )
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<AHashSet<_>>(),
            ["jane", "john.doe", "list", "sales", "support"]
                .into_iter()
                .map(|s| s.to_string())
                .collect::<AHashSet<_>>()
        );
        assert_eq!(
            store
                .list_principals("john".into(), None, &[], &[], 0, 0)
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<Vec<_>>(),
            vec!["john.doe"]
        );
        assert_eq!(
            store
                .list_principals(None, None, &[Type::Individual], &[], 0, 0)
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<AHashSet<_>>(),
            ["jane", "john.doe"]
                .into_iter()
                .map(|s| s.to_string())
                .collect::<AHashSet<_>>()
        );
        assert_eq!(
            store
                .list_principals(None, None, &[Type::Group], &[], 0, 0)
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<AHashSet<_>>(),
            ["sales", "support"]
                .into_iter()
                .map(|s| s.to_string())
                .collect::<AHashSet<_>>()
        );
        assert_eq!(
            store
                .list_principals(None, None, &[Type::List], &[], 0, 0)
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<Vec<_>>(),
            vec!["list"]
        );

        // Write records on John's and Jane's accounts
        let mut document_id = u32::MAX;
        for account_id in [john_id, jane_id] {
            document_id = store
                .write(
                    BatchBuilder::new()
                        .with_account_id(account_id)
                        .with_collection(Collection::Email)
                        .create_document()
                        .set(ValueClass::Property(0), "hello".as_bytes())
                        .build_batch(),
                )
                .await
                .unwrap()
                .last_document_id()
                .unwrap();
            assert_eq!(
                store
                    .get_value::<String>(ValueKey {
                        account_id,
                        collection: Collection::Email.into(),
                        document_id,
                        class: ValueClass::Property(0)
                    })
                    .await
                    .unwrap(),
                Some("hello".to_string())
            );
        }

        // Delete John's account and make sure his records are gone
        store.delete_principal(QueryBy::Id(john_id)).await.unwrap();
        assert_eq!(store.get_principal_id("john.doe").await.unwrap(), None);
        assert_eq!(
            store.email_to_id("john.doe@example.org").await.unwrap(),
            None
        );
        assert_eq!(
            store.rcpt("john.doe@example.org").await.unwrap(),
            RcptType::Invalid
        );
        assert_eq!(
            store
                .list_principals(
                    None,
                    None,
                    &[Type::Individual, Type::Group, Type::List],
                    &[],
                    0,
                    0
                )
                .await
                .unwrap()
                .items
                .into_iter()
                .map(|p| p.name().to_string())
                .collect::<AHashSet<_>>(),
            ["jane", "list", "sales", "support"]
                .into_iter()
                .map(|s| s.to_string())
                .collect::<AHashSet<_>>()
        );
        assert_eq!(
            store
                .get_bitmap(BitmapKey {
                    account_id: john_id,
                    collection: Collection::Email.into(),
                    class: BitmapClass::DocumentIds,
                    document_id: 0
                })
                .await
                .unwrap(),
            None
        );
        assert_eq!(
            store
                .get_value::<String>(ValueKey {
                    account_id: john_id,
                    collection: Collection::Email.into(),
                    document_id: 0,
                    class: ValueClass::Property(0)
                })
                .await
                .unwrap(),
            None
        );

        // Make sure Jane's records are still there
        assert_eq!(store.get_principal_id("jane").await.unwrap(), Some(jane_id));
        assert_eq!(
            store.email_to_id("jane@example.org").await.unwrap(),
            Some(jane_id)
        );
        assert_eq!(
            store.rcpt("jane@example.org").await.unwrap(),
            RcptType::Mailbox
        );
        assert_eq!(
            store
                .get_bitmap(BitmapKey {
                    account_id: jane_id,
                    collection: Collection::Email.into(),
                    class: BitmapClass::DocumentIds,
                    document_id: 0
                })
                .await
                .unwrap(),
            Some(RoaringBitmap::from_sorted_iter([document_id]).unwrap())
        );
        assert_eq!(
            store
                .get_value::<String>(ValueKey {
                    account_id: jane_id,
                    collection: Collection::Email.into(),
                    document_id,
                    class: ValueClass::Property(0)
                })
                .await
                .unwrap(),
            Some("hello".to_string())
        );
    }
}

#[allow(async_fn_in_trait)]
pub trait TestInternalDirectory {
    async fn create_test_user(&self, login: &str, secret: &str, name: &str, emails: &[&str])
        -> u32;
    async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32;
    async fn create_test_list(&self, login: &str, name: &str, emails: &[&str]) -> u32;
    async fn set_test_quota(&self, login: &str, quota: u32);
    async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals;
    async fn remove_from_group(&self, login: &str, group: &str) -> ChangedPrincipals;
    async fn remove_test_alias(&self, login: &str, alias: &str);
    async fn create_test_domains(&self, domains: &[&str]);
}

impl TestInternalDirectory for Store {
    async fn create_test_user(
        &self,
        login: &str,
        secret: &str,
        name: &str,
        emails: &[&str],
    ) -> u32 {
        let role = if login == "admin" { "admin" } else { "user" };
        self.create_test_domains(emails).await;
        if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() {
            self.update_principal(UpdatePrincipal::by_id(principal.id()).with_updates(vec![
                PrincipalUpdate::set(
                    PrincipalField::Secrets,
                    PrincipalValue::StringList(vec![secret.to_string()]),
                ),
                PrincipalUpdate::set(
                    PrincipalField::Description,
                    PrincipalValue::String(name.to_string()),
                ),
                PrincipalUpdate::set(
                    PrincipalField::Emails,
                    PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()),
                ),
                PrincipalUpdate::add_item(
                    PrincipalField::Roles,
                    PrincipalValue::String(role.to_string()),
                ),
            ]))
            .await
            .unwrap();
            principal.id()
        } else {
            self.create_principal(
                Principal::new(0, Type::Individual)
                    .with_field(PrincipalField::Name, login.to_string())
                    .with_field(PrincipalField::Description, name.to_string())
                    .with_field(
                        PrincipalField::Secrets,
                        PrincipalValue::StringList(vec![secret.to_string()]),
                    )
                    .with_field(
                        PrincipalField::Emails,
                        PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()),
                    )
                    .with_field(
                        PrincipalField::Roles,
                        PrincipalValue::StringList(vec![role.to_string()]),
                    ),
                None,
                None,
            )
            .await
            .unwrap()
            .id
        }
    }

    async fn create_test_group(&self, login: &str, name: &str, emails: &[&str]) -> u32 {
        self.create_test_domains(emails).await;
        if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() {
            principal.id()
        } else {
            self.create_principal(
                Principal::new(0, Type::Group)
                    .with_field(PrincipalField::Name, login.to_string())
                    .with_field(PrincipalField::Description, name.to_string())
                    .with_field(
                        PrincipalField::Emails,
                        PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()),
                    )
                    .with_field(
                        PrincipalField::Roles,
                        PrincipalValue::StringList(vec!["user".to_string()]),
                    ),
                None,
                None,
            )
            .await
            .unwrap()
            .id
        }
    }

    async fn create_test_list(&self, login: &str, name: &str, members: &[&str]) -> u32 {
        if let Some(principal) = self.query(QueryBy::Name(login), false).await.unwrap() {
            principal.id()
        } else {
            self.create_test_domains(&[login]).await;
            self.create_principal(
                Principal::new(0, Type::List)
                    .with_field(PrincipalField::Name, login.to_string())
                    .with_field(PrincipalField::Description, name.to_string())
                    .with_field(
                        PrincipalField::Members,
                        PrincipalValue::StringList(members.iter().map(|s| s.to_string()).collect()),
                    )
                    .with_field(
                        PrincipalField::Emails,
                        PrincipalValue::StringList(vec![login.to_string()]),
                    ),
                None,
                None,
            )
            .await
            .unwrap()
            .id
        }
    }

    async fn set_test_quota(&self, login: &str, quota: u32) {
        self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![
            PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(quota as u64)),
        ]))
        .await
        .unwrap();
    }

    async fn add_to_group(&self, login: &str, group: &str) -> ChangedPrincipals {
        self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![
            PrincipalUpdate::add_item(
                PrincipalField::MemberOf,
                PrincipalValue::String(group.to_string()),
            ),
        ]))
        .await
        .unwrap()
    }

    async fn remove_from_group(&self, login: &str, group: &str) -> ChangedPrincipals {
        self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![
            PrincipalUpdate::remove_item(
                PrincipalField::MemberOf,
                PrincipalValue::String(group.to_string()),
            ),
        ]))
        .await
        .unwrap()
    }

    async fn remove_test_alias(&self, login: &str, alias: &str) {
        self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![
            PrincipalUpdate::remove_item(
                PrincipalField::Emails,
                PrincipalValue::String(alias.to_string()),
            ),
        ]))
        .await
        .unwrap();
    }

    async fn create_test_domains(&self, domains: &[&str]) {
        for domain in domains {
            let domain = domain.rsplit_once('@').map_or(*domain, |(_, d)| d);
            if self
                .query(QueryBy::Name(domain), false)
                .await
                .unwrap()
                .is_none()
            {
                self.create_principal(
                    Principal::new(0, Type::Domain)
                        .with_field(PrincipalField::Name, domain.to_string()),
                    None,
                    None,
                )
                .await
                .unwrap();
            }
        }
    }
}

async fn assert_list_members(
    store: &Store,
    list_addr: &str,
    members: impl IntoIterator<Item = &str>,
) {
    match store.rcpt(list_addr).await.unwrap() {
        RcptType::List(items) => {
            assert_eq!(
                items.into_iter().collect::<AHashSet<_>>(),
                members
                    .into_iter()
                    .map(|s| s.to_string())
                    .collect::<AHashSet<_>>()
            );
        }
        other => panic!("invalid {other:?}"),
    }
}
