mod testprotocol;
mod testsupport;

use crate::testprotocol::acme::user::v1::Bucket;
use crate::testsupport::{get_config_store, init_config_store};
use pbbson::bson::{DateTime, doc};
use pbjson_types::{Struct, Value};
use saas_rs_sdk::pbbson::Model;
use saas_rs_sdk::storage::Error;
use saas_rs_sdk::storage::config_store::FindOptions;
use testprotocol::acme::user::v1::linked_account::{Owner, Provider, Type};
use testprotocol::acme::user::v1::{Account, LinkedAccount};

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_create_linked_account_and_auto_assign_id() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();

    // Setup precondition
    let account = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let account: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");

    // Test
    let linked_account = LinkedAccount {
        created_by_account_id: Some(account.id.clone()),
        owner: Some(Owner::OwnerAccountId(account.id.clone())),
        provider: Provider::Stripe as i32,
        provider_uid: xid::new().to_string(),
        r#type: Type::IdentityProvider as i32,
        ..Default::default()
    };
    let linked_account: LinkedAccount = config_store
        .create(Bucket::LinkedAccounts, Model::try_from(&linked_account).unwrap())
        .await
        .expect("Expected to be able to create a linked account")
        .try_into()
        .expect("Expected to be able to deserialize a created linked account");
    assert!(!linked_account.id.is_empty());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_create_linked_account_with_caller_assigned_xid() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();

    // Setup precondition
    let account = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let account: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");

    // Test
    let id = xid::new().to_string();
    let linked_account = LinkedAccount {
        created_by_account_id: Some(account.id.clone()),
        owner: Some(Owner::OwnerAccountId(account.id.clone())),
        id: id.clone(),
        provider: Provider::Stripe as i32,
        provider_uid: xid::new().to_string(),
        r#type: Type::IdentityProvider as i32,
        ..Default::default()
    };
    let linked_account: LinkedAccount = config_store
        .create(Bucket::LinkedAccounts, Model::try_from(&linked_account).unwrap())
        .await
        .expect("Expected to be able to create a linked account")
        .try_into()
        .expect("Expected to be able to deserialize a created linked account");
    assert_eq!(linked_account.id, id);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn cannot_delete_non_existent_linked_account() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();
    let id = xid::new().to_string();
    let res = config_store.delete(Bucket::LinkedAccounts, &id, None).await;
    assert!(res.is_err());
    assert!(matches!(res.err().unwrap(), Error::NotFound { .. }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn cannot_find_non_existent_linked_account() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();
    let id = xid::new().to_string();
    let res = config_store.find(Bucket::LinkedAccounts, &id).await;
    assert!(res.is_err());
    assert!(matches!(res.err().unwrap(), Error::NotFound { .. }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_perform_crud_operations() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();

    // Setup precondition
    let account = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let account: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");

    // Create
    let linked_account = LinkedAccount {
        created_by_account_id: Some(account.id.clone()),
        owner: Some(Owner::OwnerAccountId(account.id.clone())),
        provider: Provider::Stripe as i32,
        provider_uid: xid::new().to_string(),
        r#type: Type::IdentityProvider as i32,
        ..Default::default()
    };
    let linked_account: LinkedAccount = config_store
        .create(Bucket::LinkedAccounts, Model::try_from(&linked_account).unwrap())
        .await
        .expect("Expected to be able to create a linked account")
        .try_into()
        .expect("Expected to be able to deserialize a linked account");

    // Find One
    let linked_account_reloaded: LinkedAccount = config_store
        .find(Bucket::LinkedAccounts, &linked_account.id)
        .await
        .expect("Expected to be able to find newly created linked account")
        .try_into()
        .expect("Expected to be able to deserialize a linked account");
    assert_eq!(linked_account, linked_account_reloaded);

    // Find Many
    let linked_accounts: Vec<_> = config_store
        .find_many(Bucket::LinkedAccounts, FindOptions::new())
        .await
        .expect("Expected to be able to find newly created linked accounts")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert!(linked_accounts.contains(&linked_account));

    // Delete
    config_store
        .delete(Bucket::LinkedAccounts, &linked_account.id, None)
        .await
        .expect("Expected to be able to delete newly created linked account");

    // Find should return a record that is marked as soft-deleted
    let model = config_store
        .find(Bucket::LinkedAccounts, &linked_account.id)
        .await
        .unwrap();
    assert!(model.get("deletedAt").is_some());

    // Find Many should not contain match
    let filter = Model::from(doc! {"deletedAt": None::<DateTime>});
    let linked_accounts: Vec<_> = config_store
        .find_many(Bucket::LinkedAccounts, FindOptions::new().with_filter(filter))
        .await
        .expect("Expected to be able to find linked_accounts when no matches")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert!(!linked_accounts.contains(&linked_account));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
#[ignore] // TODO(https://gitlab.com/saas-rs/sdk/-/issues/80)
async fn can_maintain_belongs_to_account_fks() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();

    // Setup precondition
    let account = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let account: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");

    // Create
    let linked_account = LinkedAccount {
        created_by_account_id: Some(account.id.clone()),
        owner: Some(Owner::OwnerAccountId(account.id.clone())),
        provider: Provider::Stripe as i32,
        provider_uid: xid::new().to_string(),
        r#type: Type::IdentityProvider as i32,
        ..Default::default()
    };
    let linked_account: LinkedAccount = config_store
        .create(Bucket::LinkedAccounts, Model::try_from(&linked_account).unwrap())
        .await
        .expect("Expected to be able to create a linked account")
        .try_into()
        .expect("Expected to be able to deserialize a linked account");

    // Test
    let account_reloaded: Account = config_store
        .find(Bucket::Accounts, &account.id)
        .await
        .expect("Expected to be able to find newly created linked account")
        .try_into()
        .expect("Expected to be able to deserialize an account");
    assert!(account_reloaded.linked_account_ids.contains(&linked_account.id));

    // Delete
    config_store
        .delete(Bucket::LinkedAccounts, &linked_account.id, None)
        .await
        .expect("Expected to be able to delete newly created linked account");

    // Account should no longer have a foreign key
    let account_reloaded: Account = config_store
        .find(Bucket::Accounts, &account.id)
        .await
        .expect("Expected to be able to reload account")
        .try_into()
        .expect("Expected to be able to deserialize an account");
    assert!(!account_reloaded.linked_account_ids.contains(&linked_account.id));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_store_metadata() {
    let config_store = get_config_store!();
    init_config_store(&config_store).await.unwrap();

    // Setup precondition
    let account = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let account: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .unwrap();

    // Create
    let metadata: Struct = std::collections::HashMap::from([
        (String::from("bool"), Value::from(true)),
        (String::from("number"), 5.0.into()),
        (String::from("string"), "string".into()),
        (String::from("list"), vec![1.0.into(), 2.0.into()].into()),
        (
            String::from("map"),
            std::collections::HashMap::from([(String::from("key"), "value".into())]).into(),
        ),
    ])
    .into();
    let linked_account = LinkedAccount {
        created_by_account_id: Some(account.id.clone()),
        metadata: Some(metadata),
        owner: Some(Owner::OwnerAccountId(account.id.clone())),
        provider: Provider::Stripe as i32,
        provider_uid: xid::new().to_string(),
        r#type: Type::IdentityProvider as i32,
        ..Default::default()
    };
    let linked_account: LinkedAccount = config_store
        .create(Bucket::LinkedAccounts, Model::try_from(&linked_account).unwrap())
        .await
        .expect("Expected to be able to create a linked account")
        .try_into()
        .expect("Expected to be able to deserialize a linked account");

    // Find One
    let linked_account_reloaded: LinkedAccount = config_store
        .find(Bucket::LinkedAccounts, &linked_account.id)
        .await
        .expect("Expected to be able to find newly created linked account")
        .try_into()
        .expect("Expected to be able to deserialize a linked account");
    assert_eq!(linked_account, linked_account_reloaded);
}
