// TODO(v5): remove entire test module
#![allow(deprecated)]

use anyhow::Error;
use culpa::throws;
use serde::{Deserialize, Serialize};

#[macro_use]
extern crate serde_json;

mod common;

use common::*;

// Begin tests exactly matching DittoCollectionTests.swift

#[test]
#[throws]
fn test_using_a_collection_with_an_empty_string_throws_an_error() {
    let ditto = get_builder_ditto(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_using_a_collection_with_an_empty_string_throws_an_error_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let coll = store.collection("");
    assert!(coll.is_err());
}

#[test]
#[throws]
fn test_creating_a_collection_with_a_whitespace_only_name() {
    let ditto = get_builder_ditto(None)?;
    test_creating_a_collection_with_a_whitespace_only_name_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_creating_a_collection_with_a_whitespace_only_name_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_creating_a_collection_with_a_whitespace_only_name_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("   ");
    assert!(collection.is_err());
}

#[test]
#[throws]
fn test_upserting_into_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_into_a_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;
    let id = collection.upsert(json!({"hello": "again"}))?;
    let doc = collection.find_by_id(id.to_owned()).exec()?;
    assert_eq!(id, doc.id());
    assert_eq!(doc.get::<String>("hello")?, "again");
}

#[test]
#[throws]
fn test_upserting_with_a_custom_document_id_as_a_primitive_value_in_the_document_content() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_with_a_custom_document_id_as_a_primitive_value_in_the_document_content_impl(
        ditto,
    )?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_with_a_custom_document_id_as_a_primitive_value_in_the_document_content_impl(
        ditto,
    )?;
}

#[throws]
fn test_upserting_with_a_custom_document_id_as_a_primitive_value_in_the_document_content_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({ "some": "value", "_id": "from_inside" }))?;
    let expected_id = DocumentId::new(&"from_inside")?;
    assert_eq!(doc_id, expected_id);

    let doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq_ignoring_id(&*doc, json!({ "some": "value" }))?;
    assert_eq!(doc.id(), doc_id);
    assert_eq!(doc.id(), expected_id);
}

#[test]
#[throws]
fn test_upserting_with_a_custom_document_id_in_the_document_content() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_with_a_custom_document_id_in_the_document_content_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_with_a_custom_document_id_in_the_document_content_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_with_a_custom_document_id_in_the_document_content_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let content_id = DocumentId::new(&"from_inside")?;
    let doc_id = collection.upsert(json!({ "some": "value", "_id": content_id }))?;
    assert_eq!(doc_id, content_id);

    let doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq_ignoring_id(&*doc, json!({ "some": "value" }))?;
    assert_eq!(content_id, doc.id());
}

#[test]
#[throws]
fn test_upserting_with_an_id_that_already_exists_merges_with_existing_content() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_with_an_id_that_already_exists_merges_with_existing_content_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_with_an_id_that_already_exists_merges_with_existing_content_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_with_an_id_that_already_exists_merges_with_existing_content_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;
    let custom_id = DocumentId::new(&"custom_id".to_string())?;
    let id1 = collection.upsert(json!({"some": "value", "_id": custom_id}))?;
    let id2 = collection.upsert(json!({"another": "thing", "_id": custom_id}))?;
    let doc = collection.find_by_id(custom_id).exec()?;
    assert_eq!(id1, doc.id());
    assert_eq!(id2, doc.id());
    assert_eq!(doc.get::<String>("another")?, "thing");
    assert_eq!(doc.get::<String>("some")?, "value");
}

#[test]
#[throws]
fn test_upserting_default_data_if_a_document_with_a_matching_id_already_exists_is_a_no_op() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_default_data_if_a_document_with_a_matching_id_already_exists_is_a_no_op_impl(
        ditto,
    )?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_default_data_if_a_document_with_a_matching_id_already_exists_is_a_no_op_impl(
        ditto,
    )?;
}

#[throws]
fn test_upserting_default_data_if_a_document_with_a_matching_id_already_exists_is_a_no_op_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;
    let custom_id = DocumentId::new(&"custom_id")?;

    let id1 = collection.upsert_with_strategy(
        json!({"some": "value", "_id": custom_id}),
        WriteStrategy::InsertDefaultIfAbsent,
    )?;

    // Immediately try and insert some new default data
    let id2 = collection.upsert_with_strategy(
        json!({"different": "default data", "_id": custom_id}),
        WriteStrategy::InsertDefaultIfAbsent,
    )?;

    // The new default data has been ignored because a document with a matching
    // ID already existed
    let doc = collection.find_by_id(custom_id.clone()).exec()?;
    assert_eq!(doc.get::<String>("some")?, "value");
    assert!(doc.get::<String>("different").is_err());

    // Perform an upsert of a document containing *non* default data
    let id3 = collection
        .upsert_with_strategy(json!({"a": "b", "_id": custom_id}), WriteStrategy::Merge)?;

    // Again try and insert some new default data
    let id4 = collection.upsert_with_strategy(
        json!({"different": "default data", "_id": custom_id}),
        WriteStrategy::InsertDefaultIfAbsent,
    )?;

    let doc = collection.find_by_id(custom_id).exec()?;
    assert_eq!(doc.get::<String>("a")?, "b");
    assert_eq!(doc.get::<String>("some")?, "value");
    assert!(doc.get::<String>("different").is_err());
    assert_eq!(id1, doc.id());
    assert_eq!(id2, doc.id());
    assert_eq!(id2, doc.id());
    assert_eq!(id3, doc.id());
    assert_eq!(id4, doc.id());
    assert_eq!(id4, doc.id());
}

#[test]
#[throws]
fn test_upsert_with_write_strategy_insert_if_absent() {
    let ditto = get_builder_ditto(None)?;
    test_upsert_with_write_strategy_insert_if_absent_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upsert_with_write_strategy_insert_if_absent_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upsert_with_write_strategy_insert_if_absent_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let custom_id = DocumentId::new(&"custom_id")?;
    let id1 = collection.upsert(json!({"some": "value", "_id": custom_id}))?;
    let id2 = collection.upsert_with_strategy(
        json!({"another": "thing", "_id": custom_id}),
        WriteStrategy::InsertIfAbsent,
    )?;
    let doc = collection.find_by_id(custom_id).exec()?;
    assert_eq!(id1, doc.id());
    assert_eq!(id2, doc.id());
    assert_eq!(doc.get::<String>("some")?, "value");
    assert!(doc.get::<String>("another").is_err());
}

#[test]
#[throws]
fn test_upsert_roundtrip_with_different_types_as_document_ids() {
    let ditto = get_builder_ditto(None)?;
    test_upsert_roundtrip_with_different_types_as_document_ids_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upsert_roundtrip_with_different_types_as_document_ids_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upsert_roundtrip_with_different_types_as_document_ids_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let raw_doc_ids: Vec<serde_json::Value> = vec![
        "boring_old_string".to_string().into(),
        1.into(),
        0.into(),
        123.into(),
        9999.into(),
        false.into(),
        true.into(),
        (0..64)
            .map(|_| rand::random::<u8>())
            .collect::<Vec<u8>>()
            .into(),
        vec!["a", "abc", "z89{{}}@£!fv>?!,[](){{}}000"].into(),
        json!({"a": "b", "__num__@£$%^&{})(|,,./!?": -7123}),
    ];

    for raw_id in &raw_doc_ids {
        let doc_id = DocumentId::new(raw_id)?;
        let returned_id = collection.upsert(json!({"hello": "again", "_id": doc_id}))?;
        assert_eq!(doc_id, returned_id);

        let doc = collection.find_by_id(doc_id.clone()).exec()?;
        assert_eq!(doc.id(), doc_id);

        let value =
            |cbor: serde_cbor::Value| -> serde_json::Value { serde_json::to_value(&cbor).unwrap() };

        use serde_json::Value;
        match raw_id {
            Value::Bool(bool) => {
                assert_eq!(*bool, value(doc.id().value()).as_bool().unwrap());
            }
            Value::Number(num) => {
                assert_eq!(num, value(doc.id().value()).as_number().unwrap());
            }
            Value::String(string) => {
                assert_eq!(string, value(doc.id().value()).as_str().unwrap());
            }
            Value::Array(array) => {
                assert_eq!(array, value(doc.id().value()).as_array().unwrap());
            }
            Value::Object(object) => {
                assert_eq!(object, value(doc.id().value()).as_object().unwrap());
                assert_eq!(
                    object["a"].as_str().unwrap(),
                    value(doc.id().value())["a"].as_str().unwrap()
                );
                assert_eq!(
                    object["a"].as_str().unwrap(),
                    value(doc.id().value())["a"].as_str().unwrap()
                );
                assert_eq!(
                    object["a"].as_str().unwrap(),
                    doc.typed::<serde_json::Value>()?["_id"]["a"]
                        .as_str()
                        .unwrap()
                );
                assert_eq!(
                    object["__num__@£$%^&{})(|,,./!?"].as_number().unwrap(),
                    value(doc.id().value())["__num__@£$%^&{})(|,,./!?"]
                        .as_number()
                        .unwrap()
                );
                assert_eq!(
                    object["__num__@£$%^&{})(|,,./!?"].as_number().unwrap(),
                    doc.typed::<serde_json::Value>()?["_id"]["__num__@£$%^&{})(|,,./!?"]
                        .as_number()
                        .unwrap()
                );
            }
            Value::Null => {
                panic!("DocumentId should not be null");
            }
        }
    }
}

#[test]
#[throws]
fn test_upsert_roundtripping_a_document_with_a_big_integer_in_it() {
    let ditto = get_builder_ditto(None)?;
    test_upsert_roundtripping_a_document_with_a_big_integer_in_it_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upsert_roundtripping_a_document_with_a_big_integer_in_it_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upsert_roundtripping_a_document_with_a_big_integer_in_it_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let content = json!({ "big_int": 15_615_600_019_190_239_263_usize });
    let id = collection.upsert(content.clone())?;
    let doc = collection.find_by_id(&id).exec()?;
    assert_eq_ignoring_id(&*doc, content)?;

    let typed = &doc.typed::<serde_json::Value>()?;
    let big_int = typed["big_int"].as_u64().unwrap();
    assert!(big_int > i32::MAX as u64);
    assert_eq!(doc.id(), id);
}

#[test]
#[throws]
fn test_upserting_a_dictionary_into_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_a_dictionary_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_a_dictionary_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_a_dictionary_into_a_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let id = collection.upsert(json!({ "test": "some stuff to be encoded into CBOR"}))?;
    let doc = collection.find_by_id(&id).exec()?;
    let expected_doc = json!({ "test": "some stuff to be encoded into CBOR" });
    assert_eq_ignoring_id(&*doc, expected_doc)?;
    assert_eq!(id, doc.id());
}

#[test]
#[throws]
fn test_upserting_a_register_into_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_a_register_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_a_register_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_a_register_into_a_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let simple_reg = DittoRegister::new("string_register")?;
    let array_content = json!([
        1, true, null, "string", ["a", "b", { "nested": ["wow", {"further": "down"} ] } ]
    ]);
    let array_reg = DittoRegister::new(&array_content)?;

    let dict_content = json!({
        "a": "b",
        "c": { "d": { "e": "f", "g": [], "h": {"i": true } } }
    });
    let dict_reg = DittoRegister::new(&dict_content)?;

    let id = collection.upsert(json!({
        "string_reg": simple_reg,
        "array_reg": array_reg,
        "dictionary_reg": dict_reg
    }))?;
    let doc = collection.find_by_id(id).exec()?;

    // Compare String register
    let fetched_string_reg = doc.get::<DittoRegister>("string_reg")?;
    assert_eq!(
        "string_register",
        fetched_string_reg.value::<String>().unwrap()
    );

    // Compare Array register
    let fetched_array_reg = doc.get::<DittoRegister>("array_reg")?;
    assert_eq!(
        array_content,
        fetched_array_reg.value::<serde_json::Value>().unwrap()
    );

    // Compare Dict register
    let fetched_dictionary_reg = doc.get::<DittoRegister>("dictionary_reg")?;
    assert_eq!(
        dict_content,
        fetched_dictionary_reg.value::<serde_json::Value>().unwrap()
    );
}

#[test]
#[throws]
fn test_upserting_a_dictionary_with_plain_data_into_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_a_dictionary_with_plain_data_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_a_dictionary_with_plain_data_into_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_a_dictionary_with_plain_data_into_a_collection_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let bytes: Vec<u8> = {
        use rand::prelude::*;
        let mut rng = rand::thread_rng();
        let len = 2048;
        (0..len).map(|_| rng.gen_range(0..255)).collect()
    };

    let data = json!({
        "color": "Red",
        "age": 56,
        "mileage": 56444,
        "tags": ["a", "b", "d", 4],
        "data": bytes
    });

    let id = collection.upsert(json!(data))?;
    let doc = collection.find_by_id(&id).exec()?;
    let typed_doc = doc.typed::<serde_json::Value>()?;

    assert_eq!(doc.id(), id);
    assert_eq!(
        typed_doc["color"].as_str().unwrap(),
        data["color"].as_str().unwrap()
    );
    assert_eq!(
        typed_doc["age"].as_number().unwrap(),
        data["age"].as_number().unwrap()
    );
    assert_eq!(
        typed_doc["mileage"].as_number().unwrap(),
        data["mileage"].as_number().unwrap()
    );
    assert_eq!(
        typed_doc["tags"][0].as_str().unwrap(),
        data["tags"][0].as_str().unwrap()
    );
    assert_eq!(
        typed_doc["tags"][1].as_str().unwrap(),
        data["tags"][1].as_str().unwrap()
    );
    assert_eq!(
        typed_doc["tags"][2].as_str().unwrap(),
        data["tags"][2].as_str().unwrap()
    );
    assert_eq!(
        typed_doc["tags"][3].as_number().unwrap(),
        data["tags"][3].as_number().unwrap()
    );
    assert_eq!(typed_doc["data"], data["data"]);
}

#[test]
#[throws]
fn test_upserting_a_type_into_a_collection_and_getting_a_typed_document_out() {
    let ditto = get_builder_ditto(None)?;
    test_upserting_a_type_into_a_collection_and_getting_a_typed_document_out_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upserting_a_type_into_a_collection_and_getting_a_typed_document_out_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upserting_a_type_into_a_collection_and_getting_a_typed_document_out_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let bytes: Vec<u8> = {
        use rand::prelude::*;
        let mut rng = rand::thread_rng();
        let len = 2048;
        (0..len).map(|_| rng.gen_range(0..255)).collect()
    };

    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
    enum CarTag {
        String(String),
        Int(i32),
    }

    #[derive(Debug, Serialize, Deserialize)]
    struct Car {
        color: String,
        age: i32,
        mileage: i32,
        tags: Vec<CarTag>,
        data: Vec<u8>,
    }

    let car = Car {
        color: "Red".to_string(),
        age: 56,
        mileage: 56_444,
        tags: vec![
            CarTag::String("a".to_string()),
            CarTag::String("b".to_string()),
            CarTag::String("d".to_string()),
            CarTag::Int(4),
        ],
        data: bytes,
    };

    let id = collection.upsert(car)?;
    let doc = collection.find_by_id(id).exec()?;
    let car = doc.typed::<Car>()?;
    assert_eq!(car.color, "Red");
    assert_eq!(car.age, 56);
    assert_eq!(car.mileage, 56_444);
    assert_eq!(
        car.tags,
        vec![
            CarTag::String("a".to_string()),
            CarTag::String("b".to_string()),
            CarTag::String("d".to_string()),
            CarTag::Int(4)
        ]
    );
}

#[test]
#[throws]
fn test_updating_a_document() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let content = json!({ "my": "document" });
    let doc_id = collection.upsert(content.clone())?;
    let doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq_ignoring_id(&*doc, content)?;

    collection.find_by_id(&doc_id).update(|doc| {
        let doc = doc.unwrap();
        doc.set("something", "new").unwrap();
    })?;

    let doc_after_update = collection.find_by_id(&doc_id).exec()?;
    assert_eq_ignoring_id(
        &*doc_after_update,
        json!({ "something": "new", "my": "document" }),
    )?;
}

#[test]
#[throws]
fn test_updating_document_that_has_an_array_of_dictionaries_in_it() {
    let ditto = get_builder_ditto(None)?;
    test_updating_document_that_has_an_array_of_dictionaries_in_it_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_document_that_has_an_array_of_dictionaries_in_it_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_document_that_has_an_array_of_dictionaries_in_it_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let id = collection.upsert(json!({
        "hello": [
            { "number": 1 },
            { "number": 2 },
            { "number": 3 },
        ]
    }))?;
    let doc = collection.find_by_id(&id).exec()?;

    assert_eq!(
        doc.get::<serde_json::Value>("hello")?,
        json!([
            { "number": 1 },
            { "number": 2 },
            { "number": 3 },
        ])
    );

    collection.find_by_id(&id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set(
            "hello",
            json!([
                { "number": 1 },
                { "number": 2 },
                { "number": 4 },
            ]),
        )
        .unwrap();
    })?;
    let updated_doc = collection.find_by_id(&id).exec()?;
    assert_eq!(
        updated_doc.get::<serde_json::Value>("hello")?,
        json!([
            { "number": 1 },
            { "number": 2 },
            { "number": 4 },
        ])
    );
}

#[test]
#[throws]
fn test_updating_a_document_to_have_a_key_with_a_null_value() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_null_value_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_null_value_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_to_have_a_key_with_a_null_value_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({ "a": "b" }))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("a", json!(null)).unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(updated_doc.get::<serde_json::Value>("a")?, json!(null));
}

#[test]
#[throws]
fn test_updating_a_document_to_have_a_key_with_a_bool_value() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_bool_value_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_bool_value_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_to_have_a_key_with_a_bool_value_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({ "a": "b" }))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("a", json!(true)).unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(updated_doc.get::<serde_json::Value>("a")?, json!(true));
}

#[test]
#[throws]
fn test_updating_a_document_to_have_a_key_with_an_int_value() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_an_int_value_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_an_int_value_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_to_have_a_key_with_an_int_value_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({ "a": "b" }))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("a", json!(123)).unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(updated_doc.get::<serde_json::Value>("a")?, json!(123));
}

#[test]
#[throws]
fn test_updating_a_document_to_have_a_key_with_a_dictionary_value() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_dictionary_value_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_to_have_a_key_with_a_dictionary_value_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_to_have_a_key_with_a_dictionary_value_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({ "a": { "b": "c" } }))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("a.nested", json!("value")).unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(
        updated_doc.get::<serde_json::Value>("a")?,
        json!({
            "b": "c",
            "nested": "value",
        })
    );
}

#[test]
#[throws]
fn test_updating_an_empty_document_to_have_a_key_with_a_dictionary_value() {
    let ditto = get_builder_ditto(None)?;
    test_updating_an_empty_document_to_have_a_key_with_a_dictionary_value_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_an_empty_document_to_have_a_key_with_a_dictionary_value_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_an_empty_document_to_have_a_key_with_a_dictionary_value_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({}))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("a", json!({ "b": "c" })).unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(
        updated_doc.get::<serde_json::Value>("a")?,
        json!({ "b": "c" })
    );
}

#[test]
#[throws]
fn test_updating_a_document_that_has_nested_maps() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_that_has_nested_maps_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_that_has_nested_maps_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_that_has_nested_maps_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    let doc_id = collection.upsert(json!({
        "one": {
            "one_inner": { "one_inner_inner": "X" }
        },
        "two": "just a string",
    }))?;
    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("three", json!({ "something": "new" })).unwrap();
        doc.set("two", json!("small change")).unwrap();
        doc.set(
            "one",
            json!({ "one_inner": { "one_inner_inner": "marks the spot" } }),
        )
        .unwrap();
    })?;

    let updated_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(
        updated_doc.get::<serde_json::Value>("three")?,
        json!({ "something": "new" })
    );
    assert_eq!(
        updated_doc.get::<serde_json::Value>("two")?,
        json!("small change")
    );
    assert_eq!(
        updated_doc.get::<serde_json::Value>("one")?,
        json!({ "one_inner": { "one_inner_inner": "marks the spot" } })
    );
}

#[test]
#[throws]
fn test_updating_a_document_that_was_inserted_as_a_struct() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_that_was_inserted_as_a_struct_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_that_was_inserted_as_a_struct_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_that_was_inserted_as_a_struct_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
    struct Car {
        age: u32,
        color: String,
    }
    let car = Car {
        age: 27,
        color: "Red".to_string(),
    };
    let doc_id = collection.upsert(car)?;

    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("age", 23).unwrap();
    })?;

    let found_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(found_doc.get::<serde_json::Value>("age")?, json!(23));

    let mut found_car = found_doc.typed::<Car>()?;
    assert_eq!(found_car.age, 23);

    found_car.color = "Green".to_string();
    collection.find_by_id(&doc_id).update_doc(&found_car)?;

    let doc_after_struct_update = collection.find_by_id(&doc_id).exec()?.typed::<Car>()?;
    assert_eq!(
        doc_after_struct_update,
        Car {
            age: 23,
            color: "Green".to_string()
        }
    );
}

#[test]
#[throws]
fn test_updating_a_document_that_was_inserted_as_a_struct_that_has_custom_coding_keys() {
    let ditto = get_builder_ditto(None)?;
    test_updating_a_document_that_was_inserted_as_a_struct_that_has_custom_coding_keys_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_updating_a_document_that_was_inserted_as_a_struct_that_has_custom_coding_keys_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_updating_a_document_that_was_inserted_as_a_struct_that_has_custom_coding_keys_impl(
    ditto: dittolive_ditto::prelude::Ditto,
) {
    let store = ditto.store();
    let collection = store.collection("test")?;

    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
    struct Car {
        #[serde(rename = "age_in_years")]
        age: u32,
        color: String,
    }

    let car = Car {
        age: 27,
        color: "Red".to_string(),
    };
    let doc_id = collection.upsert(car)?;

    collection.find_by_id(&doc_id).update(|maybe_doc| {
        let doc = maybe_doc.unwrap();
        doc.set("age_in_years", 23).unwrap();
    })?;
    let found_doc = collection.find_by_id(&doc_id).exec()?;
    assert_eq!(
        found_doc.get::<serde_json::Value>("age_in_years")?,
        json!(23)
    );

    let found_typed_doc = found_doc.typed::<Car>()?;
    assert_eq!(found_typed_doc.age, 23);
}

#[test]
#[throws]
fn test_listing_documents_in_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_listing_documents_in_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_listing_documents_in_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_listing_documents_in_a_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let uuid = uuid::Uuid::new_v4();
    let collection = store.collection(&uuid.to_string())?;

    let doc_a_id = DocumentId::new(&"1")?;
    let doc_b_id = DocumentId::new(&"2")?;
    let doc_c_id = DocumentId::new(&"3")?;

    let doc_a_content = json!({ "_id": "1", "a": "valForA" });
    let doc_b_content = json!({ "_id": "2", "b": "valForB" });
    let doc_c_content = json!({ "_id": "3", "c": "valForC" });

    let id_a = collection.upsert(doc_a_content.clone())?;
    let id_b = collection.upsert(doc_b_content.clone())?;
    let id_c = collection.upsert(doc_c_content.clone())?;

    let docs = collection.find_all().exec()?;
    assert_eq!(docs.len(), 3);

    let expected_doc_dict = json!({
        id_a.to_string(): doc_a_content,
        id_b.to_string(): doc_b_content,
        id_c.to_string(): doc_c_content,
    });
    for doc in &docs {
        assert_eq_ignoring_id(&**doc, expected_doc_dict[doc.id().to_string()].clone())?;
    }

    let raw_ids = docs.iter().map(|doc| doc.id()).collect::<Vec<_>>();
    assert_eq!(vec![doc_a_id, doc_b_id, doc_c_id], raw_ids);
}

#[test]
#[throws]
fn test_listing_typed_documents_in_a_collection() {
    let ditto = get_builder_ditto(None)?;
    test_listing_typed_documents_in_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_listing_typed_documents_in_a_collection_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_listing_typed_documents_in_a_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let uuid = uuid::Uuid::new_v4();
    let collection = store.collection(&uuid.to_string())?;

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
    struct Car {
        _id: u32,
        model: String,
        year: u32,
    }

    let car_a = Car {
        _id: 1,
        model: "Ford".to_string(),
        year: 1954,
    };
    let car_b = Car {
        _id: 2,
        model: "Ferrari".to_string(),
        year: 1989,
    };
    let car_c = Car {
        _id: 3,
        model: "Toyota".to_string(),
        year: 2016,
    };
    let id_a = collection.upsert(car_a.clone())?;
    let id_b = collection.upsert(car_b.clone())?;
    let id_c = collection.upsert(car_c.clone())?;
    let docs = collection
        .find_all()
        .exec()?
        .into_iter()
        .map(|doc| doc.typed::<Car>())
        .collect::<Result<Vec<_>, _>>()?;
    assert_eq!(docs.len(), 3);

    let expected_docs = vec![car_a, car_b, car_c];
    assert_eq!(expected_docs, docs);

    let expected_ids = vec![id_a.to_string(), id_b.to_string(), id_c.to_string()];
    let raw_ids = docs
        .iter()
        .map(|doc| doc._id.to_string())
        .collect::<Vec<_>>();
    assert_eq!(expected_ids, raw_ids);
}

// End tests exactly matching DittoCollectionTests.swift

// Begin assorted extra Rust SDK tests

#[test]
fn find_with_args() {
    let ditto = get_builder_ditto(None).unwrap();
    find_with_args_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    find_with_args_impl(ditto);
}

fn find_with_args_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    collection
        .upsert(json!({"make": "Honda", "color": "red"}))
        .unwrap();
    collection
        .upsert(json!({"make": "Ford", "color": "blue"}))
        .unwrap();
    collection
        .upsert(json!({"make": "Ferrari", "color": "red"}))
        .unwrap();
    let sort_param = ffi_sdk::COrderByParam {
        query_c_str: c!("make"),
        direction: ffi_sdk::QuerySortDirection::Descending,
    };
    let docs = collection
        .find_with_args("color == $args.color", json!({"color": "red"}))
        .sort(vec![sort_param])
        .exec()
        .unwrap();
    assert_eq!(docs.len(), 2);
    assert_eq!(docs[0].get::<String>("make").unwrap(), "Honda");
    assert_eq!(docs[1].get::<String>("make").unwrap(), "Ferrari");
}

#[test]
fn upsert_with_provided_id() {
    let ditto = get_builder_ditto(None).unwrap();
    upsert_with_provided_id_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    upsert_with_provided_id_impl(ditto);
}

fn upsert_with_provided_id_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    let custom_id = DocumentId::new(&"a".to_string()).unwrap();
    let id = collection
        .upsert(json!({"hello": "again", "_id": custom_id}))
        .unwrap();
    let doc = collection.find_by_id(custom_id.to_owned()).exec().unwrap();
    assert_eq!(id, custom_id);
    assert_eq!(id, doc.id());
    assert_eq!(doc.get::<String>("hello").unwrap(), "again");
}

#[test]
fn get_id_subvalue_from_document_content() {
    let ditto = get_builder_ditto(None).unwrap();
    get_id_subvalue_from_document_content_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    get_id_subvalue_from_document_content_impl(ditto);
}

fn get_id_subvalue_from_document_content_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    let custom_id =
        DocumentId::new(&json!({ "nested": { "should": { "work": ["hello", true] } } })).unwrap();
    let id = collection
        .upsert(json!({"a": "1", "_id": custom_id}))
        .unwrap();
    assert_eq!(id, custom_id);
    let doc = collection.find_by_id(custom_id.to_owned()).exec().unwrap();
    assert_eq!(custom_id, doc.id());
    assert!(doc.get::<bool>("_id.nested.should.work[1]").unwrap());
}

#[test]
#[throws]
fn test_upsert_with_empty_string_id() {
    let ditto = get_builder_ditto(None)?;
    test_upsert_with_empty_string_id_impl(ditto)?;

    let ditto = get_revamp_ditto(None)?;
    test_upsert_with_empty_string_id_impl(ditto)?;

    let ditto = get_revamp_ditto_async_sync(None)?;
    test_using_a_collection_with_an_empty_string_throws_an_error_impl(ditto)?;
}

#[throws]
fn test_upsert_with_empty_string_id_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test")?;
    let id = collection.upsert(json!({"_id": "", "with": "empty id"}))?;
    let doc = collection.find_by_id(id.to_owned()).exec()?;
    assert_eq!(id, doc.id());
    assert_eq!(doc.get::<String>("with")?, "empty id");
}

#[test]
fn trying_to_update_document_id_via_id_key_fails() {
    let ditto = get_builder_ditto(None).unwrap();
    trying_to_update_document_id_via_id_key_fails_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    trying_to_update_document_id_via_id_key_fails_impl(ditto);
}

fn trying_to_update_document_id_via_id_key_fails_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    let id = collection.upsert(json!({"hello": "again"})).unwrap();

    let update_res = collection.find_by_id(id.to_owned()).update(|mut_doc| {
        if let Some(doc) = mut_doc {
            let set_res = doc.set(
                "_id",
                DocumentId::new(&"sneaky_new_id".to_string()).unwrap(),
            );
            assert!(set_res.unwrap_err().to_string().contains(
                "Attempting to update the `_id` key is an error. You cannot update a document's \
                 ID."
            ));

            let remove_res = doc.remove("_id");
            assert!(remove_res.unwrap_err().to_string().contains(
                "Attempting to remove the `_id` key is an error. You cannot remove a document's \
                 ID."
            ));

            let increment_res = doc.increment("_id", 0.3);
            assert!(increment_res.unwrap_err().to_string().contains(
                "Attempting to increment the `_id` key is an error. You cannot update a \
                 document's ID."
            ));
        }
    });

    assert_eq!(update_res.unwrap().len(), 0);

    let doc = collection.find_by_id(id.to_owned()).exec().unwrap();
    assert_eq!(id, doc.id());
}

/// Test round trip insertion and deserialization correctly preserves fields
/// of various types
#[test]
fn serialize_insert_roundtrip() {
    let ditto = get_builder_ditto(None).unwrap();
    serialize_insert_roundtrip_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    serialize_insert_roundtrip_impl(ditto);
}

fn serialize_insert_roundtrip_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let id = DocumentId::new(&1).unwrap();
    let content = TestType::random_with_id(Some(id.clone()));
    let collection = store.collection("test").unwrap();
    let ret_id = collection.upsert(content.to_owned()).unwrap();
    assert_eq!(ret_id, id);
    let doc = collection.find_by_id(id).exec().unwrap();
    let returned_content = doc.typed::<TestType>().unwrap();
    assert_eq!(content, returned_content);
}

#[test]
fn inserting_invalid_doc_throws_error() {
    let ditto = get_builder_ditto(None).unwrap();
    inserting_invalid_doc_throws_error_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    inserting_invalid_doc_throws_error_impl(ditto);
}

fn inserting_invalid_doc_throws_error_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    let result = collection.upsert(json!(["hello", "again"]));
    assert!(result.is_err());
}

#[test]
fn list_collection() {
    let ditto = get_builder_ditto(None).unwrap();
    list_collection_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    list_collection_impl(ditto);
}

fn list_collection_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let uuid = uuid::Uuid::new_v4().to_string();
    let collection = store.collection(&uuid).unwrap();

    // The insert order is no longer preserved
    let _id_a = collection
        .upsert(json!({"a": "valForA", "rank": 1}))
        .unwrap();
    let _id_b = collection
        .upsert(json!({"b": "valForB", "rank": 2}))
        .unwrap();
    let _id_c = collection
        .upsert(json!({"c": "valForC", "rank": 3}))
        .unwrap();

    let sort_param = ffi_sdk::COrderByParam {
        query_c_str: c!("rank"),
        direction: ffi_sdk::QuerySortDirection::Ascending,
    };
    let docs = collection.find_all().sort(vec![sort_param]).exec().unwrap();
    assert_eq!(docs.len(), 3);
    assert_eq!(docs[2].get::<String>("c").unwrap(), "valForC");
}

#[test]
fn document_id() {
    let ditto = get_builder_ditto(None).unwrap();
    document_id_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    document_id_impl(ditto);
}

fn document_id_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();

    let cbor_doc_id = ::serde_cbor::value::to_value(::serde_json::json!({
        "a": {
            "_b_": [
                true,
                "z",
                {
                    "c..": ["?d!"],
                },
            ],
        },
    }))
    .unwrap();

    let doc_id = DocumentId::new(&cbor_doc_id).unwrap();
    let returned_id = collection
        .upsert(json!({"hello": "again", "_id": doc_id}))
        .unwrap();
    assert_eq!(doc_id, returned_id);

    let doc = collection.find_by_id(doc_id.clone()).exec().unwrap();
    assert_eq!(doc.id(), doc_id);
    assert_eq!(doc.id().value(), cbor_doc_id);
}

#[test]
fn evict_remove_and_update_with_args() {
    let ditto = get_builder_ditto(None).unwrap();
    evict_remove_and_update_with_args_impl(ditto);

    let ditto = get_revamp_ditto(None).unwrap();
    evict_remove_and_update_with_args_impl(ditto);
}

fn evict_remove_and_update_with_args_impl(ditto: dittolive_ditto::prelude::Ditto) {
    let store = ditto.store();
    let collection = store.collection("test").unwrap();
    let honda_id = collection
        .upsert(json!({"make": "Honda", "color": "red"}))
        .unwrap();
    let ford_id = collection
        .upsert(json!({"make": "Ford", "color": "blue"}))
        .unwrap();
    let ferrari_id = collection
        .upsert(json!({"make": "Ferrari", "color": "red"}))
        .unwrap();
    let lamborghini_id = collection
        .upsert(json!({"make": "Lamborghini", "color": "yellow"}))
        .unwrap();

    let docs = collection
        .find_with_args(
            "contains([$args.color1, $args.color2], color)",
            json!({"color1": "red", "color2": "yellow"}),
        )
        .exec()
        .unwrap();
    assert_eq!(docs.len(), 3);

    let removed_ids = collection
        .find_with_args("color == $args.color", json!({"color": "yellow"}))
        .remove()
        .unwrap();
    assert_eq!(removed_ids.len(), 1);
    assert_eq!(removed_ids[0], lamborghini_id);

    let sort_param = ffi_sdk::COrderByParam {
        query_c_str: c!("make"),
        direction: ffi_sdk::QuerySortDirection::Descending,
    };
    //@ditto/snippet-start evict
    let evicted_ids = collection
        .find_with_args("$args.color == color", json!({"color": "red"}))
        .sort(vec![sort_param])
        .evict()
        .unwrap();
    //@ditto/snippet-end
    assert_eq!(evicted_ids.len(), 2);
    assert_eq!(evicted_ids[0], honda_id);
    assert_eq!(evicted_ids[1], ferrari_id);

    let docs = collection
        .find_with_args(
            "contains([$args.color1, $args.color2], color)",
            json!({"color1": "red", "color2": "yellow"}),
        )
        .exec()
        .unwrap();
    assert_eq!(docs.len(), 0);

    let _ = collection
        .find_with_args("color == $args.color", json!({"color": "blue"}))
        .update(|mut_docs| {
            assert_eq!(mut_docs.len(), 1);
            mut_docs[0].set("color", "purple").unwrap();
        });

    let docs = collection
        .find_with_args(
            "color != $args.color1 && color != $args.color2",
            json!({"color1": "red", "color2": "yellow"}),
        )
        .exec()
        .unwrap();
    assert_eq!(docs.len(), 1);
    assert_eq!(docs[0].id(), ford_id);
    assert_eq!(docs[0].get::<String>("color").unwrap(), "purple");
}
