mod testprotocol;
mod testsupport;

use crate::testprotocol::acme::user::v1::Bucket;
use crate::testsupport::{get_config_store, init_config_store};
use pbbson::Model;
use pbbson::bson::{DateTime, doc, uuid};
use saas_rs_sdk::storage::Error;
use saas_rs_sdk::storage::config_store::FindOptions;
use testprotocol::acme::user::v1::Account;
use xid;

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_create_account_and_auto_assign_id() {
    let config_store = get_config_store!();
    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");
    assert!(!account.id.is_empty());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_create_account_with_caller_assigned_xid() {
    let config_store = get_config_store!();
    let id = xid::new().to_string();
    let account = Account {
        id: id.clone(),
        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");
    assert_eq!(account.id, id);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn can_create_account_with_caller_assigned_uuid() {
    let config_store = get_config_store!();
    let id = uuid::Uuid::new().to_string();
    let account = Account {
        id: id.clone(),
        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");
    assert_eq!(account.id, id);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn cannot_delete_non_existent_account() {
    let config_store = get_config_store!();
    let id = xid::new().to_string();
    let res = config_store.delete(Bucket::Accounts, &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_account() {
    let config_store = get_config_store!();
    let id = xid::new().to_string();
    let res = config_store.find(Bucket::Accounts, &id).await;
    assert!(res.is_err());
    assert!(matches!(res.err().unwrap(), Error::NotFound { .. }));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn cannot_update_non_existent_account() {
    let config_store = get_config_store!();
    let account = Account {
        id: xid::new().to_string(),
        email: Some(format!("{}@foo.bar", xid::new())),
        ..Default::default()
    };
    let res = config_store
        .update(Bucket::Accounts, Model::try_from(&account).unwrap())
        .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();

    // Create
    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");

    // Find One
    let account_reloaded: Account = config_store
        .find(Bucket::Accounts, &account.id)
        .await
        .expect("Expected to be able to find newly created account")
        .try_into()
        .expect("Expected to be able to deserialize an account");
    assert_eq!(account, account_reloaded);

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

    // Find Many By ID
    let accounts: Vec<_> = config_store
        .find_many(
            Bucket::Accounts,
            FindOptions::new().with_filter(Model::from(doc! {"id": &account.id})),
        )
        .await
        .expect("Expected to be able to find newly created accounts")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert!(accounts.contains(&account));

    // Delete
    config_store
        .delete(Bucket::Accounts, &account.id, Some(account.id.clone()))
        .await
        .expect("Expected to be able to delete newly created account");

    // Find should return a record that is marked as soft-deleted
    let model = config_store.find(Bucket::Accounts, &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 accounts: Vec<_> = config_store
        .find_many(Bucket::Accounts, FindOptions::new().with_filter(filter))
        .await
        .expect("Expected to be able to find accounts when no matches")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert!(!accounts.contains(&account));
}

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

    // Create
    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");

    // Find Many by ID
    let accounts: Vec<Account> = config_store
        .find_many(
            Bucket::Accounts,
            FindOptions::new()
                .with_filter(Model::from(doc! {"id": &account.id}))
                .with_projection(Some(Model::from(doc! {"email": 0}))),
        )
        .await
        .expect("Expected to be able to find newly created accounts")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    let maybe_account_reloaded = accounts.first();
    assert!(maybe_account_reloaded.is_some());
    let account_reloaded = maybe_account_reloaded.unwrap();
    assert_eq!(account_reloaded.id, account.id);
    assert_eq!(
        account_reloaded.email, None,
        "Expected projection to return id fields only"
    );
}

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

    // Create
    let account_1 = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        name: Some("A".to_string()),
        ..Default::default()
    };
    let _account_1: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account_1).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");
    let account_2 = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        name: Some("B".to_string()),
        ..Default::default()
    };
    let _account_2: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account_2).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");
    let account_3 = Account {
        email: Some(format!("{}@foo.bar", xid::new())),
        name: Some("C".to_string()),
        ..Default::default()
    };
    let _account_3: Account = config_store
        .create(Bucket::Accounts, Model::try_from(&account_3).unwrap())
        .await
        .expect("Expected to be able to create a account")
        .try_into()
        .expect("Expected to be able to deserialize a created account");

    // Find Many with limit
    let accounts: Vec<Account> = config_store
        .find_many(Bucket::Accounts, FindOptions::new().with_pagination(None, Some(2)))
        .await
        .expect("Expected to be able to find newly created accounts")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert_eq!(accounts.len(), 2, "Expected limit to work");
    let first_account = accounts.first().expect("Expected to be able to find first account");

    // Find Many with offset
    let accounts: Vec<Account> = config_store
        .find_many(Bucket::Accounts, FindOptions::new().with_pagination(Some(1), Some(2)))
        .await
        .expect("Expected to be able to find newly created accounts")
        .into_iter()
        .map(|model| model.try_into().unwrap())
        .collect();
    assert_eq!(accounts.len(), 2, "Expected limit to work");
    let new_first_account = accounts.first().expect("Expected to be able to find first account");
    assert!(
        first_account != new_first_account,
        "Expected query with offset to return different first record than query without an offset"
    );
}
