//! # Account manager module.

use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};

use anyhow::{ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::task::JoinHandle;
use uuid::Uuid;

#[cfg(not(target_os = "ios"))]
use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};

use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;

/// Account manager, that can handle multiple accounts in a single place.
#[derive(Debug)]
pub struct Accounts {
    dir: PathBuf,
    config: Config,
    /// Map from account ID to the account.
    accounts: BTreeMap<u32, Context>,

    /// Event channel to emit account manager errors.
    events: Events,

    /// Stock string translations shared by all created contexts.
    ///
    /// This way changing a translation for one context automatically
    /// changes it for all other contexts.
    pub(crate) stockstrings: StockStrings,

    /// Push notification subscriber shared between accounts.
    push_subscriber: PushSubscriber,
}

impl Accounts {
    /// Loads or creates an accounts folder at the given `dir`.
    pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
        if writable && !dir.exists() {
            Accounts::create(&dir).await?;
        }

        Accounts::open(dir, writable).await
    }

    /// Creates a new default structure.
    async fn create(dir: &Path) -> Result<()> {
        fs::create_dir_all(dir)
            .await
            .context("failed to create folder")?;

        Config::new(dir).await?;

        Ok(())
    }

    /// Opens an existing accounts structure. Will error if the folder doesn't exist,
    /// no account exists and no config exists.
    async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
        ensure!(dir.exists(), "directory does not exist");

        let config_file = dir.join(CONFIG_NAME);
        ensure!(config_file.exists(), "{:?} does not exist", config_file);

        let config = Config::from_file(config_file, writable)
            .await
            .context("failed to load accounts config")?;
        let events = Events::new();
        let stockstrings = StockStrings::new();
        let push_subscriber = PushSubscriber::new();
        let accounts = config
            .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
            .await
            .context("failed to load accounts")?;

        Ok(Self {
            dir,
            config,
            accounts,
            events,
            stockstrings,
            push_subscriber,
        })
    }

    /// Returns an account by its `id`:
    pub fn get_account(&self, id: u32) -> Option<Context> {
        self.accounts.get(&id).cloned()
    }

    /// Returns the currently selected account.
    pub fn get_selected_account(&self) -> Option<Context> {
        let id = self.config.get_selected_account();
        self.accounts.get(&id).cloned()
    }

    /// Returns the currently selected account's id or None if no account is selected.
    pub fn get_selected_account_id(&self) -> Option<u32> {
        match self.config.get_selected_account() {
            0 => None,
            id => Some(id),
        }
    }

    /// Selects the given account.
    pub async fn select_account(&mut self, id: u32) -> Result<()> {
        self.config.select_account(id).await?;

        Ok(())
    }

    /// Adds a new account and opens it.
    ///
    /// Returns account ID.
    pub async fn add_account(&mut self) -> Result<u32> {
        let account_config = self.config.new_account().await?;
        let dbfile = account_config.dbfile(&self.dir);

        let ctx = ContextBuilder::new(dbfile)
            .with_id(account_config.id)
            .with_events(self.events.clone())
            .with_stock_strings(self.stockstrings.clone())
            .with_push_subscriber(self.push_subscriber.clone())
            .build()
            .await?;
        // Try to open without a passphrase,
        // but do not return an error if account is passphare-protected.
        ctx.open("".to_string()).await?;

        self.accounts.insert(account_config.id, ctx);
        self.emit_event(EventType::AccountsChanged);

        Ok(account_config.id)
    }

    /// Adds a new closed account.
    pub async fn add_closed_account(&mut self) -> Result<u32> {
        let account_config = self.config.new_account().await?;
        let dbfile = account_config.dbfile(&self.dir);

        let ctx = ContextBuilder::new(dbfile)
            .with_id(account_config.id)
            .with_events(self.events.clone())
            .with_stock_strings(self.stockstrings.clone())
            .with_push_subscriber(self.push_subscriber.clone())
            .build()
            .await?;
        self.accounts.insert(account_config.id, ctx);
        self.emit_event(EventType::AccountsChanged);

        Ok(account_config.id)
    }

    /// Removes an account.
    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
        let ctx = self
            .accounts
            .remove(&id)
            .with_context(|| format!("no account with id {id}"))?;
        ctx.stop_io().await;

        // Explicitly close the database
        // to make sure the database file is closed
        // and can be removed on Windows.
        // If some spawned task tries to use the database afterwards,
        // it will fail.
        //
        // Previously `stop_io()` aborted the tasks without awaiting them
        // and this resulted in keeping `Context` clones inside
        // `Future`s that were not dropped. This bug is fixed now,
        // but explicitly closing the database ensures that file is freed
        // even if not all `Context` references are dropped.
        ctx.sql.close().await;
        drop(ctx);

        if let Some(cfg) = self.config.get_account(id) {
            let account_path = self.dir.join(cfg.dir);

            try_many_times(|| fs::remove_dir_all(&account_path))
                .await
                .context("failed to remove account data")?;
        }
        self.config.remove_account(id).await?;
        self.emit_event(EventType::AccountsChanged);

        Ok(())
    }

    /// Migrates an existing account into this structure.
    ///
    /// Returns the ID of new account.
    pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
        let blobdir = Context::derive_blobdir(&dbfile);
        let walfile = Context::derive_walfile(&dbfile);

        ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
        ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());

        let old_id = self.config.get_selected_account();

        // create new account
        let account_config = self
            .config
            .new_account()
            .await
            .context("failed to create new account")?;

        let new_dbfile = account_config.dbfile(&self.dir);
        let new_blobdir = Context::derive_blobdir(&new_dbfile);
        let new_walfile = Context::derive_walfile(&new_dbfile);

        let res = {
            fs::create_dir_all(self.dir.join(&account_config.dir))
                .await
                .context("failed to create dir")?;
            try_many_times(|| fs::rename(&dbfile, &new_dbfile))
                .await
                .context("failed to rename dbfile")?;
            try_many_times(|| fs::rename(&blobdir, &new_blobdir))
                .await
                .context("failed to rename blobdir")?;
            if walfile.exists() {
                fs::rename(&walfile, &new_walfile)
                    .await
                    .context("failed to rename walfile")?;
            }
            Ok(())
        };

        match res {
            Ok(_) => {
                let ctx = Context::new(
                    &new_dbfile,
                    account_config.id,
                    self.events.clone(),
                    self.stockstrings.clone(),
                )
                .await?;
                self.accounts.insert(account_config.id, ctx);
                Ok(account_config.id)
            }
            Err(err) => {
                let account_path = std::path::PathBuf::from(&account_config.dir);
                try_many_times(|| fs::remove_dir_all(&account_path))
                    .await
                    .context("failed to remove account data")?;
                self.config.remove_account(account_config.id).await?;

                // set selection back
                self.select_account(old_id).await?;

                Err(err)
            }
        }
    }

    /// Get a list of all account ids.
    pub fn get_all(&self) -> Vec<u32> {
        self.accounts.keys().copied().collect()
    }

    /// Starts background tasks such as IMAP and SMTP loops for all accounts.
    pub async fn start_io(&mut self) {
        for account in self.accounts.values_mut() {
            account.start_io().await;
        }
    }

    /// Stops background tasks for all accounts.
    pub async fn stop_io(&self) {
        // Sending an event here wakes up event loop even
        // if there are no accounts.
        info!(self, "Stopping IO for all accounts.");
        for account in self.accounts.values() {
            account.stop_io().await;
        }
    }

    /// Notifies all accounts that the network may have become available.
    pub async fn maybe_network(&self) {
        for account in self.accounts.values() {
            account.scheduler.maybe_network().await;
        }
    }

    /// Notifies all accounts that the network connection may have been lost.
    pub async fn maybe_network_lost(&self) {
        for account in self.accounts.values() {
            account.scheduler.maybe_network_lost(account).await;
        }
    }

    /// Performs a background fetch for all accounts in parallel.
    ///
    /// This is an auxiliary function and not part of public API.
    /// Use [Accounts::background_fetch] instead.
    async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
        async fn background_fetch_and_log_error(account: Context) {
            if let Err(error) = account.background_fetch().await {
                warn!(account, "{error:#}");
            }
        }

        events.emit(Event {
            id: 0,
            typ: EventType::Info(format!(
                "Starting background fetch for {} accounts.",
                accounts.len()
            )),
        });
        let mut futures_unordered: FuturesUnordered<_> = accounts
            .into_iter()
            .map(background_fetch_and_log_error)
            .collect();
        while futures_unordered.next().await.is_some() {}
    }

    /// Auxiliary function for [Accounts::background_fetch].
    async fn background_fetch_with_timeout(
        accounts: Vec<Context>,
        events: Events,
        timeout: std::time::Duration,
    ) {
        if let Err(_err) = tokio::time::timeout(
            timeout,
            Self::background_fetch_no_timeout(accounts, events.clone()),
        )
        .await
        {
            events.emit(Event {
                id: 0,
                typ: EventType::Warning("Background fetch timed out.".to_string()),
            });
        }
        events.emit(Event {
            id: 0,
            typ: EventType::AccountsBackgroundFetchDone,
        });
    }

    /// Performs a background fetch for all accounts in parallel with a timeout.
    ///
    /// The `AccountsBackgroundFetchDone` event is emitted at the end,
    /// process all events until you get this one and you can safely return to the background
    /// without forgetting to create notifications caused by timing race conditions.
    ///
    /// Returns a future that resolves when background fetch is done,
    /// but does not capture `&self`.
    pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
        let accounts: Vec<Context> = self.accounts.values().cloned().collect();
        let events = self.events.clone();
        Self::background_fetch_with_timeout(accounts, events, timeout)
    }

    /// Emits a single event.
    pub fn emit_event(&self, event: EventType) {
        self.events.emit(Event { id: 0, typ: event })
    }

    /// Returns event emitter.
    pub fn get_event_emitter(&self) -> EventEmitter {
        self.events.get_emitter()
    }

    /// Sets notification token for Apple Push Notification service.
    pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
        self.push_subscriber.set_device_token(token).await;
        Ok(())
    }
}

/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";

/// Lockfile name.
#[cfg(not(target_os = "ios"))]
const LOCKFILE_NAME: &str = "accounts.lock";

/// Database file name.
const DB_NAME: &str = "dc.db";

/// Account manager configuration file.
#[derive(Debug)]
struct Config {
    file: PathBuf,
    inner: InnerConfig,
    // We lock the lockfile in the Config constructors to protect also from having multiple Config
    // objects for the same config file.
    lock_task: Option<JoinHandle<anyhow::Result<()>>>,
}

/// Account manager configuration file contents.
///
/// This is serialized into TOML.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct InnerConfig {
    /// The currently selected account.
    pub selected_account: u32,
    pub next_id: u32,
    pub accounts: Vec<AccountConfig>,
}

impl Drop for Config {
    fn drop(&mut self) {
        if let Some(lock_task) = self.lock_task.take() {
            lock_task.abort();
        }
    }
}

impl Config {
    #[cfg(target_os = "ios")]
    async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
        // Do not lock accounts.toml on iOS.
        // This results in 0xdead10cc crashes on suspend.
        // iOS itself ensures that multiple instances of Delta Chat are not running.
        Ok(None)
    }

    #[cfg(not(target_os = "ios"))]
    async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
        let lockfile = dir.join(LOCKFILE_NAME);
        let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
        let (locked_tx, locked_rx) = oneshot::channel();
        let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
            let mut timeout = Duration::from_millis(100);
            let _guard = loop {
                match lock.try_write() {
                    Ok(guard) => break Ok(guard),
                    Err(err) => {
                        if timeout.as_millis() > 1600 {
                            break Err(err);
                        }
                        // We need to wait for the previous lock_task to be aborted thus unlocking
                        // the lockfile. We don't open configs for writing often outside of the
                        // tests, so this adds delays to the tests, but otherwise ok.
                        sleep(timeout).await;
                        if err.kind() == std::io::ErrorKind::WouldBlock {
                            timeout *= 2;
                        }
                    }
                }
            }?;
            locked_tx
                .send(())
                .ok()
                .context("Cannot notify about lockfile locking")?;
            let (_tx, rx) = oneshot::channel();
            rx.await?;
            Ok(())
        });
        locked_rx.await?;
        Ok(Some(lock_task))
    }

    /// Creates a new Config for `file`, but doesn't open/sync it.
    async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
        let dir = file.parent().context("Cannot get config file directory")?;
        let inner = InnerConfig {
            accounts: Vec::new(),
            selected_account: 0,
            next_id: 1,
        };
        if !lock {
            let cfg = Self {
                file,
                inner,
                lock_task: None,
            };
            return Ok(cfg);
        }
        let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
        let cfg = Self {
            file,
            inner,
            lock_task,
        };
        Ok(cfg)
    }

    /// Creates a new configuration file in the given account manager directory.
    pub async fn new(dir: &Path) -> Result<Self> {
        let lock = true;
        let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
        cfg.sync().await?;

        Ok(cfg)
    }

    /// Sync the inmemory representation to disk.
    /// Takes a mutable reference because the saved file is a part of the `Config` state. This
    /// protects from parallel calls resulting to a wrong file contents.
    async fn sync(&mut self) -> Result<()> {
        #[cfg(not(target_os = "ios"))]
        ensure!(!self
            .lock_task
            .as_ref()
            .context("Config is read-only")?
            .is_finished());

        let tmp_path = self.file.with_extension("toml.tmp");
        let mut file = fs::File::create(&tmp_path)
            .await
            .context("failed to create a tmp config")?;
        file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
            .await
            .context("failed to write a tmp config")?;
        file.sync_data()
            .await
            .context("failed to sync a tmp config")?;
        drop(file);
        fs::rename(&tmp_path, &self.file)
            .await
            .context("failed to rename config")?;
        Ok(())
    }

    /// Read a configuration from the given file into memory.
    pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
        let mut config = Self::new_nosync(file, writable).await?;
        let bytes = fs::read(&config.file)
            .await
            .context("Failed to read file")?;
        let s = std::str::from_utf8(&bytes)?;
        config.inner = toml::from_str(s).context("Failed to parse config")?;

        // Previous versions of the core stored absolute paths in account config.
        // Convert them to relative paths.
        let mut modified = false;
        for account in &mut config.inner.accounts {
            if account.dir.is_absolute() {
                if let Some(old_path_parent) = account.dir.parent() {
                    if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
                        account.dir = new_path.to_path_buf();
                        modified = true;
                    }
                }
            }
        }
        if modified && writable {
            config.sync().await?;
        }

        Ok(config)
    }

    /// Loads all accounts defined in the configuration file.
    ///
    /// Created contexts share the same event channel and stock string
    /// translations.
    pub async fn load_accounts(
        &self,
        events: &Events,
        stockstrings: &StockStrings,
        push_subscriber: PushSubscriber,
        dir: &Path,
    ) -> Result<BTreeMap<u32, Context>> {
        let mut accounts = BTreeMap::new();

        for account_config in &self.inner.accounts {
            let dbfile = account_config.dbfile(dir);
            let ctx = ContextBuilder::new(dbfile.clone())
                .with_id(account_config.id)
                .with_events(events.clone())
                .with_stock_strings(stockstrings.clone())
                .with_push_subscriber(push_subscriber.clone())
                .build()
                .await
                .with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
            // Try to open without a passphrase,
            // but do not return an error if account is passphare-protected.
            ctx.open("".to_string()).await?;

            accounts.insert(account_config.id, ctx);
        }

        Ok(accounts)
    }

    /// Creates a new account in the account manager directory.
    async fn new_account(&mut self) -> Result<AccountConfig> {
        let id = {
            let id = self.inner.next_id;
            let uuid = Uuid::new_v4();
            let target_dir = PathBuf::from(uuid.to_string());

            self.inner.accounts.push(AccountConfig {
                id,
                dir: target_dir,
                uuid,
            });
            self.inner.next_id += 1;
            id
        };

        self.sync().await?;

        self.select_account(id)
            .await
            .context("failed to select just added account")?;
        let cfg = self
            .get_account(id)
            .context("failed to get just added account")?;
        Ok(cfg)
    }

    /// Removes an existing account entirely.
    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
        {
            if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
                // remove account from the configs
                self.inner.accounts.remove(idx);
            }
            if self.inner.selected_account == id {
                // reset selected account
                self.inner.selected_account = self
                    .inner
                    .accounts
                    .first()
                    .map(|e| e.id)
                    .unwrap_or_default();
            }
        }

        self.sync().await
    }

    /// Returns configuration file section for the given account ID.
    fn get_account(&self, id: u32) -> Option<AccountConfig> {
        self.inner.accounts.iter().find(|e| e.id == id).cloned()
    }

    /// Returns the ID of selected account.
    pub fn get_selected_account(&self) -> u32 {
        self.inner.selected_account
    }

    /// Changes selected account ID.
    pub async fn select_account(&mut self, id: u32) -> Result<()> {
        {
            ensure!(
                self.inner.accounts.iter().any(|e| e.id == id),
                "invalid account id: {}",
                id
            );

            self.inner.selected_account = id;
        }

        self.sync().await?;
        Ok(())
    }
}

/// Spend up to 1 minute trying to do the operation.
///
/// Even if Delta Chat itself does not hold the file lock,
/// there may be other processes such as antivirus,
/// or the filesystem may be network-mounted.
///
/// Without this workaround removing account may fail on Windows with an error
/// "The process cannot access the file because it is being used by another process. (os error 32)".
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
    F: Fn() -> Fut,
    Fut: Future<Output = std::result::Result<(), T>>,
{
    let mut counter = 0;
    loop {
        counter += 1;

        if let Err(err) = f().await {
            if counter > 60 {
                return Err(err);
            }

            // Wait 1 second and try again.
            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
        } else {
            break;
        }
    }
    Ok(())
}

/// Configuration of a single account.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct AccountConfig {
    /// Unique id.
    pub id: u32,

    /// Root directory for all data for this account.
    ///
    /// The path is relative to the account manager directory.
    pub dir: std::path::PathBuf,

    /// Universally unique account identifier.
    pub uuid: Uuid,
}

impl AccountConfig {
    /// Get the canonical dbfile name for this configuration.
    pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
        accounts_dir.join(&self.dir).join(DB_NAME)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::stock_str::{self, StockMessage};

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_account_new_open() {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts1");

        {
            let writable = true;
            let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
            accounts.add_account().await.unwrap();

            assert_eq!(accounts.accounts.len(), 1);
            assert_eq!(accounts.config.get_selected_account(), 1);
        }
        for writable in [true, false] {
            let accounts = Accounts::new(p.clone(), writable).await.unwrap();

            assert_eq!(accounts.accounts.len(), 1);
            assert_eq!(accounts.config.get_selected_account(), 1);
        }
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_account_new_open_conflict() {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");
        let writable = true;
        let _accounts = Accounts::new(p.clone(), writable).await.unwrap();

        let writable = true;
        assert!(Accounts::new(p.clone(), writable).await.is_err());

        let writable = false;
        let accounts = Accounts::new(p, writable).await.unwrap();
        assert_eq!(accounts.accounts.len(), 0);
        assert_eq!(accounts.config.get_selected_account(), 0);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_account_new_add_remove() {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
        assert_eq!(accounts.accounts.len(), 0);
        assert_eq!(accounts.config.get_selected_account(), 0);

        let id = accounts.add_account().await.unwrap();
        assert_eq!(id, 1);
        assert_eq!(accounts.accounts.len(), 1);
        assert_eq!(accounts.config.get_selected_account(), 1);

        let id = accounts.add_account().await.unwrap();
        assert_eq!(id, 2);
        assert_eq!(accounts.config.get_selected_account(), id);
        assert_eq!(accounts.accounts.len(), 2);

        accounts.select_account(1).await.unwrap();
        assert_eq!(accounts.config.get_selected_account(), 1);

        accounts.remove_account(1).await.unwrap();
        assert_eq!(accounts.config.get_selected_account(), 2);
        assert_eq!(accounts.accounts.len(), 1);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_accounts_remove_last() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable).await?;
        assert!(accounts.get_selected_account().is_none());
        assert_eq!(accounts.config.get_selected_account(), 0);

        let id = accounts.add_account().await?;
        assert!(accounts.get_selected_account().is_some());
        assert_eq!(id, 1);
        assert_eq!(accounts.accounts.len(), 1);
        assert_eq!(accounts.config.get_selected_account(), id);

        accounts.remove_account(id).await?;
        assert!(accounts.get_selected_account().is_none());

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_migrate_account() {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
        assert_eq!(accounts.accounts.len(), 0);
        assert_eq!(accounts.config.get_selected_account(), 0);

        let extern_dbfile: PathBuf = dir.path().join("other");
        let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
            .await
            .unwrap();
        ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
            .await
            .unwrap();

        drop(ctx);

        accounts
            .migrate_account(extern_dbfile.clone())
            .await
            .unwrap();
        assert_eq!(accounts.accounts.len(), 1);
        assert_eq!(accounts.config.get_selected_account(), 1);

        let ctx = accounts.get_selected_account().unwrap();
        assert_eq!(
            "me@mail.com",
            ctx.get_config(crate::config::Config::Addr)
                .await
                .unwrap()
                .unwrap()
        );
    }

    /// Tests that accounts are sorted by ID.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_accounts_sorted() {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();

        for expected_id in 1..10 {
            let id = accounts.add_account().await.unwrap();
            assert_eq!(id, expected_id);
        }

        let ids = accounts.get_all();
        for (i, expected_id) in (1..10).enumerate() {
            assert_eq!(ids.get(i), Some(&expected_id));
        }
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let p: PathBuf = dir.path().join("accounts");
        let dummy_accounts = 10;

        let (id0, id1, id2) = {
            let writable = true;
            let mut accounts = Accounts::new(p.clone(), writable).await?;
            accounts.add_account().await?;
            let ids = accounts.get_all();
            assert_eq!(ids.len(), 1);

            let id0 = *ids.first().unwrap();
            let ctx = accounts.get_account(id0).unwrap();
            ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
                .await?;

            let id1 = accounts.add_account().await?;
            let ctx = accounts.get_account(id1).unwrap();
            ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
                .await?;

            // add and remove some accounts and force a gap (ids must not be reused)
            for _ in 0..dummy_accounts {
                let to_delete = accounts.add_account().await?;
                accounts.remove_account(to_delete).await?;
            }

            let id2 = accounts.add_account().await?;
            let ctx = accounts.get_account(id2).unwrap();
            ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
                .await?;

            accounts.select_account(id1).await?;

            (id0, id1, id2)
        };
        assert!(id0 > 0);
        assert!(id1 > id0);
        assert!(id2 > id1 + dummy_accounts);

        let (id0_reopened, id1_reopened, id2_reopened) = {
            let writable = false;
            let accounts = Accounts::new(p.clone(), writable).await?;
            let ctx = accounts.get_selected_account().unwrap();
            assert_eq!(
                ctx.get_config(crate::config::Config::Addr).await?,
                Some("two@example.org".to_string())
            );

            let ids = accounts.get_all();
            assert_eq!(ids.len(), 3);

            let id0 = *ids.first().unwrap();
            let ctx = accounts.get_account(id0).unwrap();
            assert_eq!(
                ctx.get_config(crate::config::Config::Addr).await?,
                Some("one@example.org".to_string())
            );

            let id1 = *ids.get(1).unwrap();
            let t = accounts.get_account(id1).unwrap();
            assert_eq!(
                t.get_config(crate::config::Config::Addr).await?,
                Some("two@example.org".to_string())
            );

            let id2 = *ids.get(2).unwrap();
            let ctx = accounts.get_account(id2).unwrap();
            assert_eq!(
                ctx.get_config(crate::config::Config::Addr).await?,
                Some("three@example.org".to_string())
            );

            (id0, id1, id2)
        };
        assert_eq!(id0, id0_reopened);
        assert_eq!(id1, id1_reopened);
        assert_eq!(id2, id2_reopened);

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_no_accounts_event_emitter() -> Result<()> {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let accounts = Accounts::new(p.clone(), writable).await?;

        // Make sure there are no accounts.
        assert_eq!(accounts.accounts.len(), 0);

        // Create event emitter.
        let event_emitter = accounts.get_event_emitter();

        // Test that event emitter does not return `None` immediately.
        let duration = std::time::Duration::from_millis(1);
        assert!(tokio::time::timeout(duration, event_emitter.recv())
            .await
            .is_err());

        // When account manager is dropped, event emitter is exhausted.
        drop(accounts);
        assert_eq!(event_emitter.recv().await, None);

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_encrypted_account() -> Result<()> {
        let dir = tempfile::tempdir().context("failed to create tempdir")?;
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable)
            .await
            .context("failed to create accounts manager")?;

        assert_eq!(accounts.accounts.len(), 0);
        let account_id = accounts
            .add_closed_account()
            .await
            .context("failed to add closed account")?;
        let account = accounts
            .get_selected_account()
            .context("failed to get account")?;
        assert_eq!(account.id, account_id);
        let passphrase_set_success = account
            .open("foobar".to_string())
            .await
            .context("failed to set passphrase")?;
        assert!(passphrase_set_success);
        drop(accounts);

        let writable = false;
        let accounts = Accounts::new(p.clone(), writable)
            .await
            .context("failed to create second accounts manager")?;
        let account = accounts
            .get_selected_account()
            .context("failed to get account")?;
        assert_eq!(account.is_open().await, false);

        // Try wrong passphrase.
        assert_eq!(account.open("barfoo".to_string()).await?, false);
        assert_eq!(account.open("".to_string()).await?, false);

        assert_eq!(account.open("foobar".to_string()).await?, true);
        assert_eq!(account.is_open().await, true);

        Ok(())
    }

    /// Tests that accounts share stock string translations.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_accounts_share_translations() -> Result<()> {
        let dir = tempfile::tempdir().unwrap();
        let p: PathBuf = dir.path().join("accounts");

        let writable = true;
        let mut accounts = Accounts::new(p.clone(), writable).await?;
        accounts.add_account().await?;
        accounts.add_account().await?;

        let account1 = accounts.get_account(1).context("failed to get account 1")?;
        let account2 = accounts.get_account(2).context("failed to get account 2")?;

        assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
        assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
        account1
            .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
            .await?;
        assert_eq!(stock_str::no_messages(&account1).await, "foobar");
        assert_eq!(stock_str::no_messages(&account2).await, "foobar");

        Ok(())
    }
}
