//
// melib - jmap module.
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later

#[test]
fn test_jmap_query() {
    use std::sync::Arc;

    use futures::lock::Mutex as FutureMutex;

    use crate::jmap::{
        comparator::Comparator,
        email::{EmailFilterCondition, EmailObject, EmailQuery},
        filters::Filter,
        methods::Query,
        protocol::Request,
    };
    assert_eq!(
        r#"{"inMailbox":"mailbox_id"}"#,
        serde_json::to_string(&Filter::<EmailFilterCondition, EmailObject>::Condition(
            EmailFilterCondition::new().in_mailbox(Some("mailbox_id".to_string().into())),
        ))
        .unwrap()
        .as_str()
    );
    assert_eq!(
        serde_json::to_string(
            &serde_json::from_str::<Filter<EmailFilterCondition, EmailObject>>(
                r#"{"inMailbox":"mailbox_id"}"#
            )
            .unwrap()
        )
        .unwrap()
        .as_str(),
        serde_json::to_string(&Filter::<EmailFilterCondition, EmailObject>::Condition(
            EmailFilterCondition::new().in_mailbox(Some("mailbox_id".to_string().into()))
        ))
        .unwrap()
        .as_str()
    );
    serde_json::from_str::<EmailQuery>(
        r#"{"accountId":"A13824","calculateTotal":false,"collapseThreads":false,"filter":{"inMailbox":"872f54fd-0367-422b-89a0-1c76efec1653"},"limit":256,"position":0,"sort":[{"collation":null,"isAscending":false,"property":"receivedAt"}]}"#).unwrap();
    assert_eq!(
        serde_json::to_string(
            &serde_json::from_str::<EmailQuery>(
        r#"{"accountId":"A13824","calculateTotal":false,"collapseThreads":false,"filter":{"inMailbox":"872f54fd-0367-422b-89a0-1c76efec1653"},"limit":256,"position":0,"sort":[{"collation":null,"isAscending":false,"property":"receivedAt"}]}"#).unwrap()).unwrap().as_str(),
        serde_json::to_string(&EmailQuery {
            query_call: Query {
                account_id: "A13824".into(),
                filter: Some(Filter::Condition ( EmailFilterCondition { in_mailbox: Some("872f54fd-0367-422b-89a0-1c76efec1653".into()), ..EmailFilterCondition::default() } )),
                sort: Some(vec![Comparator {
                    collation: None,
                    is_ascending: false,
                    property: "receivedAt".to_string().into(),
                    additional_properties: Default::default(),
                    _ph: std::marker::PhantomData,
                }]),
                position: 0,
                anchor: None,
                anchor_offset: 0,
                limit: Some(256),
                calculate_total: false,
                _ph: std::marker::PhantomData,
            },
            collapse_threads: false,
        })
        .unwrap()
        .as_str()
    );

    let q: crate::search::Query = crate::search::Query::try_from(
        "subject:wah or (from:Manos and (subject:foo or subject:bar))",
    )
    .unwrap();
    let f: Filter<EmailFilterCondition, EmailObject> = Filter::from(q);
    assert_eq!(
        r#"{"operator":"OR","conditions":[{"subject":"wah"},{"operator":"AND","conditions":[{"from":"Manos"},{"operator":"OR","conditions":[{"subject":"foo"},{"subject":"bar"}]}]}]}"#,
        serde_json::to_string(&f).unwrap().as_str()
    );
    let filter = {
        let mailbox_id = "mailbox_id".to_string();

        let mut r =
            Filter::Condition(EmailFilterCondition::new().in_mailbox(Some(mailbox_id.into())));
        r &= f;
        r
    };

    let email_call: EmailQuery = EmailQuery::new(
        Query::new()
            .account_id("account_id".to_string().into())
            .filter(Some(filter))
            .position(0),
    )
    .collapse_threads(false);

    let request_no = Arc::new(FutureMutex::new(0));
    let mut req = Request::new(request_no.clone());
    futures::executor::block_on(req.add_call(&email_call));

    assert_eq!(
        r#"{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail","urn:ietf:params:jmap:submission"],"methodCalls":[["Email/query",{"accountId":"account_id","calculateTotal":false,"collapseThreads":false,"filter":{"conditions":[{"inMailbox":"mailbox_id"},{"conditions":[{"subject":"wah"},{"conditions":[{"from":"Manos"},{"conditions":[{"subject":"foo"},{"subject":"bar"}],"operator":"OR"}],"operator":"AND"}],"operator":"OR"}],"operator":"AND"},"position":0,"sort":[{"collation":null,"isAscending":false,"property":"receivedAt"}]},"m0"]]}"#,
        serde_json::to_string(&req).unwrap().as_str()
    );
    assert_eq!(*futures::executor::block_on(request_no.lock()), 1);
}

#[test]
fn test_jmap_undo_status() {
    use serde_json::json;

    use crate::jmap::{
        email::EmailObject,
        identity::Identity,
        objects::{Account, Id},
        submission::{EmailSubmissionObject, UndoStatus},
    };
    let account_id: Id<Account> = "blahblah".into();
    let ident_id: Id<Identity> = "sdusssssss".into();
    let email_id: Id<EmailObject> = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf");
    let mut obj = EmailSubmissionObject::new(
        account_id,
        ident_id.clone(),
        email_id.clone(),
        None,
        /* undo_status */ None,
    );

    assert_eq!(
        json!(&obj),
        json!({
            "emailId": email_id,
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "final",
        })
    );

    obj.undo_status = UndoStatus::Pending;
    assert_eq!(
        json!(&obj),
        json!({
            "emailId": email_id,
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "pending",
        })
    );
    obj.undo_status = UndoStatus::Final;
    assert_eq!(
        json!(&obj),
        json!({
            "emailId": email_id,
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "final",
        })
    );
    obj.undo_status = UndoStatus::Canceled;
    assert_eq!(
        json!(&obj),
        json!({
            "emailId": email_id,
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "canceled",
        })
    );
}

#[test]
fn test_jmap_email_submission_object() {
    use serde_json::json;

    use crate::jmap::{
        argument::Argument,
        email::{EmailImport, EmailObject},
        identity::Identity,
        methods::ResultField,
        objects::{Account, Id},
        submission::{EmailSubmissionObject, UndoStatus},
    };
    let account_id: Id<Account> = "blahblah".into();
    let ident_id: Id<Identity> = "sdusssssss".into();
    let email_id: Id<EmailObject> = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf");
    let obj = EmailSubmissionObject::new(
        account_id.clone(),
        ident_id.clone(),
        email_id.clone(),
        None,
        /* undo_status */ None,
    );

    assert_eq!(
        json!(&obj),
        json!({
            "emailId": email_id,
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "final",
        })
    );

    let obj = EmailSubmissionObject::new(
        account_id,
        ident_id.clone(),
        /* email_id: */
        Argument::reference::<EmailImport, EmailObject, EmailObject>(
            42,
            ResultField::<EmailImport, EmailObject>::new("/id"),
        ),
        None,
        Some(UndoStatus::Final),
    );

    assert_eq!(
        json!(&obj),
        json!({
            "#emailId": {
                "name": "Email/import",
                "path": "/id",
                "resultOf": "m42",
             },
            "envelope": null,
            "identityId": &ident_id,
            "undoStatus": "final",
        })
    );
}

#[test]
fn test_jmap_identity_methods() {
    use std::sync::Arc;

    use futures::lock::Mutex as FutureMutex;
    use serde_json::json;

    use crate::jmap::{
        identity::{Identity, IdentityGet, IdentitySet},
        methods::Set,
        objects::Id,
        protocol::Request,
    };
    let account_id = "blahblah";
    let prev_seq = 33;
    let main_identity = "user@example.com";
    let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq)));

    let identity_set = IdentitySet(
        Set::<Identity>::new(None)
            .account_id(account_id.into())
            .create(Some({
                let id: Id<Identity> = main_identity.into();
                let address = crate::email::Address::try_from(main_identity)
                    .unwrap_or_else(|_| crate::email::Address::new(None, main_identity.into()));
                indexmap! {
                    id.clone().into() => Identity {
                        id,
                        name: address.get_display_name().unwrap_or_default(),
                        email: address.get_email(),
                        ..Identity::default()
                    }
                }
            })),
    );
    futures::executor::block_on(req.add_call(&identity_set));

    let identity_get = IdentityGet::new().account_id(account_id.into());
    futures::executor::block_on(req.add_call(&identity_get));

    assert_eq!(
        json! {&req},
        json! {{
            "methodCalls" : [
                [
                    "Identity/set",
                    {
                        "accountId" : account_id,
                        "create" : {
                            "user@example.com" : {
                                "bcc" : null,
                                "email" : main_identity,
                                "htmlSignature" : "",
                                "name" : "",
                                "replyTo" : null,
                                "textSignature" : ""
                            }
                        },
                    },
                    "m33"
                ],
                [
                    "Identity/get",
                    {
                        "accountId": account_id
                    },
                    "m34"
                ]
                ],
                "using" : [
                    "urn:ietf:params:jmap:core",
                    "urn:ietf:params:jmap:mail",
                    "urn:ietf:params:jmap:submission"
                ]
        }},
    );
}

#[test]
fn test_jmap_argument_serde() {
    use std::sync::Arc;

    use futures::lock::Mutex as FutureMutex;
    use serde_json::json;

    use crate::jmap::{
        argument::Argument,
        email::{EmailImport, EmailImportObject, EmailObject},
        mailbox::MailboxObject,
        methods::{ResultField, Set},
        objects::{BlobObject, Id},
        protocol::Request,
        submission::{EmailSubmissionObject, EmailSubmissionSet},
    };

    let account_id = "blahblah";
    let blob_id: Id<BlobObject> = Id::new_random();
    let draft_mailbox_id: Id<MailboxObject> = Id::new_random();
    let sent_mailbox_id: Id<MailboxObject> = Id::new_random();
    let prev_seq = 33;

    let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq)));
    let creation_id: Id<EmailObject> = "1".into();
    let import_call: EmailImport =
        EmailImport::new()
            .account_id(account_id.into())
            .emails(indexmap! {
                creation_id =>
                    EmailImportObject::new()
                    .blob_id(blob_id.clone())
                    .keywords(indexmap! {
                        "$draft".to_string() => true,
                    })
                .mailbox_ids(indexmap! {
                    draft_mailbox_id.clone() => true,
                }),
            });

    let prev_seq = futures::executor::block_on(req.add_call(&import_call));

    let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new(
                Set::<EmailSubmissionObject>::new(None)
                    .account_id(account_id.into())
                    .create(Some(indexmap! {
                        Argument::from(Id::from("k1490")) => EmailSubmissionObject::new(
                            /* account_id: */ account_id.into(),
                            /* identity_id: */ account_id.into(),
                            /* email_id: */ Argument::reference::<EmailImport, EmailObject, EmailObject>(prev_seq, ResultField::<EmailImport, EmailObject>::new("/id")),
                            /* envelope: */ None,
                            /* undo_status: */ None
                            )
                    })),
            )
            .on_success_update_email(Some(
                indexmap! {
                    "#k1490".into() => json!({
                        format!("mailboxIds/{draft_mailbox_id}"): null,
                        format!("mailboxIds/{sent_mailbox_id}"): true,
                        "keywords/$draft": null
                    })
                }
            ));
    _ = futures::executor::block_on(req.add_call(&subm_set_call));

    assert_eq!(
        json! {&subm_set_call},
        json! {{
            "accountId": account_id,
            "create": {
                "k1490": {
                    "#emailId": {
                        "name": "Email/import",
                        "path":"/id",
                        "resultOf":"m33"
                    },
                    "envelope": null,
                    "identityId": account_id,
                    "undoStatus": "final"
                }
            },
            "onSuccessDestroyEmail": null,
            "onSuccessUpdateEmail": {
                "#k1490": {
                    "keywords/$draft": null,
                    format!("mailboxIds/{draft_mailbox_id}"): null,
                    format!("mailboxIds/{sent_mailbox_id}"): true
                }
            },
        }},
    );
    assert_eq!(
        json! {&req},
        json! {{
            "methodCalls": [
                [
                    "Email/import",
                    {
                        "accountId": account_id,
                        "emails": {
                            "1": {
                                "blobId": blob_id.to_string(),
                                "keywords": {
                                    "$draft": true
                                },
                                "mailboxIds": {
                                    draft_mailbox_id.to_string(): true
                                },
                                "receivedAt": null
                            }
                        }
                    },
                    "m33"
                ],
                [
                    "EmailSubmission/set",
                    {
                        "accountId": account_id,
                        "create": {
                            "k1490": {
                                "#emailId": {
                                    "name": "Email/import",
                                    "path": "/id",
                                    "resultOf": "m33"
                                },
                                "envelope": null,
                                "identityId": account_id,
                                "undoStatus": "final"
                            }
                        },
                        "onSuccessDestroyEmail": null,
                        "onSuccessUpdateEmail": {
                            "#k1490": {
                                "keywords/$draft": null,
                                format!("mailboxIds/{draft_mailbox_id}"): null,
                                format!("mailboxIds/{sent_mailbox_id}"): true
                            }
                        },
                    },
                    "m34"
                        ]
                        ],
                        "using": [
                            "urn:ietf:params:jmap:core",
                            "urn:ietf:params:jmap:mail",
                            "urn:ietf:params:jmap:submission"
                        ]
        }},
    );
}

#[test]
fn test_jmap_request_url_template() {
    use serde_json::json;
    use url::Url;

    use crate::jmap::{
        methods::{download_request_format, upload_request_format, RequestUrlTemplate},
        objects::{Account, BlobObject, Id},
    };

    const DOWNLOAD_TEMPLATE: &str = "https://jmap.example.com/download/{accountId}/{blobId}/{name}";
    const UPLOAD_TEMPLATE: &str = "https://jmap.example.com/upload/{accountId}/";

    let account_id: Id<Account> = "blahblah".into();
    let blob_id: Id<BlobObject> = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf");
    let download_template_url: RequestUrlTemplate =
        serde_json::from_value(json!(DOWNLOAD_TEMPLATE)).unwrap();
    assert_eq!(
        download_request_format(
            &download_template_url,
            &account_id,
            &blob_id,
            Some("attachment.txt".into())
        )
        .unwrap(),
        serde_json::from_str::<Url>(
            &json!("https://jmap.example.com/download/blahblah/683f9246-56d4-4d7d-bd0c-3d4de6db7cbf/attachment.txt").to_string()
        )
        .unwrap()
    );
    assert_eq!(
        download_request_format(
            &download_template_url,
            &account_id,
            &blob_id,
            Some("attachment filename.txt".into()),
        )
        .unwrap(),
        serde_json::from_str::<Url>(
            &json!("https://jmap.example.com/download/blahblah/683f9246-56d4-4d7d-bd0c-3d4de6db7cbf/attachment%20filename.txt").to_string()
        )
        .unwrap()
    );

    let upload_template_url: RequestUrlTemplate =
        serde_json::from_value(json!(UPLOAD_TEMPLATE)).unwrap();
    assert_eq!(
        upload_request_format(&upload_template_url, &account_id).unwrap(),
        serde_json::from_str::<Url>(
            &json!("https://jmap.example.com/upload/blahblah/").to_string()
        )
        .unwrap()
    );
}

#[test]
fn test_jmap_session_serde() {
    use serde_json::json;

    use crate::jmap::{
        objects::{Account, Id, State},
        session::{CapabilitiesObject, Session},
    };

    const RFC_EXAMPLE: &str = r###"{
     "capabilities": {
       "urn:ietf:params:jmap:core": {
         "maxSizeUpload": 50000000,
         "maxConcurrentUpload": 8,
         "maxSizeRequest": 10000000,
         "maxConcurrentRequests": 8,
         "maxCallsInRequest": 32,
         "maxObjectsInGet": 256,
         "maxObjectsInSet": 128,
         "collationAlgorithms": [
           "i;ascii-numeric",
           "i;ascii-casemap",
           "i;unicode-casemap"
         ]
       },
       "urn:ietf:params:jmap:mail": {},
       "urn:ietf:params:jmap:contacts": {},
       "https://example.com/apis/foobar": {
         "maxFoosFinangled": 42
       }
     },
     "accounts": {
       "A13824": {
         "name": "john@example.com",
         "isPersonal": true,
         "isReadOnly": false,
         "accountCapabilities": {
           "urn:ietf:params:jmap:mail": {
             "maxMailboxesPerEmail": null,
             "maxMailboxDepth": 10
           },
           "urn:ietf:params:jmap:contacts": {
           }
         }
       },
       "A97813": {
         "name": "jane@example.com",
         "isPersonal": false,
         "isReadOnly": true,
         "accountCapabilities": {
           "urn:ietf:params:jmap:mail": {
             "maxMailboxesPerEmail": 1,
             "maxMailboxDepth": 10
           }
         }
       }
     },
     "primaryAccounts": {
       "urn:ietf:params:jmap:mail": "A13824",
       "urn:ietf:params:jmap:contacts": "A13824"
     },
     "username": "john@example.com",
     "apiUrl": "https://jmap.example.com/api/",
     "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}",
     "uploadUrl": "https://jmap.example.com/upload/{accountId}/",
     "eventSourceUrl": "https://jmap.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
     "state": "75128aab4b1b"
   }"###;
    let expected = Session {
        capabilities: indexmap::indexmap! {
            "urn:ietf:params:jmap:core".to_string() => CapabilitiesObject {
                max_size_upload: 50000000,
                max_concurrent_upload: 8,
                max_size_request: 10000000,
                max_concurrent_requests: 8,
                max_calls_in_request: 32,
                max_objects_in_get: 256,
                max_objects_in_set: 128,
                collation_algorithms: vec![
                    "i;ascii-numeric".to_string(),
                    "i;ascii-casemap".to_string(),
                    "i;unicode-casemap".to_string(),
                ],
                extra_properties: indexmap::IndexMap::default(),
            },
            "urn:ietf:params:jmap:mail".to_string() => CapabilitiesObject {
                max_size_upload: 0,
                max_concurrent_upload: 0,
                max_size_request: 0,
                max_concurrent_requests: 0,
                max_calls_in_request: 0,
                max_objects_in_get: 0,
                max_objects_in_set: 0,
                collation_algorithms: vec![],
                extra_properties: indexmap::IndexMap::default(),
            },
            "urn:ietf:params:jmap:contacts".to_string() => CapabilitiesObject {
                max_size_upload: 0,
                max_concurrent_upload: 0,
                max_size_request: 0,
                max_concurrent_requests: 0,
                max_calls_in_request: 0,
                max_objects_in_get: 0,
                max_objects_in_set: 0,
                collation_algorithms: vec![],
                extra_properties: indexmap::IndexMap::default(),
            },
            "https://example.com/apis/foobar".to_string() => CapabilitiesObject {
                max_size_upload: 0,
                max_concurrent_upload: 0,
                max_size_request: 0,
                max_concurrent_requests: 0,
                max_calls_in_request: 0,
                max_objects_in_get: 0,
                max_objects_in_set: 0,
                collation_algorithms: vec![],
                extra_properties: indexmap::indexmap! {
                    "maxFoosFinangled".to_string() => json!(42),
                },
            },
        },
        accounts: indexmap::indexmap! {
            Id::<Account>::from(
                "A13824",
            ) => Account {
                name: "john@example.com".to_string(),
                is_personal: true,
                is_read_only: false,
                account_capabilities: indexmap::indexmap! {
                    "urn:ietf:params:jmap:mail".to_string() => json!({
                        "maxMailboxDepth": 10,
                        "maxMailboxesPerEmail": null
                    }),
                    "urn:ietf:params:jmap:contacts".to_string() => json!({}),
                },
                extra_properties: indexmap::indexmap! {},
            },
            Id::<Account>::from(
                "A97813",
            ) => Account {
                name: "jane@example.com".to_string(),
                is_personal: false,
                is_read_only: true,
                account_capabilities: indexmap::indexmap! {
                    "urn:ietf:params:jmap:mail".to_string() => json!({
                        "maxMailboxDepth": 10,
                        "maxMailboxesPerEmail": 1
                    }),
                },
                extra_properties: indexmap::indexmap! {},
            },
        },
        primary_accounts: indexmap::indexmap! {
            "urn:ietf:params:jmap:mail".to_string() => Id::<Account>::from(
                "A13824",
            ),
            "urn:ietf:params:jmap:contacts".to_string() => Id::<Account>::from(
                "A13824",
            ),
        },
        identities: indexmap::indexmap! {},
        username: "john@example.com".to_string(),
        api_url: serde_json::from_value(json!("https://jmap.example.com/api/")).unwrap(),
        download_url: serde_json::from_value(json!(
            "https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}"
        ))
        .unwrap(),
        upload_url: serde_json::from_value(json!("https://jmap.example.com/upload/{accountId}/"))
            .unwrap(),
        event_source_url: serde_json::from_value(json!(
            "https://jmap.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}"
        ))
        .unwrap(),
        state: State {
            inner: "75128aab4b1b".into(),
            _ph: std::marker::PhantomData,
        },
        extra_properties: indexmap::indexmap! {},
    };
    assert_eq!(
        json!(serde_json::from_str::<Session>(RFC_EXAMPLE).unwrap()),
        json!(expected)
    );
    assert_eq!(
        json!(serde_json::from_str::<serde_json::Value>(RFC_EXAMPLE).unwrap()),
        json!(expected)
    );
    assert_eq!(
        json!(serde_json::from_str::<serde_json::Value>(RFC_EXAMPLE).unwrap()),
        json!(serde_json::from_str::<Session>(RFC_EXAMPLE).unwrap()),
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(
            &json!(serde_json::from_str::<serde_json::Value>(RFC_EXAMPLE).unwrap()).to_string()
        )
        .unwrap(),
        json!(serde_json::from_str::<Session>(RFC_EXAMPLE).unwrap()),
    );
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(
            &json!(serde_json::from_str::<serde_json::Value>(RFC_EXAMPLE).unwrap()).to_string()
        )
        .unwrap(),
        serde_json::from_str::<serde_json::Value>(
            &json!(serde_json::from_str::<Session>(RFC_EXAMPLE).unwrap()).to_string()
        )
        .unwrap(),
    );
}

#[test]
/// Check that `Set` method calls and `Set` responses are serialized and
/// deserialized properly.
fn test_jmap_server_set_method_and_response() {
    use std::sync::Arc;

    use futures::lock::Mutex as FutureMutex;
    use serde_json::json;

    use crate::jmap::{
        mailbox,
        methods::{Set, SetError, SetResponse},
        objects::Id,
        protocol::Request,
    };
    let account_id = "blahblah";
    let prev_seq = 33;
    let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq)));
    let path = "new_mbox";

    let mailbox_set_call = mailbox::MailboxSet::new(
        Set::<mailbox::MailboxObject>::new(None)
            .account_id(account_id.into())
            .create(Some({
                let id: Id<mailbox::MailboxObject> = path.into();
                indexmap! {
                    id.clone().into() => mailbox::MailboxObject {
                        id,
                        name: path.to_string(),
                        ..mailbox::MailboxObject::default()
                    }
                }
            })),
    )
    .on_destroy_remove_emails(true);
    futures::executor::block_on(req.add_call(&mailbox_set_call));

    assert_eq!(
        json! {&req},
        json! {{
            "methodCalls" : [
                [
                    "Mailbox/set",
                    {
                        "accountId" : account_id,
                        "create" : {
                            "new_mbox": {
                                "isSubscribed": false,
                                "name": path,
                                "parentId": null,
                                "role": null,
                                "sortOrder": 0,
                            }
                        },
                        "onDestroyRemoveEmails": true
                    },
                    "m33"
                ],
                ],
                "using" : [
                    "urn:ietf:params:jmap:core",
                    "urn:ietf:params:jmap:mail",
                    "urn:ietf:params:jmap:submission"
                ]
        }},
    );
    // Example "where we try to rename one Mailbox and destroy another" from RFC
    // 8621 section-2.6
    let id = Id::<mailbox::MailboxObject>::from("MB674cc24095db49ce");
    let destroy_id = Id::<mailbox::MailboxObject>::from("MB23cfa8094c0f41e6");
    let mailbox_set_call = mailbox::MailboxSet::new(
        Set::<mailbox::MailboxObject>::new(None)
            .account_id(account_id.into())
            .if_in_state(Some("78542".to_string().into()))
            .update(Some({
                indexmap! {
                    id.clone().into() => json! {{
                        "name" : "Maybe important mail"
                    }}
                }
            }))
            .destroy(Some(vec![destroy_id.clone().into()])),
    );
    let expected_json = json! {{
        "accountId": &account_id,
        "ifInState": "78542",
        "update": {
            id.as_str(): {
                "name": "Maybe important mail"
            }
        },
        "destroy": [ destroy_id.as_str() ]
    }};
    assert_eq!(json! {&mailbox_set_call}, expected_json);
    assert_eq!(
        json! {&serde_json::from_value::<mailbox::MailboxSet>(json! {&mailbox_set_call}).unwrap()},
        expected_json,
    );
    // > Suppose the rename succeeds, but we don't have permission to destroy the
    // > Mailbox we tried
    // > to destroy; we might get back:

    let expected_json = json! {{
        "accountId": "blahblah",
        "oldState": "78542",
        "newState": "78549",
        "updated": {
            "MB674cc24095db49ce": null
        },
        "notDestroyed": {
            "MB23cfa8094c0f41e6": {
                "type": "forbidden"
            }
        }
    }};
    let mailbox_set_response: SetResponse<mailbox::MailboxObject> = SetResponse {
        account_id: account_id.into(),
        old_state: Some("78542".to_string().into()),
        new_state: "78549".to_string().into(),
        created: None,
        updated: Some(indexmap! {
            id => None
        }),
        destroyed: None,
        not_created: None,
        not_updated: None,
        not_destroyed: Some(indexmap! {
            destroy_id => SetError::Forbidden(None)
        }),
    };
    assert_eq!(json! {&mailbox_set_response}, expected_json);
    assert_eq!(
        json! {&serde_json::from_value::<SetResponse<mailbox::MailboxObject>>(json! {&mailbox_set_response}).unwrap()},
        expected_json,
    );

    // Check SetError ser/de
    let description = Some("some text".to_string());
    let set_errors = [
        (json! {{ "type": "forbidden" }}, SetError::Forbidden(None)),
        (
            json! {{ "type": "forbidden", "description": "some text" }},
            SetError::Forbidden(description.clone()),
        ),
        (json! {{ "type": "overQuota" }}, SetError::OverQuota(None)),
        (
            json! {{ "type": "overQuota", "description": "some text" }},
            SetError::OverQuota(description.clone()),
        ),
        (json! {{ "type": "tooLarge" }}, SetError::TooLarge(None)),
        (
            json! {{ "type": "tooLarge", "description": "some text" }},
            SetError::TooLarge(description.clone()),
        ),
        (json! {{ "type": "rateLimit" }}, SetError::RateLimit(None)),
        (
            json! {{ "type": "rateLimit", "description": "some text" }},
            SetError::RateLimit(description.clone()),
        ),
        (json! {{ "type": "notFound" }}, SetError::NotFound(None)),
        (
            json! {{ "type": "notFound", "description": "some text" }},
            SetError::NotFound(description.clone()),
        ),
        (
            json! {{ "type": "invalidPatch" }},
            SetError::InvalidPatch(None),
        ),
        (
            json! {{ "type": "invalidPatch", "description": "some text" }},
            SetError::InvalidPatch(description.clone()),
        ),
        (
            json! {{ "type": "willDestroy" }},
            SetError::WillDestroy(None),
        ),
        (
            json! {{ "type": "willDestroy", "description": "some text" }},
            SetError::WillDestroy(description.clone()),
        ),
        (
            json! {{ "type": "invalidProperties", "properties": ["someProperty"] }},
            SetError::InvalidProperties {
                description: None,
                properties: vec!["someProperty".to_string()],
            },
        ),
        (
            json! {{ "type": "invalidProperties", "description": "some text", "properties": ["someProperty"] }},
            SetError::InvalidProperties {
                description: description.clone(),
                properties: vec!["someProperty".to_string()],
            },
        ),
        (json! {{ "type": "singleton" }}, SetError::Singleton(None)),
        (
            json! {{ "type": "singleton", "description": "some text" }},
            SetError::Singleton(description.clone()),
        ),
        (
            json! {{ "type": "requestTooLarge" }},
            SetError::RequestTooLarge(None),
        ),
        (
            json! {{ "type": "requestTooLarge", "description": "some text" }},
            SetError::RequestTooLarge(description.clone()),
        ),
        (
            json! {{ "type": "stateMismatch" }},
            SetError::StateMismatch(None),
        ),
        (
            json! {{ "type": "stateMismatch", "description": "some text" }},
            SetError::StateMismatch(description),
        ),
    ];
    for (expected_json, set_error) in set_errors {
        assert_eq!(json! {&set_error}, expected_json);
        assert_eq!(
            json! {&serde_json::from_value::<SetError>(json! {&set_error}).unwrap()},
            expected_json,
        );
    }
    assert_eq!(
        serde_json::from_value::<SetError>(json! {{
            "type": "invalidProperties",
        }})
        .unwrap_err()
        .to_string(),
        "missing field `properties`"
    );
    assert_eq!(
        serde_json::from_value::<SetError>(json! {{
            "type": "forbidden",
            "properties": vec!["someProperty".to_string()],
        }})
        .unwrap_err()
        .to_string(),
        "unknown field `properties`, expected `type` or `description`"
    );
    assert_eq!(
        serde_json::from_value::<SetError>(json! {{
            "type": "unknown variant",
            "description": "foo",
        }})
        .unwrap_err()
        .to_string(),
        "unknown variant `unknown variant`, expected one of `forbidden`, `overQuota`, `tooLarge`, \
         `rateLimit`, `notFound`, `invalidPatch`, `willDestroy`, `invalidProperties`, \
         `singleton`, `requestTooLarge`, `stateMismatch`"
    );
}

#[test]
/// Check that `Get` method calls and `Get` responses are serialized and
/// deserialized properly.
fn test_jmap_server_get_method_and_response() {
    use std::sync::Arc;

    use futures::lock::Mutex as FutureMutex;
    use serde_json::json;

    use crate::jmap::{
        argument::Argument,
        email, mailbox,
        methods::{Get, GetResponse, ResultField},
        protocol::Request,
    };
    let account_id = "blahblah";
    let prev_seq = 33;
    let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq)));

    let mailbox_get_call = mailbox::MailboxGet::new(
        Get::<mailbox::MailboxObject>::new()
            .account_id(account_id.into())
            .ids(None),
    );
    futures::executor::block_on(req.add_call(&mailbox_get_call));

    assert_eq!(
        json! {&req},
        json! {{
            "methodCalls" : [
                [
                    "Mailbox/get",
                    {
                        "accountId" : account_id
                    },
                    "m33"
                ],
                ],
                "using" : [
                    "urn:ietf:params:jmap:core",
                    "urn:ietf:params:jmap:mail",
                    "urn:ietf:params:jmap:submission"
                ]
        }},
    );
    let gets: &[(serde_json::Value, mailbox::MailboxGet)] = &[
        (
            json! {{
                "accountId" : account_id
            }},
            mailbox::MailboxGet::new(
                Get::<mailbox::MailboxObject>::new()
                    .account_id(account_id.into())
                    .ids(None),
            ),
        ),
        (
            json! {{
                "accountId" : account_id,
                "#ids": {
                    "resultOf": "m42",
                    "name": "Email/changes",
                    "path": "/created"
                }
            }},
            mailbox::MailboxGet::new(
                Get::<mailbox::MailboxObject>::new()
                    .account_id(account_id.into())
                    .ids(Some(Argument::reference::<
                        email::EmailChanges,
                        email::EmailObject,
                        email::EmailObject,
                    >(
                        42,
                        ResultField::<email::EmailChanges, email::EmailObject>::new("/created"),
                    ))),
            ),
        ),
    ];
    for (expected_json, mailbox_get) in gets {
        assert_eq!(&json! {&mailbox_get}, expected_json);
        assert_eq!(
            &json! {&serde_json::from_value::<mailbox::MailboxGet>(json! {&mailbox_get}).unwrap()},
            expected_json,
        );
    }
    let responses: &[(serde_json::Value, GetResponse<mailbox::MailboxObject>)] = &[
        (
            json! {{
                "accountId": "blahblah",
                "list": [],
                "notFound": [],
                "state": "75128aab4b1b",
            }},
            GetResponse {
                account_id: account_id.into(),
                state: "75128aab4b1b".to_string().into(),
                list: vec![],
                not_found: vec![],
            },
        ),
        (
            json! {{
                "accountId": "blahblah",
                "list": [{
                    "id": "MB23cfa8094c0f41e6",
                    "name": "Inbox",
                    "parentId": null,
                    "role": "inbox",
                    "sortOrder": 10,
                    "totalEmails": 16307,
                    "unreadEmails": 13905,
                    "totalThreads": 5833,
                    "unreadThreads": 5128,
                    "myRights": {
                        "mayAddItems": true,
                        "mayRename": false,
                        "maySubmit": true,
                        "mayDelete": false,
                        "maySetKeywords": true,
                        "mayRemoveItems": true,
                        "mayCreateChild": true,
                        "maySetSeen": true,
                        "mayReadItems": true
                    },
                    "isSubscribed": true
                }],
                "notFound": ["MB23cfa8094c0f41e7"],
                "state": "75128aab4b1b",
            }},
            GetResponse {
                account_id: account_id.into(),
                state: "75128aab4b1b".to_string().into(),
                list: vec![mailbox::MailboxObject {
                    id: "MB23cfa8094c0f41e6".into(),
                    name: "Inbox".into(),
                    parent_id: None,
                    role: Some("inbox".into()),
                    sort_order: 10,
                    total_emails: 16307,
                    unread_emails: 13905,
                    total_threads: 5833,
                    unread_threads: 5128,
                    my_rights: mailbox::JmapRights {
                        may_add_items: true,
                        may_rename: false,
                        may_submit: true,
                        may_delete: false,
                        may_set_keywords: true,
                        may_remove_items: true,
                        may_create_child: true,
                        may_set_seen: true,
                        may_read_items: true,
                    },
                    is_subscribed: true,
                }],
                not_found: vec!["MB23cfa8094c0f41e7".into()],
            },
        ),
    ];
    for (expected_json, mailbox_get_response) in responses {
        assert_eq!(&json! {&mailbox_get_response}, expected_json);
        assert_eq!(
            &json! {&serde_json::from_value::<GetResponse<mailbox::MailboxObject>>(json! {&mailbox_get_response}).unwrap()},
            expected_json,
        );
    }
}
