//! # Chat list module.

use anyhow::{ensure, Context as _, Result};
use once_cell::sync::Lazy;

use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
    Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
    DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::stock_str;
use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;

/// Regex to find out if a query should filter by unread messages.
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
    Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());

/// An object representing a single chatlist in memory.
///
/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
/// The chatlist object is not updated; if you want an update, you have to recreate the object.
///
/// For a **typical chat overview**, the idea is to get the list of all chats via dc_get_chatlist()
/// without any listflags (see below) and to implement a "virtual list" or so
/// (the count of chats is known by chatlist.len()).
///
/// Only for the items that are in view (the list may have several hundreds chats),
/// the UI should call chatlist.get_summary() then.
/// chatlist.get_summary() provides all elements needed for painting the item.
///
/// On a click of such an item, the UI should change to the chat view
/// and get all messages from this view via dc_get_chat_msgs().
/// Again, a "virtual list" is created (the count of messages is known)
/// and for each messages that is scrolled into view, dc_get_msg() is called then.
///
/// Why no listflags?
/// Without listflags, dc_get_chatlist() adds the archive "link" automatically as needed.
/// The UI can just render these items differently then.
#[derive(Debug)]
pub struct Chatlist {
    /// Stores pairs of `chat_id, message_id`
    ids: Vec<(ChatId, Option<MsgId>)>,
}

impl Chatlist {
    /// Get a list of chats.
    /// The list can be filtered by query parameters.
    ///
    /// The list is already sorted and starts with the most recent chat in use.
    /// The sorting takes care of invalid sending dates, drafts and chats without messages.
    /// Clients should not try to re-sort the list as this would be an expensive action
    /// and would result in inconsistencies between clients.
    ///
    /// To get information about each entry, use eg. chatlist.get_summary().
    ///
    /// By default, the function adds some special entries to the list.
    /// These special entries can be identified by the ID returned by chatlist.get_chat_id():
    /// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
    ///   archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
    ///   "Show archived chats", if the user clicks this item, the UI should show a
    ///   list of all archived chats that can be created by this function hen using
    ///   the DC_GCL_ARCHIVED_ONLY flag.
    /// - DC_CHAT_ID_ALLDONE_HINT (7) - this special chat is present
    ///   if DC_GCL_ADD_ALLDONE_HINT is added to listflags
    ///   and if there are only archived chats.
    ///
    /// The `listflags` is a combination of flags:
    /// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned.
    ///   if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
    ///   the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
    ///   chats
    /// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
    ///   and hides the device-chat and contact requests
    ///   typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
    /// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
    ///   to the list (may be used eg. for selecting chats on forwarding, the flag is
    ///   not needed when DC_GCL_ARCHIVED_ONLY is already set)
    /// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
    ///   is added as needed.
    ///
    /// `query`: An optional query for filtering the list. Only chats matching this query
    /// are returned. When `is:unread` is contained in the query, the chatlist is
    /// filtered such that only chats with unread messages show up.
    ///
    /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
    /// are returned.
    pub async fn try_load(
        context: &Context,
        listflags: usize,
        query: Option<&str>,
        query_contact_id: Option<ContactId>,
    ) -> Result<Self> {
        let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
        let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
        let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
        let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;

        let process_row = |row: &rusqlite::Row| {
            let chat_id: ChatId = row.get(0)?;
            let msg_id: Option<MsgId> = row.get(1)?;
            Ok((chat_id, msg_id))
        };

        let process_rows = |rows: rusqlite::MappedRows<_>| {
            rows.collect::<std::result::Result<Vec<_>, _>>()
                .map_err(Into::into)
        };

        let skip_id = if flag_for_forwarding {
            ChatId::lookup_by_contact(context, ContactId::DEVICE)
                .await?
                .unwrap_or_default()
        } else {
            ChatId::new(0)
        };

        // select with left join and minimum:
        //
        // - the inner select must use `hidden` and _not_ `m.hidden`
        //   which would refer the outer select and take a lot of time
        // - `GROUP BY` is needed several messages may have the same
        //   timestamp
        // - the list starts with the newest chats
        //
        // The query shows messages from blocked contacts in
        // groups. Otherwise it would be hard to follow conversations.
        let ids = if let Some(query_contact_id) = query_contact_id {
            // show chats shared with a given contact
            context.sql.query_map(
                "SELECT c.id, m.id
                 FROM chats c
                 LEFT JOIN msgs m
                        ON c.id=m.chat_id
                       AND m.id=(
                               SELECT id
                                 FROM msgs
                                WHERE chat_id=c.id
                                  AND (hidden=0 OR state=?1)
                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
                 WHERE c.id>9
                   AND c.blocked!=1
                   AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
                 GROUP BY c.id
                 ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
                (MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
                process_row,
                process_rows,
            ).await?
        } else if flag_archived_only {
            // show archived chats
            // (this includes the archived device-chat; we could skip it,
            // however, then the number of archived chats do not match, which might be even more irritating.
            // and adapting the number requires larger refactorings and seems not to be worth the effort)
            context
                .sql
                .query_map(
                    "SELECT c.id, m.id
                 FROM chats c
                 LEFT JOIN msgs m
                        ON c.id=m.chat_id
                       AND m.id=(
                               SELECT id
                                 FROM msgs
                                WHERE chat_id=c.id
                                  AND (hidden=0 OR state=?)
                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
                 WHERE c.id>9
                   AND c.blocked!=1
                   AND c.archived=1
                 GROUP BY c.id
                 ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
                    (MessageState::OutDraft,),
                    process_row,
                    process_rows,
                )
                .await?
        } else if let Some(query) = query {
            let mut query = query.trim().to_string();
            ensure!(!query.is_empty(), "query mustn't be empty");
            let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
            query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();

            // allow searching over special names that may change at any time
            // when the ui calls set_stock_translation()
            if let Err(err) = update_special_chat_names(context).await {
                warn!(context, "Cannot update special chat names: {err:#}.")
            }

            let str_like_cmd = format!("%{query}%");
            context
                .sql
                .query_map(
                    "SELECT c.id, m.id
                 FROM chats c
                 LEFT JOIN msgs m
                        ON c.id=m.chat_id
                       AND m.id=(
                               SELECT id
                                 FROM msgs
                                WHERE chat_id=c.id
                                  AND (hidden=0 OR state=?1)
                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
                 WHERE c.id>9 AND c.id!=?2
                   AND c.blocked!=1
                   AND c.name LIKE ?3
                   AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
                 GROUP BY c.id
                 ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
                    (MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
                    process_row,
                    process_rows,
                )
                .await?
        } else {
            let mut ids = if flag_for_forwarding {
                let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
                    .await?
                    .unwrap_or_default();
                let process_row = |row: &rusqlite::Row| {
                    let chat_id: ChatId = row.get(0)?;
                    let typ: Chattype = row.get(1)?;
                    let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
                    let msg_id: Option<MsgId> = row.get(3)?;
                    Ok((chat_id, typ, param, msg_id))
                };
                let process_rows = |rows: rusqlite::MappedRows<_>| {
                    rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
                        Ok((chat_id, typ, param, msg_id)) => {
                            if typ == Chattype::Mailinglist
                                && param.get(Param::ListPost).is_none_or_empty()
                            {
                                None
                            } else {
                                Some(Ok((chat_id, msg_id)))
                            }
                        }
                        Err(e) => Some(Err(e)),
                    })
                    .collect::<std::result::Result<Vec<_>, _>>()
                    .map_err(Into::into)
                };
                // Return ProtectionBroken chats also, as that may happen to a verified chat at any
                // time. It may be confusing if a chat that is normally in the list disappears
                // suddenly. The UI need to deal with that case anyway.
                context.sql.query_map(
                    "SELECT c.id, c.type, c.param, m.id
                     FROM chats c
                     LEFT JOIN msgs m
                            ON c.id=m.chat_id
                           AND m.id=(
                                   SELECT id
                                     FROM msgs
                                    WHERE chat_id=c.id
                                      AND (hidden=0 OR state=?)
                                      ORDER BY timestamp DESC, id DESC LIMIT 1)
                     WHERE c.id>9 AND c.id!=?
                       AND c.blocked=0
                       AND NOT c.archived=?
                       AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
                     GROUP BY c.id
                     ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
                    (
                        MessageState::OutDraft, skip_id, ChatVisibility::Archived,
                        Chattype::Group, ContactId::SELF,
                        sort_id_up, ChatVisibility::Pinned,
                    ),
                    process_row,
                    process_rows,
                ).await?
            } else {
                //  show normal chatlist
                context.sql.query_map(
                    "SELECT c.id, m.id
                     FROM chats c
                     LEFT JOIN msgs m
                            ON c.id=m.chat_id
                           AND m.id=(
                                   SELECT id
                                     FROM msgs
                                    WHERE chat_id=c.id
                                      AND (hidden=0 OR state=?)
                                      ORDER BY timestamp DESC, id DESC LIMIT 1)
                     WHERE c.id>9 AND c.id!=?
                       AND (c.blocked=0 OR c.blocked=2)
                       AND NOT c.archived=?
                     GROUP BY c.id
                     ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
                    (MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
                    process_row,
                    process_rows,
                ).await?
            };
            if !flag_no_specials && get_archived_cnt(context).await? > 0 {
                if ids.is_empty() && flag_add_alldone_hint {
                    ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
                }
                ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None));
            }
            ids
        };

        Ok(Chatlist { ids })
    }

    /// Converts list of chat IDs to a chatlist.
    pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
        let mut ids = Vec::new();
        for &chat_id in chat_ids {
            let msg_id: Option<MsgId> = context
                .sql
                .query_get_value(
                    "SELECT id
                   FROM msgs
                  WHERE chat_id=?1
                    AND (hidden=0 OR state=?2)
                  ORDER BY timestamp DESC, id DESC LIMIT 1",
                    (chat_id, MessageState::OutDraft),
                )
                .await
                .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
            ids.push((chat_id, msg_id));
        }
        Ok(Chatlist { ids })
    }

    /// Find out the number of chats.
    pub fn len(&self) -> usize {
        self.ids.len()
    }

    /// Returns true if chatlist is empty.
    pub fn is_empty(&self) -> bool {
        self.ids.is_empty()
    }

    /// Get a single chat ID of a chatlist.
    ///
    /// To get the message object from the message ID, use dc_get_chat().
    pub fn get_chat_id(&self, index: usize) -> Result<ChatId> {
        let (chat_id, _msg_id) = self
            .ids
            .get(index)
            .context("chatlist index is out of range")?;
        Ok(*chat_id)
    }

    /// Get a single message ID of a chatlist.
    ///
    /// To get the message object from the message ID, use dc_get_msg().
    pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
        let (_chat_id, msg_id) = self
            .ids
            .get(index)
            .context("chatlist index is out of range")?;
        Ok(*msg_id)
    }

    /// Returns a summary for a given chatlist index.
    pub async fn get_summary(
        &self,
        context: &Context,
        index: usize,
        chat: Option<&Chat>,
    ) -> Result<Summary> {
        // The summary is created by the chat, not by the last message.
        // This is because we may want to display drafts here or stuff as
        // "is typing".
        // Also, sth. as "No messages" would not work if the summary comes from a message.
        let (chat_id, lastmsg_id) = self
            .ids
            .get(index)
            .context("chatlist index is out of range")?;
        Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
    }

    /// Returns a summary for a given chatlist item.
    pub async fn get_summary2(
        context: &Context,
        chat_id: ChatId,
        lastmsg_id: Option<MsgId>,
        chat: Option<&Chat>,
    ) -> Result<Summary> {
        let chat_loaded: Chat;
        let chat = if let Some(chat) = chat {
            chat
        } else {
            let chat = Chat::load_from_db(context, chat_id).await?;
            chat_loaded = chat;
            &chat_loaded
        };

        let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
            // Message may be deleted by the time we try to load it,
            // so use `load_from_db_optional` instead of `load_from_db`.
            Message::load_from_db_optional(context, lastmsg_id)
                .await
                .context("Loading message failed")?
        } else {
            None
        };

        let lastcontact = if let Some(lastmsg) = &lastmsg {
            if lastmsg.from_id == ContactId::SELF {
                None
            } else {
                match chat.typ {
                    Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
                        let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
                            .await
                            .context("loading contact failed")?;
                        Some(lastcontact)
                    }
                    Chattype::Single => None,
                }
            }
        } else {
            None
        };

        if chat.id.is_archived_link() {
            Ok(Default::default())
        } else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
            Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
        } else {
            Ok(Summary {
                text: stock_str::no_messages(context).await,
                ..Default::default()
            })
        }
    }

    /// Returns chatlist item position for the given chat ID.
    pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
        self.ids.iter().position(|(chat_id, _)| chat_id == &id)
    }

    /// An iterator visiting all chatlist items.
    pub fn iter(&self) -> impl Iterator<Item = &(ChatId, Option<MsgId>)> {
        self.ids.iter()
    }
}

/// Returns the number of archived chats
pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
    let count = context
        .sql
        .count(
            "SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
            (Blocked::Yes, ChatVisibility::Archived),
        )
        .await?;
    Ok(count)
}

/// Gets the last message of a chat, the message that would also be displayed in the ChatList
/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
pub async fn get_last_message_for_chat(
    context: &Context,
    chat_id: ChatId,
) -> Result<Option<MsgId>> {
    context
        .sql
        .query_get_value(
            "SELECT id
                FROM msgs
                WHERE chat_id=?2
                AND (hidden=0 OR state=?1)
                ORDER BY timestamp DESC, id DESC LIMIT 1",
            (MessageState::OutDraft, chat_id),
        )
        .await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chat::{
        add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
        send_text_msg, ProtectionStatus,
    };
    use crate::receive_imf::receive_imf;
    use crate::stock_str::StockMessage;
    use crate::test_utils::TestContext;

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_try_load() {
        let t = TestContext::new_bob().await;
        let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
            .await
            .unwrap();
        let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
            .await
            .unwrap();
        let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
            .await
            .unwrap();

        // check that the chatlist starts with the most recent message
        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        assert_eq!(chats.len(), 3);
        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
        assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
        assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);

        // New drafts are sorted to the top
        // We have to set a draft on the other two messages, too, as
        // chat timestamps are only exact to the second and sorting by timestamp
        // would not work.
        // Message timestamps are "smeared" and unique, so we don't have this problem
        // if we have any message (can be a draft) in all chats.
        // Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
        // 2s here.
        for chat_id in &[chat_id1, chat_id3, chat_id2] {
            let mut msg = Message::new_text("hello".to_string());
            chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
        }

        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);

        // check chatlist query and archive functionality
        let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
        assert_eq!(chats.len(), 1);

        // receive a message from alice
        let alice = TestContext::new_alice().await;
        let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
            .await
            .unwrap();
        add_contact_to_chat(
            &alice,
            alice_chat_id,
            Contact::create(&alice, "bob", "bob@example.net")
                .await
                .unwrap(),
        )
        .await
        .unwrap();
        send_text_msg(&alice, alice_chat_id, "hi".into())
            .await
            .unwrap();
        let sent_msg = alice.pop_sent_msg().await;

        t.recv_msg(&sent_msg).await;
        let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
            .await
            .unwrap();
        assert!(chats.len() == 1);

        let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 0);

        chat_id1
            .set_visibility(&t, ChatVisibility::Archived)
            .await
            .ok();
        let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 1);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_sort_self_talk_up_on_forward() {
        let t = TestContext::new().await;
        t.update_device_chats().await.unwrap();
        create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
            .await
            .unwrap();

        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        assert!(chats.len() == 3);
        assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
            .await
            .unwrap()
            .is_self_talk());

        let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
            .await
            .unwrap();
        assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
        assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
            .await
            .unwrap()
            .is_self_talk());

        remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
            .await
            .unwrap();
        let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
            .await
            .unwrap();
        assert!(chats.len() == 1);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_search_special_chat_names() {
        let t = TestContext::new().await;
        t.update_device_chats().await.unwrap();

        let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 0);
        let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 0);

        t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
            .await
            .unwrap();
        let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 1);

        t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
            .await
            .unwrap();
        let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
            .await
            .unwrap();
        assert_eq!(chats.len(), 1);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_search_single_chat() -> anyhow::Result<()> {
        let t = TestContext::new_alice().await;

        // receive a one-to-one-message
        receive_imf(
            &t,
            b"From: Bob Authname <bob@example.org>\n\
                 To: alice@example.org\n\
                 Subject: foo\n\
                 Message-ID: <msg1234@example.org>\n\
                 Chat-Version: 1.0\n\
                 Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
                 \n\
                 hello foo\n",
            false,
        )
        .await?;

        let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
        // Contact request should be searchable
        assert_eq!(chats.len(), 1);

        let msg = t.get_last_msg().await;
        let chat_id = msg.get_chat_id();
        chat_id.accept(&t).await.unwrap();

        let contacts = get_chat_contacts(&t, chat_id).await?;
        let contact_id = *contacts.first().unwrap();
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "Bob Authname");

        // check, the one-to-one-chat can be found using chatlist search query
        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
        assert_eq!(chats.len(), 1);
        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id);

        // change the name of the contact; this also changes the name of the one-to-one-chat
        let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
        assert_eq!(contact_id, test_id);
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "Bob Nickname");
        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
        assert_eq!(chats.len(), 0);
        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
        assert_eq!(chats.len(), 1);

        // revert contact to authname, this again changes the name of the one-to-one-chat
        let test_id = Contact::create(&t, "", "bob@example.org").await?;
        assert_eq!(contact_id, test_id);
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "Bob Authname");
        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
        assert_eq!(chats.len(), 1);
        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
        assert_eq!(chats.len(), 0);

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
        let t = TestContext::new_alice().await;

        // receive a one-to-one-message without authname set
        receive_imf(
            &t,
            b"From: bob@example.org\n\
                 To: alice@example.org\n\
                 Subject: foo\n\
                 Message-ID: <msg5678@example.org>\n\
                 Chat-Version: 1.0\n\
                 Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
                 \n\
                 hello foo\n",
            false,
        )
        .await?;

        let msg = t.get_last_msg().await;
        let chat_id = msg.get_chat_id();
        chat_id.accept(&t).await.unwrap();
        let contacts = get_chat_contacts(&t, chat_id).await?;
        let contact_id = *contacts.first().unwrap();
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "bob@example.org");

        // check, the one-to-one-chat can be found using chatlist search query
        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
        assert_eq!(chats.len(), 1);
        assert_eq!(chats.get_chat_id(0)?, chat_id);

        // change the name of the contact; this also changes the name of the one-to-one-chat
        let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
        assert_eq!(contact_id, test_id);
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "Bob Nickname");
        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
        assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
        let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
        assert_eq!(chats.len(), 1);
        assert_eq!(chats.get_chat_id(0)?, chat_id);

        // revert name change, this again changes the name of the one-to-one-chat to the email-address
        let test_id = Contact::create(&t, "", "bob@example.org").await?;
        assert_eq!(contact_id, test_id);
        let chat = Chat::load_from_db(&t, chat_id).await?;
        assert_eq!(chat.get_name(), "bob@example.org");
        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
        assert_eq!(chats.len(), 1);
        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
        assert_eq!(chats.len(), 0);

        // finally, also check that a simple substring-search is working with email-addresses
        let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
        assert_eq!(chats.len(), 1);
        let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
        assert_eq!(chats.len(), 0);

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_summary_unwrap() {
        let t = TestContext::new().await;
        let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
            .await
            .unwrap();

        let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
        chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();

        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        let summary = chats.get_summary(&t, 0, None).await.unwrap();
        assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
    }

    /// Tests that summary does not fail to load
    /// if the draft was deleted after loading the chatlist.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_summary_deleted_draft() {
        let t = TestContext::new().await;

        let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
            .await
            .unwrap();
        let mut msg = Message::new_text("Foobar".to_string());
        chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();

        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        chat_id.set_draft(&t, None).await.unwrap();

        let summary_res = chats.get_summary(&t, 0, None).await;
        assert!(summary_res.is_ok());
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_load_broken() {
        let t = TestContext::new_bob().await;
        let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
            .await
            .unwrap();
        create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
            .await
            .unwrap();
        create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
            .await
            .unwrap();

        // check that the chatlist starts with the most recent message
        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
        assert_eq!(chats.len(), 3);

        // obfuscated one chat
        t.sql
            .execute("UPDATE chats SET type=10 WHERE id=?", (chat_id1,))
            .await
            .unwrap();

        // obfuscated chat can't be loaded
        assert!(Chat::load_from_db(&t, chat_id1).await.is_err());

        // chatlist loads fine
        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

        // only corrupted chat fails to create summary
        assert!(chats.get_summary(&t, 0, None).await.is_ok());
        assert!(chats.get_summary(&t, 1, None).await.is_ok());
        assert!(chats.get_summary(&t, 2, None).await.is_err());
        assert_eq!(chats.get_index_for_id(chat_id1).unwrap(), 2);
    }
}
