/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <common-b64-private.h>
#include <common-bson-dsl-private.h>

#include <mongoc/mongoc.h>

#include <mlib/str.h>
#include <mlib/time_point.h>

#include <json-test.h>
#include <test-libmongoc.h>

/* _mongoc_host_list_from_string_with_err */
#include <mongoc/mongoc-cluster-aws-private.h>
#include <mongoc/mongoc-host-list-private.h>

/* MONGOC_SERVER_ERR_NS_NOT_FOUND */
#include <mongoc/mongoc-client-side-encryption-private.h>
#include <mongoc/mongoc-error-private.h>
#include <mongoc/mongoc-http-private.h>

#include <mlib/duration.h>
#include <mlib/timer.h>

/* _mongoc_crypt_get_libmongocrypt_version */
#include <mongoc/mongoc-crypt-private.h>

#include <mongoc/mongoc-uri.h>

#include <mlib/cmp.h>

static void
_before_test(json_test_ctx_t *ctx, const bson_t *test)
{
   mongoc_client_t *client;
   mongoc_collection_t *keyvault_coll;
   bson_iter_t iter;
   bson_error_t error;
   bool ret;
   mongoc_write_concern_t *wc;
   bson_t insert_opts;

   BSON_UNUSED(test);

   /* Insert data into the key vault. */
   client = test_framework_new_default_client();
   wc = mongoc_write_concern_new();
   mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
   bson_init(&insert_opts);
   mongoc_write_concern_append(wc, &insert_opts);

   if (bson_iter_init_find(&iter, ctx->config->scenario, "key_vault_data")) {
      keyvault_coll = mongoc_client_get_collection(client, "keyvault", "datakeys");

      /* Drop and recreate, inserting data. */
      ret = mongoc_collection_drop(keyvault_coll, &error);
      if (!ret) {
         /* Ignore "namespace does not exist" error. */
         ASSERT_OR_PRINT(error.code == 26, error);
      }

      bson_iter_recurse(&iter, &iter);
      while (bson_iter_next(&iter)) {
         bson_t doc;

         bson_iter_bson(&iter, &doc);
         ret = mongoc_collection_insert_one(keyvault_coll, &doc, &insert_opts, NULL /* reply */, &error);
         ASSERT_OR_PRINT(ret, error);
      }
      mongoc_collection_destroy(keyvault_coll);
   }

   bson_destroy(&insert_opts);
   mongoc_write_concern_destroy(wc);
   mongoc_client_destroy(client);
}

static bool
_run_operation(json_test_ctx_t *ctx, const bson_t *test, const bson_t *operation)
{
   bson_t reply;
   bool res;

   res = json_test_operation(ctx, test, operation, ctx->collection, NULL, &reply);

   bson_destroy(&reply);

   return res;
}

static void
test_client_side_encryption_cb(void *scenario)
{
   json_test_config_t config = JSON_TEST_CONFIG_INIT;
   config.before_test_cb = _before_test;
   config.run_operation_cb = _run_operation;
   config.scenario = scenario;
   config.command_started_events_only = true;
   config.command_monitoring_allow_subset = false;
   run_json_general_test(&config);
}

/* This is the hex form of the base64 encoded value:
 * Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk
 * From the client side encryption spec.
 */
#define LOCAL_MASTERKEY                                                       \
   "\x32\x78\x34\x34\x2b\x78\x64\x75\x54\x61\x42\x42\x6b\x59\x31\x36\x45\x72" \
   "\x35\x44\x75\x41\x44\x61\x67\x68\x76\x53\x34\x76\x77\x64\x6b\x67\x38\x74" \
   "\x70\x50\x70\x33\x74\x7a\x36\x67\x56\x30\x31\x41\x31\x43\x77\x62\x44\x39" \
   "\x69\x74\x51\x32\x48\x46\x44\x67\x50\x57\x4f\x70\x38\x65\x4d\x61\x43\x31" \
   "\x4f\x69\x37\x36\x36\x4a\x7a\x58\x5a\x42\x64\x42\x64\x62\x64\x4d\x75\x72" \
   "\x64\x6f\x6e\x4a\x31\x64"

static void
_set_extra_bypass(bson_t *extra)
{
   if (test_framework_getenv_bool("MONGOC_TEST_MONGOCRYPTD_BYPASS_SPAWN")) {
      BSON_APPEND_BOOL(extra, "mongocryptdBypassSpawn", true);
   }
}

static void
_set_extra_crypt_shared(bson_t *extra)
{
   char *const path = test_framework_getenv("MONGOC_TEST_CRYPT_SHARED_LIB_PATH");
   if (path) {
      BSON_APPEND_UTF8(extra, "cryptSharedLibPath", path);
      bson_free(path);
   }
}

/* Convenience helper to check if spawning mongocryptd should be bypassed */
static void
_check_bypass(mongoc_auto_encryption_opts_t *opts)
{
   bson_t extra = BSON_INITIALIZER;
   _set_extra_bypass(&extra);
   mongoc_auto_encryption_opts_set_extra(opts, &extra);
   bson_destroy(&extra);
}

static bson_t *
_make_aws_kms_provider(bson_t *kms_providers)
{
   char *aws_secret_access_key;
   char *aws_access_key_id;

   aws_secret_access_key = test_framework_getenv_required("MONGOC_TEST_AWS_SECRET_ACCESS_KEY");
   aws_access_key_id = test_framework_getenv_required("MONGOC_TEST_AWS_ACCESS_KEY_ID");

   if (!kms_providers) {
      kms_providers = bson_new();
   }

   bson_concat(
      kms_providers,
      tmp_bson("{ 'aws': { 'secretAccessKey': '%s', 'accessKeyId': '%s' }}", aws_secret_access_key, aws_access_key_id));

   bson_free(aws_secret_access_key);
   bson_free(aws_access_key_id);

   return kms_providers;
}

static bson_t *
_make_azure_kms_provider(bson_t *kms_providers)
{
   char *azure_tenant_id;
   char *azure_client_id;
   char *azure_client_secret;

   azure_tenant_id = test_framework_getenv_required("MONGOC_TEST_AZURE_TENANT_ID");
   azure_client_id = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_ID");
   azure_client_secret = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_SECRET");

   if (!kms_providers) {
      kms_providers = bson_new();
   }

   bson_concat(kms_providers,
               tmp_bson("{ 'azure': { 'tenantId': '%s', 'clientId': '%s', "
                        "'clientSecret': '%s' }}",
                        azure_tenant_id,
                        azure_client_id,
                        azure_client_secret));

   bson_free(azure_tenant_id);
   bson_free(azure_client_id);
   bson_free(azure_client_secret);

   return kms_providers;
}

static bson_t *
_make_gcp_kms_provider(bson_t *kms_providers)
{
   char *gcp_email;
   char *gcp_privatekey;


   gcp_email = test_framework_getenv_required("MONGOC_TEST_GCP_EMAIL");
   gcp_privatekey = test_framework_getenv_required("MONGOC_TEST_GCP_PRIVATEKEY");

   if (!gcp_email || !gcp_privatekey) {
      test_error("Set MONGOC_TEST_GCP_EMAIL and MONGOC_TEST_GCP_PRIVATEKEY to "
                 "enable CSFLE tests.");
   }

   if (!kms_providers) {
      kms_providers = bson_new();
   }

   bson_concat(kms_providers, tmp_bson("{ 'gcp': { 'email': '%s', 'privateKey': '%s' }}", gcp_email, gcp_privatekey));

   bson_free(gcp_email);
   bson_free(gcp_privatekey);

   return kms_providers;
}

static bson_t *
_make_local_kms_provider(bson_t *kms_providers)
{
   bson_t *local = BCON_NEW("local", "{", "key", BCON_BIN(0, (uint8_t *)LOCAL_MASTERKEY, 96), "}");

   if (!kms_providers) {
      kms_providers = bson_new();
   }

   bson_concat(kms_providers, local);
   bson_destroy(local);

   return kms_providers;
}

static bson_t *
_make_kmip_kms_provider(bson_t *kms_providers)
{
   if (!kms_providers) {
      kms_providers = bson_new();
   }

   bson_concat(kms_providers, tmp_bson("{ 'kmip': { 'endpoint': 'localhost:5698' } }"));


   return kms_providers;
}

/* Convenience helper for creating KMS providers doc */
static bson_t *
_make_kms_providers(bool with_aws, bool with_local)
{
   bson_t *kms_providers = bson_new();

   if (with_aws) {
      _make_aws_kms_provider(kms_providers);
      _make_azure_kms_provider(kms_providers);
      _make_gcp_kms_provider(kms_providers);
      _make_kmip_kms_provider(kms_providers);
   }

   if (with_local) {
      _make_local_kms_provider(kms_providers);
   }

   return kms_providers;
}

static bson_t *
_make_tls_opts(void)
{
   bson_t *tls_opts = bson_new();
   char *kmip_tls_ca_file;
   char *kmip_tls_certificate_key_file;

   kmip_tls_ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
   kmip_tls_certificate_key_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");

   bson_concat(tls_opts,
               tmp_bson("{ 'kmip': {  'tlsCAFile': '%s', 'tlsCertificateKeyFile': '%s' } }",
                        kmip_tls_ca_file,
                        kmip_tls_certificate_key_file));

   bson_free(kmip_tls_ca_file);
   bson_free(kmip_tls_certificate_key_file);
   return tls_opts;
}

static bson_t *
_make_kms_masterkey(char const *provider)
{
   if (strcmp(provider, "aws") == 0) {
      return BCON_NEW("region",
                      "us-east-1",
                      "key",
                      "arn:aws:kms:us-east-1:579766882180:key/"
                      "89fcc2c4-08b0-4bd9-9f25-e30687b580d0");
   }

   if (strcmp(provider, "azure") == 0) {
      return BCON_NEW("keyVaultEndpoint", "key-vault-csfle.vault.azure.net", "keyName", "key-name-csfle");
   }

   if (strcmp(provider, "gcp") == 0) {
      return BCON_NEW("projectId",
                      "devprod-drivers",
                      "location",
                      "global",
                      "keyRing",
                      "key-ring-csfle",
                      "keyName",
                      "key-name-csfle");
   }

   if (strcmp(provider, "kmip") == 0) {
      return bson_new();
   }

   if (strcmp(provider, "local") == 0) {
      return bson_new();
   }

   return NULL;
}

typedef struct {
   int num_inserts;
   int num_bulk_writes;
} limits_apm_ctx_t;

static void
_command_started(const mongoc_apm_command_started_t *event)
{
   limits_apm_ctx_t *ctx;

   ctx = (limits_apm_ctx_t *)mongoc_apm_command_started_get_context(event);
   const char *cmd_name = mongoc_apm_command_started_get_command_name(event);
   if (0 == strcmp("insert", cmd_name)) {
      ctx->num_inserts++;
   }
   if (0 == strcmp("bulkWrite", cmd_name)) {
      ctx->num_bulk_writes++;
   }
}

/* Prose Test 4: BSON Size Limits and Batch Splitting */
static void
test_bson_size_limits_and_batch_splitting(bool with_qe)
{
   /* Expect an insert of two documents over 2MiB to split into two inserts but
    * still succeed. */
   mongoc_client_t *client;
   mongoc_auto_encryption_opts_t *opts;
   mongoc_uri_t *uri;
   mongoc_collection_t *coll;
   bson_error_t error;
   bson_t *corpus_schema;
   bson_t *datakey;
   bson_t *cmd;
   bson_t *kms_providers;
   bson_t *docs[2];
   char *as;
   limits_apm_ctx_t ctx = {0};
   mongoc_apm_callbacks_t *callbacks;
   /* Values from the spec to test boundaries. */
   const int size_16mib = 16777216;
   const int size_2mib = 2097152;
   const int exceeds_2mib_after_encryption = size_2mib - 2000;
   const int exceeds_16mib_after_encryption = size_16mib - 2000;

   /* Do the test setup. */

   /* Drop and create db.coll configured with limits-schema.json */
   uri = test_framework_get_uri();
   client = test_framework_client_new_from_uri(uri, NULL);
   test_framework_set_ssl_opts(client);
   mongoc_client_set_error_api(client, MONGOC_ERROR_API_VERSION_2);
   coll = mongoc_client_get_collection(client, "db", "coll");
   (void)mongoc_collection_drop(coll, NULL);
   corpus_schema = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-schema.json");
   cmd = BCON_NEW("create", "coll", "validator", "{", "$jsonSchema", BCON_DOCUMENT(corpus_schema), "}");
   ASSERT_OR_PRINT(mongoc_client_command_simple(client, "db", cmd, NULL /* read prefs */, NULL /* reply */, &error),
                   error);

   mongoc_collection_destroy(coll);
   /* Drop and create the key vault collection, keyvault.datakeys. */
   {
      mongoc_write_concern_t *wc;

      coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      (void)mongoc_collection_drop(coll, NULL);
      datakey = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-key.json");
      wc = mongoc_write_concern_new();
      mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
      mongoc_collection_set_write_concern(coll, wc);
      ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, datakey, NULL /* opts */, NULL /* reply */, &error), error);
      mongoc_write_concern_destroy(wc);
      mongoc_collection_destroy(coll);
   }

   mongoc_client_destroy(client);

   client = test_framework_client_new_from_uri(uri, NULL);
   test_framework_set_ssl_opts(client);
   mongoc_client_set_error_api(client, MONGOC_ERROR_API_VERSION_2);

   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   opts = mongoc_auto_encryption_opts_new();
   _check_bypass(opts);
   mongoc_auto_encryption_opts_set_keyvault_namespace(opts, "keyvault", "datakeys");
   mongoc_auto_encryption_opts_set_kms_providers(opts, kms_providers);

   ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(client, opts, &error), error);

   callbacks = mongoc_apm_callbacks_new();
   mongoc_apm_set_command_started_cb(callbacks, _command_started);
   mongoc_client_set_apm_callbacks(client, callbacks, &ctx);

   coll = mongoc_client_get_collection(client, "db", "coll");
   /* End of setup */

   /* Insert { "_id": "over_2mib_under_16mib", "unencrypted": <the string "a"
    * repeated 2097152 times> } */
   docs[0] = BCON_NEW("_id", "over_2mib_under_16mib");
   as = bson_malloc(size_16mib);
   memset(as, 'a', size_16mib);
   bson_append_utf8(docs[0], "unencrypted", -1, as, 2097152);
   ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, docs[0], NULL /* opts */, NULL /* reply */, &error), error);
   bson_destroy(docs[0]);

   /* Insert the document `limits/limits-doc.json <../limits/limits-doc.json>`_
    * concatenated with ``{ "_id": "encryption_exceeds_2mib", "unencrypted": <
    * the string "a" repeated (2097152 - 2000) times > }`` */
   docs[0] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-doc.json");
   bson_append_utf8(docs[0], "_id", -1, "encryption_exceeds_2mib", -1);
   bson_append_utf8(docs[0], "unencrypted", -1, as, exceeds_2mib_after_encryption);
   ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, docs[0], NULL /* opts */, NULL /* reply */, &error), error);
   bson_destroy(docs[0]);

   /* Insert two documents that each exceed 2MiB but no encryption occurs.
    * Expect the bulk write to succeed and run as two separate inserts.
    */
   docs[0] = BCON_NEW("_id", "over_2mib_1");
   bson_append_utf8(docs[0], "unencrypted", -1, as, size_2mib);
   docs[1] = BCON_NEW("_id", "over_2mib_2");
   bson_append_utf8(docs[1], "unencrypted", -1, as, size_2mib);
   ctx.num_inserts = 0;
   ASSERT_OR_PRINT(
      mongoc_collection_insert_many(coll, (const bson_t **)docs, 2, NULL /* opts */, NULL /* reply */, &error), error);
   ASSERT_CMPINT(ctx.num_inserts, ==, 2);
   bson_destroy(docs[0]);
   bson_destroy(docs[1]);

   /* Insert two documents that each exceed 2MiB after encryption occurs. Expect
    * the bulk write to succeed and run as two separate inserts.
    */

   docs[0] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-doc.json");
   bson_append_utf8(docs[0], "_id", -1, "encryption_exceeds_2mib_1", -1);
   bson_append_utf8(docs[0], "unencrypted", -1, as, exceeds_2mib_after_encryption);
   docs[1] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-doc.json");
   bson_append_utf8(docs[1], "_id", -1, "encryption_exceeds_2mib_2", -1);
   bson_append_utf8(docs[1], "unencrypted", -1, as, exceeds_2mib_after_encryption);
   ctx.num_inserts = 0;
   ASSERT_OR_PRINT(
      mongoc_collection_insert_many(coll, (const bson_t **)docs, 2, NULL /* opts */, NULL /* reply */, &error), error);
   ASSERT_CMPINT(ctx.num_inserts, ==, 2);
   bson_destroy(docs[0]);
   bson_destroy(docs[1]);

   /* Check that inserting close to, but not exceeding, 16MiB, passes */
   docs[0] = bson_new();
   bson_append_utf8(docs[0], "_id", -1, "under_16mib", -1);
   bson_append_utf8(docs[0], "unencrypted", -1, as, exceeds_16mib_after_encryption);
   ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, docs[0], NULL /* opts */, NULL /* reply */, &error), error);
   bson_destroy(docs[0]);

   /* but.. exceeding 16 MiB fails */
   docs[0] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-doc.json");
   bson_append_utf8(docs[0], "_id", -1, "under_16mib", -1);
   bson_append_utf8(docs[0], "unencrypted", -1, as, exceeds_16mib_after_encryption);
   BSON_ASSERT(!mongoc_collection_insert_one(coll, docs[0], NULL /* opts */, NULL /* reply */, &error));
   {
      const uint32_t too_large = 10334;
      // SERVER-104405 changed the expected error code from 2 to 10334:
      const uint32_t too_large_old = 2;
      ASSERT_CMPUINT32(error.domain, ==, (uint32_t)MONGOC_ERROR_SERVER);
      if (error.code != too_large && error.code != too_large_old) {
         test_error(
            "Unexpected error: %" PRIu32 ". Expected %" PRIu32 " or %" PRIu32, error.code, too_large, too_large_old);
      }
      ASSERT_CONTAINS(error.message, "too large");
   }
   bson_destroy(docs[0]);

   if (with_qe) {
      mongoc_bulkwriteopts_t *bw_opts = mongoc_bulkwriteopts_new();
      mongoc_bulkwriteopts_set_verboseresults(bw_opts, true);

      bson_t *corpus_encryptedFields =
         get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-encryptedFields.json");
      bson_t *coll_opts = BCON_NEW("encryptedFields", BCON_DOCUMENT(corpus_encryptedFields));
      mongoc_database_t *db = mongoc_client_get_database(client, "db");
      (void)mongoc_collection_drop(coll, NULL);
      // Create a newly named collection to avoid cached previous JSON Schema.
      mongoc_collection_t *coll2 = mongoc_database_create_collection(db, "coll2", coll_opts, &error);
      ASSERT_OR_PRINT(coll2, error);
      mongoc_collection_destroy(coll2);

      /* Insert two documents that each exceed 2MiB but no encryption occurs.
       * Expect two separate bulkWrite commands.
       */
      docs[0] = BCON_NEW("_id", "over_2mib_3");
      bson_append_utf8(docs[0], "unencrypted", -1, as, size_2mib - 1500);
      docs[1] = BCON_NEW("_id", "over_2mib_4");
      bson_append_utf8(docs[1], "unencrypted", -1, as, size_2mib - 1500);

      mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new(client);
      ASSERT_OR_PRINT(mongoc_bulkwrite_append_insertone(bw, "db.coll2", docs[0], NULL, &error), error);
      ASSERT_OR_PRINT(mongoc_bulkwrite_append_insertone(bw, "db.coll2", docs[1], NULL, &error), error);

      ctx.num_bulk_writes = 0;
      mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute(bw, bw_opts);
      ASSERT_NO_BULKWRITEEXCEPTION(bwr);
      ASSERT_CMPINT(ctx.num_bulk_writes, ==, 2);
      bson_destroy(docs[0]);
      bson_destroy(docs[1]);
      mongoc_bulkwrite_destroy(bw);
      mongoc_bulkwriteresult_destroy(bwr.res);
      mongoc_bulkwriteexception_destroy(bwr.exc);

      /* Insert two documents that each exceed 2MiB after encryption occurs. Expect
       * the bulk write to succeed and run as two separate bulkWrite commands.
       */
      docs[0] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-qe-doc.json");
      bson_append_utf8(docs[0], "_id", -1, "encryption_exceeds_2mib_3", -1);
      bson_append_utf8(docs[0], "foo", -1, as, exceeds_2mib_after_encryption - 1500);
      docs[1] = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-qe-doc.json");
      bson_append_utf8(docs[1], "_id", -1, "encryption_exceeds_2mib_4", -1);
      bson_append_utf8(docs[1], "foo", -1, as, exceeds_2mib_after_encryption - 1500);

      bw = mongoc_client_bulkwrite_new(client);
      ASSERT_OR_PRINT(mongoc_bulkwrite_append_insertone(bw, "db.coll2", docs[0], NULL, &error), error);
      ASSERT_OR_PRINT(mongoc_bulkwrite_append_insertone(bw, "db.coll2", docs[1], NULL, &error), error);

      ctx.num_bulk_writes = 0;
      bwr = mongoc_bulkwrite_execute(bw, bw_opts);
      ASSERT_NO_BULKWRITEEXCEPTION(bwr);
      ASSERT_CMPINT(ctx.num_bulk_writes, ==, 2);
      bson_destroy(docs[0]);
      bson_destroy(docs[1]);
      mongoc_bulkwrite_destroy(bw);
      mongoc_bulkwriteresult_destroy(bwr.res);
      mongoc_bulkwriteexception_destroy(bwr.exc);
      mongoc_bulkwriteopts_destroy(bw_opts);
      bson_destroy(corpus_encryptedFields);
      bson_destroy(coll_opts);
      mongoc_database_destroy(db);
   }

   bson_free(as);
   bson_destroy(kms_providers);
   bson_destroy(corpus_schema);
   bson_destroy(cmd);
   bson_destroy(datakey);
   mongoc_collection_destroy(coll);
   mongoc_client_destroy(client);
   mongoc_uri_destroy(uri);
   mongoc_apm_callbacks_destroy(callbacks);
   mongoc_auto_encryption_opts_destroy(opts);
}

static void
test_bson_size_limits_and_batch_splitting_no_qe(void *unused)
{
   BSON_UNUSED(unused);
   test_bson_size_limits_and_batch_splitting(false);
}

static void
test_bson_size_limits_and_batch_splitting_qe(void *unused)
{
   BSON_UNUSED(unused);
   test_bson_size_limits_and_batch_splitting(true);
}

typedef struct {
   bson_t *last_cmd;
} _datakey_and_double_encryption_ctx_t;

static void
_datakey_and_double_encryption_command_started(const mongoc_apm_command_started_t *event)
{
   _datakey_and_double_encryption_ctx_t *ctx;

   ctx = (_datakey_and_double_encryption_ctx_t *)mongoc_apm_command_started_get_context(event);
   bson_destroy(ctx->last_cmd);
   ctx->last_cmd = bson_copy(mongoc_apm_command_started_get_command(event));
}

static void
test_datakey_and_double_encryption_creating_and_using(mongoc_client_encryption_t *client_encryption,
                                                      mongoc_client_t *client,
                                                      mongoc_client_t *client_encrypted,
                                                      const char *kms_provider,
                                                      _datakey_and_double_encryption_ctx_t *test_ctx)
{
   bson_value_t keyid;
   bson_error_t error;
   bool ret;
   mongoc_client_encryption_datakey_opts_t *opts;
   mongoc_client_encryption_encrypt_opts_t *encrypt_opts;
   char *altname;
   mongoc_collection_t *coll;
   mongoc_cursor_t *cursor;
   bson_t filter;
   const bson_t *doc;
   bson_value_t to_encrypt;
   bson_value_t encrypted;
   bson_value_t encrypted_via_altname;
   bson_t to_insert = BSON_INITIALIZER;
   char *hello;

   opts = mongoc_client_encryption_datakey_opts_new();

   if (0 == strcmp(kms_provider, "aws")) {
      mongoc_client_encryption_datakey_opts_set_masterkey(opts,
                                                          tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                   "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                   "89fcc2c4-08b0-4bd9-9f25-e30687b580d0' }"));
   } else if (0 == strcmp(kms_provider, "azure")) {
      mongoc_client_encryption_datakey_opts_set_masterkey(
         opts,
         tmp_bson("{'keyVaultEndpoint': 'key-vault-csfle.vault.azure.net', "
                  "'keyName': 'key-name-csfle'}"));
   } else if (0 == strcmp(kms_provider, "gcp")) {
      mongoc_client_encryption_datakey_opts_set_masterkey(opts,
                                                          tmp_bson("{'projectId': 'devprod-drivers','location': "
                                                                   "'global','keyRing': 'key-ring-csfle','keyName': "
                                                                   "'key-name-csfle'}"));
   } else if (0 == strcmp(kms_provider, "kmip")) {
      mongoc_client_encryption_datakey_opts_set_masterkey(opts, tmp_bson("{}"));
   }

   altname = bson_strdup_printf("%s_altname", kms_provider);
   mongoc_client_encryption_datakey_opts_set_keyaltnames(opts, &altname, 1);

   ret = mongoc_client_encryption_create_datakey(client_encryption, kms_provider, opts, &keyid, &error);
   ASSERT_OR_PRINT(ret, error);

   /* Expect a BSON binary with subtype 4 to be returned */
   BSON_ASSERT(keyid.value_type == BSON_TYPE_BINARY);
   BSON_ASSERT(keyid.value.v_binary.subtype == BSON_SUBTYPE_UUID);

   /* Check that client captured a command_started event for the insert command
    * containing a majority writeConcern. */
   assert_match_bson(
      test_ctx->last_cmd, tmp_bson("{'insert': 'datakeys', 'writeConcern': { 'w': 'majority' } }"), false);

   /* Use client to run a find on keyvault.datakeys */
   coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
   bson_init(&filter);
   BSON_APPEND_VALUE(&filter, "_id", &keyid);
   cursor = mongoc_collection_find_with_opts(coll, &filter, NULL /* opts */, NULL /* read prefs */);
   mongoc_collection_destroy(coll);

   /* Expect that exactly one document is returned with the "masterKey.provider"
    * equal to <kms_provider> */
   BSON_ASSERT(mongoc_cursor_next(cursor, &doc));
   BSON_ASSERT(0 == strcmp(kms_provider, bson_lookup_utf8(doc, "masterKey.provider")));
   BSON_ASSERT(!mongoc_cursor_next(cursor, &doc));
   ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
   mongoc_cursor_destroy(cursor);

   /* Call client_encryption.encrypt() with the value "hello <kms provider>" */
   encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                       MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
   mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, &keyid);

   hello = bson_strdup_printf("hello %s", kms_provider);

   {
      to_encrypt.value_type = BSON_TYPE_UTF8;
      to_encrypt.value.v_utf8.str = bson_strdup(hello);

      const size_t len = strlen(to_encrypt.value.v_utf8.str);
      ASSERT(mlib_in_range(uint32_t, len));
      to_encrypt.value.v_utf8.len = (uint32_t)len;
   }

   ret = mongoc_client_encryption_encrypt(client_encryption, &to_encrypt, encrypt_opts, &encrypted, &error);
   ASSERT_OR_PRINT(ret, error);
   mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);

   /* Expect the return value to be a BSON binary subtype 6 */
   BSON_ASSERT(encrypted.value_type == BSON_TYPE_BINARY);
   BSON_ASSERT(encrypted.value.v_binary.subtype == BSON_SUBTYPE_ENCRYPTED);

   /* Use client_encrypted to insert { _id: "<kms provider>", "value":
    * <encrypted> } into db.coll */
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   BSON_APPEND_UTF8(&to_insert, "_id", kms_provider);
   BSON_APPEND_VALUE(&to_insert, "value", &encrypted);
   ret = mongoc_collection_insert_one(coll, &to_insert, NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(ret, error);

   /* Use client_encrypted to run a find querying with _id of <kms_provider> and
    * expect value to be "hello <kms_provider>". */
   cursor = mongoc_collection_find_with_opts(
      coll, tmp_bson("{ '_id': '%s' }", kms_provider), NULL /* opts */, NULL /* read prefs */);
   BSON_ASSERT(mongoc_cursor_next(cursor, &doc));
   BSON_ASSERT(0 == strcmp(hello, bson_lookup_utf8(doc, "value")));
   BSON_ASSERT(!mongoc_cursor_next(cursor, &doc));
   ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
   mongoc_cursor_destroy(cursor);
   mongoc_collection_destroy(coll);

   /* Call client_encryption.encrypt() with the value "hello <kms provider>",
    * the algorithm AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic, and the
    * key_alt_name of <kms provider>_altname. */
   encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                       MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
   mongoc_client_encryption_encrypt_opts_set_keyaltname(encrypt_opts, altname);

   ret = mongoc_client_encryption_encrypt(client_encryption, &to_encrypt, encrypt_opts, &encrypted_via_altname, &error);
   ASSERT_OR_PRINT(ret, error);
   mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);

   /* Expect the return value to be a BSON binary subtype 6. Expect the value to
    * exactly match the value of encrypted. */
   BSON_ASSERT(encrypted_via_altname.value_type == BSON_TYPE_BINARY);
   BSON_ASSERT(encrypted_via_altname.value.v_binary.subtype == BSON_SUBTYPE_ENCRYPTED);
   BSON_ASSERT(encrypted_via_altname.value.v_binary.data_len == encrypted.value.v_binary.data_len);
   BSON_ASSERT(0 == memcmp(encrypted_via_altname.value.v_binary.data,
                           encrypted.value.v_binary.data,
                           encrypted.value.v_binary.data_len));

   bson_value_destroy(&encrypted);
   bson_value_destroy(&encrypted_via_altname);
   bson_free(hello);
   bson_destroy(&to_insert);
   bson_value_destroy(&to_encrypt);
   bson_value_destroy(&keyid);
   bson_free(altname);
   bson_destroy(&filter);
   mongoc_client_encryption_datakey_opts_destroy(opts);
}

/* Prose Test 1: Custom Key Material Test */
static void
test_create_datakey_with_custom_key_material(void *unused)
{
   mongoc_client_t *client = NULL;
   mongoc_client_encryption_t *client_encryption = NULL;
   bson_error_t error;
   bson_t datakey = BSON_INITIALIZER;

   BSON_UNUSED(unused);

   /* Create a MongoClient object (referred to as client). */
   client = test_framework_new_default_client();

   /* Using client, drop the collection keyvault.datakeys. */
   {
      mongoc_collection_t *const datakeys = mongoc_client_get_collection(client, "keyvault", "datakeys");
      (void)mongoc_collection_drop(datakeys, NULL);
      mongoc_collection_destroy(datakeys);
   }

   /* Create a ClientEncryption object (referred to as client_encryption) with
    * client set as the keyVaultClient. */
   {
      mongoc_client_encryption_opts_t *const client_encryption_opts = mongoc_client_encryption_opts_new();
      bson_t *const kms_providers = _make_kms_providers(true /* aws */, true /* local */);
      bson_t *const tls_opts = _make_tls_opts();

      mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
      mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);
      mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
      client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
      ASSERT_OR_PRINT(client_encryption, error);

      mongoc_client_encryption_opts_destroy(client_encryption_opts);
      bson_destroy(kms_providers);
      bson_destroy(tls_opts);
   }

   /* Using client_encryption, create a data key with a local KMS provider and
    * the following custom key material: */
   {
      const char key_material[] = "xPTAjBRG5JiPm+d3fj6XLi2q5DMXUS/"
                                  "f1f+SMAlhhwkhDRL0kr8r9GDLIGTAGlvC+HVjSIgdL+"
                                  "RKwZCvpXSyxTICWSXTUYsWYPyu3IoHbuBZdmw2faM3WhcRIgbMReU5";
      uint8_t data[96];
      mongoc_client_encryption_datakey_opts_t *datakey_opts = mongoc_client_encryption_datakey_opts_new();
      bson_value_t keyid;

      BSON_ASSERT(mcommon_b64_pton(key_material, data, sizeof(key_material)) == 96);

      mongoc_client_encryption_datakey_opts_set_keymaterial(datakey_opts, data, sizeof(data));

      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(client_encryption, "local", datakey_opts, &keyid, &error),
                      error);

      ASSERT(keyid.value_type == BSON_TYPE_BINARY);
      ASSERT(keyid.value.v_binary.subtype == BSON_SUBTYPE_UUID);
      ASSERT(keyid.value.v_binary.data_len != 0);

      mongoc_client_encryption_datakey_opts_destroy(datakey_opts);
      bson_value_destroy(&keyid);
   }

   /* Find the resulting key document in keyvault.datakeys, save a copy of the
    * key document, then remove the key document from the collection. */
   {
      mongoc_collection_t *const datakeys = mongoc_client_get_collection(client, "keyvault", "datakeys");
      mongoc_cursor_t *cursor =
         mongoc_collection_find_with_opts(datakeys, tmp_bson("{}"), NULL /* opts */, NULL /* read prefs */);
      const bson_t *bson;

      ASSERT(mongoc_cursor_next(cursor, &bson));
      bson_copy_to(bson, &datakey);
      mongoc_cursor_destroy(cursor);

      (void)mongoc_collection_drop(datakeys, &error);
      mongoc_collection_destroy(datakeys);
   }

   /* Replace the _id field in the copied key document with a UUID with base64
    * value AAAAAAAAAAAAAAAAAAAAAA== (16 bytes all equal to 0x00) and insert the
    * modified key document into keyvault.datakeys with majority write concern.
    */
   {
      mongoc_collection_t *const datakeys = mongoc_client_get_collection(client, "keyvault", "datakeys");
      bson_t modified_datakey = BSON_INITIALIZER;
      uint8_t bytes[16] = {0};
      mongoc_write_concern_t *const wc = mongoc_write_concern_new();
      bson_t opts = BSON_INITIALIZER;

      bson_copy_to_excluding_noinit(&datakey, &modified_datakey, "_id", NULL);
      BSON_ASSERT(BSON_APPEND_BINARY(&modified_datakey, "_id", BSON_SUBTYPE_UUID, bytes, sizeof(bytes)));

      mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
      mongoc_write_concern_append(wc, &opts);

      ASSERT_OR_PRINT(mongoc_collection_insert_one(datakeys, &modified_datakey, &opts, NULL, &error), error);

      mongoc_collection_destroy(datakeys);
      bson_destroy(&modified_datakey);
      mongoc_write_concern_destroy(wc);
      bson_destroy(&opts);
   }

   /* Using client_encryption, encrypt the string "test" with the modified data
    * key using the AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic algorithm and
    * assert the resulting value is equal to the following (given as base64): */
   {
      const char expected[] = "AQAAAAAAAAAAAAAAAAAAAAACz0ZOLuuhEYi807ZXTdhbqhLaS2/"
                              "t9wLifJnnNYwiw79d75QYIZ6M/aYC1h9nCzCjZ7pGUpAuNnkUhnIXM3PjrA==";
      mongoc_client_encryption_encrypt_opts_t *const encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
      bson_value_t keyid = {0};
      bson_value_t to_encrypt = {0};
      bson_value_t ciphertext = {0};

      keyid.value_type = BSON_TYPE_BINARY;
      keyid.value.v_binary.subtype = BSON_SUBTYPE_UUID;
      keyid.value.v_binary.data = bson_malloc0(16);
      keyid.value.v_binary.data_len = 16u;

      to_encrypt.value_type = BSON_TYPE_UTF8;
      to_encrypt.value.v_utf8.str = bson_strdup("test");
      to_encrypt.value.v_utf8.len = 4u;

      mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, &keyid);
      mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                          MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
      ASSERT_OR_PRINT(
         mongoc_client_encryption_encrypt(client_encryption, &to_encrypt, encrypt_opts, &ciphertext, &error), error);

      {
         char actual[256];

         /* Need room for null terminator. */
         ASSERT(mcommon_b64_ntop(
                   ciphertext.value.v_binary.data, ciphertext.value.v_binary.data_len, actual, sizeof(actual)) < 255);

         ASSERT_CMPSTR(expected, actual);
      }

      bson_value_destroy(&keyid);
      bson_value_destroy(&ciphertext);
      bson_value_destroy(&to_encrypt);
      mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);
   }

   mongoc_client_destroy(client);
   mongoc_client_encryption_destroy(client_encryption);
   bson_destroy(&datakey);
}

/* Prose Test 2: Data Key and Double Encryption */
static void
test_datakey_and_double_encryption(void *unused)
{
   mongoc_client_t *client;
   mongoc_client_t *client_encrypted;
   mongoc_client_encryption_t *client_encryption;
   mongoc_apm_callbacks_t *callbacks;
   mongoc_collection_t *coll;
   bson_error_t error;
   bson_t *kms_providers;
   bson_t *tls_opts;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   bson_t *schema_map;
   bool ret;
   _datakey_and_double_encryption_ctx_t test_ctx = {0};

   BSON_UNUSED(unused);

   /* Test setup */
   /* Create a MongoClient without encryption enabled (referred to as client).
    * Enable command monitoring to listen for command_started events. */
   client = test_framework_new_default_client();
   callbacks = mongoc_apm_callbacks_new();
   mongoc_apm_set_command_started_cb(callbacks, _datakey_and_double_encryption_command_started);
   mongoc_client_set_apm_callbacks(client, callbacks, &test_ctx);

   /* Using client, drop the collections keyvault.datakeys and db.coll. */
   coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
   (void)mongoc_collection_drop(coll, NULL);
   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client, "db", "coll");
   (void)mongoc_collection_drop(coll, NULL);
   mongoc_collection_destroy(coll);

   /* Create a MongoClient configured with auto encryption (referred to as
    * client_encrypted) */
   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   kms_providers = _make_kms_providers(true /* aws */, true /* local */);
   tls_opts = _make_tls_opts();
   _check_bypass(auto_encryption_opts);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_tls_opts(auto_encryption_opts, tls_opts);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   schema_map = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/"
                                        "datakey-and-double-encryption-schemamap.json");
   mongoc_auto_encryption_opts_set_schema_map(auto_encryption_opts, schema_map);

   client_encrypted = test_framework_new_default_client();
   ret = mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error);
   ASSERT_OR_PRINT(ret, error);

   /* Create a ClientEncryption object (referred to as client_encryption) */
   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
   client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   test_datakey_and_double_encryption_creating_and_using(
      client_encryption, client, client_encrypted, "local", &test_ctx);
   test_datakey_and_double_encryption_creating_and_using(client_encryption, client, client_encrypted, "aws", &test_ctx);
   test_datakey_and_double_encryption_creating_and_using(
      client_encryption, client, client_encrypted, "azure", &test_ctx);
   test_datakey_and_double_encryption_creating_and_using(client_encryption, client, client_encrypted, "gcp", &test_ctx);
   test_datakey_and_double_encryption_creating_and_using(
      client_encryption, client, client_encrypted, "kmip", &test_ctx);

   bson_destroy(kms_providers);
   bson_destroy(tls_opts);
   bson_destroy(schema_map);
   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   mongoc_apm_callbacks_destroy(callbacks);
   mongoc_client_destroy(client);
   mongoc_client_destroy(client_encrypted);
   mongoc_client_encryption_destroy(client_encryption);
   bson_destroy(test_ctx.last_cmd);
}

static void
_test_key_vault(bool with_external_key_vault)
{
   mongoc_client_t *client;
   mongoc_client_t *client_encrypted;
   mongoc_client_t *client_external;
   mongoc_client_encryption_t *client_encryption;
   mongoc_uri_t *external_uri;
   mongoc_collection_t *coll;
   bson_t *datakey;
   bson_t *kms_providers;
   bson_error_t error;
   mongoc_write_concern_t *wc;
   bson_t *schema;
   bson_t *schema_map;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   mongoc_client_encryption_encrypt_opts_t *encrypt_opts;
   bool res;
   const bson_value_t *keyid;
   bson_value_t value;
   bson_value_t ciphertext;
   bson_iter_t iter;

   external_uri = test_framework_get_uri();
   mongoc_uri_set_username(external_uri, "fake-user");
   mongoc_uri_set_password(external_uri, "fake-pwd");
   client_external = test_framework_client_new_from_uri(external_uri, NULL);
   test_framework_set_ssl_opts(client_external);

   /* Using client, drop the collections keyvault.datakeys and db.coll. */
   client = test_framework_new_default_client();
   coll = mongoc_client_get_collection(client, "db", "coll");
   (void)mongoc_collection_drop(coll, NULL);
   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
   (void)mongoc_collection_drop(coll, NULL);

   /* Insert the document external-key.json into ``keyvault.datakeys``. */
   wc = mongoc_write_concern_new();
   mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
   mongoc_collection_set_write_concern(coll, wc);
   datakey = get_bson_from_json_file("./src/libmongoc/tests/"
                                     "client_side_encryption_prose/external/"
                                     "external-key.json");
   ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, datakey, NULL, NULL, &error), error);
   mongoc_collection_destroy(coll);

   /* Create a MongoClient configured with auto encryption. */
   client_encrypted = test_framework_new_default_client();
   mongoc_client_set_error_api(client_encrypted, MONGOC_ERROR_API_VERSION_2);
   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   _check_bypass(auto_encryption_opts);
   schema = get_bson_from_json_file("./src/libmongoc/tests/"
                                    "client_side_encryption_prose/external/"
                                    "external-schema.json");
   schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   mongoc_auto_encryption_opts_set_schema_map(auto_encryption_opts, schema_map);
   if (with_external_key_vault) {
      mongoc_auto_encryption_opts_set_keyvault_client(auto_encryption_opts, client_external);
   }
   ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error), error);

   /* Create a ClientEncryption object. */
   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   if (with_external_key_vault) {
      mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client_external);
   } else {
      mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
   }
   client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   /* Use client_encrypted to insert the document {"encrypted": "test"} into
    * db.coll. */
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   res = mongoc_collection_insert_one(coll, tmp_bson("{'encrypted': 'test'}"), NULL, NULL, &error);
   if (with_external_key_vault) {
      BSON_ASSERT(!res);
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Authentication failed");
   } else {
      ASSERT_OR_PRINT(res, error);
   }

   /* Use client_encryption to explicitly encrypt the string "test" with key ID
    * ``LOCALAAAAAAAAAAAAAAAAA==`` and deterministic algorithm. */
   encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                       MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
   BSON_ASSERT(bson_iter_init_find(&iter, datakey, "_id"));
   keyid = bson_iter_value(&iter);
   mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, keyid);
   value.value_type = BSON_TYPE_UTF8;
   value.value.v_utf8.str = "test";
   value.value.v_utf8.len = 4;
   res = mongoc_client_encryption_encrypt(client_encryption, &value, encrypt_opts, &ciphertext, &error);
   if (with_external_key_vault) {
      BSON_ASSERT(!res);
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Authentication failed");
   } else {
      ASSERT_OR_PRINT(res, error);
   }

   bson_destroy(schema);
   bson_destroy(schema_map);
   bson_destroy(datakey);
   bson_value_destroy(&ciphertext);
   mongoc_write_concern_destroy(wc);
   bson_destroy(kms_providers);
   mongoc_collection_destroy(coll);
   mongoc_client_destroy(client);
   mongoc_client_destroy(client_encrypted);
   mongoc_client_destroy(client_external);
   mongoc_uri_destroy(external_uri);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);
}

/* Prose Test 3: External Key Vault Test */
static void
test_external_key_vault(void *unused)
{
   BSON_UNUSED(unused);

   _test_key_vault(false /* external */);
   _test_key_vault(true /* external */);
}

/* Prose Test 5: Views Are Prohibited */
static void
test_views_are_prohibited(void *unused)
{
   mongoc_client_t *client;
   mongoc_client_t *client_encrypted;
   mongoc_collection_t *coll;
   bool res;
   bson_error_t error;

   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   bson_t *kms_providers;

   BSON_UNUSED(unused);

   client = test_framework_new_default_client();

   /* Using client, drop and create a view named db.view with an empty pipeline.
    * E.g. using the command { "create": "view", "viewOn": "coll" }. */
   coll = mongoc_client_get_collection(client, "db", "view");
   (void)mongoc_collection_drop(coll, NULL);
   res =
      mongoc_client_command_simple(client, "db", tmp_bson("{'create': 'view', 'viewOn': 'coll'}"), NULL, NULL, &error);
   ASSERT_OR_PRINT(res, error);

   client_encrypted = test_framework_new_default_client();
   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   _check_bypass(auto_encryption_opts);
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error), error);

   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client_encrypted, "db", "view");
   res = mongoc_collection_insert_one(coll, tmp_bson("{'x': 1}"), NULL, NULL, &error);
   BSON_ASSERT(!res);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "cannot auto encrypt a view");

   bson_destroy(kms_providers);
   mongoc_collection_destroy(coll);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   mongoc_client_destroy(client_encrypted);
   mongoc_client_destroy(client);
}

static void
_endpoint_setup(mongoc_client_t *keyvault_client,
                mongoc_client_encryption_t **client_encryption,
                mongoc_client_encryption_t **client_encryption_invalid)
{
   bson_t *tls_opts;
   bson_t *kms_providers;
   bson_t *kms_providers_invalid;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   mongoc_client_encryption_opts_t *client_encryption_opts_invalid;
   bson_error_t error;

   ASSERT(keyvault_client);

   char *mongoc_test_aws_access_key_id = test_framework_getenv_required("MONGOC_TEST_AWS_ACCESS_KEY_ID");
   char *mongoc_test_aws_secret_access_key = test_framework_getenv_required("MONGOC_TEST_AWS_SECRET_ACCESS_KEY");
   char *mongoc_test_azure_tenant_id = test_framework_getenv_required("MONGOC_TEST_AZURE_TENANT_ID");
   char *mongoc_test_azure_client_id = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_ID");
   char *mongoc_test_azure_client_secret = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_SECRET");
   char *mongoc_test_gcp_email = test_framework_getenv_required("MONGOC_TEST_GCP_EMAIL");
   char *mongoc_test_gcp_privatekey = test_framework_getenv_required("MONGOC_TEST_GCP_PRIVATEKEY");
   char *ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
   char *certificate_key_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");


   kms_providers = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s'}}",
                            mongoc_test_aws_access_key_id,
                            mongoc_test_aws_secret_access_key);
   bson_concat(kms_providers,
               tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', 'clientSecret': '%s', "
                        "'identityPlatformEndpoint': 'login.microsoftonline.com:443'}}",
                        mongoc_test_azure_tenant_id,
                        mongoc_test_azure_client_id,
                        mongoc_test_azure_client_secret));
   bson_concat(kms_providers,
               tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                        "'endpoint': 'oauth2.googleapis.com:443'}}",
                        mongoc_test_gcp_email,
                        mongoc_test_gcp_privatekey));
   bson_concat(kms_providers, tmp_bson("{'kmip': { 'endpoint': 'localhost:5698' }}"));
   tls_opts =
      tmp_bson("{'kmip': {  'tlsCAFile': '%s', 'tlsCertificateKeyFile': '%s' }}", ca_file, certificate_key_file);

   kms_providers_invalid = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s'}}",
                                    mongoc_test_aws_access_key_id,
                                    mongoc_test_aws_secret_access_key);
   bson_concat(kms_providers_invalid,
               tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', 'clientSecret': '%s', "
                        "'identityPlatformEndpoint': 'doesnotexist.invalid:443'}}",
                        mongoc_test_azure_tenant_id,
                        mongoc_test_azure_client_id,
                        mongoc_test_azure_client_secret));
   bson_concat(kms_providers_invalid,
               tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                        "'endpoint': 'doesnotexist.invalid'}}",
                        mongoc_test_gcp_email,
                        mongoc_test_gcp_privatekey));
   bson_concat(kms_providers_invalid, tmp_bson("{'kmip': { 'endpoint': 'doesnotexist.invalid:5698' }}"));

   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, keyvault_client);
   *client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   client_encryption_opts_invalid = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts_invalid, kms_providers_invalid);
   mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts_invalid, tls_opts);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts_invalid, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts_invalid, keyvault_client);
   *client_encryption_invalid = mongoc_client_encryption_new(client_encryption_opts_invalid, &error);
   ASSERT_OR_PRINT(client_encryption_invalid, error);

   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   mongoc_client_encryption_opts_destroy(client_encryption_opts_invalid);
   bson_free(mongoc_test_aws_access_key_id);
   bson_free(mongoc_test_aws_secret_access_key);
   bson_free(mongoc_test_azure_tenant_id);
   bson_free(mongoc_test_azure_client_id);
   bson_free(mongoc_test_azure_client_secret);
   bson_free(mongoc_test_gcp_email);
   bson_free(mongoc_test_gcp_privatekey);
   bson_free(ca_file);
   bson_free(certificate_key_file);
}

/* Use the returned UUID of the key to explicitly encrypt and decrypt the
 * string "test" to validate it works. */
#define TEST_ENCRYPT_DECRYPT(keyid, client_encryption, res, error)                                             \
   do {                                                                                                        \
      bson_value_t ciphertext;                                                                                 \
      bson_value_t plaintext;                                                                                  \
      bson_value_t test;                                                                                       \
      mongoc_client_encryption_encrypt_opts_t *encrypt_opts;                                                   \
                                                                                                               \
      test.value_type = BSON_TYPE_UTF8;                                                                        \
      test.value.v_utf8.str = "test";                                                                          \
      test.value.v_utf8.len = 4;                                                                               \
                                                                                                               \
      encrypt_opts = mongoc_client_encryption_encrypt_opts_new();                                              \
      mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,                                        \
                                                          MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC); \
      mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, keyid);                                    \
      res = mongoc_client_encryption_encrypt(client_encryption, &test, encrypt_opts, &ciphertext, &error);     \
      ASSERT_OR_PRINT(res, error);                                                                             \
      res = mongoc_client_encryption_decrypt(client_encryption, &ciphertext, &plaintext, &error);              \
      ASSERT_OR_PRINT(res, error);                                                                             \
      if (plaintext.value_type != BSON_TYPE_UTF8) {                                                            \
         test_error("expected decrypted result to be value type UTF-8, got %s",                                \
                    _mongoc_bson_type_to_str(plaintext.value_type));                                           \
      }                                                                                                        \
      ASSERT_CMPSTR(plaintext.value.v_utf8.str, test.value.v_utf8.str);                                        \
      bson_value_destroy(&ciphertext);                                                                         \
      bson_value_destroy(&plaintext);                                                                          \
      mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);                                             \
   } while (0)

/* Prose Test 7: Custom Endpoint Test */
static void
test_custom_endpoint(void *unused)
{
   mongoc_client_t *keyvault_client;
   mongoc_client_encryption_t *client_encryption = NULL;
   mongoc_client_encryption_t *client_encryption_invalid = NULL;
   mongoc_client_encryption_datakey_opts_t *datakey_opts;
   bson_error_t error;
   bool res;
   bson_t *masterkey;
   bson_value_t keyid;

   BSON_UNUSED(unused);

   keyvault_client = test_framework_new_default_client();

   datakey_opts = mongoc_client_encryption_datakey_opts_new();

   /* Case 1: No endpoint, expect to succeed. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("region",
                        "us-east-1",
                        "key",
                        "arn:aws:kms:us-east-1:579766882180:key/"
                        "89fcc2c4-08b0-4bd9-9f25-e30687b580d0");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "aws", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 2: Custom endpoint, with the same as the default. Expect to succeed
    */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("region",
                        "us-east-1",
                        "key",
                        "arn:aws:kms:us-east-1:579766882180:key/"
                        "89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
                        "endpoint",
                        "kms.us-east-1.amazonaws.com");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "aws", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 3: Custom endpoint, with the same as the default but port included.
    * Expect to succeed */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("region",
                        "us-east-1",
                        "key",
                        "arn:aws:kms:us-east-1:579766882180:key/"
                        "89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
                        "endpoint",
                        "kms.us-east-1.amazonaws.com:443");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "aws", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 4: Custom endpoint, with the same as the default but wrong port
    * included.
    * Expect to fail with socket error */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("keyId", "1", "endpoint", "localhost:12345");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "kmip", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_CONNECT, "Failed to connect");
   BSON_ASSERT(!res);
   bson_value_destroy(&keyid);

   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 5: Custom endpoint, but wrong region. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("region",
                        "us-east-1",
                        "key",
                        "arn:aws:kms:us-east-1:579766882180:key/"
                        "89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
                        "endpoint",
                        "kms.us-east-2.amazonaws.com");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   memset(&error, 0, sizeof(bson_error_t));
   res = mongoc_client_encryption_create_datakey(client_encryption, "aws", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "");
   BSON_ASSERT(!res);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 6: Custom endpoint to doesnotexist.invalid. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("region",
                        "us-east-1",
                        "key",
                        "arn:aws:kms:us-east-1:579766882180:key/"
                        "89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
                        "endpoint",
                        "doesnotexist.invalid");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   memset(&error, 0, sizeof(bson_error_t));
   res = mongoc_client_encryption_create_datakey(client_encryption, "aws", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_NAME_RESOLUTION, "Failed to resolve");
   BSON_ASSERT(!res);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 7: Azure successful case */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("keyVaultEndpoint", "key-vault-csfle.vault.azure.net", "keyName", "key-name-csfle");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "azure", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Azure invalid case. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid, "azure", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_NAME_RESOLUTION, "Failed to resolve");
   BSON_ASSERT(!res);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 8: GCP successful case. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("projectId",
                        "devprod-drivers",
                        "location",
                        "global",
                        "keyRing",
                        "key-ring-csfle",
                        "keyName",
                        "key-name-csfle",
                        "endpoint",
                        "cloudkms.googleapis.com:443");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "gcp", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);
   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* GCP invalid case. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid, "gcp", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_NAME_RESOLUTION, "Failed to resolve");
   BSON_ASSERT(!res);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 9: GCP invalid key endpoint. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("projectId",
                        "devprod-drivers",
                        "location",
                        "global",
                        "keyRing",
                        "key-ring-csfle",
                        "keyName",
                        "key-name-csfle",
                        "endpoint",
                        "doesnotexist.invalid:443");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "gcp", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "Invalid KMS response");
   BSON_ASSERT(!res);

   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 10: KMIP no endpoint. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("keyId", "1");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "kmip", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);

   /* Attempt to use client_encryption_invalid with the same masterKey. Expect
    * an error. */
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid, "kmip", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_NAME_RESOLUTION, "Failed to resolve");
   BSON_ASSERT(!res);
   bson_value_destroy(&keyid);

   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 11: KMIP overriding with valid endpoint. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("keyId", "1", "endpoint", "localhost:5698");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "kmip", datakey_opts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   TEST_ENCRYPT_DECRYPT(&keyid, client_encryption, res, error);
   bson_value_destroy(&keyid);

   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   /* Case 12: KMIP overriding with invalid endpoint. */
   _endpoint_setup(keyvault_client, &client_encryption, &client_encryption_invalid);
   masterkey = BCON_NEW("keyId", "1", "endpoint", "doesnotexist.invalid:5698");
   mongoc_client_encryption_datakey_opts_set_masterkey(datakey_opts, masterkey);
   res = mongoc_client_encryption_create_datakey(client_encryption, "kmip", datakey_opts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_NAME_RESOLUTION, "Failed to resolve");
   BSON_ASSERT(!res);
   bson_value_destroy(&keyid);

   bson_destroy(masterkey);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_encryption_destroy(client_encryption_invalid);

   mongoc_client_encryption_datakey_opts_destroy(datakey_opts);
   mongoc_client_destroy(keyvault_client);
}

typedef struct {
   const char *kms;
   const char *type;
   const char *algo;
   const char *method;
   const char *identifier;
   bool allowed;
   bson_value_t value; /* a copy */
} corpus_field_t;

static corpus_field_t *
_corpus_field_new(bson_iter_t *top_iter)
{
   bson_iter_t iter;
   corpus_field_t *field;

   field = bson_malloc0(sizeof(corpus_field_t));
   memset(field, 0, sizeof(*field));
   BSON_ASSERT(BSON_ITER_HOLDS_DOCUMENT(top_iter));
   bson_iter_recurse(top_iter, &iter);
   while (bson_iter_next(&iter)) {
      if (0 == strcmp("kms", bson_iter_key(&iter))) {
         field->kms = bson_iter_utf8(&iter, NULL);
      } else if (0 == strcmp("type", bson_iter_key(&iter))) {
         field->type = bson_iter_utf8(&iter, NULL);
      } else if (0 == strcmp("algo", bson_iter_key(&iter))) {
         field->algo = bson_iter_utf8(&iter, NULL);
      } else if (0 == strcmp("method", bson_iter_key(&iter))) {
         field->method = bson_iter_utf8(&iter, NULL);
      } else if (0 == strcmp("identifier", bson_iter_key(&iter))) {
         field->identifier = bson_iter_utf8(&iter, NULL);
      } else if (0 == strcmp("allowed", bson_iter_key(&iter))) {
         field->allowed = bson_iter_bool(&iter);
      } else if (0 == strcmp("value", bson_iter_key(&iter))) {
         bson_value_copy(bson_iter_value(&iter), &field->value);
      } else {
         fprintf(stderr, "unexpected field: %s\n", bson_iter_key(&iter));
         BSON_ASSERT(false);
      }
   }
   return field;
}

static void
_corpus_field_destroy(corpus_field_t *field)
{
   if (!field) {
      return;
   }
   bson_value_destroy(&field->value);
   bson_free(field);
}

#define LOCAL_UUID "\x2c\xe0\x80\x2c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
#define AWS_UUID "\x01\x64\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
#define AZURE_UUID "\x01\x95\x11\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
#define GCP_UUID "\x18\x23\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
#define KMIP_UUID "\x28\xc2\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

static void
_corpus_copy_field(mongoc_client_encryption_t *client_encryption, bson_iter_t *iter, bson_t *corpus_copied)
{
   corpus_field_t *field;
   const char *key = bson_iter_key(iter);
   mongoc_client_encryption_encrypt_opts_t *encrypt_opts;
   bson_value_t ciphertext;
   bool res;
   bson_error_t error;

   if (0 == strcmp("_id", key) || 0 == strcmp("altname_aws", key) || 0 == strcmp("altname_local", key) ||
       0 == strcmp("altname_azure", key) || 0 == strcmp("altname_gcp", key) || 0 == strcmp("altname_kmip", key)) {
      bson_append_value(corpus_copied, key, -1, bson_iter_value(iter));
      return;
   }
   field = _corpus_field_new(iter);

   if (0 == strcmp("auto", field->method)) {
      bson_append_value(corpus_copied, key, -1, bson_iter_value(iter));
      _corpus_field_destroy(field);
      return;
   }

   /* Otherwise, use explicit encryption. */
   encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
   if (0 == strcmp("rand", field->algo)) {
      mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts, MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM);
   } else if (0 == strcmp("det", field->algo)) {
      mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                          MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
   }

   if (0 == strcmp("id", field->identifier)) {
      bson_value_t uuid;
      uuid.value_type = BSON_TYPE_BINARY;
      uuid.value.v_binary.subtype = BSON_SUBTYPE_UUID;
      uuid.value.v_binary.data_len = 16;
      if (0 == strcmp("local", field->kms)) {
         uuid.value.v_binary.data = (uint8_t *)LOCAL_UUID;
      } else if (0 == strcmp("aws", field->kms)) {
         uuid.value.v_binary.data = (uint8_t *)AWS_UUID;
      } else if (0 == strcmp("azure", field->kms)) {
         uuid.value.v_binary.data = (uint8_t *)AZURE_UUID;
      } else if (0 == strcmp("gcp", field->kms)) {
         uuid.value.v_binary.data = (uint8_t *)GCP_UUID;
      } else if (0 == strcmp("kmip", field->kms)) {
         uuid.value.v_binary.data = (uint8_t *)KMIP_UUID;
      }
      mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, &uuid);
   } else if (0 == strcmp("altname", field->identifier)) {
      mongoc_client_encryption_encrypt_opts_set_keyaltname(encrypt_opts, field->kms);
   }

   res = mongoc_client_encryption_encrypt(client_encryption, &field->value, encrypt_opts, &ciphertext, &error);

   if (field->allowed) {
      bson_t new_field;
      ASSERT_OR_PRINT(res, error);
      bson_append_document_begin(corpus_copied, key, -1, &new_field);
      BSON_APPEND_UTF8(&new_field, "kms", field->kms);
      BSON_APPEND_UTF8(&new_field, "type", field->type);
      BSON_APPEND_UTF8(&new_field, "algo", field->algo);
      BSON_APPEND_UTF8(&new_field, "method", field->method);
      BSON_APPEND_UTF8(&new_field, "identifier", field->identifier);
      BSON_APPEND_BOOL(&new_field, "allowed", field->allowed);
      BSON_APPEND_VALUE(&new_field, "value", &ciphertext);
      bson_append_document_end(corpus_copied, &new_field);
   } else {
      BSON_ASSERT(!res);
      bson_append_value(corpus_copied, key, -1, bson_iter_value(iter));
   }

   bson_value_destroy(&ciphertext);
   mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);
   _corpus_field_destroy(field);
}

static void
_corpus_check_encrypted(mongoc_client_encryption_t *client_encryption,
                        bson_iter_t *expected_iter,
                        bson_iter_t *actual_iter)
{
   corpus_field_t *expected;
   corpus_field_t *actual;
   const char *key;
   bson_error_t error;
   match_ctx_t match_ctx;

   memset(&match_ctx, 0, sizeof(match_ctx));
   key = bson_iter_key(expected_iter);
   if (0 == strcmp("_id", key) || 0 == strcmp("altname_aws", key) || 0 == strcmp("altname_local", key) ||
       0 == strcmp("altname_azure", key) || 0 == strcmp("altname_gcp", key) || 0 == strcmp("altname_kmip", key)) {
      return;
   }

   expected = _corpus_field_new(expected_iter);
   actual = _corpus_field_new(actual_iter);

   /* If the algo is det, that the value equals the value of the corresponding
    * field
    * in corpus_encrypted_actual.
    */
   if (0 == strcmp(expected->algo, "det")) {
      BSON_ASSERT(match_bson_value(&expected->value, &actual->value, &match_ctx));
   }

   /* If the algo is rand and allowed is true, that the value does not equal the
    * value of the corresponding field in corpus_encrypted_actual. */
   if (0 == strcmp(expected->algo, "rand") && expected->allowed) {
      BSON_ASSERT(!match_bson_value(&expected->value, &actual->value, &match_ctx));
   }

   /* If allowed is true, decrypt the value with client_encryption. Decrypt the
    * value of the corresponding field of corpus_encrypted and validate that
    * they are both equal */
   if (expected->allowed) {
      bson_value_t expected_decrypted;
      bson_value_t actual_decrypted;
      bool res;

      res = mongoc_client_encryption_decrypt(client_encryption, &expected->value, &expected_decrypted, &error);
      ASSERT_OR_PRINT(res, error);

      res = mongoc_client_encryption_decrypt(client_encryption, &actual->value, &actual_decrypted, &error);
      ASSERT_OR_PRINT(res, error);

      BSON_ASSERT(match_bson_value(&expected_decrypted, &actual_decrypted, &match_ctx));
      bson_value_destroy(&expected_decrypted);
      bson_value_destroy(&actual_decrypted);
   }

   /* If allowed is false, validate the value exactly equals the value of the
    * corresponding field of corpus (neither was encrypted). */
   if (!expected->allowed) {
      BSON_ASSERT(match_bson_value(&expected->value, &actual->value, &match_ctx));
   }

   _corpus_field_destroy(expected);
   _corpus_field_destroy(actual);
}

static void
_insert_from_file(mongoc_collection_t *coll, char *path)
{
   bson_t *datakey;
   bool res;
   bson_error_t error;

   datakey = get_bson_from_json_file(path);
   res = mongoc_collection_insert_one(coll, datakey, NULL, NULL, &error);
   ASSERT_OR_PRINT(res, error);
   bson_destroy(datakey);
}

static void
_test_corpus(bool local_schema)
{
   mongoc_client_t *client;
   mongoc_collection_t *coll;
   bson_t *schema;
   bson_t *create_cmd;
   bool res;
   bson_error_t error;
   mongoc_write_concern_t *wc;
   mongoc_client_t *client_encrypted;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   bson_t *kms_providers;
   mongoc_client_encryption_t *client_encryption;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   bson_t *corpus;
   bson_t corpus_copied;
   const bson_t *corpus_decrypted;
   bson_t *corpus_encrypted_expected;
   const bson_t *corpus_encrypted_actual;
   bson_iter_t iter;
   mongoc_cursor_t *cursor;
   bson_t *schema_map;
   bson_t *tls_opts;

   /* Create a MongoClient without encryption enabled */
   client = test_framework_new_default_client();
   coll = mongoc_client_get_collection(client, "db", "coll");
   (void)mongoc_collection_drop(coll, NULL);
   schema = get_bson_from_json_file("./src/libmongoc/tests/"
                                    "client_side_encryption_prose/corpus/"
                                    "corpus-schema.json");
   schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));
   create_cmd = BCON_NEW("create", "coll", "validator", "{", "$jsonSchema", BCON_DOCUMENT(schema), "}");

   if (!local_schema) {
      /* Drop and create the collection db.coll configured with the included
       * JSON schema corpus-schema.json */
      res = mongoc_client_command_simple(client, "db", create_cmd, NULL, NULL, &error);
      ASSERT_OR_PRINT(res, error);
   }

   /* Drop the collection keyvault.datakeys. Insert the key documents for each
    * KMS provider. */
   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
   (void)mongoc_collection_drop(coll, NULL);
   wc = mongoc_write_concern_new();
   mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
   mongoc_collection_set_write_concern(coll, wc);
   _insert_from_file(coll,
                     "./src/libmongoc/tests/client_side_encryption_prose/"
                     "corpus/corpus-key-aws.json");
   _insert_from_file(coll,
                     "./src/libmongoc/tests/client_side_encryption_prose/"
                     "corpus/corpus-key-azure.json");
   _insert_from_file(coll,
                     "./src/libmongoc/tests/client_side_encryption_prose/"
                     "corpus/corpus-key-gcp.json");
   _insert_from_file(coll,
                     "./src/libmongoc/tests/client_side_encryption_prose/"
                     "corpus/corpus-key-local.json");
   _insert_from_file(coll,
                     "./src/libmongoc/tests/client_side_encryption_prose/"
                     "corpus/corpus-key-kmip.json");

   /* Create a MongoClient configured with auto encryption */
   client_encrypted = test_framework_new_default_client();
   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   mongoc_auto_encryption_opts_set_schema_map(auto_encryption_opts, schema_map);
   _check_bypass(auto_encryption_opts);
   kms_providers = _make_kms_providers(true /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   tls_opts = _make_tls_opts();
   mongoc_auto_encryption_opts_set_tls_opts(auto_encryption_opts, tls_opts);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   res = mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error);
   ASSERT_OR_PRINT(res, error);

   /* Create a ClientEncryption object */
   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
   client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   corpus = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/corpus/corpus.json");

   /* Try each field individually */
   bson_iter_init(&iter, corpus);
   bson_init(&corpus_copied);
   while (bson_iter_next(&iter)) {
      _corpus_copy_field(client_encryption, &iter, &corpus_copied);
   }

   /* Insert corpus_copied with auto encryption  */
   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   res = mongoc_collection_insert_one(coll, &corpus_copied, NULL, NULL, &error);
   ASSERT_OR_PRINT(res, error);

   /* Get the automatically decrypted corpus */
   cursor = mongoc_collection_find_with_opts(coll, tmp_bson("{}"), NULL, NULL);
   BSON_ASSERT(mongoc_cursor_next(cursor, &corpus_decrypted));

   /* It should exactly match corpus. match_bson does a subset match, so match
    * in  both directions */
   assert_match_bson(corpus, corpus_decrypted, false);
   assert_match_bson(corpus_decrypted, corpus, false);
   mongoc_cursor_destroy(cursor);

   /* Load corpus-encrypted.json */
   corpus_encrypted_expected = get_bson_from_json_file("./src/libmongoc/tests/"
                                                       "client_side_encryption_prose/"
                                                       "corpus/corpus-encrypted.json");
   /* Get the actual encrypted document from unencrypted client */
   mongoc_collection_destroy(coll);
   coll = mongoc_client_get_collection(client, "db", "coll");
   cursor = mongoc_collection_find_with_opts(coll, tmp_bson("{}"), NULL, NULL);
   BSON_ASSERT(mongoc_cursor_next(cursor, &corpus_encrypted_actual));

   /* Iterate over corpus_encrypted_expected, and check corpus_encrypted_actual
    */
   bson_iter_init(&iter, corpus_encrypted_expected);
   while (bson_iter_next(&iter)) {
      bson_iter_t actual_iter;

      BSON_ASSERT(bson_iter_init_find(&actual_iter, corpus_encrypted_actual, bson_iter_key(&iter)));
      _corpus_check_encrypted(client_encryption, &iter, &actual_iter);
   }

   mongoc_cursor_destroy(cursor);
   bson_destroy(corpus_encrypted_expected);
   bson_destroy(corpus);
   bson_destroy(&corpus_copied);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   mongoc_client_destroy(client_encrypted);
   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   mongoc_client_encryption_destroy(client_encryption);
   bson_destroy(tls_opts);
   bson_destroy(kms_providers);
   mongoc_write_concern_destroy(wc);
   mongoc_collection_destroy(coll);
   bson_destroy(schema);
   bson_destroy(schema_map);
   bson_destroy(create_cmd);
   mongoc_client_destroy(client);
}

/* Prose Test 6: Corpus Test */
static void
test_corpus(void *unused)
{
   BSON_UNUSED(unused);

   _test_corpus(false /* local schema */);
   _test_corpus(true /* local schema */);
}

/* Begin C driver specific, non-spec tests: */
static void
_reset(mongoc_client_pool_t **pool,
       mongoc_client_t **singled_threaded_client,
       mongoc_client_t **multi_threaded_client,
       mongoc_auto_encryption_opts_t **opts,
       bool recreate)
{
   bson_t *kms_providers;
   mongoc_uri_t *uri;
   bson_t *schema;
   bson_t *schema_map;

   ASSERT(pool);

   mongoc_auto_encryption_opts_destroy(*opts);
   *opts = mongoc_auto_encryption_opts_new();
   {
      bson_t extra = BSON_INITIALIZER;
      _set_extra_bypass(&extra);
      _set_extra_crypt_shared(&extra);
      mongoc_auto_encryption_opts_set_extra(*opts, &extra);
      bson_destroy(&extra);
   }
   mongoc_auto_encryption_opts_set_keyvault_namespace(*opts, "db", "keyvault");
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(*opts, kms_providers);
   schema = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/schema.json");
   BSON_ASSERT(schema);
   schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));
   mongoc_auto_encryption_opts_set_schema_map(*opts, schema_map);

   if (*multi_threaded_client) {
      mongoc_client_pool_push(*pool, *multi_threaded_client);
   }

   mongoc_client_destroy(*singled_threaded_client);
   /* Workaround to hide unnecessary logs per CDRIVER-3322 */
   capture_logs(true);
   mongoc_client_pool_destroy(*pool);
   capture_logs(false);

   if (recreate) {
      mongoc_collection_t *coll;
      bson_t *datakey;
      bson_error_t error;
      mongoc_write_concern_t *wc;

      uri = test_framework_get_uri();
      *pool = test_framework_client_pool_new_from_uri(uri, NULL);
      test_framework_set_pool_ssl_opts(*pool);
      *singled_threaded_client = test_framework_client_new_from_uri(uri, NULL);
      test_framework_set_ssl_opts(*singled_threaded_client);
      *multi_threaded_client = mongoc_client_pool_pop(*pool);
      mongoc_uri_destroy(uri);

      /* create key */
      coll = mongoc_client_get_collection(*singled_threaded_client, "db", "keyvault");
      (void)mongoc_collection_drop(coll, NULL);
      wc = mongoc_write_concern_new();
      mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
      mongoc_collection_set_write_concern(coll, wc);
      datakey = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-key.json");
      BSON_ASSERT(datakey);
      ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, datakey, NULL /* opts */, NULL /* reply */, &error), error);

      bson_destroy(datakey);
      mongoc_write_concern_destroy(wc);
      mongoc_collection_destroy(coll);
   }
   bson_destroy(schema);
   bson_destroy(schema_map);
   bson_destroy(kms_providers);
}

static void
_perform_op(mongoc_client_t *client_encrypted)
{
   bool ret;
   bson_error_t error;
   mongoc_collection_t *coll;

   ASSERT(client_encrypted);

   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   ret = mongoc_collection_insert_one(
      coll, tmp_bson("{'encrypted_string': 'abc'}"), NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(ret, error);
   mongoc_collection_destroy(coll);
}

static void
_perform_op_pooled(mongoc_client_pool_t *client_pool_encrypted)
{
   mongoc_client_t *client_encrypted;

   ASSERT(client_pool_encrypted);

   client_encrypted = mongoc_client_pool_pop(client_pool_encrypted);
   _perform_op(client_encrypted);
   mongoc_client_pool_push(client_pool_encrypted, client_encrypted);
}

static void
test_invalid_single_and_pool_mismatches(void *unused)
{
   mongoc_client_pool_t *pool = NULL;
   mongoc_client_t *single_threaded_client = NULL;
   mongoc_client_t *multi_threaded_client = NULL;
   mongoc_auto_encryption_opts_t *opts = NULL;
   bson_error_t error;
   bool ret;

   BSON_UNUSED(unused);

   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);

   /* single threaded client, single threaded setter => ok */
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op(single_threaded_client);

   /* multi threaded client, single threaded setter => bad */
   ret = mongoc_client_enable_auto_encryption(multi_threaded_client, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "Cannot enable auto encryption on a pooled client");

   /* pool - pool setter */
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op_pooled(pool);

   /* single threaded client, single threaded key vault client => ok */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client(opts, single_threaded_client);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op(single_threaded_client);

   /* single threaded client, multi threaded key vault client => bad */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client(opts, multi_threaded_client);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "The key vault client must be single threaded, not "
                         "be from a client pool");

   /* single threaded client, pool key vault client => bad */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client_pool(opts, pool);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "The key vault client pool only applies to a client "
                         "pool, not a single threaded client");

   /* pool, singled threaded key vault client => bad */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client(opts, single_threaded_client);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "The key vault client only applies to a single "
                         "threaded client not a client pool. Set a "
                         "key vault client pool");

   /* pool, multi threaded key vault client => bad */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client(opts, multi_threaded_client);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "The key vault client only applies to a single "
                         "threaded client not a client pool. Set a "
                         "key vault client pool");

   /* pool, pool key vault client => ok */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client_pool(opts, pool);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op_pooled(pool);

   /* double enabling */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_STATE, "Automatic encryption already set");
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_STATE, "Automatic encryption already set");

   /* single threaded, using self as key vault client => redundant, but ok */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client(opts, single_threaded_client);
   ret = mongoc_client_enable_auto_encryption(single_threaded_client, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op(single_threaded_client);

   /* pool, using self as key vault client pool => redundant, but ok */
   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, true);
   mongoc_auto_encryption_opts_set_keyvault_client_pool(opts, pool);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   ASSERT_OR_PRINT(ret, error);
   _perform_op_pooled(pool);

   _reset(&pool, &single_threaded_client, &multi_threaded_client, &opts, false);
   mongoc_auto_encryption_opts_destroy(opts);
}

static BSON_THREAD_FUN(_worker_thread, client_ptr)
{
   mongoc_client_t *client_encrypted;
   mongoc_collection_t *coll;
   mongoc_cursor_t *cursor;
   const bson_t *doc;
   bson_t filter = BSON_INITIALIZER;
   bson_t *to_insert = BCON_NEW("encrypted_string", "abc");
   int i;
   bool ret;
   bson_error_t error;

   client_encrypted = client_ptr;
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");

   for (i = 0; i < 100; i++) {
      ret = mongoc_collection_insert_one(coll, to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ret, error);
      cursor = mongoc_collection_find_with_opts(coll, &filter, NULL /* opts */, NULL /* read_prefs */);
      mongoc_cursor_next(cursor, &doc);
      mongoc_cursor_destroy(cursor);
   }
   mongoc_collection_destroy(coll);
   bson_destroy(&filter);
   bson_destroy(to_insert);
   BSON_THREAD_RETURN;
}

static void
_test_multi_threaded(bool external_key_vault)
{
   /* Spawn two threads and do repeated encryption/decryption operations. */
   bson_thread_t threads[2];
   mongoc_uri_t *uri;
   mongoc_client_pool_t *pool;
   mongoc_client_t *client;
   mongoc_client_t *client1;
   mongoc_client_t *client2;
   mongoc_auto_encryption_opts_t *opts;
   bson_t *datakey;
   mongoc_collection_t *coll;
   bson_t *schema;
   bson_t *schema_map;
   bool ret;
   bson_error_t error;
   bson_t *kms_providers;
   int r;
   int i;
   mongoc_write_concern_t *wc;

   uri = test_framework_get_uri();
   pool = test_framework_client_pool_new_from_uri(uri, NULL);
   test_framework_set_pool_ssl_opts(pool);
   client = test_framework_client_new_from_uri(uri, NULL);
   test_framework_set_ssl_opts(client);
   opts = mongoc_auto_encryption_opts_new();

   /* Do setup: create a data key and configure pool for auto encryption. */
   coll = mongoc_client_get_collection(client, "db", "keyvault");
   (void)mongoc_collection_drop(coll, NULL);
   datakey = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/limits-key.json");
   BSON_ASSERT(datakey);
   wc = mongoc_write_concern_new();
   mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
   mongoc_collection_set_write_concern(coll, wc);
   ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, datakey, NULL /* opts */, NULL /* reply */, &error), error);

   /* create pool with auto encryption */
   _check_bypass(opts);

   mongoc_auto_encryption_opts_set_keyvault_namespace(opts, "db", "keyvault");
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(opts, kms_providers);

   if (external_key_vault) {
      mongoc_auto_encryption_opts_set_keyvault_client_pool(opts, pool);
   }

   schema = get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/schema.json");
   BSON_ASSERT(schema);
   schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));
   mongoc_auto_encryption_opts_set_schema_map(opts, schema_map);
   ret = mongoc_client_pool_enable_auto_encryption(pool, opts, &error);
   ASSERT_OR_PRINT(ret, error);

   client1 = mongoc_client_pool_pop(pool);
   client2 = mongoc_client_pool_pop(pool);

   r = mcommon_thread_create(threads, _worker_thread, client1);
   BSON_ASSERT(r == 0);

   r = mcommon_thread_create(threads + 1, _worker_thread, client2);
   BSON_ASSERT(r == 0);

   for (i = 0; i < 2; i++) {
      r = mcommon_thread_join(threads[i]);
      BSON_ASSERT(r == 0);
   }

   mongoc_write_concern_destroy(wc);
   mongoc_collection_destroy(coll);
   mongoc_client_destroy(client);
   mongoc_client_pool_push(pool, client1);
   mongoc_client_pool_push(pool, client2);
   mongoc_client_pool_destroy(pool);
   bson_destroy(schema);
   bson_destroy(schema_map);
   bson_destroy(datakey);
   mongoc_auto_encryption_opts_destroy(opts);
   mongoc_uri_destroy(uri);
   bson_destroy(kms_providers);
}

static void
test_multi_threaded(void *ctx_unused)
{
   BSON_UNUSED(ctx_unused);

   _test_multi_threaded(true);
   _test_multi_threaded(false);
}

static void
test_malformed_explicit(void *unused)
{
   mongoc_client_t *client;
   bson_t *kms_providers;
   mongoc_client_encryption_t *client_encryption;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   bson_value_t value;
   bson_value_t ciphertext;
   bool ret;
   bson_error_t error;

   BSON_UNUSED(unused);

   /* Create a MongoClient without encryption enabled */
   client = test_framework_new_default_client();
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);

   /* Create a ClientEncryption object */
   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
   client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   /* Test attempting to decrypt a malformed value */
   ciphertext.value_type = BSON_TYPE_DOUBLE;
   ciphertext.value.v_double = 1.23;
   ret = mongoc_client_encryption_decrypt(client_encryption, &ciphertext, &value, &error);
   BSON_ASSERT(!ret);
   bson_value_destroy(&value);

   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   mongoc_client_encryption_destroy(client_encryption);
   bson_destroy(kms_providers);
   mongoc_client_destroy(client);
}

static void
_check_mongocryptd_not_spawned(void)
{
   mongoc_client_t *client;
   bson_t *cmd;
   bson_error_t error;
   bool ret;

   /* Set up client. */
   {
      mongoc_uri_t *uri = mongoc_uri_new("mongodb://localhost:27021");
      ASSERT(mongoc_uri_set_option_as_int32(uri, MONGOC_URI_SERVERSELECTIONTIMEOUTMS, 1000));
      /* Set SERVERSELECTIONTRYONCE to false so client will wait for the full
       * second before giving up on server selection. */
      ASSERT(mongoc_uri_set_option_as_bool(uri, MONGOC_URI_SERVERSELECTIONTRYONCE, false));

      client = mongoc_client_new_from_uri(uri);
      /* Bypass the 5 second cooldown so attempts to connect are repeated.
       * Single threaded clients wait for 5 second cooldown period after failing
       * to connect to a server before connecting again. If mongocryptd just
       * spawned, it may take time before connections are accepted. */
      _mongoc_topology_bypass_cooldown(client->topology);
      mongoc_uri_destroy(uri);
   }
   cmd = BCON_NEW(HANDSHAKE_CMD_LEGACY_HELLO, BCON_INT32(1));
   ret = mongoc_client_command_simple(client, "keyvault", cmd, NULL /* read prefs */, NULL /* reply */, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_SERVER_SELECTION, MONGOC_ERROR_SERVER_SELECTION_FAILURE, "No suitable servers");
   mongoc_client_destroy(client);
   bson_destroy(cmd);
}

/* Prose Test 8: Bypass Spawning mongocryptd - Via mongocryptdBypassSpawn */
static void
test_bypass_spawning_via_mongocryptdBypassSpawn(void *unused)
{
   mongoc_client_t *client_encrypted;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   bson_t *kms_providers;
   bson_t *doc_to_insert;
   bson_t *extra;
   bson_t *schema_map;
   bson_t *schema;
   bool ret;
   bson_error_t error;
   mongoc_collection_t *coll;

   BSON_UNUSED(unused);

   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   schema = get_bson_from_json_file("./src/libmongoc/tests/"
                                    "client_side_encryption_prose/external/"
                                    "external-schema.json");
   schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));

   /* Create a MongoClient with encryption enabled */
   client_encrypted = test_framework_new_default_client();
   extra = BCON_NEW("mongocryptdBypassSpawn",
                    BCON_BOOL(true),
                    "mongocryptdSpawnArgs",
                    "[",
                    "--pidfilepath=bypass-spawning-mongocryptd.pid",
                    "--port=27021",
                    "]",
                    "mongocryptdURI",
                    "mongodb://localhost:27021/?serverSelectionTimeoutMS=1000");
   mongoc_auto_encryption_opts_set_extra(auto_encryption_opts, extra);
   mongoc_auto_encryption_opts_set_schema_map(auto_encryption_opts, schema_map);
   bson_destroy(extra);
   ret = mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error);
   ASSERT_OR_PRINT(ret, error);

   /* Insert { 'encrypt': 'test' }. Should fail with a server selection error.
    */
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   doc_to_insert = BCON_NEW("encrypt", "test");
   ret = mongoc_collection_insert_one(coll, doc_to_insert, NULL /* opts */, NULL /* reply */, &error);
   BSON_ASSERT(!ret);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_SERVER_SELECTION,
                         MONGOC_ERROR_SERVER_SELECTION_FAILURE,
                         "mongocryptd error: No suitable servers found");

   _check_mongocryptd_not_spawned();
   bson_destroy(schema_map);
   bson_destroy(schema);
   mongoc_collection_destroy(coll);
   mongoc_client_destroy(client_encrypted);
   bson_destroy(doc_to_insert);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   bson_destroy(kms_providers);
}

static void
test_bypass_spawning_via_helper(const char *auto_encryption_opt)
{
   mongoc_client_t *client_encrypted;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   bson_t *kms_providers;
   bson_t *doc_to_insert;
   bson_t *extra = bson_new();
   bool ret;
   bson_error_t error;
   bool check_crypt_shared = false;
   mongoc_collection_t *coll;

   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   kms_providers = _make_kms_providers(false /* aws */, true /* local */);
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");
   if (0 == strcmp(auto_encryption_opt, "bypass_auto_encryption")) {
      mongoc_auto_encryption_opts_set_bypass_auto_encryption(auto_encryption_opts, true);
   } else if (0 == strcmp(auto_encryption_opt, "bypass_query_analysis")) {
      mongoc_auto_encryption_opts_set_bypass_query_analysis(auto_encryption_opts, true);
   } else if (0 == strcmp(auto_encryption_opt, "cryptSharedLibRequired")) {
      bson_t *schema = get_bson_from_json_file("./src/libmongoc/tests/"
                                               "client_side_encryption_prose/external/"
                                               "external-schema.json");
      BSON_ASSERT(schema);
      bson_t *schema_map = BCON_NEW("db.coll", BCON_DOCUMENT(schema));
      mongoc_auto_encryption_opts_set_schema_map(auto_encryption_opts, schema_map);
      check_crypt_shared = true;
      char *env_cryptSharedLibPath = test_framework_getenv("MONGOC_TEST_CRYPT_SHARED_LIB_PATH");
      BSON_ASSERT(env_cryptSharedLibPath);
      BSON_APPEND_UTF8(extra, "cryptSharedLibPath", env_cryptSharedLibPath);
      BSON_APPEND_BOOL(extra, "cryptSharedLibRequired", true);
      BSON_APPEND_UTF8(extra, "mongocryptdURI", "mongodb://localhost:27021/db?serverSelectionTimeoutMS=1000");
      bson_free(env_cryptSharedLibPath);
      bson_destroy(schema);
      bson_destroy(schema_map);
   } else {
      test_error("Unexpected 'auto_encryption_opt' argument: %s", auto_encryption_opt);
   }

   /* Create a MongoClient with encryption enabled */
   client_encrypted = test_framework_new_default_client();
   BCON_APPEND(
      extra, "mongocryptdSpawnArgs", "[", "--pidfilepath=bypass-spawning-mongocryptd.pid", "--port=27021", "]");
   mongoc_auto_encryption_opts_set_extra(auto_encryption_opts, extra);
   bson_destroy(extra);
   ret = mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error);
   ASSERT_OR_PRINT(ret, error);

   if (check_crypt_shared) {
      BSON_ASSERT(mongoc_client_get_crypt_shared_version(client_encrypted) != NULL);
   }

   /* Insert { 'unencrypted': 'test' }. Should succeed. */
   coll = mongoc_client_get_collection(client_encrypted, "db", "coll");
   doc_to_insert = BCON_NEW("unencrypted", "test");
   ret = mongoc_collection_insert_one(coll, doc_to_insert, NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(ret, error);

   _check_mongocryptd_not_spawned();

   mongoc_collection_destroy(coll);
   mongoc_client_destroy(client_encrypted);
   bson_destroy(doc_to_insert);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   bson_destroy(kms_providers);
}

/* Prose Test 8: Bypass Spawning mongocryptd - Via bypassAutoEncryption */
static void
test_bypass_spawning_via_bypassAutoEncryption(void *unused)
{
   BSON_UNUSED(unused);

   test_bypass_spawning_via_helper("bypass_auto_encryption");
}

/* Prose Test 8: Bypass Spawning mongocryptd - Via bypassQueryAnalysis */
static void
test_bypass_spawning_via_bypassQueryAnalysis(void *unused)
{
   BSON_UNUSED(unused);

   test_bypass_spawning_via_helper("bypass_query_analysis");
}

/* Prose Test 8: Bypass Spawning mongocryptd - Via loading shared library */
static void
test_bypass_spawning_via_cryptSharedLibLoaded(void *unused)
{
   BSON_UNUSED(unused);
   test_bypass_spawning_via_helper("cryptSharedLibRequired");
}

static int
_skip_if_no_crypt_shared(void)
{
   char *env = test_framework_getenv("MONGOC_TEST_CRYPT_SHARED_LIB_PATH");
   if (!env) {
      return 0; // Skip!
   }
   bson_free(env);
   return 1; // Do not skip
}

static mongoc_client_encryption_t *
_make_kms_certificate_client_encryption(mongoc_client_t *client, bson_error_t *error)
{
   mongoc_client_encryption_t *client_encryption;

   ASSERT(client);

   mongoc_client_encryption_opts_t *client_encryption_opts = mongoc_client_encryption_opts_new();

   {
      bson_t *kms_providers = _make_aws_kms_provider(NULL);
      mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
      bson_destroy(kms_providers);
   }

   {
      char *tls_ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
      char *tls_cert_key_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");
      bson_t *tls_opts = tmp_bson("{ 'aws': { '%s': '%s', '%s': '%s' } }",
                                  MONGOC_URI_TLSCAFILE,
                                  tls_ca_file,
                                  MONGOC_URI_TLSCERTIFICATEKEYFILE,
                                  tls_cert_key_file);

      mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);

      bson_free(tls_cert_key_file);
      bson_free(tls_ca_file);
   }

   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);

   client_encryption = mongoc_client_encryption_new(client_encryption_opts, error);
   ASSERT_OR_PRINT(client_encryption, (*error));

   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   return client_encryption;
}


static void
test_kms_tls_cert_valid(void *unused)
{
   const int32_t connecttimeoutms = MONGOC_DEFAULT_CONNECTTIMEOUTMS;
   const int is_client = 1;

   bson_error_t error;
   mongoc_host_list_t host;
   mongoc_stream_t *base_stream;
   mongoc_ssl_opt_t ssl_opts;
   mongoc_stream_t *tls_stream;

   char *tls_ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
   char *tls_cert_key_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");

   BSON_UNUSED(unused);

#if defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
   /* Certificate verification fails with Secure Channel given
    * "127.0.0.1:8999" with error: "hostname doesn't match certificate". */
   ASSERT_OR_PRINT(_mongoc_host_list_from_string_with_err(&host, "localhost:8999", &error), error);
#else
   ASSERT_OR_PRINT(_mongoc_host_list_from_string_with_err(&host, "127.0.0.1:8999", &error), error);
#endif

   base_stream = mongoc_client_connect_tcp(connecttimeoutms, &host, &error);
   ASSERT_OR_PRINT(base_stream, error);

   ssl_opts = *test_framework_get_ssl_opts();
   ssl_opts.ca_file = tls_ca_file;
   ssl_opts.pem_file = tls_cert_key_file;

   tls_stream = mongoc_stream_tls_new_with_hostname(base_stream, host.host, &ssl_opts, is_client);

   ASSERT_OR_PRINT(mongoc_stream_tls_handshake_block(tls_stream, host.host, connecttimeoutms, &error), error);

   mongoc_stream_destroy(tls_stream); /* Also destroys base_stream. */

   bson_free(tls_cert_key_file);
   bson_free(tls_ca_file);
}

/* Prose Test 10: KMS TLS Tests - Invalid KMS Certificate */
static void
test_kms_tls_cert_expired(void *unused)
{
   bool ret;

   bson_error_t error;

   bson_value_t keyid;

   mongoc_client_t *client = test_framework_new_default_client();

   mongoc_client_encryption_t *client_encryption = _make_kms_certificate_client_encryption(client, &error);

   mongoc_client_encryption_datakey_opts_t *opts = mongoc_client_encryption_datakey_opts_new();

   BSON_UNUSED(unused);

   mongoc_client_encryption_datakey_opts_set_masterkey(opts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', "
                                                                "'endpoint': '127.0.0.1:9000' }"));

   ret = mongoc_client_encryption_create_datakey(client_encryption, "aws", opts, &keyid, &error);

   BSON_ASSERT(!ret);

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
   ASSERT_CONTAINS(error.message, "certificate has expired");
#elif defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
   ASSERT_CONTAINS(error.message, "CSSMERR_TP_CERT_EXPIRED");
#elif defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
   ASSERT_CONTAINS(error.message, "certificate has expired");
#endif

   mongoc_client_encryption_datakey_opts_destroy(opts);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_destroy(client);
}


/* Prose Test 10: KMS TLS Tests - Invalid Hostname in KMS Certificate */
static void
test_kms_tls_cert_wrong_host(void *unused)
{
   bool ret;

   bson_error_t error;

   bson_value_t keyid;

   mongoc_client_t *client = test_framework_new_default_client();

   mongoc_client_encryption_t *client_encryption = _make_kms_certificate_client_encryption(client, &error);

   mongoc_client_encryption_datakey_opts_t *opts = mongoc_client_encryption_datakey_opts_new();

   BSON_UNUSED(unused);

   mongoc_client_encryption_datakey_opts_set_masterkey(opts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', "
                                                                "'endpoint': '127.0.0.1:9001' }"));

   ret = mongoc_client_encryption_create_datakey(client_encryption, "aws", opts, &keyid, &error);

   BSON_ASSERT(!ret);

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
   ASSERT_CONTAINS(error.message, "IP address mismatch");
#elif defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
   ASSERT_CONTAINS(error.message, "Host name mismatch");
#elif defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
   ASSERT_CONTAINS(error.message, "hostname doesn't match certificate");
#endif

   mongoc_client_encryption_datakey_opts_destroy(opts);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_destroy(client);
}

typedef enum { NO_CLIENT_CERT, WITH_TLS, INVALID_HOSTNAME, EXPIRED, WITH_NAMES, RETRY } tls_test_ce_t;

static mongoc_client_encryption_t *
_tls_test_make_client_encryption(mongoc_client_t *keyvault_client, tls_test_ce_t test_ce)
{
   bson_t *kms_providers;
   mongoc_client_encryption_opts_t *client_encryption_opts;
   bson_error_t error = {0};
   mongoc_client_encryption_t *client_encryption;
   bson_t *tls_opts = NULL;

   ASSERT(keyvault_client);

   char *mongoc_test_aws_access_key_id = test_framework_getenv_required("MONGOC_TEST_AWS_ACCESS_KEY_ID");
   char *mongoc_test_aws_secret_access_key = test_framework_getenv_required("MONGOC_TEST_AWS_SECRET_ACCESS_KEY");
   char *mongoc_test_azure_tenant_id = test_framework_getenv_required("MONGOC_TEST_AZURE_TENANT_ID");
   char *mongoc_test_azure_client_id = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_ID");
   char *mongoc_test_azure_client_secret = test_framework_getenv_required("MONGOC_TEST_AZURE_CLIENT_SECRET");
   char *mongoc_test_gcp_email = test_framework_getenv_required("MONGOC_TEST_GCP_EMAIL");
   char *mongoc_test_gcp_privatekey = test_framework_getenv_required("MONGOC_TEST_GCP_PRIVATEKEY");
   char *ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
   char *certificate_key_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");

   if (test_ce == WITH_TLS || test_ce == RETRY) {
      const char *port = test_ce == RETRY ? "9003" : "9002";

      kms_providers = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s' }}",
                               mongoc_test_aws_access_key_id,
                               mongoc_test_aws_secret_access_key);

      tls_opts =
         tmp_bson("{'aws': {'tlsCaFile': '%s', 'tlsCertificateKeyFile': '%s' }}", ca_file, certificate_key_file);
      bson_concat(kms_providers,
                  tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', "
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:%s' }}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret,
                           port));
      bson_concat(
         tls_opts,
         tmp_bson("{'azure': {'tlsCaFile': '%s', 'tlsCertificateKeyFile': '%s' }}", ca_file, certificate_key_file));

      bson_concat(kms_providers,
                  tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:%s' }}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey,
                           port));
      bson_concat(
         tls_opts,
         tmp_bson("{'gcp': {'tlsCaFile': '%s', 'tlsCertificateKeyFile': '%s' }}", ca_file, certificate_key_file));

      bson_concat(kms_providers, tmp_bson("{'kmip': { 'endpoint': '127.0.0.1:5698'}}"));
      bson_concat(
         tls_opts,
         tmp_bson("{'kmip': {'tlsCaFile': '%s', 'tlsCertificateKeyFile': '%s' }}", ca_file, certificate_key_file));
   } else if (test_ce == NO_CLIENT_CERT) {
      kms_providers = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s' }}",
                               mongoc_test_aws_access_key_id,
                               mongoc_test_aws_secret_access_key);
      bson_concat(kms_providers,
                  tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', "
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:9002'}}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret));
      bson_concat(kms_providers,
                  tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:9002'}}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey));
      bson_concat(kms_providers, tmp_bson("{'kmip': { 'endpoint': '127.0.0.1:5698' }}"));
   } else if (test_ce == EXPIRED) {
      kms_providers = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s' }}",
                               mongoc_test_aws_access_key_id,
                               mongoc_test_aws_secret_access_key);
      tls_opts = tmp_bson("{'aws': {'tlsCaFile': '%s'} }", ca_file);

      bson_concat(kms_providers,
                  tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', "
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:9000'}}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret));
      bson_concat(tls_opts, tmp_bson("{'azure': {'tlsCaFile': '%s'} }", ca_file));

      bson_concat(kms_providers,
                  tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:9000'}}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey));
      bson_concat(tls_opts, tmp_bson("{'gcp': {'tlsCaFile': '%s'} }", ca_file));

      bson_concat(kms_providers, tmp_bson("{'kmip': { 'endpoint': '127.0.0.1:9000' }}"));
      bson_concat(tls_opts, tmp_bson("{'kmip': {'tlsCaFile': '%s'} }", ca_file));
   } else if (test_ce == INVALID_HOSTNAME) {
      kms_providers = tmp_bson("{'aws': {'accessKeyId': '%s', 'secretAccessKey': '%s' } }",
                               mongoc_test_aws_access_key_id,
                               mongoc_test_aws_secret_access_key);
      tls_opts = tmp_bson("{'aws': {'tlsCaFile': '%s'} }", ca_file);

      bson_concat(kms_providers,
                  tmp_bson("{'azure': {'tenantId': '%s', 'clientId': '%s', "
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:9001' }}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret));
      bson_concat(tls_opts, tmp_bson("{'azure': {'tlsCaFile': '%s'} }", ca_file));

      bson_concat(kms_providers,
                  tmp_bson("{'gcp': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:9001' }}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey));
      bson_concat(tls_opts, tmp_bson("{'gcp': {'tlsCaFile': '%s'} }", ca_file));

      bson_concat(kms_providers, tmp_bson("{'kmip': { 'endpoint': '127.0.0.1:9001' }}"));
      bson_concat(tls_opts, tmp_bson("{'kmip': {'tlsCaFile': '%s'} }", ca_file));
   } else if (test_ce == WITH_NAMES) {
      kms_providers = tmp_bson("{'aws:no_client_cert': {'accessKeyId': '%s', "
                               "'secretAccessKey': '%s' }}",
                               mongoc_test_aws_access_key_id,
                               mongoc_test_aws_secret_access_key);
      tls_opts = tmp_bson("{'aws:no_client_cert': {'tlsCaFile': '%s' }}", ca_file);

      bson_concat(kms_providers,
                  tmp_bson("{'aws:with_tls': {'accessKeyId': '%s', 'secretAccessKey': '%s' }}",
                           mongoc_test_aws_access_key_id,
                           mongoc_test_aws_secret_access_key));
      bson_concat(tls_opts,
                  tmp_bson("{'aws:with_tls': {'tlsCaFile': '%s', "
                           "'tlsCertificateKeyFile': '%s' }}",
                           ca_file,
                           certificate_key_file));

      bson_concat(kms_providers,
                  tmp_bson("{'azure:no_client_cert': {'tenantId': '%s', 'clientId': '%s',"
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:9002'"
                           "}}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret));
      bson_concat(tls_opts, tmp_bson("{'azure:no_client_cert': {'tlsCaFile': '%s' }}", ca_file));

      bson_concat(kms_providers,
                  tmp_bson("{'azure:with_tls': {'tenantId': '%s', 'clientId': '%s',"
                           "'clientSecret': '%s', "
                           "'identityPlatformEndpoint': '127.0.0.1:9002'"
                           "}}",
                           mongoc_test_azure_tenant_id,
                           mongoc_test_azure_client_id,
                           mongoc_test_azure_client_secret));
      bson_concat(tls_opts,
                  tmp_bson("{'azure:with_tls': {'tlsCaFile': '%s', "
                           "'tlsCertificateKeyFile': '%s' }}",
                           ca_file,
                           certificate_key_file));

      bson_concat(kms_providers,
                  tmp_bson("{'gcp:no_client_cert': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:9002' }}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey));
      bson_concat(tls_opts, tmp_bson("{'gcp:no_client_cert': {'tlsCaFile': '%s' }}", ca_file, certificate_key_file));

      bson_concat(kms_providers,
                  tmp_bson("{'gcp:with_tls': { 'email': '%s', 'privateKey': '%s', "
                           "'endpoint': '127.0.0.1:9002' }}",
                           mongoc_test_gcp_email,
                           mongoc_test_gcp_privatekey));
      bson_concat(tls_opts,
                  tmp_bson("{'gcp:with_tls': {'tlsCaFile': '%s', "
                           "'tlsCertificateKeyFile': '%s' }}",
                           ca_file,
                           certificate_key_file));

      bson_concat(kms_providers, tmp_bson("{'kmip:no_client_cert': { 'endpoint': '127.0.0.1:5698'}}"));
      bson_concat(tls_opts, tmp_bson("{'kmip:no_client_cert': {'tlsCaFile': '%s' }}", ca_file));

      bson_concat(kms_providers, tmp_bson("{'kmip:with_tls': { 'endpoint': '127.0.0.1:5698'}}"));
      bson_concat(tls_opts,
                  tmp_bson("{'kmip:with_tls': {'tlsCaFile': '%s', "
                           "'tlsCertificateKeyFile': '%s' }}",
                           ca_file,
                           certificate_key_file));
   } else {
      BSON_UNREACHABLE("Invalid value for test_ce");
   }

   client_encryption_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
   mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, keyvault_client);
   mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);

   client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
   ASSERT_OR_PRINT(client_encryption, error);

   mongoc_client_encryption_opts_destroy(client_encryption_opts);
   bson_free(mongoc_test_aws_access_key_id);
   bson_free(mongoc_test_aws_secret_access_key);
   bson_free(mongoc_test_azure_tenant_id);
   bson_free(mongoc_test_azure_client_id);
   bson_free(mongoc_test_azure_client_secret);
   bson_free(mongoc_test_gcp_email);
   bson_free(mongoc_test_gcp_privatekey);
   bson_free(ca_file);
   bson_free(certificate_key_file);

   return client_encryption;
}

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
#define ASSERT_EXPIRED(error) ASSERT_CONTAINS(error.message, "certificate has expired")
#elif defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
#define ASSERT_EXPIRED(error) ASSERT_CONTAINS(error.message, "CSSMERR_TP_CERT_EXPIRED")
#elif defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
#define ASSERT_EXPIRED(error) ASSERT_CONTAINS(error.message, "certificate has expired")
#else
#define ASSERT_EXPIRED(error)
#endif

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
#define ASSERT_INVALID_HOSTNAME(error) ASSERT_CONTAINS(error.message, "IP address mismatch")
#elif defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
#define ASSERT_INVALID_HOSTNAME(error) ASSERT_CONTAINS(error.message, "Host name mismatch")
#elif defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
#define ASSERT_INVALID_HOSTNAME(error) ASSERT_CONTAINS(error.message, "hostname doesn't match certificate")
#else
#define ASSERT_INVALID_HOSTNAME(error)
#endif

/* Prose Test 11: KMS TLS Options Tests */
static void
test_kms_tls_options(void *unused)
{
   mongoc_client_t *keyvault_client;
   mongoc_client_encryption_t *client_encryption_no_client_cert = NULL;
   mongoc_client_encryption_t *client_encryption_with_tls = NULL;
   mongoc_client_encryption_t *client_encryption_expired = NULL;
   mongoc_client_encryption_t *client_encryption_invalid_hostname = NULL;
   mongoc_client_encryption_t *client_encryption_with_names = NULL;
   bson_value_t keyid;
   mongoc_client_encryption_datakey_opts_t *dkopts;
   bson_error_t error;
   bool res;
   const int mongocrypt_errno = 1; /* libmongocrypt returns all errors with code 1. */

   BSON_UNUSED(unused);

   keyvault_client = test_framework_new_default_client();
   client_encryption_no_client_cert = _tls_test_make_client_encryption(keyvault_client, NO_CLIENT_CERT);
   client_encryption_with_tls = _tls_test_make_client_encryption(keyvault_client, WITH_TLS);
   client_encryption_expired = _tls_test_make_client_encryption(keyvault_client, EXPIRED);
   client_encryption_invalid_hostname = _tls_test_make_client_encryption(keyvault_client, INVALID_HOSTNAME);
   client_encryption_with_names = _tls_test_make_client_encryption(keyvault_client, WITH_NAMES);

   /* Case 1: AWS - no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9002' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_no_client_cert, "aws", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* AWS - named with no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9002' }"));
   res = mongoc_client_encryption_create_datakey(
      client_encryption_with_names, "aws:no_client_cert", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* AWS - with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9002' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_tls, "aws", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "parse error");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* AWS - named with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9002' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_names, "aws:with_tls", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "parse error");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* AWS - expired. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9000' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_expired, "aws", dkopts, &keyid, &error);
   ASSERT_EXPIRED(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* AWS - invalid hostname. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'endpoint': "
                                                                "'127.0.0.1:9001' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid_hostname, "aws", dkopts, &keyid, &error);
   ASSERT_INVALID_HOSTNAME(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Case 2: Azure - no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_no_client_cert, "azure", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Azure - named with no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res = mongoc_client_encryption_create_datakey(
      client_encryption_with_names, "azure:no_client_cert", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Azure - with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_tls, "azure", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "HTTP status=404");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Azure - named with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res =
      mongoc_client_encryption_create_datakey(client_encryption_with_names, "azure:with_tls", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "HTTP status=404");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Azure - expired. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_expired, "azure", dkopts, &keyid, &error);
   ASSERT_EXPIRED(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Azure - invalid hostname. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts, tmp_bson("{ 'keyVaultEndpoint': 'doesnotexist.invalid', 'keyName': 'foo' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid_hostname, "azure", dkopts, &keyid, &error);
   ASSERT_INVALID_HOSTNAME(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Case 3: GCP - no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_no_client_cert, "gcp", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* GCP - named with no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(
      client_encryption_with_names, "gcp:no_client_cert", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* GCP - with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_tls, "gcp", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "HTTP status=404");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* GCP - named with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_names, "gcp:with_tls", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, mongocrypt_errno, "HTTP status=404");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* GCP - expired. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_expired, "gcp", dkopts, &keyid, &error);
   ASSERT_EXPIRED(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* GCP - invalid hostname. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(
      dkopts,
      tmp_bson("{ 'projectId': 'pid', 'location': 'l', 'keyRing': 'kr', "
               "'keyName': 'kn' }"));
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid_hostname, "gcp", dkopts, &keyid, &error);
   ASSERT_INVALID_HOSTNAME(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* Case 4: KMIP - no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(client_encryption_no_client_cert, "kmip", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* KMIP - named with no client cert. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(
      client_encryption_with_names, "kmip:no_client_cert", dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "");
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* KMIP - with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_tls, "kmip", dkopts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);
   bson_value_destroy(&keyid);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* KMIP - named with TLS. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(client_encryption_with_names, "kmip:with_tls", dkopts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);
   bson_value_destroy(&keyid);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* KMIP - expired. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(client_encryption_expired, "kmip", dkopts, &keyid, &error);
   ASSERT_EXPIRED(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   /* KMIP - invalid hostname. */
   memset(&error, 0, sizeof(bson_error_t));
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, tmp_bson("{}"));
   res = mongoc_client_encryption_create_datakey(client_encryption_invalid_hostname, "kmip", dkopts, &keyid, &error);
   ASSERT_INVALID_HOSTNAME(error);
   ASSERT(!res);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   mongoc_client_encryption_destroy(client_encryption_with_names);
   mongoc_client_encryption_destroy(client_encryption_invalid_hostname);
   mongoc_client_encryption_destroy(client_encryption_expired);
   mongoc_client_encryption_destroy(client_encryption_with_tls);
   mongoc_client_encryption_destroy(client_encryption_no_client_cert);
   mongoc_client_destroy(keyvault_client);
}

static void
test_kms_tls_options_extra_rejected(void *unused)
{
   mongoc_client_encryption_t *ce;
   mongoc_client_encryption_opts_t *ce_opts;
   mongoc_client_t *keyvault_client;
   bson_error_t error;
   bson_t *kms_providers = tmp_bson("{'aws': {'accessKeyId': 'foo', 'secretAccessKey': 'bar'}}");

   BSON_UNUSED(unused);

   keyvault_client = test_framework_new_default_client();

   /* Test that the "local" KMS provider is rejected. */
   ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ce_opts, keyvault_client);
   mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(ce_opts, tmp_bson("{'local': {'tlsCaFile': 'ca.pem'}}"));
   ce = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "Cannot configure TLS options for KMS provider: local");
   ASSERT(NULL == ce);
   mongoc_client_encryption_opts_destroy(ce_opts);

   /* Test that insecure TLS options are rejected. */
   memset(&error, 0, sizeof(bson_error_t));
   ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ce_opts, keyvault_client);
   mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(ce_opts, tmp_bson("{'aws': {'tlsInsecure': true}}"));
   ce = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "Error setting TLS option tlsInsecure for aws. "
                         "Insecure TLS options prohibited");
   ASSERT(NULL == ce);
   mongoc_client_encryption_opts_destroy(ce_opts);

   /* Test that extra TLS options are rejected. */
   memset(&error, 0, sizeof(bson_error_t));
   ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ce_opts, keyvault_client);
   mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(ce_opts, tmp_bson("{'aws': {'extra': true}}"));
   ce = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "Error setting TLS option extra for aws. Insecure "
                         "TLS options prohibited");
   ASSERT(NULL == ce);
   mongoc_client_encryption_opts_destroy(ce_opts);

   /* Test that TLS options for duplicate providers are rejected. */
   memset(&error, 0, sizeof(bson_error_t));
   ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ce_opts, keyvault_client);
   mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(
      ce_opts, tmp_bson("{'aws': {'tlsCAFile': 'foo.pem'}, 'aws': {'tlsCAFile': 'foo.pem'}}"));
   ce = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION,
                         MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                         "Error parsing duplicate TLS options for aws");
   ASSERT(NULL == ce);
   mongoc_client_encryption_opts_destroy(ce_opts);

   /* Test that tlsDisableOCSPEndpointCheck may be set. */
   memset(&error, 0, sizeof(bson_error_t));
   ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ce_opts, keyvault_client);
   mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
   mongoc_client_encryption_opts_set_tls_opts(
      ce_opts, tmp_bson("{'aws': {'%s': true}}", MONGOC_URI_TLSDISABLEOCSPENDPOINTCHECK));
   ce = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_OR_PRINT(ce, error);
   mongoc_client_encryption_destroy(ce);
   mongoc_client_encryption_opts_destroy(ce_opts);

   mongoc_client_destroy(keyvault_client);
}

static const char *failpoint_server_ip = "127.0.0.1";
static const int failpoint_server_port = 9003;

static void
reset_failpoints(mongoc_ssl_opt_t *ssl_opts)
{
   mongoc_http_request_t req;
   mongoc_http_response_t res;
   bool r;
   bson_error_t error = {0};

   _mongoc_http_request_init(&req);
   _mongoc_http_response_init(&res);

   req.method = "POST";
   req.host = failpoint_server_ip;
   req.port = failpoint_server_port;
   req.path = "/reset";

   r = _mongoc_http_send(&req, mlib_expires_after(mlib_duration(10, s)), true, ssl_opts, &res, &error);
   ASSERT_OR_PRINT(r, error);
   _mongoc_http_response_cleanup(&res);
}

static void
set_retry_failpoint(mongoc_ssl_opt_t *ssl_opts, bool network, uint32_t count)
{
   mongoc_http_request_t req;
   mongoc_http_response_t res;
   bool r;
   bson_error_t error = {0};

   _mongoc_http_request_init(&req);
   _mongoc_http_response_init(&res);

   req.method = "POST";
   req.host = failpoint_server_ip;
   req.port = failpoint_server_port;
   if (network) {
      req.path = "/set_failpoint/network";
   } else {
      req.path = "/set_failpoint/http";
   }
   req.extra_headers = "Content-Type: application/json\r\n";
   char count_json[25];
   sprintf(count_json, "{\"count\": %" PRIu32 "}", count);
   req.body = count_json;
   req.body_len = strlen(count_json);

   r = _mongoc_http_send(&req, mlib_expires_after(mlib_duration(10, s)), true, ssl_opts, &res, &error);
   ASSERT_OR_PRINT(r, error);
   _mongoc_http_response_cleanup(&res);
}

/* ee_fixture is a fixture for the Explicit Encryption prose test. */
typedef struct {
   bson_value_t key1ID;
   mongoc_client_t *keyVaultClient;
   mongoc_client_encryption_t *clientEncryption;
   mongoc_client_t *encryptedClient;
   mongoc_collection_t *encryptedColl;
} ee_fixture;

static ee_fixture *
explicit_encryption_setup_full(const char *encrypted_fields_path, const char *key_path)
{
   ee_fixture *eef = (ee_fixture *)bson_malloc0(sizeof(ee_fixture));
   bson_t *encryptedFields = get_bson_from_json_file(encrypted_fields_path);
   bson_t *key1Document = get_bson_from_json_file(key_path);
   mongoc_client_t *setupClient = test_framework_new_default_client();


   /* Read the ``"_id"`` field of ``key1Document`` as ``key1ID``. */
   {
      bson_iter_t iter;
      const bson_value_t *value;

      ASSERT(bson_iter_init_find(&iter, key1Document, "_id"));
      value = bson_iter_value(&iter);
      bson_value_copy(value, &eef->key1ID);
   }

   /* Drop and create the collection ``db.explicit_encryption`` using
    * ``encryptedFields`` as an option. */
   {
      mongoc_database_t *db = mongoc_client_get_database(setupClient, "db");
      mongoc_collection_t *coll = mongoc_database_get_collection(db, "explicit_encryption");
      bson_error_t error;
      bson_t *opts;

      opts = BCON_NEW("encryptedFields", BCON_DOCUMENT(encryptedFields));

      if (!mongoc_collection_drop_with_opts(coll, opts, &error)) {
         if (error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
            test_error("unexpected error in drop: %s", error.message);
         }
      }
      mongoc_collection_destroy(coll);

      coll = mongoc_database_create_collection(db, "explicit_encryption", opts, &error);
      ASSERT_OR_PRINT(coll, error);

      mongoc_collection_destroy(coll);
      bson_destroy(opts);
      mongoc_database_destroy(db);
   }

   /* Drop and create the collection ``keyvault.datakeys``. */
   {
      mongoc_database_t *db = mongoc_client_get_database(setupClient, "keyvault");
      mongoc_collection_t *coll = mongoc_database_get_collection(db, "datakeys");
      bson_error_t error;
      bson_t iopts = BSON_INITIALIZER;
      mongoc_write_concern_t *wc;

      if (!mongoc_collection_drop(coll, &error)) {
         if (error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
            test_error("unexpected error in drop: %s", error.message);
         }
      }
      mongoc_collection_destroy(coll);

      coll = mongoc_database_create_collection(db, "datakeys", NULL /* opts */, &error);
      ASSERT_OR_PRINT(coll, error);

      /* Insert keyDocument1 with write concern majority */
      wc = mongoc_write_concern_new();
      mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
      ASSERT(mongoc_write_concern_append(wc, &iopts));
      ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, key1Document, &iopts, NULL /* reply */, &error), error);

      mongoc_write_concern_destroy(wc);
      bson_destroy(&iopts);
      mongoc_collection_destroy(coll);
      mongoc_database_destroy(db);
   }

   eef->keyVaultClient = test_framework_new_default_client();

   /* Create a ClientEncryption object named ``clientEncryption`` */
   {
      mongoc_client_encryption_opts_t *ceOpts = mongoc_client_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;

      mongoc_client_encryption_opts_set_keyvault_client(ceOpts, eef->keyVaultClient);
      mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_kms_providers(ceOpts, kms_providers);

      eef->clientEncryption = mongoc_client_encryption_new(ceOpts, &error);
      ASSERT_OR_PRINT(eef->clientEncryption, error);

      bson_destroy(kms_providers);
      mongoc_client_encryption_opts_destroy(ceOpts);
   }

   /* Create a MongoClient named ``encryptedClient``. */
   {
      mongoc_auto_encryption_opts_t *aeOpts = mongoc_auto_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;

      mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
      mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kms_providers);
      mongoc_auto_encryption_opts_set_bypass_query_analysis(aeOpts, true);
      eef->encryptedClient = test_framework_new_default_client();
      ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(eef->encryptedClient, aeOpts, &error), error);

      bson_destroy(kms_providers);
      mongoc_auto_encryption_opts_destroy(aeOpts);
      eef->encryptedColl = mongoc_client_get_collection(eef->encryptedClient, "db", "explicit_encryption");
   }

   mongoc_client_destroy(setupClient);
   bson_destroy(key1Document);
   bson_destroy(encryptedFields);
   return eef;
}

static ee_fixture *
explicit_encryption_setup(void)
{
   return explicit_encryption_setup_full("./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                         "encryptedFields.json",
                                         "./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                         "key1-document.json");
}

static void
explicit_encryption_destroy(ee_fixture *eef)
{
   if (!eef) {
      return;
   }

   mongoc_collection_destroy(eef->encryptedColl);
   mongoc_client_destroy(eef->encryptedClient);
   mongoc_client_encryption_destroy(eef->clientEncryption);
   mongoc_client_destroy(eef->keyVaultClient);
   bson_value_destroy(&eef->key1ID);
   bson_free(eef);
}

/* ree_fixture is a fixture for the Range Explicit Encryption prose test. */
typedef struct {
   bson_value_t key1ID;
   mongoc_client_t *keyVaultClient;
   mongoc_client_encryption_t *clientEncryption;
   mongoc_client_t *encryptedClient;
   mongoc_collection_t *encryptedColl;
   // typeStr is DoublePrecision, DoubleNoPrecision, Date, Int, or Long
   const char *typeStr;
   char *fieldName;
   bson_value_t zero;
   bson_value_t six;
   bson_value_t thirty;
   bson_value_t twoHundred;
   bson_value_t twoHundredOne;
   mongoc_client_encryption_encrypt_range_opts_t *ro;
} ree_fixture;

static ree_fixture *
range_explicit_encryption_setup(const char *typeStr)
{
   ree_fixture *reef = (ree_fixture *)bson_malloc0(sizeof(ree_fixture));
   reef->typeStr = typeStr;
   reef->fieldName = bson_strdup_printf("encrypted%s", typeStr);
   char *filepath = bson_strdup_printf("./src/libmongoc/tests/client_side_encryption_prose/"
                                       "explicit_encryption/range-encryptedFields-%s.json",
                                       typeStr);
   bson_t *encryptedFields = get_bson_from_json_file(filepath);
   ASSERT(encryptedFields);
   bson_free(filepath);
   bson_t *key1Document =
      get_bson_from_json_file("./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                              "key1-document.json");
   ASSERT(key1Document);
   mongoc_client_t *setupClient = test_framework_new_default_client();


   /* Read the ``"_id"`` field of ``key1Document`` as ``key1ID``. */
   {
      bson_iter_t iter;
      const bson_value_t *value;

      ASSERT(bson_iter_init_find(&iter, key1Document, "_id"));
      value = bson_iter_value(&iter);
      bson_value_copy(value, &reef->key1ID);
   }

   /* Drop and create the collection ``db.explicit_encryption`` using
    * ``encryptedFields`` as an option. */
   {
      mongoc_database_t *db = mongoc_client_get_database(setupClient, "db");
      mongoc_collection_t *coll = mongoc_database_get_collection(db, "explicit_encryption");
      bson_error_t error;
      bson_t *opts;

      opts = BCON_NEW("encryptedFields", BCON_DOCUMENT(encryptedFields));

      if (!mongoc_collection_drop_with_opts(coll, opts, &error)) {
         if (error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
            test_error("unexpected error in drop: %s", error.message);
         }
      }
      mongoc_collection_destroy(coll);

      coll = mongoc_database_create_collection(db, "explicit_encryption", opts, &error);
      ASSERT_OR_PRINT(coll, error);

      mongoc_collection_destroy(coll);
      bson_destroy(opts);
      mongoc_database_destroy(db);
   }

   /* Drop and create the collection ``keyvault.datakeys``. */
   {
      mongoc_database_t *db = mongoc_client_get_database(setupClient, "keyvault");
      mongoc_collection_t *coll = mongoc_database_get_collection(db, "datakeys");
      bson_error_t error;
      bson_t iopts = BSON_INITIALIZER;
      mongoc_write_concern_t *wc;

      if (!mongoc_collection_drop(coll, &error)) {
         if (error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
            test_error("unexpected error in drop: %s", error.message);
         }
      }
      mongoc_collection_destroy(coll);

      coll = mongoc_database_create_collection(db, "datakeys", NULL /* opts */, &error);
      ASSERT_OR_PRINT(coll, error);

      /* Insert keyDocument1 with write concern majority */
      wc = mongoc_write_concern_new();
      mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
      ASSERT(mongoc_write_concern_append(wc, &iopts));
      ASSERT_OR_PRINT(mongoc_collection_insert_one(coll, key1Document, &iopts, NULL /* reply */, &error), error);

      mongoc_write_concern_destroy(wc);
      bson_destroy(&iopts);
      mongoc_collection_destroy(coll);
      mongoc_database_destroy(db);
   }

   reef->keyVaultClient = test_framework_new_default_client();

   /* Create a ClientEncryption object named ``clientEncryption`` */
   {
      mongoc_client_encryption_opts_t *ceOpts = mongoc_client_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;

      mongoc_client_encryption_opts_set_keyvault_client(ceOpts, reef->keyVaultClient);
      mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_kms_providers(ceOpts, kms_providers);

      reef->clientEncryption = mongoc_client_encryption_new(ceOpts, &error);
      ASSERT_OR_PRINT(reef->clientEncryption, error);

      bson_destroy(kms_providers);
      mongoc_client_encryption_opts_destroy(ceOpts);
   }

   /* Create a MongoClient named ``encryptedClient``. */
   {
      mongoc_auto_encryption_opts_t *aeOpts = mongoc_auto_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;

      mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
      mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kms_providers);
      mongoc_auto_encryption_opts_set_bypass_query_analysis(aeOpts, true);
      reef->encryptedClient = test_framework_new_default_client();
      ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(reef->encryptedClient, aeOpts, &error), error);

      bson_destroy(kms_providers);
      mongoc_auto_encryption_opts_destroy(aeOpts);
      reef->encryptedColl = mongoc_client_get_collection(reef->encryptedClient, "db", "explicit_encryption");
   }

   /* Create the values 0, 6, 30, 200, and 201 as BSON values. */
   {
      if (0 == strcmp("DecimalNoPrecision", typeStr) || 0 == strcmp("DecimalPrecision", typeStr)) {
         reef->zero.value_type = BSON_TYPE_DECIMAL128;
         ASSERT(bson_decimal128_from_string("0", &reef->zero.value.v_decimal128));
         reef->six.value_type = BSON_TYPE_DECIMAL128;
         ASSERT(bson_decimal128_from_string("6", &reef->six.value.v_decimal128));
         reef->thirty.value_type = BSON_TYPE_DECIMAL128;
         ASSERT(bson_decimal128_from_string("30", &reef->thirty.value.v_decimal128));
         reef->twoHundred.value_type = BSON_TYPE_DECIMAL128;
         ASSERT(bson_decimal128_from_string("200", &reef->twoHundred.value.v_decimal128));
         reef->twoHundredOne.value_type = BSON_TYPE_DECIMAL128;
         ASSERT(bson_decimal128_from_string("201", &reef->twoHundredOne.value.v_decimal128));
      } else if (0 == strcmp("DoubleNoPrecision", typeStr) || 0 == strcmp("DoublePrecision", typeStr)) {
         reef->zero.value_type = BSON_TYPE_DOUBLE;
         reef->zero.value.v_double = 0;
         reef->six.value_type = BSON_TYPE_DOUBLE;
         reef->six.value.v_double = 6;
         reef->thirty.value_type = BSON_TYPE_DOUBLE;
         reef->thirty.value.v_double = 30;
         reef->twoHundred.value_type = BSON_TYPE_DOUBLE;
         reef->twoHundred.value.v_double = 200;
         reef->twoHundredOne.value_type = BSON_TYPE_DOUBLE;
         reef->twoHundredOne.value.v_double = 201;
      } else if (0 == strcmp("Date", typeStr)) {
         reef->zero.value_type = BSON_TYPE_DATE_TIME;
         reef->zero.value.v_datetime = 0;
         reef->six.value_type = BSON_TYPE_DATE_TIME;
         reef->six.value.v_datetime = 6;
         reef->thirty.value_type = BSON_TYPE_DATE_TIME;
         reef->thirty.value.v_datetime = 30;
         reef->twoHundred.value_type = BSON_TYPE_DATE_TIME;
         reef->twoHundred.value.v_datetime = 200;
         reef->twoHundredOne.value_type = BSON_TYPE_DATE_TIME;
         reef->twoHundredOne.value.v_datetime = 201;
      } else if (0 == strcmp("Int", typeStr)) {
         reef->zero.value_type = BSON_TYPE_INT32;
         reef->zero.value.v_int32 = 0;
         reef->six.value_type = BSON_TYPE_INT32;
         reef->six.value.v_int32 = 6;
         reef->thirty.value_type = BSON_TYPE_INT32;
         reef->thirty.value.v_int32 = 30;
         reef->twoHundred.value_type = BSON_TYPE_INT32;
         reef->twoHundred.value.v_int32 = 200;
         reef->twoHundredOne.value_type = BSON_TYPE_INT32;
         reef->twoHundredOne.value.v_int32 = 201;
      } else if (0 == strcmp("Long", typeStr)) {
         reef->zero.value_type = BSON_TYPE_INT64;
         reef->zero.value.v_int64 = 0;
         reef->six.value_type = BSON_TYPE_INT64;
         reef->six.value.v_int64 = 6;
         reef->thirty.value_type = BSON_TYPE_INT64;
         reef->thirty.value.v_int64 = 30;
         reef->twoHundred.value_type = BSON_TYPE_INT64;
         reef->twoHundred.value.v_int64 = 200;
         reef->twoHundredOne.value_type = BSON_TYPE_INT64;
         reef->twoHundredOne.value.v_int64 = 201;
      } else {
         test_error("Unexpected type string: %s\n", typeStr);
      }
   }

   /* Create the RangeOpts depending on the type. */
   {
      reef->ro = mongoc_client_encryption_encrypt_range_opts_new();
      mongoc_client_encryption_encrypt_range_opts_set_sparsity(reef->ro, 1);
      mongoc_client_encryption_encrypt_range_opts_set_trim_factor(reef->ro, 1);
      if (0 == strcmp("DoubleNoPrecision", typeStr) || 0 == strcmp("DecimalNoPrecision", typeStr)) {
         // DoubleNoPrecision does not need more range options.
      } else if (0 == strcmp("DoublePrecision", typeStr) || 0 == strcmp("DecimalPrecision", typeStr)) {
         mongoc_client_encryption_encrypt_range_opts_set_min(reef->ro, &reef->zero);
         mongoc_client_encryption_encrypt_range_opts_set_max(reef->ro, &reef->twoHundred);
         mongoc_client_encryption_encrypt_range_opts_set_precision(reef->ro, 2);
      } else if (0 == strcmp("Date", typeStr) || 0 == strcmp("Int", typeStr) || 0 == strcmp("Long", typeStr)) {
         mongoc_client_encryption_encrypt_range_opts_set_min(reef->ro, &reef->zero);
         mongoc_client_encryption_encrypt_range_opts_set_max(reef->ro, &reef->twoHundred);
      } else {
         test_error("Unexpected type string: %s\n", typeStr);
      }
   }

   /* Encrypt and insert 0, 6, 30, and 200. */
   {
      mongoc_client_encryption_encrypt_opts_t *eo;
      bool ok;
      bson_error_t error;

      eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      bson_value_t *values[4];
      values[0] = &reef->zero;
      values[1] = &reef->six;
      values[2] = &reef->thirty;
      values[3] = &reef->twoHundred;

      for (size_t i = 0; i < sizeof values / sizeof values[0]; i++) {
         bson_value_t *value = values[i];
         bson_value_t ciphertext;
         ok = mongoc_client_encryption_encrypt(reef->clientEncryption, value, eo, &ciphertext, &error);
         ASSERT_OR_PRINT(ok, error);
         bson_t *doc = bson_new();
         BSON_APPEND_INT32(doc, "_id", (int32_t)i);
         BSON_APPEND_VALUE(doc, reef->fieldName, &ciphertext);
         ok = mongoc_collection_insert_one(reef->encryptedColl, doc, NULL /* opts */, NULL /* reply */, &error);
         ASSERT_OR_PRINT(ok, error);
         bson_destroy(doc);
         bson_value_destroy(&ciphertext);
      }

      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   mongoc_client_destroy(setupClient);
   bson_destroy(key1Document);
   bson_destroy(encryptedFields);
   return reef;
}

static void
range_explicit_encryption_destroy(ree_fixture *reef)
{
   if (!reef) {
      return;
   }

   bson_free(reef->fieldName);
   mongoc_client_encryption_encrypt_range_opts_destroy(reef->ro);
   mongoc_collection_destroy(reef->encryptedColl);
   mongoc_client_destroy(reef->encryptedClient);
   mongoc_client_encryption_destroy(reef->clientEncryption);
   mongoc_client_destroy(reef->keyVaultClient);
   bson_value_destroy(&reef->key1ID);
   bson_free(reef);
}

// range_explicit_encryption_assert_cursor_results asserts that the encrypted
// field in documents returned by the cursor have match a list of values. The
// variadic args are a list of const bson_value_t *. The variadic args must be
// NULL terminated.
static void
range_explicit_encryption_assert_cursor_results(ree_fixture *reef, mongoc_cursor_t *cursor, ...)
{
   const bson_t *got;
   va_list args;
   bson_error_t error;

   va_start(args, cursor);
   while (true) {
      const bson_value_t *expect = va_arg(args, const bson_value_t *);
      if (!expect) {
         break;
      }
      if (!mongoc_cursor_next(cursor, &got)) {
         if (mongoc_cursor_error(cursor, &error)) {
            test_error("Got unexpected error in mongoc_cursor_next: %s", error.message);
         }
         test_error("Expected document with value: %s but got end of cursor", bson_value_to_str(expect));
      }
      bson_iter_t goti;
      const bson_value_t *gotv;
      if (!bson_iter_init_find(&goti, got, reef->fieldName)) {
         test_error(
            "Expected to find field %s, but got %s", reef->fieldName, bson_as_canonical_extended_json(got, NULL));
      }
      gotv = bson_iter_value(&goti);
      ASSERT_BSONVALUE_EQ(expect, gotv);
   }
   va_end(args);
   if (mongoc_cursor_next(cursor, &got)) {
      test_error("Expected end of cursor, but got extra document: %s", bson_as_canonical_extended_json(got, NULL));
   }
}

static void
test_range_explicit_encryption_case1(void *ctx)
{
   // Case 1: can decrypt a payload
   const char *typeStr = (const char *)ctx;
   mongoc_client_encryption_encrypt_opts_t *eo;
   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;
   bson_value_t insertPayload;
   bson_value_t decrypted;

   eo = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
   mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
   mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
   mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

   /* Use clientEncryption.encrypt() to encrypt the value 6. */
   ok = mongoc_client_encryption_encrypt(reef->clientEncryption, &reef->six, eo, &insertPayload, &error);
   ASSERT_OR_PRINT(ok, error);

   /* Use clientEncryption to decrypt insertPayload. Assert the returned
      value equals 6. */
   ok = mongoc_client_encryption_decrypt(reef->clientEncryption, &insertPayload, &decrypted, &error);
   ASSERT_OR_PRINT(ok, error);
   ASSERT_BSONVALUE_EQ(&decrypted, &reef->six);

   bson_value_destroy(&insertPayload);
   mongoc_client_encryption_encrypt_opts_destroy(eo);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case2(void *ctx)
{
   // Case 2: can find encrypted range and return the maximum
   const char *typeStr = (const char *)ctx;
   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_t findPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      bsonBuildDecl(expr,
                    kv("$and",
                       array(                                         //
                          doc(kv(reef->fieldName,                     //
                                 doc(kv("$gte", value(reef->six))))), //
                          doc(kv(reef->fieldName, doc(kv("$lte", value(reef->twoHundred))))))));

      ok = mongoc_client_encryption_encrypt_expression(reef->clientEncryption, &expr, eo, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   // Query and check results.
   {
      bson_t *opts = tmp_bson("{'sort': { '_id': 1 }}");
      mongoc_cursor_t *cursor =
         mongoc_collection_find_with_opts(reef->encryptedColl, &findPayload, opts, NULL /* read_prefs */);
      range_explicit_encryption_assert_cursor_results(reef, cursor, &reef->six, &reef->thirty, &reef->twoHundred, NULL);
      mongoc_cursor_destroy(cursor);
   }

   bson_destroy(&findPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case3(void *ctx)
{
   // Case 3: can find encrypted range and return the minimum
   const char *typeStr = (const char *)ctx;
   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_t findPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      bsonBuildDecl(expr,
                    kv("$and",
                       array(                                          //
                          doc(kv(reef->fieldName,                      //
                                 doc(kv("$gte", value(reef->zero))))), //
                          doc(kv(reef->fieldName, doc(kv("$lte", value(reef->six))))))));

      ok = mongoc_client_encryption_encrypt_expression(reef->clientEncryption, &expr, eo, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   // Query and check results.
   {
      bson_t *opts = tmp_bson("{'sort': { '_id': 1 }}");
      mongoc_cursor_t *cursor =
         mongoc_collection_find_with_opts(reef->encryptedColl, &findPayload, opts, NULL /* read_prefs */);
      range_explicit_encryption_assert_cursor_results(reef, cursor, &reef->zero, &reef->six, NULL);
      mongoc_cursor_destroy(cursor);
   }

   bson_destroy(&findPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case4(void *ctx)
{
   // Case 4: can find encrypted range with an open range query
   const char *typeStr = (const char *)ctx;
   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_t findPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      bsonBuildDecl(expr,
                    kv("$and",
                       array(                                          //
                          doc(kv(reef->fieldName,                      //
                                 doc(kv("$gt", value(reef->thirty))))) //
                          )));

      ok = mongoc_client_encryption_encrypt_expression(reef->clientEncryption, &expr, eo, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   // Query and check results.
   {
      bson_t *opts = tmp_bson("{'sort': { '_id': 1 }}");
      mongoc_cursor_t *cursor =
         mongoc_collection_find_with_opts(reef->encryptedColl, &findPayload, opts, NULL /* read_prefs */);
      range_explicit_encryption_assert_cursor_results(reef, cursor, &reef->twoHundred, NULL);
      mongoc_cursor_destroy(cursor);
   }

   bson_destroy(&findPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case5(void *ctx)
{
   // Case 5: can run an aggregation expression inside $expr
   const char *typeStr = (const char *)ctx;
   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_t findPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);
      char *fieldPath = bson_strdup_printf("$%s", reef->fieldName);

      // Encrypt this:
      // {'$and': [ { '$lt': [ '$encrypted<Type>', 30 ] } ] } }
      bsonBuildDecl(expr,
                    kv("$and",
                       array(           //
                          doc(kv("$lt", //
                                 array(cstr(fieldPath), value(reef->thirty)))))));

      ok = mongoc_client_encryption_encrypt_expression(reef->clientEncryption, &expr, eo, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);
      bson_free(fieldPath);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   // Query and check results.
   {
      bson_t *opts = tmp_bson("{'sort': { '_id': 1 }}");
      bsonBuildDecl(filter, kv("$expr", bson(findPayload)));
      mongoc_cursor_t *cursor =
         mongoc_collection_find_with_opts(reef->encryptedColl, &filter, opts, NULL /* read_prefs */);
      range_explicit_encryption_assert_cursor_results(reef, cursor, &reef->zero, &reef->six, NULL);
      bson_destroy(&filter);
      mongoc_cursor_destroy(cursor);
   }

   bson_destroy(&findPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case6(void *ctx)
{
   // Case 6: encrypting a document greater than the maximum errors
   const char *typeStr = (const char *)ctx;

   // This test case should be skipped if the encrypted field is
   // encryptedDoubleNoPrecision or encryptedDecimalNoPrecision.
   if (0 == strcmp(typeStr, "DoubleNoPrecision") || 0 == strcmp(typeStr, "DecimalNoPrecision")) {
      MONGOC_DEBUG("skipping test");
      return;
   }

   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_value_t insertPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      ok = mongoc_client_encryption_encrypt(reef->clientEncryption, &reef->twoHundredOne, eo, &insertPayload, &error);
      ASSERT(!ok);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   bson_value_destroy(&insertPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case7(void *ctx)
{
   // Case 7: encrypting a document of a different type errors
   const char *typeStr = (const char *)ctx;

   // This test case should be skipped if the encrypted field is
   // encryptedDoubleNoPrecision or encryptedDecimalNoPrecision.
   if (0 == strcmp(typeStr, "DoubleNoPrecision") || 0 == strcmp(typeStr, "DecimalNoPrecision")) {
      MONGOC_DEBUG("skipping test");
      return;
   }

   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_value_t insertPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      bson_value_t wrongType;
      if (0 == strcmp("encryptedInt", reef->fieldName)) {
         wrongType.value_type = BSON_TYPE_DOUBLE;
         wrongType.value.v_double = 6;
      } else {
         wrongType.value_type = BSON_TYPE_INT32;
         wrongType.value.v_int32 = 6;
      }

      ok = mongoc_client_encryption_encrypt(reef->clientEncryption, &wrongType, eo, &insertPayload, &error);
      ASSERT(!ok);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   bson_value_destroy(&insertPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_range_explicit_encryption_case8(void *ctx)
{
   // Case 8: setting precision errors if the type is not a double
   const char *typeStr = (const char *)ctx;

   // This test case should be skipped if the encrypted field is
   // encryptedDoublePrecision or encryptedDoubleNoPrecision or
   // encryptedDecimalPrecision or encryptedDecimalNoPrecision.
   if (0 == strcmp(typeStr, "DoubleNoPrecision") || 0 == strcmp(typeStr, "DoublePrecision") ||
       0 == strcmp(typeStr, "DecimalPrecision") || 0 == strcmp(typeStr, "DecimalNoPrecision")) {
      MONGOC_DEBUG("skipping test");
      return;
   }

   ree_fixture *reef = range_explicit_encryption_setup(typeStr);
   bool ok;
   bson_error_t error;

   // Encrypt.
   bson_value_t insertPayload;
   {
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &reef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);
      mongoc_client_encryption_encrypt_range_opts_set_precision(reef->ro, 2);
      mongoc_client_encryption_encrypt_opts_set_range_opts(eo, reef->ro);

      ok = mongoc_client_encryption_encrypt(reef->clientEncryption, &reef->six, eo, &insertPayload, &error);
      ASSERT(!ok);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   bson_value_destroy(&insertPayload);
   range_explicit_encryption_destroy(reef);
}

static void
test_explicit_encryption_case1(void *unused)
{
   /* Case 1: can insert encrypted indexed and find */
   bson_error_t error;
   bool ok;
   mongoc_client_encryption_encrypt_opts_t *eopts;
   bson_value_t plaintext = {0};
   ee_fixture *eef = explicit_encryption_setup();

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "encrypted indexed value";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   /* Use ``encryptedClient`` to insert the document ``{ "encryptedIndexed":
    * <insertPayload> }``. */
   {
      bson_value_t insertPayload;
      bson_t to_insert = BSON_INITIALIZER;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 0);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &insertPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&to_insert, "encryptedIndexed", &insertPayload));

      ok = mongoc_collection_insert_one(eef->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ok, error);

      bson_value_destroy(&insertPayload);
      bson_destroy(&to_insert);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
   }

   /* Use ``encryptedClient`` to run a "find" operation on the
    * ``db.explicit_encryption`` collection with the filter ``{
    * "encryptedIndexed": <findPayload> }``. */
   {
      bson_value_t findPayload;
      mongoc_cursor_t *cursor;
      bson_t filter = BSON_INITIALIZER;
      const bson_t *got;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_query_type(eopts, MONGOC_ENCRYPT_QUERY_TYPE_EQUALITY);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 0);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&filter, "encryptedIndexed", &findPayload));

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &filter, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(mongoc_cursor_next(cursor, &got));
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_MATCH(got, "{ 'encryptedIndexed': 'encrypted indexed value' }");
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected one document to be returned, got more than one");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
      bson_destroy(&filter);
   }

   explicit_encryption_destroy(eef);
}

static void
test_explicit_encryption_case2(void *unused)
{
   /* Case 2: can insert encrypted indexed and find with non-zero contention */
   bson_error_t error;
   bool ok;
   mongoc_client_encryption_encrypt_opts_t *eopts;
   bson_value_t plaintext = {0};
   int i = 0;
   ee_fixture *eef = explicit_encryption_setup();

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "encrypted indexed value";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   /* Insert 10 documents ``{ "encryptedIndexed": <insertPayload> }`` with
    * contention factor 10. */
   for (i = 0; i < 10; i++) {
      bson_value_t insertPayload;
      bson_t to_insert = BSON_INITIALIZER;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 10);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &insertPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&to_insert, "encryptedIndexed", &insertPayload));

      ok = mongoc_collection_insert_one(eef->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ok, error);

      bson_value_destroy(&insertPayload);
      bson_destroy(&to_insert);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
   }

   /* Find with contention factor of 0. Expect < 10 documents returned.
    */
   {
      bson_value_t findPayload;
      mongoc_cursor_t *cursor;
      bson_t filter = BSON_INITIALIZER;
      const bson_t *got;
      int got_count = 0;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_query_type(eopts, MONGOC_ENCRYPT_QUERY_TYPE_EQUALITY);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 0);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&filter, "encryptedIndexed", &findPayload));

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &filter, NULL /* opts */, NULL /* read_prefs */);

      while (mongoc_cursor_next(cursor, &got)) {
         got_count++;
         ASSERT_MATCH(got, "{ 'encryptedIndexed': 'encrypted indexed value' }");
      }
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_CMPINT(got_count, <, 10);

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
      bson_destroy(&filter);
   }

   /* Find with contention factor of 10. Expect all 10 documents returned. */
   {
      bson_value_t findPayload;
      mongoc_cursor_t *cursor;
      bson_t filter = BSON_INITIALIZER;
      const bson_t *got;
      int got_count = 0;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_query_type(eopts, MONGOC_ENCRYPT_QUERY_TYPE_EQUALITY);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 10);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &findPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&filter, "encryptedIndexed", &findPayload));

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &filter, NULL /* opts */, NULL /* read_prefs */);

      while (mongoc_cursor_next(cursor, &got)) {
         got_count++;
         ASSERT_MATCH(got, "{ 'encryptedIndexed': 'encrypted indexed value' }");
      }
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_CMPINT(got_count, ==, 10);

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
      bson_destroy(&filter);
   }

   explicit_encryption_destroy(eef);
}

static void
test_explicit_encryption_case3(void *unused)
{
   /* Case 3: can insert encrypted unindexed */
   bson_error_t error;
   bool ok;
   mongoc_client_encryption_encrypt_opts_t *eopts;
   bson_value_t plaintext = {0};
   ee_fixture *eef = explicit_encryption_setup();

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "encrypted unindexed value";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   /* Use ``encryptedClient`` to insert the document ``{ "_id": 1,
    * "encryptedUnindexed": <insertPayload> }``. */
   {
      bson_value_t insertPayload;
      bson_t to_insert = BSON_INITIALIZER;

      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_UNINDEXED);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &insertPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_INT32(&to_insert, "_id", 1));
      ASSERT(BSON_APPEND_VALUE(&to_insert, "encryptedUnindexed", &insertPayload));

      ok = mongoc_collection_insert_one(eef->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ok, error);

      bson_value_destroy(&insertPayload);
      bson_destroy(&to_insert);
      mongoc_client_encryption_encrypt_opts_destroy(eopts);
   }

   /* Use ``encryptedClient`` to run a "find" operation on the
    * ``db.explicit_encryption`` collection with the filter ``{ "_id": 1 }``. */
   {
      mongoc_cursor_t *cursor;
      bson_t filter = BSON_INITIALIZER;
      const bson_t *got;

      ASSERT(BSON_APPEND_INT32(&filter, "_id", 1));

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &filter, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(mongoc_cursor_next(cursor, &got));
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_MATCH(got, "{ 'encryptedUnindexed': 'encrypted unindexed value' }");
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected one document to be returned, got more than one");

      mongoc_cursor_destroy(cursor);
      bson_destroy(&filter);
   }

   explicit_encryption_destroy(eef);
}

static void
test_explicit_encryption_case4(void *unused)
{
   /* Case 4: can roundtrip encrypted indexed */
   bson_error_t error;
   bool ok;
   mongoc_client_encryption_encrypt_opts_t *eopts;
   bson_value_t plaintext = {0};
   bson_value_t payload;
   ee_fixture *eef = explicit_encryption_setup();

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "encrypted indexed value";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   /* Use ``clientEncryption`` to encrypt the value "encrypted indexed value".
    */
   {
      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_INDEXED);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eopts, 0);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &payload, &error);
      ASSERT_OR_PRINT(ok, error);

      mongoc_client_encryption_encrypt_opts_destroy(eopts);
   }

   /* Use ``clientEncryption`` to decrypt ``payload`` */
   {
      bson_value_t got;

      ok = mongoc_client_encryption_decrypt(eef->clientEncryption, &payload, &got, &error);
      ASSERT_OR_PRINT(ok, error);
      ASSERT(got.value_type == BSON_TYPE_UTF8);
      ASSERT_CMPSTR(got.value.v_utf8.str, "encrypted indexed value");
      bson_value_destroy(&got);
   }

   bson_value_destroy(&payload);
   explicit_encryption_destroy(eef);
}

static void
test_explicit_encryption_case5(void *unused)
{
   /* Case 5: can roundtrip encrypted unindexed */
   bson_error_t error;
   bool ok;
   mongoc_client_encryption_encrypt_opts_t *eopts;
   bson_value_t plaintext = {0};
   bson_value_t payload;
   ee_fixture *eef = explicit_encryption_setup();

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "encrypted unindexed value";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   /* Use ``clientEncryption`` to encrypt the value "encrypted unindexed value".
    */
   {
      eopts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eopts, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eopts, MONGOC_ENCRYPT_ALGORITHM_UNINDEXED);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eopts, &payload, &error);
      ASSERT_OR_PRINT(ok, error);

      mongoc_client_encryption_encrypt_opts_destroy(eopts);
   }

   /* Use ``clientEncryption`` to decrypt ``payload`` */
   {
      bson_value_t got;

      ok = mongoc_client_encryption_decrypt(eef->clientEncryption, &payload, &got, &error);
      ASSERT_OR_PRINT(ok, error);
      ASSERT(got.value_type == BSON_TYPE_UTF8);
      ASSERT_CMPSTR(got.value.v_utf8.str, "encrypted unindexed value");
      bson_value_destroy(&got);
   }

   bson_value_destroy(&payload);
   explicit_encryption_destroy(eef);
}

static void
test_explicit_encryption_text(void *unused)
{
   bson_error_t error;
   bool ok;
   bson_value_t plaintext = {0};
   ee_fixture *eef =
      explicit_encryption_setup_full("./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                     "encryptedFields-prefix-suffix.json",
                                     "./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                     "key1-document.json");

   BSON_UNUSED(unused);

   plaintext.value_type = BSON_TYPE_UTF8;
   plaintext.value.v_utf8.str = "foobarbaz";
   plaintext.value.v_utf8.len = (uint32_t)strlen(plaintext.value.v_utf8.str);

   mongoc_client_encryption_encrypt_text_prefix_opts_t *popts = mongoc_client_encryption_encrypt_text_prefix_opts_new();
   mongoc_client_encryption_encrypt_text_prefix_opts_set_str_max_query_length(popts, 10);
   mongoc_client_encryption_encrypt_text_prefix_opts_set_str_min_query_length(popts, 2);
   mongoc_client_encryption_encrypt_text_suffix_opts_t *sopts = mongoc_client_encryption_encrypt_text_suffix_opts_new();
   mongoc_client_encryption_encrypt_text_suffix_opts_set_str_max_query_length(sopts, 10);
   mongoc_client_encryption_encrypt_text_suffix_opts_set_str_min_query_length(sopts, 2);
   mongoc_client_encryption_encrypt_text_substring_opts_t *ssopts =
      mongoc_client_encryption_encrypt_text_substring_opts_new();
   mongoc_client_encryption_encrypt_text_substring_opts_set_str_max_length(ssopts, 10);
   mongoc_client_encryption_encrypt_text_substring_opts_set_str_max_query_length(ssopts, 10);
   mongoc_client_encryption_encrypt_text_substring_opts_set_str_min_query_length(ssopts, 2);

   /* Prefix and suffix tests */
   /* Insert 'foobarbaz' with both prefix and suffix indexing */
   {
      bson_value_t insertPayload;
      bson_t to_insert = BSON_INITIALIZER;

      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_prefix(topts, popts);
      mongoc_client_encryption_encrypt_text_opts_set_suffix(topts, sopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &insertPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&to_insert, "encryptedText", &insertPayload));

      ok = mongoc_collection_insert_one(eef->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ok, error);

      bson_value_destroy(&insertPayload);
      bson_destroy(&to_insert);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Find the document using the 'foo' prefix */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_PREFIXPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_prefix(topts, popts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "foo";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrStartsWith", doc(kv("input", cstr("$encryptedText")), kv("prefix", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(mongoc_cursor_next(cursor, &got));
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_MATCH(got, "{ 'encryptedText': 'foobarbaz' }");
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected one document to be returned, got more than one");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Find the document using the 'baz' suffix */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_SUFFIXPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_suffix(topts, sopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "baz";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrEndsWith", doc(kv("input", cstr("$encryptedText")), kv("suffix", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(mongoc_cursor_next(cursor, &got));
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_MATCH(got, "{ 'encryptedText': 'foobarbaz' }");
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected one document to be returned, got more than one");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Ensure querying for a 'foo' suffix returns no documents */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_SUFFIXPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_suffix(topts, sopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "foo";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrEndsWith", doc(kv("input", cstr("$encryptedText")), kv("suffix", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected no documents to be returned, got some");
      mongoc_cursor_next(cursor, &got);

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Ensure querying for a 'baz' prefix returns no documents */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_PREFIXPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_prefix(topts, popts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "baz";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrStartsWith", doc(kv("input", cstr("$encryptedText")), kv("prefix", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected no documents to be returned, got some");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }


   /* Substring tests */
   explicit_encryption_destroy(eef);
   eef = explicit_encryption_setup_full("./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                        "encryptedFields-substring.json",
                                        "./src/libmongoc/tests/client_side_encryption_prose/explicit_encryption/"
                                        "key1-document.json");
   /* Insert 'foobarbaz' with substring indexing */
   {
      bson_value_t insertPayload;
      bson_t to_insert = BSON_INITIALIZER;

      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_substring(topts, ssopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "foobarbaz";
      plaintext.value.v_utf8.len = 9;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &insertPayload, &error);
      ASSERT_OR_PRINT(ok, error);

      ASSERT(BSON_APPEND_VALUE(&to_insert, "encryptedText", &insertPayload));

      ok = mongoc_collection_insert_one(eef->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
      ASSERT_OR_PRINT(ok, error);

      bson_value_destroy(&insertPayload);
      bson_destroy(&to_insert);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Find the document using the 'bar' substring */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_SUBSTRINGPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_substring(topts, ssopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "bar";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrContains", doc(kv("input", cstr("$encryptedText")), kv("substring", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(mongoc_cursor_next(cursor, &got));
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT_MATCH(got, "{ 'encryptedText': 'foobarbaz' }");
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected one document to be returned, got more than one");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }

   /* Ensure querying for a 'qux' substring returns no documents */
   {
      bson_value_t findPayload;
      mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eo, &eef->key1ID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_TEXTPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_query_type(eo, MONGOC_ENCRYPT_QUERY_TYPE_SUBSTRINGPREVIEW);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eo, 0);

      mongoc_client_encryption_encrypt_text_opts_t *topts = mongoc_client_encryption_encrypt_text_opts_new();
      mongoc_client_encryption_encrypt_text_opts_set_substring(topts, ssopts);
      mongoc_client_encryption_encrypt_text_opts_set_diacritic_sensitive(topts, true);
      mongoc_client_encryption_encrypt_text_opts_set_case_sensitive(topts, true);
      mongoc_client_encryption_encrypt_opts_set_text_opts(eo, topts);

      plaintext.value.v_utf8.str = "qux";
      plaintext.value.v_utf8.len = 3;
      ok = mongoc_client_encryption_encrypt(eef->clientEncryption, &plaintext, eo, &findPayload, &error);

      bsonBuildDecl(
         expr,
         kv("$expr",
            doc(kv("$encStrContains", doc(kv("input", cstr("$encryptedText")), kv("substring", value(findPayload)))))));
      ASSERT_OR_PRINT(ok, error);

      mongoc_cursor_t *cursor;
      const bson_t *got;

      cursor = mongoc_collection_find_with_opts(eef->encryptedColl, &expr, NULL /* opts */, NULL /* read_prefs */);
      ASSERT(!mongoc_cursor_next(cursor, &got) && "expected no documents to be returned, got some");

      bson_value_destroy(&findPayload);
      mongoc_cursor_destroy(cursor);
      bson_destroy(&expr);
      mongoc_client_encryption_encrypt_text_opts_destroy(topts);
      mongoc_client_encryption_encrypt_opts_destroy(eo);
   }
   mongoc_client_encryption_encrypt_text_prefix_opts_destroy(popts);
   mongoc_client_encryption_encrypt_text_suffix_opts_destroy(sopts);
   mongoc_client_encryption_encrypt_text_substring_opts_destroy(ssopts);
   explicit_encryption_destroy(eef);
}

static void
_test_unique_index_on_keyaltnames_setup(void (*test_case)(mongoc_client_encryption_t *, const bson_value_t *keyid))
{
   mongoc_client_t *client = NULL;
   mongoc_database_t *keyvault = NULL;
   mongoc_client_encryption_t *client_encryption = NULL;
   bson_value_t existing_key;
   bson_error_t error;

   /* Create a MongoClient object (referred to as client). */
   client = test_framework_new_default_client();
   keyvault = mongoc_client_get_database(client, "keyvault");

   /* Using client, drop the collection keyvault.datakeys. */
   {
      mongoc_collection_t *const datakeys = mongoc_database_get_collection(keyvault, "datakeys");
      mongoc_collection_drop(datakeys, &error);
      mongoc_collection_destroy(datakeys);
   }

   /* Using client, create a unique index on keyAltNames with a partial index
    * filter for only documents where keyAltNames exists. */
   {
      mongoc_collection_t *const datakeys = mongoc_database_get_collection(keyvault, "datakeys");

      mongoc_index_model_t *im =
         mongoc_index_model_new(tmp_bson("{'keyAltNames': 1}"), tmp_bson(BSON_STR({
                                   "name" : "keyAltNames_1",
                                   "unique" : true,
                                   "partialFilterExpression" : {"keyAltNames" : {"$exists" : true}}
                                })));

      bson_t *const command_opts = tmp_bson("{'writeConcern': { 'w': 'majority' }}");

      ASSERT_OR_PRINT(
         mongoc_collection_create_indexes_with_opts(datakeys, &im, 1, command_opts, NULL /* reply */, &error), error);
      mongoc_index_model_destroy(im);
      mongoc_collection_destroy(datakeys);
   }

   /* Create a ClientEncryption object (referred to as client_encryption) with
    * client set as the keyVaultClient. */
   {
      mongoc_client_encryption_opts_t *const client_encryption_opts = mongoc_client_encryption_opts_new();
      bson_t *const kms_providers = _make_kms_providers(true /* aws */, true /* local */);
      bson_t *const tls_opts = _make_tls_opts();

      mongoc_client_encryption_opts_set_kms_providers(client_encryption_opts, kms_providers);
      mongoc_client_encryption_opts_set_tls_opts(client_encryption_opts, tls_opts);
      mongoc_client_encryption_opts_set_keyvault_namespace(client_encryption_opts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_keyvault_client(client_encryption_opts, client);
      client_encryption = mongoc_client_encryption_new(client_encryption_opts, &error);
      ASSERT_OR_PRINT(client_encryption, error);

      mongoc_client_encryption_opts_destroy(client_encryption_opts);
      bson_destroy(kms_providers);
      bson_destroy(tls_opts);
   }

   /* Using client_encryption, create a data key with a local KMS provider
    * and the keyAltName "def" (referred to as "the existing key"). */
   {
      mongoc_client_encryption_datakey_opts_t *const opts = mongoc_client_encryption_datakey_opts_new();
      const char *const keyaltname[] = {"def"};

      mongoc_client_encryption_datakey_opts_set_keyaltnames(opts, (char **)keyaltname, 1u);

      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(client_encryption, "local", opts, &existing_key, &error),
                      error);

      mongoc_client_encryption_datakey_opts_destroy(opts);
   }

   test_case(client_encryption, &existing_key);

   mongoc_client_destroy(client);
   mongoc_database_destroy(keyvault);
   mongoc_client_encryption_destroy(client_encryption);
   bson_value_destroy(&existing_key);
}

static void
_test_unique_index_on_keyaltnames_case_1(mongoc_client_encryption_t *client_encryption,
                                         const bson_value_t *existing_key)
{
   bson_error_t error;

   BSON_UNUSED(existing_key);

   /* Step 1: Use client_encryption to create a new local data key with a
    * keyAltName "abc" and assert the operation does not fail. */
   {
      mongoc_client_encryption_datakey_opts_t *const opts = mongoc_client_encryption_datakey_opts_new();
      const char *const keyaltname[] = {"abc"};
      bson_value_t keyid;

      mongoc_client_encryption_datakey_opts_set_keyaltnames(opts, (char **)keyaltname, 1u);

      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(client_encryption, "local", opts, &keyid, &error), error);

      mongoc_client_encryption_datakey_opts_destroy(opts);
      bson_value_destroy(&keyid);
   }

   /* Step 2: Repeat Step 1 and assert the operation fails due to a duplicate
    * key server error (error code 11000). */
   {
      mongoc_client_encryption_datakey_opts_t *const opts = mongoc_client_encryption_datakey_opts_new();
      const char *const keyaltname[] = {"abc"};
      bson_value_t keyid;

      mongoc_client_encryption_datakey_opts_set_keyaltnames(opts, (char **)keyaltname, 1u);

      ASSERT(!mongoc_client_encryption_create_datakey(client_encryption, "local", opts, &keyid, &error));
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_COLLECTION, MONGOC_ERROR_DUPLICATE_KEY, "keyAltNames: \"abc\"");

      mongoc_client_encryption_datakey_opts_destroy(opts);
      bson_value_destroy(&keyid);
   }

   /* Step 3: Use client_encryption to create a new local data key with a
    * keyAltName "def" and assert the operation fails due to a duplicate key
    * server error (error code 11000). */
   {
      mongoc_client_encryption_datakey_opts_t *const opts = mongoc_client_encryption_datakey_opts_new();
      const char *const keyaltname[] = {"def"};
      bson_value_t keyid;

      mongoc_client_encryption_datakey_opts_set_keyaltnames(opts, (char **)keyaltname, 1u);

      ASSERT(!mongoc_client_encryption_create_datakey(client_encryption, "local", opts, &keyid, &error));
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_COLLECTION, MONGOC_ERROR_DUPLICATE_KEY, "keyAltNames: \"def\"");

      mongoc_client_encryption_datakey_opts_destroy(opts);
      bson_value_destroy(&keyid);
   }
}

static void
_test_unique_index_on_keyaltnames_case_2(mongoc_client_encryption_t *client_encryption,
                                         const bson_value_t *existing_key)
{
   bson_value_t new_key;
   bson_error_t error;
   mongoc_client_encryption_datakey_opts_t *const opts = mongoc_client_encryption_datakey_opts_new();

   /* Step 1: Use client_encryption to create a new local data key and assert
    * the operation does not fail. */
   ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(client_encryption, "local", opts, &new_key, &error), error);

   /* Step 2: Use client_encryption to add a keyAltName "abc" to the key created
    * in Step 1 and assert the operation does not fail. */
   {
      bson_t key_doc;
      ASSERT_OR_PRINT(mongoc_client_encryption_add_key_alt_name(client_encryption, &new_key, "abc", &key_doc, &error),
                      error);
      bson_destroy(&key_doc);
   }

   /* Step 3: Repeat Step 2, assert the operation does not fail, and assert the
    * returned key document contains the keyAltName "abc" added in Step 2. */
   {
      bson_t key_doc;
      ASSERT_OR_PRINT(mongoc_client_encryption_add_key_alt_name(client_encryption, &new_key, "abc", &key_doc, &error),
                      error);
      ASSERT(match_bson(&key_doc, tmp_bson("{'keyAltNames': ['abc']}"), false));
      bson_destroy(&key_doc);
   }

   /* Step 4: Use client_encryption to add a keyAltName "def" to the key created
    * in Step 1 and assert the operation fails due to a duplicate key server
    * error (error code 11000). */
   {
      bson_t key_doc;
      ASSERT(!mongoc_client_encryption_add_key_alt_name(client_encryption, &new_key, "def", &key_doc, &error));
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_QUERY, MONGOC_ERROR_DUPLICATE_KEY, "keyAltNames: \"def\"");
      bson_destroy(&key_doc);
   }

   /* Step 5: Use client_encryption to add a keyAltName "def" to the existing
    * key, assert the operation does not fail, and assert the returned key
    * document contains the keyAltName "def" added during Setup. */
   {
      bson_t key_doc;
      ASSERT_OR_PRINT(
         mongoc_client_encryption_add_key_alt_name(client_encryption, existing_key, "def", &key_doc, &error), error);
      ASSERT(match_bson(&key_doc, tmp_bson("{'keyAltNames': ['def']}"), false));
      bson_destroy(&key_doc);
   }

   bson_value_destroy(&new_key);
   mongoc_client_encryption_datakey_opts_destroy(opts);
}

/* Prose Test 13: Unique Index on keyAltNames */
static void
test_unique_index_on_keyaltnames(void *unused)
{
   BSON_UNUSED(unused);

   _test_unique_index_on_keyaltnames_setup(_test_unique_index_on_keyaltnames_case_1);

   _test_unique_index_on_keyaltnames_setup(_test_unique_index_on_keyaltnames_case_2);
}

typedef struct {
   mongoc_client_t *setupClient;
   mongoc_client_t *encryptedClient;
   mongoc_collection_t *encryptedColl;
   bson_value_t ciphertext;
   bson_value_t malformedCiphertext;
   /* aggEvent is the CommandSucceeded or CommandFailed event observed for the
    * 'aggregate' command run in the test. */
   struct {
      const char *gotType; /* "none", "succeeded", or "failed" */
      bson_error_t gotFailedError;
      bson_t *gotSucceededReply;
   } aggEvent;
} decryption_events_fixture;

static void
decryption_events_succeeded_cb(const mongoc_apm_command_succeeded_t *event)
{
   decryption_events_fixture *def = (decryption_events_fixture *)mongoc_apm_command_succeeded_get_context(event);
   /* Only match the 'aggregate' command. */
   if (0 != strcmp(mongoc_apm_command_succeeded_get_command_name(event), "aggregate")) {
      return;
   }
   ASSERT_CMPSTR(def->aggEvent.gotType, "none");
   def->aggEvent.gotType = "succeeded";
   def->aggEvent.gotSucceededReply = bson_copy(mongoc_apm_command_succeeded_get_reply(event));
}

static void
decryption_events_failed_cb(const mongoc_apm_command_failed_t *event)
{
   decryption_events_fixture *def = (decryption_events_fixture *)mongoc_apm_command_failed_get_context(event);

   /* Only match the 'aggregate' command. */
   if (0 != strcmp(mongoc_apm_command_failed_get_command_name(event), "aggregate")) {
      return;
   }
   ASSERT_CMPSTR(def->aggEvent.gotType, "none");
   def->aggEvent.gotType = "failed";
   mongoc_apm_command_failed_get_error(event, &def->aggEvent.gotFailedError);
}

decryption_events_fixture *
decryption_events_setup(void)
{
   decryption_events_fixture *def = (decryption_events_fixture *)bson_malloc0(sizeof(decryption_events_fixture));
   mongoc_client_encryption_t *clientEncryption;
   bson_value_t keyID;

   def->setupClient = test_framework_new_default_client();
   def->aggEvent.gotType = "none";

   /* Drop and create the collection ``db.decryption_events`` */
   {
      mongoc_collection_t *coll = mongoc_client_get_collection(def->setupClient, "db", "decryption_events");
      bson_error_t error;

      if (!mongoc_collection_drop(coll, &error)) {
         if (error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
            test_error("unexpected error in drop: %s", error.message);
         }
      }
      mongoc_collection_destroy(coll);
   }

   /* Create a ClientEncryption object */
   {
      mongoc_client_encryption_opts_t *ceOpts = mongoc_client_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;

      mongoc_client_encryption_opts_set_keyvault_client(ceOpts, def->setupClient);
      mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_kms_providers(ceOpts, kms_providers);

      clientEncryption = mongoc_client_encryption_new(ceOpts, &error);
      ASSERT_OR_PRINT(clientEncryption, error);

      bson_destroy(kms_providers);
      mongoc_client_encryption_opts_destroy(ceOpts);
   }

   /* Create a data key. */
   {
      mongoc_client_encryption_datakey_opts_t *dkOpts;
      bson_error_t error;
      bool res;

      dkOpts = mongoc_client_encryption_datakey_opts_new();

      res = mongoc_client_encryption_create_datakey(clientEncryption, "local", dkOpts, &keyID, &error);
      ASSERT_OR_PRINT(res, error);

      mongoc_client_encryption_datakey_opts_destroy(dkOpts);
   }

   /* Create a valid ciphertext. */
   {
      mongoc_client_encryption_encrypt_opts_t *eOpts;
      bson_error_t error;
      bson_value_t plaintext;
      eOpts = mongoc_client_encryption_encrypt_opts_new();
      plaintext.value_type = BSON_TYPE_UTF8;
      plaintext.value.v_utf8.str = "hello";

      const size_t len = strlen(plaintext.value.v_utf8.str);
      ASSERT(mlib_in_range(uint32_t, len));

      plaintext.value.v_utf8.len = (uint32_t)len;

      mongoc_client_encryption_encrypt_opts_set_algorithm(eOpts, MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
      mongoc_client_encryption_encrypt_opts_set_keyid(eOpts, &keyID);

      ASSERT_OR_PRINT(mongoc_client_encryption_encrypt(clientEncryption, &plaintext, eOpts, &def->ciphertext, &error),
                      error);

      mongoc_client_encryption_encrypt_opts_destroy(eOpts);
   }

   /* Create a malformed ciphertext. */
   {
      bson_value_copy(&def->ciphertext, &def->malformedCiphertext);
      ASSERT(def->ciphertext.value_type == BSON_TYPE_BINARY);
      /* Change the last data byte to make malformed. The last data byte is
       * part of the HMAC tag. */
      def->malformedCiphertext.value.v_binary.data[def->malformedCiphertext.value.v_binary.data_len - 1]++;
   }

   /* Create a MongoClient with automatic decryption. */
   {
      mongoc_auto_encryption_opts_t *aeOpts = mongoc_auto_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);
      bson_error_t error;
      mongoc_uri_t *uri;

      mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
      mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kms_providers);
      uri = test_framework_get_uri();
      /* disable retryable reads so only one event is emitted on failure. */
      mongoc_uri_set_option_as_bool(uri, MONGOC_URI_RETRYREADS, false);
      def->encryptedClient = test_framework_client_new_from_uri(uri, NULL /* api */);
      test_framework_set_ssl_opts(def->encryptedClient);

      ASSERT(mongoc_client_set_error_api(def->encryptedClient, MONGOC_ERROR_API_VERSION_2));
      ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(def->encryptedClient, aeOpts, &error), error);

      def->encryptedColl = mongoc_client_get_collection(def->encryptedClient, "db", "decryption_events");

      bson_destroy(kms_providers);
      mongoc_auto_encryption_opts_destroy(aeOpts);
      mongoc_uri_destroy(uri);
   }

   /* Monitor for CommandSucceeded and CommandFailed events. */
   {
      mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new();
      mongoc_apm_set_command_succeeded_cb(cbs, decryption_events_succeeded_cb);
      mongoc_apm_set_command_failed_cb(cbs, decryption_events_failed_cb);
      mongoc_client_set_apm_callbacks(def->encryptedClient, cbs, def);
      mongoc_apm_callbacks_destroy(cbs);
   }

   mongoc_client_encryption_destroy(clientEncryption);
   bson_value_destroy(&keyID);
   return def;
}

static void
decryption_events_fixture_destroy(decryption_events_fixture *def)
{
   mongoc_client_destroy(def->setupClient);
   mongoc_client_destroy(def->encryptedClient);
   mongoc_collection_destroy(def->encryptedColl);
   bson_value_destroy(&def->ciphertext);
   bson_value_destroy(&def->malformedCiphertext);
   bson_destroy(def->aggEvent.gotSucceededReply);
   bson_free(def);
}

/* Prose test 14: Case 1: Command Error */
/* test_decryption_events_command_error is a regression test for CDRIVER-4401.
 * Send a command on an encrypted client resulting in a { 'ok': 0 } reply.
 * Expect an error returned and a CommandFailed event to be emitted. */
static void
test_decryption_events_case1(void *unused)
{
   bool got;
   bson_error_t error;
   decryption_events_fixture *def = decryption_events_setup();
   const bson_t *found;
   mongoc_cursor_t *cursor;

   BSON_UNUSED(unused);

   got = mongoc_client_command_simple(def->setupClient,
                                      "admin",
                                      tmp_bson("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
                                               "'data': {'errorCode': 123, 'failCommands': ['aggregate']}}"),
                                      NULL /* read prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(got, error);

   cursor = mongoc_collection_aggregate(
      def->encryptedColl, MONGOC_QUERY_NONE, tmp_bson("{}"), NULL /* opts */, NULL /* read prefs */);

   got = mongoc_cursor_next(cursor, &found);
   ASSERT_WITH_MSG(!got, "Expected error in mongoc_cursor_next, but got success");

   ASSERT(mongoc_cursor_error(cursor, &error));

   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 123, "failpoint");

   ASSERT_CMPSTR(def->aggEvent.gotType, "failed");
   ASSERT_ERROR_CONTAINS(def->aggEvent.gotFailedError, MONGOC_ERROR_SERVER, 123, "failpoint");
   ASSERT(!got);
   mongoc_cursor_destroy(cursor);
   decryption_events_fixture_destroy(def);
}

/* Prose test 14: Case 2: Network Error */
/* test_decryption_events_network_error is a regression test for CDRIVER-4401.
 * Send a command on an encrypted client resulting in a network error.
 * Expect an error returned and a CommandFailed event to be emitted. */
static void
test_decryption_events_case2(void *unused)

{
   bool got;
   bson_error_t error;
   decryption_events_fixture *def = decryption_events_setup();
   const bson_t *found;
   mongoc_cursor_t *cursor;

   BSON_UNUSED(unused);

   got = mongoc_client_command_simple(def->setupClient,
                                      "admin",
                                      tmp_bson("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
                                               "'data': {'errorCode': 123, 'closeConnection': true, "
                                               "'failCommands': ['aggregate']}}"),
                                      NULL /* read prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(got, error);

   cursor = mongoc_collection_aggregate(
      def->encryptedColl, MONGOC_QUERY_NONE, tmp_bson("{}"), NULL /* opts */, NULL /* read prefs */);
   got = mongoc_cursor_next(cursor, &found);
   ASSERT_WITH_MSG(!got, "Expected error in mongoc_cursor_next, but got success");
   ASSERT(mongoc_cursor_error(cursor, &error));
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "socket error");

   ASSERT_CMPSTR(def->aggEvent.gotType, "failed");
   ASSERT_ERROR_CONTAINS(def->aggEvent.gotFailedError, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "socket error");
   ASSERT(!got);
   mongoc_cursor_destroy(cursor);
   decryption_events_fixture_destroy(def);
}

/* Prose test 14: Case 3: Decrypt Error. */
/* test_decryption_events_decrypt_error is a regression test for CDRIVER-4401.
 * Decrypt a reply with a malformed ciphertext.
 * Expect an error returned and a CommandSucceeded event to be emitted with
 * ciphertext. */
static void
test_decryption_events_case3(void *unused)
{
   bool got;
   bson_error_t error;
   decryption_events_fixture *def = decryption_events_setup();
   bson_t to_insert = BSON_INITIALIZER;
   const bson_t *found;
   mongoc_cursor_t *cursor;

   BSON_UNUSED(unused);

   BSON_APPEND_VALUE(&to_insert, "encrypted", &def->malformedCiphertext);

   got = mongoc_collection_insert_one(def->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(got, error);

   cursor = mongoc_collection_aggregate(
      def->encryptedColl, MONGOC_QUERY_NONE, tmp_bson("{}"), NULL /* opts */, NULL /* read prefs */);
   got = mongoc_cursor_next(cursor, &found);
   ASSERT_WITH_MSG(!got, "Expected error in mongoc_cursor_next, but got success");

   ASSERT(mongoc_cursor_error(cursor, &error));

   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "HMAC validation failure");

   ASSERT_CMPSTR(def->aggEvent.gotType, "succeeded");
   ASSERT_MATCH(def->aggEvent.gotSucceededReply,
                "{ 'cursor' : { 'firstBatch' : [ { 'encrypted': { "
                "'$$type': 'binData' }} ] } }");

   mongoc_cursor_destroy(cursor);
   bson_destroy(&to_insert);
   decryption_events_fixture_destroy(def);
}

/* Prose test 14: Case 4: Decrypt Success. */
/* test_decryption_events_decrypt_success is a regression test for CDRIVER-4401.
 * Decrypt a reply with a valid ciphertext.
 * Expect a successful return and a CommandSucceeded event to be emitted with
 * ciphertext. */
static void
test_decryption_events_case4(void *unused)
{
   bool got;
   bson_error_t error;
   decryption_events_fixture *def = decryption_events_setup();
   bson_t to_insert = BSON_INITIALIZER;
   const bson_t *found;
   mongoc_cursor_t *cursor;

   BSON_UNUSED(unused);

   BSON_APPEND_VALUE(&to_insert, "encrypted", &def->ciphertext);

   got = mongoc_collection_insert_one(def->encryptedColl, &to_insert, NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(got, error);

   cursor = mongoc_collection_aggregate(
      def->encryptedColl, MONGOC_QUERY_NONE, tmp_bson("{}"), NULL /* opts */, NULL /* read prefs */);
   got = mongoc_cursor_next(cursor, &found);
   ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
   ASSERT(got);

   ASSERT_CMPSTR(def->aggEvent.gotType, "succeeded");

   ASSERT_MATCH(def->aggEvent.gotSucceededReply,
                "{ 'cursor' : { 'firstBatch' : [ { 'encrypted': { "
                "'$$type': 'binData' }} ] } }");

   mongoc_cursor_destroy(cursor);
   bson_destroy(&to_insert);
   decryption_events_fixture_destroy(def);
}

static void
_test_rewrap_with_separate_client_encryption(const char *src_provider, const char *dst_provider)
{
   mongoc_uri_t *const uri = test_framework_get_uri();
   mongoc_client_encryption_opts_t *const ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_t *const src_client = test_framework_client_new_from_uri(uri, NULL);
   mongoc_client_t *const dst_client = test_framework_client_new_from_uri(uri, NULL);

   bson_error_t error = {0};
   bson_value_t keyid = {0};

   BSON_ASSERT(uri);
   BSON_ASSERT(ce_opts);
   BSON_ASSERT(src_client);
   BSON_ASSERT(dst_client);

   test_framework_set_ssl_opts(src_client);
   test_framework_set_ssl_opts(dst_client);

   {
      mongoc_client_encryption_opts_set_keyvault_client(ce_opts, src_client);
      mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");

      {
         bson_t *const kms_providers = _make_kms_providers(true, true);
         BSON_ASSERT(kms_providers);
         mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
         bson_destroy(kms_providers);
      }

      {
         bson_t *const tls_opts = _make_tls_opts();
         BSON_ASSERT(tls_opts);
         mongoc_client_encryption_opts_set_tls_opts(ce_opts, tls_opts);
         bson_destroy(tls_opts);
      }
   }


   // 1. Drop the collection keyvault.datakeys.
   {
      mongoc_collection_t *datakeys = mongoc_client_get_collection(src_client, "keyvault", "datakeys");
      ASSERT(datakeys);
      (void)mongoc_collection_drop(datakeys, NULL);
      mongoc_collection_destroy(datakeys);
   }

   // 2. Create a ClientEncryption object named clientEncryption1 with these
   // options: (see ce_opts).
   mongoc_client_encryption_t *clientEncryption1 = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_OR_PRINT(clientEncryption1, error);

   // 3. Call clientEncryption1.createDataKey with srcProvider and these
   // options: (see below).
   {
      mongoc_client_encryption_datakey_opts_t *dk_opts = mongoc_client_encryption_datakey_opts_new();

      {
         bson_t *const src_masterkey = _make_kms_masterkey(src_provider);
         BSON_ASSERT(src_masterkey);
         mongoc_client_encryption_datakey_opts_set_masterkey(dk_opts, src_masterkey);
         bson_destroy(src_masterkey);
      }

      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(clientEncryption1, src_provider, dk_opts, &keyid, &error),
                      error);

      mongoc_client_encryption_datakey_opts_destroy(dk_opts);
   }

   bson_value_t ciphertext = {0};

   // 4. Call clientEncryption1.encrypt with the value "test" and these options:
   // (see below).
   {
      char message[] = "test";

      bson_value_t bson_value;

      bson_value.value_type = BSON_TYPE_UTF8;
      bson_value.value.v_utf8.str = message;
      bson_value.value.v_utf8.len = (uint32_t)strlen(bson_value.value.v_utf8.str);

      mongoc_client_encryption_encrypt_opts_t *const e_opts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(e_opts, &keyid);
      mongoc_client_encryption_encrypt_opts_set_algorithm(e_opts, MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);
      ASSERT_OR_PRINT(mongoc_client_encryption_encrypt(clientEncryption1, &bson_value, e_opts, &ciphertext, &error),
                      error);
      mongoc_client_encryption_encrypt_opts_destroy(e_opts);
   }

   // 5. Create a ClientEncryption object named clientEncryption2 with these
   // options: (see ce_opts).
   mongoc_client_encryption_t *clientEncryption2 = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_OR_PRINT(clientEncryption2, error);

   mongoc_client_encryption_rewrap_many_datakey_result_t *const result =
      mongoc_client_encryption_rewrap_many_datakey_result_new();
   BSON_ASSERT(result);

   // 6. Call clientEncryption2.rewrapManyDataKey with an empty filter and these
   // options: (see below).
   {
      {
         bson_t *const dst_masterkey = _make_kms_masterkey(dst_provider);
         BSON_ASSERT(dst_masterkey);
         ASSERT_OR_PRINT(mongoc_client_encryption_rewrap_many_datakey(
                            clientEncryption2, NULL, dst_provider, dst_masterkey, result, &error),
                         error);
         bson_destroy(dst_masterkey);
      }

      // Assert that the returned
      // RewrapManyDataKeyResult.bulkWriteResult.modifiedCount is 1.
      const bson_t *const bulk_write_result =
         mongoc_client_encryption_rewrap_many_datakey_result_get_bulk_write_result(result);
      ASSERT(bulk_write_result);
      ASSERT_WITH_MSG(match_bson(bulk_write_result, tmp_bson("{'nModified': 1}"), false),
                      "'%s' does not match expected value",
                      tmp_json(bulk_write_result));
   }

   // 7. Call clientEncryption1.decrypt with the ciphertext. Assert the return
   // value is "test".
   {
      bson_value_t decrypted = {0};

      ASSERT(mongoc_client_encryption_decrypt(clientEncryption1, &ciphertext, &decrypted, &error));

      ASSERT(decrypted.value_type == BSON_TYPE_UTF8);
      ASSERT(decrypted.value.v_utf8.len != 0u);
      ASSERT_CMPSTR(decrypted.value.v_utf8.str, "test");

      bson_value_destroy(&decrypted);
   }

   // 8. Call clientEncryption2.decrypt with the ciphertext. Assert the return
   // value is "test".
   {
      bson_value_t decrypted = {0};

      ASSERT(mongoc_client_encryption_decrypt(clientEncryption2, &ciphertext, &decrypted, &error));

      ASSERT(decrypted.value_type == BSON_TYPE_UTF8);
      ASSERT(decrypted.value.v_utf8.len != 0u);
      ASSERT_CMPSTR(decrypted.value.v_utf8.str, "test");

      bson_value_destroy(&decrypted);
   }

   mongoc_client_encryption_rewrap_many_datakey_result_destroy(result);
   mongoc_client_encryption_destroy(clientEncryption2);
   bson_value_destroy(&ciphertext);
   bson_value_destroy(&keyid);
   mongoc_client_encryption_destroy(clientEncryption1);
   mongoc_client_encryption_opts_destroy(ce_opts);
   mongoc_uri_destroy(uri);
   mongoc_client_destroy(dst_client);
   mongoc_client_destroy(src_client);
}

/* Prose Test 16: Case 1: Rewrap with separate ClientEncryption */
static void
test_rewrap_with_separate_client_encryption(void *unused)
{
   BSON_UNUSED(unused);

   const char *const providers[] = {"aws", "azure", "gcp", "kmip", "local"};
   const size_t providers_len = sizeof(providers) / sizeof(*providers);

   for (size_t src_idx = 0u; src_idx < providers_len; ++src_idx) {
      for (size_t dst_idx = 0u; dst_idx < providers_len; ++dst_idx) {
         const char *const src = providers[src_idx];
         const char *const dst = providers[dst_idx];

         if (test_suite_debug_output()) {
            printf("  - %s -> %s\n", src, dst);
            fflush(stdout);
         }

         _test_rewrap_with_separate_client_encryption(src, dst);
      }
   }
}

/* Prose Test 16: Case 2: RewrapManyDataKeyOpts.provider is not optional. */
static void
test_rewrap_without_provider(void *unused)
{
   BSON_UNUSED(unused);

   mongoc_uri_t *const uri = test_framework_get_uri();
   mongoc_client_encryption_opts_t *const ce_opts = mongoc_client_encryption_opts_new();
   mongoc_client_t *const key_vault_client = test_framework_client_new_from_uri(uri, NULL);

   bson_error_t error = {0};

   BSON_ASSERT(uri);
   BSON_ASSERT(ce_opts);
   BSON_ASSERT(key_vault_client);

   test_framework_set_ssl_opts(key_vault_client);

   {
      mongoc_client_encryption_opts_set_keyvault_client(ce_opts, key_vault_client);
      mongoc_client_encryption_opts_set_keyvault_namespace(ce_opts, "keyvault", "datakeys");

      {
         bson_t *const kms_providers = _make_kms_providers(true, true);
         BSON_ASSERT(kms_providers);
         mongoc_client_encryption_opts_set_kms_providers(ce_opts, kms_providers);
         bson_destroy(kms_providers);
      }

      {
         bson_t *const tls_opts = _make_tls_opts();
         BSON_ASSERT(tls_opts);
         mongoc_client_encryption_opts_set_tls_opts(ce_opts, tls_opts);
         bson_destroy(tls_opts);
      }
   }

   // 1. Create a ClientEncryption object named clientEncryption with these
   // options: (see ce_opts).
   mongoc_client_encryption_t *clientEncryption = mongoc_client_encryption_new(ce_opts, &error);
   ASSERT_OR_PRINT(clientEncryption, error);

   // 2. Call ``clientEncryption.rewrapManyDataKey`` with an empty ``filter``
   // and these options: (see below).
   {
      bool ok = mongoc_client_encryption_rewrap_many_datakey(
         clientEncryption, NULL /* filter */, NULL /* kms_provider */, tmp_bson("{}"), NULL /* result */, &error);
      // Assert an error is returned from the driver suggesting that the
      // ``provider`` option is required.
      ASSERT_WITH_MSG(!ok, "expected error, but got success");
      ASSERT_ERROR_CONTAINS(error,
                            MONGOC_ERROR_CLIENT,
                            MONGOC_ERROR_CLIENT_INVALID_ENCRYPTION_ARG,
                            "expected 'provider' to be set to identify type of 'master_key'");
   }

   mongoc_client_encryption_destroy(clientEncryption);
   mongoc_client_encryption_opts_destroy(ce_opts);
   mongoc_uri_destroy(uri);
   mongoc_client_destroy(key_vault_client);
}

/* test_qe_docs_example tests the documentation example requested in
 * CDRIVER-4379. */
static void
test_qe_docs_example(void *unused)
{
   bson_error_t error;
   mongoc_client_t *const client = test_framework_new_default_client();
   bson_t *const kmsProviders = _make_kms_providers(false /* with aws */, true /* with local */);
   bson_value_t key1ID;
   bson_value_t key2ID;
   bson_t *encryptedFieldsMap;
   mongoc_client_t *encryptedClient;
   mongoc_collection_t *encryptedColl;

   BSON_UNUSED(unused);

   /* Drop data from prior test runs. */
   {
      mongoc_database_t *db;
      mongoc_collection_t *coll;

      coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      if (!mongoc_collection_drop(coll, &error)) {
         bool ignored_error = NULL != strstr(error.message, "ns not found");
         ASSERT_OR_PRINT(ignored_error, error);
      }
      mongoc_collection_destroy(coll);

      db = mongoc_client_get_database(client, "docsExamples");
      ASSERT_OR_PRINT(mongoc_database_drop(db, &error), error);
      mongoc_database_destroy(db);
   }

   /* Create two data keys. */
   {
      mongoc_client_encryption_opts_t *ceOpts;
      mongoc_client_encryption_t *ce;
      mongoc_client_encryption_datakey_opts_t *dkOpts;

      ceOpts = mongoc_client_encryption_opts_new();
      mongoc_client_encryption_opts_set_kms_providers(ceOpts, kmsProviders);
      mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_keyvault_client(ceOpts, client);
      ce = mongoc_client_encryption_new(ceOpts, &error);
      ASSERT_OR_PRINT(ce, error);

      dkOpts = mongoc_client_encryption_datakey_opts_new();
      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(ce, "local", dkOpts, &key1ID, &error), error);
      ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(ce, "local", dkOpts, &key2ID, &error), error);

      mongoc_client_encryption_datakey_opts_destroy(dkOpts);
      mongoc_client_encryption_destroy(ce);
      mongoc_client_encryption_opts_destroy(ceOpts);
   }

   /* Create an encryptedFieldsMap. */
   /* clang-format off */
   encryptedFieldsMap = BCON_NEW (
      "docsExamples.encrypted", "{",
         "fields", "[",
            "{",
               "path", "encryptedIndexed",
               "bsonType", "string",
               "keyId", BCON_BIN (key1ID.value.v_binary.subtype,
                                  key1ID.value.v_binary.data,
                                  key1ID.value.v_binary.data_len),
               "queries", "[", "{", "queryType", "equality", "}", "]",
            "}",
            "{",
               "path", "encryptedUnindexed",
               "bsonType", "string",
               "keyId", BCON_BIN (key2ID.value.v_binary.subtype,
                                  key2ID.value.v_binary.data,
                                  key2ID.value.v_binary.data_len),
            "}",
         "]",
      "}"
   );
   /* clang-format on */

   /* Create an Queryable Encryption collection. */
   {
      mongoc_auto_encryption_opts_t *aeOpts;
      mongoc_database_t *db;

      encryptedClient = test_framework_new_default_client();
      aeOpts = mongoc_auto_encryption_opts_new();
      mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kmsProviders);
      mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
      mongoc_auto_encryption_opts_set_encrypted_fields_map(aeOpts, encryptedFieldsMap);
      ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(encryptedClient, aeOpts, &error), error);
      /* Create the Queryable Encryption collection docsExample.encrypted. */
      db = mongoc_client_get_database(encryptedClient, "docsExamples");
      /* Because docsExample.encrypted is in encryptedFieldsMap, it is created
       * with Queryable Encryption support. */
      encryptedColl = mongoc_database_create_collection(db, "encrypted", NULL /* opts */, &error);
      ASSERT_OR_PRINT(encryptedColl, error);

      mongoc_database_destroy(db);
      mongoc_auto_encryption_opts_destroy(aeOpts);
   }

   /* Auto encrypt an insert and find. */
   {
      /* Encrypt an insert. */
      bson_t *to_insert;
      bson_t *filter;
      const bson_t *got;
      mongoc_cursor_t *cursor;
      bool found;
      bson_iter_t iter;

      to_insert =
         BCON_NEW("_id", BCON_INT32(1), "encryptedIndexed", "indexedValue", "encryptedUnindexed", "unindexedValue");

      ASSERT_OR_PRINT(mongoc_collection_insert_one(encryptedColl, to_insert, NULL /* opts */, NULL /* reply */, &error),
                      error);

      filter = BCON_NEW("encryptedIndexed", "indexedValue");
      cursor = mongoc_collection_find_with_opts(encryptedColl, filter, NULL /* opts */, NULL /* read prefs */);
      found = mongoc_cursor_next(cursor, &got);
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT(found);
      ASSERT(bson_iter_init_find(&iter, got, "encryptedIndexed"));
      ASSERT(BSON_ITER_HOLDS_UTF8(&iter));
      ASSERT_CMPSTR(bson_iter_utf8(&iter, NULL /* length */), "indexedValue");
      ASSERT(bson_iter_init_find(&iter, got, "encryptedUnindexed"));
      ASSERT(BSON_ITER_HOLDS_UTF8(&iter));
      ASSERT_CMPSTR(bson_iter_utf8(&iter, NULL /* length */), "unindexedValue");

      mongoc_cursor_destroy(cursor);
      bson_destroy(filter);
      bson_destroy(to_insert);
   }

   /* Find documents without decryption. */
   {
      mongoc_collection_t *unencryptedColl;
      bson_t *filter;
      const bson_t *got;
      mongoc_cursor_t *cursor;
      bool found;
      bson_iter_t iter;

      unencryptedColl = mongoc_client_get_collection(client, "docsExamples", "encrypted");
      filter = BCON_NEW("_id", BCON_INT32(1));
      cursor = mongoc_collection_find_with_opts(unencryptedColl, filter, NULL /* opts */, NULL /* read prefs */);
      found = mongoc_cursor_next(cursor, &got);
      ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);
      ASSERT(found);
      ASSERT(bson_iter_init_find(&iter, got, "encryptedIndexed"));
      ASSERT(BSON_ITER_HOLDS_BINARY(&iter));
      ASSERT(bson_iter_init_find(&iter, got, "encryptedUnindexed"));
      ASSERT(BSON_ITER_HOLDS_BINARY(&iter));

      mongoc_cursor_destroy(cursor);
      bson_destroy(filter);
      mongoc_collection_destroy(unencryptedColl);
   }

   mongoc_collection_destroy(encryptedColl);
   mongoc_client_destroy(encryptedClient);
   bson_destroy(encryptedFieldsMap);
   bson_value_destroy(&key2ID);
   bson_value_destroy(&key1ID);
   bson_destroy(kmsProviders);
   mongoc_client_destroy(client);
}

struct kms_callback_data {
   int value;
   const char *set_error;
   bool provide_creds;
};

static bool
_kms_callback(void *userdata, const bson_t *params, bson_t *out, bson_error_t *error)
{
   struct kms_callback_data *ctx = userdata;

   BSON_UNUSED(params);

   ctx->value = 42;
   if (ctx->set_error) {
      bson_set_error(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1729, "%s", ctx->set_error);
      return false;
   }
   if (ctx->provide_creds) {
      uint8_t keydata[96] = {0};
      BCON_APPEND(out, "local", "{", "key", BCON_BIN(BSON_SUBTYPE_BINARY, keydata, sizeof keydata), "}");
   }
   return true;
}

static void
test_kms_callback(void *unused)
{
   // No interesting datakey options
   mongoc_client_encryption_datakey_opts_t *dk_opts = mongoc_client_encryption_datakey_opts_new();

   // Create a client encryption object
   mongoc_client_encryption_opts_t *opts = mongoc_client_encryption_opts_new();
   mongoc_client_t *cl = test_framework_new_default_client();
   mongoc_client_encryption_opts_set_keyvault_client(opts, cl);

   BSON_UNUSED(unused);

   // Given it an on-demand 'local' provider
   bson_t *empty_local = tmp_bson("{'local': {}}");
   mongoc_client_encryption_opts_set_kms_providers(opts, empty_local);
   mongoc_client_encryption_opts_set_keyvault_namespace(opts, "testing", "testing");

   {
      // Attempting to create a key from 'local' will fail immediately
      // Create a client encryption object for it.
      bson_error_t error;
      mongoc_client_encryption_t *enc = mongoc_client_encryption_new(opts, &error);
      ASSERT_OR_PRINT(enc, error);

      bson_value_t keyid;
      mongoc_client_encryption_create_datakey(enc, "local", dk_opts, &keyid, &error);
      mongoc_client_encryption_destroy(enc);

      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "no kms provider set");
   }


   {
      // Now attach a callback
      struct kms_callback_data callback_data = {0};
      mongoc_client_encryption_opts_set_kms_credential_provider_callback(opts, _kms_callback, &callback_data);
      BSON_ASSERT(callback_data.value == 0);

      bson_error_t error;
      mongoc_client_encryption_t *enc = mongoc_client_encryption_new(opts, &error);
      ASSERT_OR_PRINT(enc, error);

      bson_value_t keyid;

      {
         mongoc_client_encryption_create_datakey(enc, "local", dk_opts, &keyid, &error);

         // The callback will have set a value when it was called
         BSON_ASSERT(callback_data.value == 42);

         // But we still get an error, because we didn't fill in 'local'
         ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, "no kms provider set");
      }

      {
         // Now actually provide a key
         callback_data.provide_creds = true;
         ASSERT_OR_PRINT(mongoc_client_encryption_create_datakey(enc, "local", dk_opts, &keyid, &error), error);

         // The callback will have set a value when it was called
         BSON_ASSERT(callback_data.value == 42);
         bson_value_destroy(&keyid);
      }

      // Clear the value and tell the callback to set its own error
      callback_data.value = 0;
      callback_data.set_error = "This is the error that should appear from the callback";

      {
         mongoc_client_encryption_create_datakey(enc, "local", dk_opts, &keyid, &error);
         // It was called again:
         BSON_ASSERT(callback_data.value == 42);

         // This time the callback provided an error
         ASSERT_ERROR_CONTAINS(
            error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1729, "This is the error that should appear from the callback");
      }

      mongoc_client_encryption_destroy(enc);
   }

   mongoc_client_encryption_datakey_opts_destroy(dk_opts);
   mongoc_client_encryption_opts_destroy(opts);
   mongoc_client_destroy(cl);
}

static void
_test_auto_aws(bool should_succeed)
{
   // Datakey options for AWS
   mongoc_client_encryption_datakey_opts_t *dk_opts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dk_opts,
                                                       tmp_bson("{ 'region': 'us-east-1', 'key': "
                                                                "'arn:aws:kms:us-east-1:579766882180:key/"
                                                                "89fcc2c4-08b0-4bd9-9f25-e30687b580d0' }"));

   // Create a client encryption object
   mongoc_client_encryption_opts_t *opts = mongoc_client_encryption_opts_new();
   mongoc_client_t *cl = test_framework_new_default_client();
   mongoc_client_encryption_opts_set_keyvault_client(opts, cl);

   // Given it an on-demand 'aws' provider
   bson_t *empty_aws = tmp_bson("{'aws': {}}");
   mongoc_client_encryption_opts_set_kms_providers(opts, empty_aws);
   mongoc_client_encryption_opts_set_keyvault_namespace(opts, "testing", "testing");

   {
      // Attempting to create a key from 'aws' will require credentials in the
      // environment immediately. Create a client encryption object for it.
      bson_error_t error;
      mongoc_client_encryption_t *enc = mongoc_client_encryption_new(opts, &error);
      ASSERT_OR_PRINT(enc, error);

      bson_value_t keyid;
      mongoc_client_encryption_create_datakey(enc, "aws", dk_opts, &keyid, &error);
      mongoc_client_encryption_destroy(enc);

      if (should_succeed) {
         bson_value_destroy(&keyid);
         ASSERT_OR_PRINT(error.code == 0, error);
      } else {
         // We should encounter an error while attempting to connect to the EC2
         // metadata server.
         ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "");
      }
   }

   mongoc_client_encryption_datakey_opts_destroy(dk_opts);
   mongoc_client_encryption_opts_destroy(opts);
   mongoc_client_destroy(cl);
}

static void
test_auto_aws_fail(void *unused)
{
   BSON_UNUSED(unused);
   _test_auto_aws(false);
}

static void
test_auto_aws_succeed(void *unused)
{
   BSON_UNUSED(unused);
   _test_auto_aws(true);
}

static int
_have_aws_creds_env(void *unused)
{
   BSON_UNUSED(unused);

   // State variable:
   //    Zero: Haven't checked yet
   //    One: We have AWS creds
   //    Two = We do not have AWS creds
   static int creds_check_state = 0;
   if (creds_check_state == 0) {
      // We need to do a check
      _mongoc_aws_credentials_t creds = {0};
      bson_error_t error;
      bool got_creds = _mongoc_aws_credentials_obtain(NULL, &creds, &error);
      _mongoc_aws_credentials_cleanup(&creds);
      if (got_creds) {
         creds_check_state = 1;
      } else {
         creds_check_state = 2;
      }
   }
   return creds_check_state == 1;
}

static int
_not_have_aws_creds_env(void *unused)
{
   return !_have_aws_creds_env(unused);
}

// Test calling mongoc_collection_drop with a NULL bson_error_t when the state
// collections do not exist. This is a regression test for CDRIVER-4457.
static void
test_drop_qe_null_error(void *unused)
{
   bson_error_t error;
   mongoc_client_t *const client = test_framework_new_default_client();
   bson_t *const kmsProviders = _make_kms_providers(false /* with aws */, true /* with local */);
   bson_t *encryptedFieldsMap;
   mongoc_client_t *encryptedClient;
   mongoc_auto_encryption_opts_t *aeOpts;
   mongoc_collection_t *coll;

   BSON_UNUSED(unused);

   /* Create an encryptedFieldsMap. */
   encryptedFieldsMap = BCON_NEW("db.encrypted", "{", "fields", "[", "]", "}");
   encryptedClient = test_framework_new_default_client();
   aeOpts = mongoc_auto_encryption_opts_new();
   mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kmsProviders);
   mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
   mongoc_auto_encryption_opts_set_encrypted_fields_map(aeOpts, encryptedFieldsMap);
   ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(encryptedClient, aeOpts, &error), error);
   coll = mongoc_client_get_collection(encryptedClient, "db", "encrypted");
   ASSERT(mongoc_collection_drop(coll, NULL));

   mongoc_collection_destroy(coll);
   mongoc_auto_encryption_opts_destroy(aeOpts);
   mongoc_client_destroy(encryptedClient);
   bson_destroy(encryptedFieldsMap);
   bson_destroy(kmsProviders);
   mongoc_client_destroy(client);
}

static bool
_auto_datakeys(struct auto_datakey_context *ctx)
{
   ctx->out_keyid->value.v_int32 = 42;
   ctx->out_keyid->value_type = BSON_TYPE_INT32;
   return true;
}

static bool
_auto_datakeys_error(struct auto_datakey_context *ctx)
{
   bson_set_error(ctx->out_error, 42, 1729, "I am an error");
   return false;
}

static bool
_auto_datakeys_error_noset(struct auto_datakey_context *ctx)
{
   // Do not set an error code, but indicate error anyway
   BSON_UNUSED(ctx);
   return false;
}

static void
test_auto_datakeys(void *unused)
{
   BSON_UNUSED(unused);

   bson_error_t error = {0};
   bson_t in_fields = BSON_INITIALIZER;
   bsonBuildArray( //
      in_fields,
      doc(kv("keyId", cstr("keepme"))), //
      doc(kv("keyId", null)));
   bson_t out_fields = BSON_INITIALIZER;
   bool okay = _mongoc_encryptedFields_fill_auto_datakeys(&out_fields, &in_fields, _auto_datakeys, NULL, &error);
   ASSERT_ERROR_CONTAINS(error, 0, 0, "");
   ASSERT(okay);
   bsonParse( //
      out_fields,
      require(keyWithType("0", doc), //
              parse(require(allOf(key("keyId"), strEqual("keepme")), nop))),
      require(keyWithType("1", doc),
              parse(require(allOf(keyWithType("keyId", int32)), do(ASSERT_CMPINT32(bsonAs(int32), ==, 42))))));
   ASSERT(bsonParseError == NULL);
   bson_destroy(&out_fields);

   // Do it again, but we will generate an error
   okay = _mongoc_encryptedFields_fill_auto_datakeys(&out_fields, &in_fields, _auto_datakeys_error, NULL, &error);
   ASSERT(!okay);
   ASSERT_ERROR_CONTAINS(error, 42, 1729, "I am an error");
   bson_destroy(&out_fields);

   // Do it again, but we will generate an error without the factory setting the
   // error
   okay = _mongoc_encryptedFields_fill_auto_datakeys(&out_fields, &in_fields, _auto_datakeys_error_noset, NULL, &error);
   ASSERT(!okay);
   // Generic error, since the factory didn't provide one:
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_BSON, MONGOC_ERROR_BSON_INVALID, "indicated failure");
   bson_destroy(&out_fields);
   bson_destroy(&in_fields);
}

static void
_do_cec_test(void (*test)(const char *kmsProvider))
{
   test("local");
   test("aws");
}

// Declare a createEncryptedCollection test case (See usage below)
#define CEC_TEST(name, ...)              \
   static void name##_impl(__VA_ARGS__); \
   static void name(void *unused)        \
   {                                     \
      BSON_UNUSED(unused);               \
      _do_cec_test(name##_impl);         \
   }                                     \
   static void name##_impl(__VA_ARGS__)

CEC_TEST(test_create_encrypted_collection_simple, const char *kmsProvider)
{
   bson_error_t error = {0};
   mongoc_client_t *const client = test_framework_new_default_client();
   bson_t *const kmsProviders = _make_kms_providers(true, true);
   bson_t *const tlsOptions = _make_tls_opts();

   const char *const dbName = "cec-test-db";

   // Drop prior data
   {
      mongoc_collection_t *const coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      if (coll) {
         mongoc_collection_drop(coll, &error);
         bool okay = error.code == 0 || strstr(error.message, "ns not found") != NULL;
         ASSERT_OR_PRINT(okay, error);
      }
      mongoc_collection_destroy(coll);

      mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
      ASSERT_OR_PRINT(mongoc_database_drop(db, &error), error);
      mongoc_database_destroy(db);
   }

   // Create a CE
   mongoc_client_encryption_opts_t *const ceOpts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(ceOpts, kmsProviders);
   mongoc_client_encryption_opts_set_tls_opts(ceOpts, tlsOptions);
   mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ceOpts, client);
   mongoc_client_encryption_t *const ce = mongoc_client_encryption_new(ceOpts, &error);
   mongoc_client_encryption_opts_destroy(ceOpts);
   ASSERT_OR_PRINT(ce, error);

   // Create the encrypted collection
   bsonBuildDecl(
      ccOpts,
      kv("encryptedFields",
         doc(kv("fields", array(doc(kv("path", cstr("ssn")), kv("bsonType", cstr("string")), kv("keyId", null)))))));
   mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
   bson_t *const mkey = _make_kms_masterkey(kmsProvider);
   mongoc_collection_t *const coll = mongoc_client_encryption_create_encrypted_collection(
      ce, db, "test-coll", &ccOpts, NULL, kmsProvider, mkey, &error);
   ASSERT_OR_PRINT(coll, error);
   bson_destroy(&ccOpts);
   bson_destroy(mkey);

   bsonBuildDecl(doc, kv("ssn", cstr("123-45-6789")));
   const bool okay = mongoc_collection_insert_one(coll, &doc, NULL, NULL, &error);
   // Expect a failure: We didn't encrypt the field, and we don't have
   // auto-encryption enabled, but the server expects this to be encrypted.
   ASSERT(!okay);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_COLLECTION, 121, "failed validation");
   bson_destroy(&doc);

   bson_destroy(kmsProviders);
   bson_destroy(tlsOptions);
   mongoc_collection_destroy(coll);
   mongoc_database_drop(db, &error);
   mongoc_database_destroy(db);
   mongoc_client_encryption_destroy(ce);
   mongoc_client_destroy(client);
}

static void
test_create_encrypted_collection_no_encryptedFields_helper(mongoc_client_t *client,
                                                           const char *dbName,
                                                           const char *collName,
                                                           const char *kmsProvider)
{
   bson_error_t error = {0};
   bson_t *const kmsProviders = _make_kms_providers(true, true);
   bson_t *const tlsOptions = _make_tls_opts();

   ASSERT(client);

   // Drop prior data
   {
      mongoc_collection_t *const coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      if (coll) {
         mongoc_collection_drop(coll, &error);
         bool okay = error.code == 0 || strstr(error.message, "ns not found") != NULL;
         ASSERT_OR_PRINT(okay, error);
      }
      mongoc_collection_destroy(coll);

      mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
      ASSERT_OR_PRINT(mongoc_database_drop(db, &error), error);
      mongoc_database_destroy(db);
   }

   // Create a CE
   mongoc_client_encryption_opts_t *const ceOpts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(ceOpts, kmsProviders);
   mongoc_client_encryption_opts_set_tls_opts(ceOpts, tlsOptions);
   mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ceOpts, client);
   mongoc_client_encryption_t *const ce = mongoc_client_encryption_new(ceOpts, &error);
   mongoc_client_encryption_opts_destroy(ceOpts);
   ASSERT_OR_PRINT(ce, error);

   // Create the encrypted collection
   bsonBuildDecl(ccOpts, do());
   mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
   bson_t *const mkey = _make_kms_masterkey(kmsProvider);
   mongoc_collection_t *const coll =
      mongoc_client_encryption_create_encrypted_collection(ce, db, collName, &ccOpts, NULL, kmsProvider, mkey, &error);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "No 'encryptedFields' are defined");
   bson_destroy(&ccOpts);
   bson_destroy(mkey);

   bson_destroy(kmsProviders);
   bson_destroy(tlsOptions);
   mongoc_collection_destroy(coll);
   mongoc_database_drop(db, &error);
   mongoc_database_destroy(db);
   mongoc_client_encryption_destroy(ce);
}

CEC_TEST(test_create_encrypted_collection_no_encryptedFields, const char *kmsProvider)
{
   const char *dbName = "cec-test-db";
   const char *collName = "test-coll";

   // Test with a default client.
   {
      mongoc_client_t *const client = test_framework_new_default_client();
      test_create_encrypted_collection_no_encryptedFields_helper(client, dbName, collName, kmsProvider);
      mongoc_client_destroy(client);
   }

   // Test with a client configured with an encryptedFieldsMap.
   // This is not a required test. But a prior implementation checked if the
   // target collection was configured in encryptedFieldsMap.
   {
      mongoc_client_t *const client = test_framework_new_default_client();
      mongoc_auto_encryption_opts_t *aeOpts = mongoc_auto_encryption_opts_new();
      bson_t *const kmsProviders = _make_kms_providers(true /* with aws */, true /* with local */);
      char *namespace = bson_strdup_printf("%s.%s", dbName, collName);
      bson_t *encryptedFieldsMap = tmp_bson("{'%s': {'fields': []}}", namespace);
      bson_error_t error;

      mongoc_auto_encryption_opts_set_kms_providers(aeOpts, kmsProviders);
      mongoc_auto_encryption_opts_set_keyvault_namespace(aeOpts, "keyvault", "datakeys");
      mongoc_auto_encryption_opts_set_encrypted_fields_map(aeOpts, encryptedFieldsMap);
      ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(client, aeOpts, &error), error);

      test_create_encrypted_collection_no_encryptedFields_helper(client, dbName, collName, kmsProvider);

      bson_free(namespace);
      bson_destroy(kmsProviders);
      mongoc_auto_encryption_opts_destroy(aeOpts);
      mongoc_client_destroy(client);
   }
}

CEC_TEST(test_create_encrypted_collection_bad_keyId, const char *const kmsProvider)
{
   bson_error_t error = {0};
   mongoc_client_t *const client = test_framework_new_default_client();
   bson_t *const kmsProviders = _make_kms_providers(true, true);
   bson_t *const tlsOptions = _make_tls_opts();

   const char *const dbName = "cec-test-db";

   // Drop prior data
   {
      mongoc_collection_t *const coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      if (coll) {
         mongoc_collection_drop(coll, &error);
         bool okay = error.code == 0 || strstr(error.message, "ns not found") != NULL;
         ASSERT_OR_PRINT(okay, error);
      }
      mongoc_collection_destroy(coll);

      mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
      ASSERT_OR_PRINT(mongoc_database_drop(db, &error), error);
      mongoc_database_destroy(db);
   }

   // Create a CE
   mongoc_client_encryption_opts_t *const ceOpts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(ceOpts, kmsProviders);
   mongoc_client_encryption_opts_set_tls_opts(ceOpts, tlsOptions);
   mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ceOpts, client);
   mongoc_client_encryption_t *const ce = mongoc_client_encryption_new(ceOpts, &error);
   mongoc_client_encryption_opts_destroy(ceOpts);
   ASSERT_OR_PRINT(ce, error);

   // Create the encrypted collection
   bsonBuildDecl(
      ccOpts,
      kv("encryptedFields",
         doc(kv("fields",
                array(doc(kv("path", cstr("ssn")), kv("bsonType", cstr("string")), kv("keyId", boolean(true))))))));
   mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
   bson_t *const mkey = _make_kms_masterkey(kmsProvider);
   mongoc_collection_t *const coll = mongoc_client_encryption_create_encrypted_collection(
      ce, db, "test-coll", &ccOpts, NULL, kmsProvider, mkey, &error);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_QUERY, MONGOC_ERROR_PROTOCOL_INVALID_REPLY, "create.encryptedFields.fields.keyId");
   bson_destroy(&ccOpts);
   bson_destroy(mkey);

   bson_destroy(kmsProviders);
   bson_destroy(tlsOptions);
   mongoc_collection_destroy(coll);
   mongoc_database_drop(db, &error);
   mongoc_database_destroy(db);
   mongoc_client_encryption_destroy(ce);
   mongoc_client_destroy(client);
}

// Implements Prose Test 21. Case: 4.
CEC_TEST(test_create_encrypted_collection_insert, const char *const kmsProvider)
{
   bson_error_t error = {0};
   mongoc_client_t *const client = test_framework_new_default_client();
   bson_t *const kmsProviders = _make_kms_providers(true, true);
   bson_t *const tlsOptions = _make_tls_opts();

   const char *const dbName = "cec-test-db";

   // Drop prior data
   {
      mongoc_collection_t *const coll = mongoc_client_get_collection(client, "keyvault", "datakeys");
      if (coll) {
         mongoc_collection_drop(coll, &error);
         bool okay = error.code == 0 || strstr(error.message, "ns not found") != NULL;
         ASSERT_OR_PRINT(okay, error);
      }
      mongoc_collection_destroy(coll);

      mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
      ASSERT_OR_PRINT(mongoc_database_drop(db, &error), error);
      mongoc_database_destroy(db);
   }

   // Create a CE
   mongoc_client_encryption_opts_t *const ceOpts = mongoc_client_encryption_opts_new();
   mongoc_client_encryption_opts_set_kms_providers(ceOpts, kmsProviders);
   mongoc_client_encryption_opts_set_tls_opts(ceOpts, tlsOptions);
   mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
   mongoc_client_encryption_opts_set_keyvault_client(ceOpts, client);
   mongoc_client_encryption_t *const ce = mongoc_client_encryption_new(ceOpts, &error);
   mongoc_client_encryption_opts_destroy(ceOpts);
   ASSERT_OR_PRINT(ce, error);

   // Create the encrypted collection
   bsonBuildDecl(
      ccOpts,
      kv("encryptedFields",
         doc(kv("fields", array(doc(kv("path", cstr("ssn")), kv("bsonType", cstr("string")), kv("keyId", null)))))));
   mongoc_database_t *const db = mongoc_client_get_database(client, dbName);
   bson_t new_opts;
   bson_t *const mkey = _make_kms_masterkey(kmsProvider);
   mongoc_collection_t *const coll = mongoc_client_encryption_create_encrypted_collection(
      ce, db, "testing1", &ccOpts, &new_opts, kmsProvider, mkey, &error);
   ASSERT_OR_PRINT(coll, error);
   bson_destroy(&ccOpts);
   bson_destroy(mkey);

   // Extract the encryption key ID that was generated by
   // CreateEncryptedCollection:
   bson_value_t new_keyid;
   bsonParse(new_opts,
             require(keyWithType("encryptedFields", doc),
                     parse(require(keyWithType("fields", array),
                                   visitEach(require(type(doc)),
                                             parse(require(key("keyId"),
                                                           require(type(binary)),
                                                           do({
                                                              bson_value_copy(
                                                                 bson_iter_value((bson_iter_t *)&bsonVisitIter),
                                                                 &new_keyid);
                                                           }),
                                                           halt)))))));
   ASSERT_CMPSTR(bsonParseError, NULL);

   // Generate some plaintext:
   bson_value_t plain;
   plain.value_type = BSON_TYPE_UTF8;
   plain.value.v_utf8.str = "123-45-6789";
   plain.value.v_utf8.len = (uint32_t)strlen(plain.value.v_utf8.str);

   // Encrypt the value using the new encryption key:
   mongoc_client_encryption_encrypt_opts_t *eo = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_keyid(eo, &new_keyid);
   mongoc_client_encryption_encrypt_opts_set_algorithm(eo, MONGOC_ENCRYPT_ALGORITHM_UNINDEXED);
   bson_value_t ciphertext;
   bool okay = mongoc_client_encryption_encrypt(ce, &plain, eo, &ciphertext, &error);
   ASSERT_OR_PRINT(okay, error);
   mongoc_client_encryption_encrypt_opts_destroy(eo);
   bson_value_destroy(&new_keyid);

   // Insert the ciphertext:
   bsonBuildDecl(doc, kv("ssn", value(ciphertext)));
   okay = mongoc_collection_insert_one(coll, &doc, NULL, NULL, &error);
   ASSERT_OR_PRINT(okay, error);
   // Success!

   bson_destroy(&doc);
   bson_value_destroy(&ciphertext);
   bson_destroy(kmsProviders);
   bson_destroy(tlsOptions);
   mongoc_collection_destroy(coll);
   mongoc_database_drop(db, &error);
   mongoc_database_destroy(db);
   mongoc_client_encryption_destroy(ce);
   mongoc_client_destroy(client);
   bson_destroy(&new_opts);
}


typedef struct listen_socket {
   mongoc_socket_t *socket;
   mongoc_cond_t cond;
   bson_mutex_t mutex;
   bool failed;
   char ip[16];
   unsigned short port;
   bool complete;
} listen_socket_args_t;

static BSON_THREAD_FUN(listen_socket, arg)
{
   listen_socket_args_t *args = arg;
   mongoc_socket_t *socket;
   struct sockaddr_in server_addr = {0};
   args->failed = false;

   // create a TcpListener on 127.0.0.1 endpoint
   socket = mongoc_socket_new(AF_INET, SOCK_STREAM, 0);
   BSON_ASSERT(socket);

   server_addr.sin_family = AF_INET;
   server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
   server_addr.sin_port = htons(0);
   mongoc_socklen_t addr_len = (mongoc_socklen_t)sizeof server_addr;

   int r = mongoc_socket_bind(socket, (struct sockaddr *)&server_addr, addr_len);
   BSON_ASSERT(r == 0);

   // forward the port and ip for mongocryptdURI
   r = mongoc_socket_getsockname(socket, (struct sockaddr *)&server_addr, &addr_len);
   BSON_ASSERT(r == 0);

   bson_mutex_lock(&args->mutex);
   args->port = ntohs(server_addr.sin_port);
   inet_ntop(AF_INET, &server_addr.sin_addr, args->ip, sizeof(args->ip));
   mongoc_cond_signal(&args->cond);
   bson_mutex_unlock(&args->mutex);

   // listen on socket
   r = mongoc_socket_listen(socket, 100);
   BSON_ASSERT(r == 0);
   mlib_sleep_for(1, ms);
   mongoc_socket_t *ret = mongoc_socket_accept(socket, bson_get_monotonic_time() + 100);
   if (ret) {
      // not null received a connection and test should fail
      args->failed = true;
   }
   // signal that test is complete.
   bson_mutex_lock(&args->mutex);
   args->complete = true;
   mongoc_cond_signal(&args->cond);
   bson_mutex_unlock(&args->mutex);

   mongoc_socket_destroy(socket);
   BSON_THREAD_RETURN;
}

/* Prose Test 20: Bypass creating mongocryptd client when shared library is
 * loaded */
static void
test_bypass_mongocryptd_shared_library(void *unused)
{
   BSON_UNUSED(unused);
   mongoc_client_t *client_encrypted;
   mongoc_auto_encryption_opts_t *auto_encryption_opts;
   bson_t *kms_providers;
   mongoc_database_t *db;
   mongoc_collection_t *coll;
   bson_error_t error;
   bson_thread_t socket_thread;

   // start the socket on a thread
   listen_socket_args_t *args = bson_malloc0(sizeof(listen_socket_args_t));
   bson_mutex_init(&args->mutex);
   mongoc_cond_init(&args->cond);
   ASSERT_CMPINT(0, ==, mcommon_thread_create(&socket_thread, listen_socket, args));

   // configure mongoclient with auto encryption
   char *env_cryptSharedLibPath = test_framework_getenv("MONGOC_TEST_CRYPT_SHARED_LIB_PATH");
   BSON_ASSERT(env_cryptSharedLibPath);
   auto_encryption_opts = mongoc_auto_encryption_opts_new();
   kms_providers = BCON_NEW("local", "{", "key", BCON_BIN(0, (uint8_t *)LOCAL_MASTERKEY, 96), "}");
   mongoc_auto_encryption_opts_set_kms_providers(auto_encryption_opts, kms_providers);
   mongoc_auto_encryption_opts_set_keyvault_namespace(auto_encryption_opts, "keyvault", "datakeys");

   // wait for port and ip to be set on the other thread
   bson_mutex_lock(&args->mutex);
   while (!args->port) {
      int cond_ret = mongoc_cond_timedwait(&args->cond, &args->mutex, 5000);
      /* ret non-zero indicates an error (a timeout) */
      BSON_ASSERT(!cond_ret);
   }
   bson_mutex_unlock(&args->mutex);
   BSON_ASSERT(strlen(args->ip) > 0);

   // configure extra options
   bson_t *extra = tmp_bson("{'mongocryptdURI': 'mongodb://%s:%d', "
                            "'cryptSharedLibPath': '%s'}",
                            args->ip,
                            args->port,
                            env_cryptSharedLibPath);
   mongoc_auto_encryption_opts_set_extra(auto_encryption_opts, extra);
   bson_free(env_cryptSharedLibPath);
   // get the client
   client_encrypted = test_framework_new_default_client();
   bool ret = mongoc_client_enable_auto_encryption(client_encrypted, auto_encryption_opts, &error);
   ASSERT_OR_PRINT(ret, error);

   // insert a document
   db = mongoc_client_get_database(client_encrypted, "db");
   coll = mongoc_database_get_collection(db, "coll");
   ret = mongoc_collection_insert_one(
      coll, tmp_bson("{'unencrypted': 'test'}"), NULL /* opts */, NULL /* reply */, &error);
   ASSERT_OR_PRINT(ret, error);

   // Wait for listener thread to complete.
   bson_mutex_lock(&args->mutex);
   while (!args->complete) {
      int cond_ret = mongoc_cond_timedwait(&args->cond, &args->mutex, 5000);
      /* ret non-zero indicates an error (a timeout) */
      BSON_ASSERT(!cond_ret);
   }
   bson_mutex_unlock(&args->mutex);
   // failed should be false if the signal did not receive a connection
   BSON_ASSERT(!args->failed);
   mcommon_thread_join(socket_thread);

   bson_mutex_destroy(&args->mutex);
   mongoc_cond_destroy(&args->cond);
   bson_destroy(kms_providers);
   mongoc_auto_encryption_opts_destroy(auto_encryption_opts);
   mongoc_collection_destroy(coll);
   mongoc_database_destroy(db);
   mongoc_client_destroy(client_encrypted);
   bson_free(args);
}

static void
test_range_explicit_encryption_applies_defaults(void *unused)
{
   BSON_UNUSED(unused);

   bson_error_t error;
   mongoc_client_t *keyVaultClient = test_framework_new_default_client();

   // Create a ClientEncryption object.
   mongoc_client_encryption_t *clientEncryption;
   {
      mongoc_client_encryption_opts_t *ceOpts = mongoc_client_encryption_opts_new();
      bson_t *kms_providers = _make_local_kms_provider(NULL);

      mongoc_client_encryption_opts_set_keyvault_client(ceOpts, keyVaultClient);
      mongoc_client_encryption_opts_set_keyvault_namespace(ceOpts, "keyvault", "datakeys");
      mongoc_client_encryption_opts_set_kms_providers(ceOpts, kms_providers);

      clientEncryption = mongoc_client_encryption_new(ceOpts, &error);
      ASSERT_OR_PRINT(clientEncryption, error);

      bson_destroy(kms_providers);
      mongoc_client_encryption_opts_destroy(ceOpts);
   }

   // Create a data key.
   bson_value_t keyID;
   {
      mongoc_client_encryption_datakey_opts_t *dkOpts = mongoc_client_encryption_datakey_opts_new();

      bool ok = mongoc_client_encryption_create_datakey(clientEncryption, "local", dkOpts, &keyID, &error);
      ASSERT_OR_PRINT(ok, error);

      mongoc_client_encryption_datakey_opts_destroy(dkOpts);
   }

   bson_value_t minValue = {.value_type = BSON_TYPE_INT32, .value.v_int32 = 0};
   bson_value_t maxValue = {.value_type = BSON_TYPE_INT32, .value.v_int32 = 1000};
   bson_value_t toEncrypt = {.value_type = BSON_TYPE_INT32, .value.v_int32 = 123};

   // Create `payload_defaults`.
   bson_value_t payload_defaults;
   {
      mongoc_client_encryption_encrypt_opts_t *eOpts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eOpts, &keyID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eOpts, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eOpts, 0);

      // Apply range options. Omit `sparsity` and `trimFactor`.
      {
         mongoc_client_encryption_encrypt_range_opts_t *rOpts = mongoc_client_encryption_encrypt_range_opts_new();
         mongoc_client_encryption_encrypt_range_opts_set_min(rOpts, &minValue);
         mongoc_client_encryption_encrypt_range_opts_set_max(rOpts, &maxValue);
         mongoc_client_encryption_encrypt_opts_set_range_opts(eOpts, rOpts);
         mongoc_client_encryption_encrypt_range_opts_destroy(rOpts);
      }

      bool ok = mongoc_client_encryption_encrypt(clientEncryption, &toEncrypt, eOpts, &payload_defaults, &error);
      ASSERT_OR_PRINT(ok, error);
      ASSERT(payload_defaults.value_type == BSON_TYPE_BINARY);

      mongoc_client_encryption_encrypt_opts_destroy(eOpts);
   }

   // Case 1: Uses libmongocrypt defaults.
   {
      mongoc_client_encryption_encrypt_opts_t *eOpts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eOpts, &keyID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eOpts, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eOpts, 0);

      // Apply range options. Include `sparsity` and `trimFactor`.
      {
         mongoc_client_encryption_encrypt_range_opts_t *rOpts = mongoc_client_encryption_encrypt_range_opts_new();
         mongoc_client_encryption_encrypt_range_opts_set_min(rOpts, &minValue);
         mongoc_client_encryption_encrypt_range_opts_set_max(rOpts, &maxValue);
         mongoc_client_encryption_encrypt_range_opts_set_sparsity(rOpts, 2);
         mongoc_client_encryption_encrypt_range_opts_set_trim_factor(rOpts, 6);
         mongoc_client_encryption_encrypt_opts_set_range_opts(eOpts, rOpts);
         mongoc_client_encryption_encrypt_range_opts_destroy(rOpts);
      }

      bson_value_t payload;
      bool ok = mongoc_client_encryption_encrypt(clientEncryption, &toEncrypt, eOpts, &payload, &error);
      ASSERT_OR_PRINT(ok, error);

      // Assert both payloads have equal length. Intended to check they used the same `trimFactor` and `sparsity`.
      ASSERT(payload.value_type == BSON_TYPE_BINARY);
      ASSERT_CMPUINT32(payload.value.v_binary.data_len, ==, payload_defaults.value.v_binary.data_len);

      mongoc_client_encryption_encrypt_opts_destroy(eOpts);
      bson_value_destroy(&payload);
   }

   // Case 1: Accepts `trimFactor` 0.
   {
      mongoc_client_encryption_encrypt_opts_t *eOpts = mongoc_client_encryption_encrypt_opts_new();
      mongoc_client_encryption_encrypt_opts_set_keyid(eOpts, &keyID);
      mongoc_client_encryption_encrypt_opts_set_algorithm(eOpts, MONGOC_ENCRYPT_ALGORITHM_RANGE);
      mongoc_client_encryption_encrypt_opts_set_contention_factor(eOpts, 0);

      // Apply range options. Omit `sparsity`, but include `trimFactor=0`.
      {
         mongoc_client_encryption_encrypt_range_opts_t *rOpts = mongoc_client_encryption_encrypt_range_opts_new();
         mongoc_client_encryption_encrypt_range_opts_set_min(rOpts, &minValue);
         mongoc_client_encryption_encrypt_range_opts_set_max(rOpts, &maxValue);
         mongoc_client_encryption_encrypt_range_opts_set_trim_factor(rOpts, 0);
         mongoc_client_encryption_encrypt_opts_set_range_opts(eOpts, rOpts);
         mongoc_client_encryption_encrypt_range_opts_destroy(rOpts);
      }

      bson_value_t payload;
      bool ok = mongoc_client_encryption_encrypt(clientEncryption, &toEncrypt, eOpts, &payload, &error);
      ASSERT_OR_PRINT(ok, error);

      // Assert payload with `trimFactor=0` has greater length.
      ASSERT(payload.value_type == BSON_TYPE_BINARY);
      ASSERT_CMPUINT32(payload.value.v_binary.data_len, >, payload_defaults.value.v_binary.data_len);

      mongoc_client_encryption_encrypt_opts_destroy(eOpts);
      bson_value_destroy(&payload);
   }

   bson_value_destroy(&payload_defaults);
   bson_value_destroy(&keyID);
   mongoc_client_encryption_destroy(clientEncryption);
   mongoc_client_destroy(keyVaultClient);
}

static void
_test_retry_with_masterkey(const char *provider, bson_t *masterkey)
{
   mongoc_client_t *keyvault_client = test_framework_new_default_client();
   mongoc_client_encryption_t *client_encryption = _tls_test_make_client_encryption(keyvault_client, RETRY);
   bson_error_t error = {0};
   bson_value_t keyid;
   mongoc_client_encryption_datakey_opts_t *dkopts;
   char *ca_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CA_FILE");
   char *pem_file = test_framework_getenv_required("MONGOC_TEST_CSFLE_TLS_CERTIFICATE_KEY_FILE");
   mongoc_ssl_opt_t ssl_opts = {.ca_file = ca_file, .pem_file = pem_file};
   bool res;

   bson_value_t to_encrypt = {.value_type = BSON_TYPE_INT32, .value.v_int32 = 123};
   bson_value_t encrypted_field = {0};
   mongoc_client_encryption_encrypt_opts_t *encrypt_opts = mongoc_client_encryption_encrypt_opts_new();
   mongoc_client_encryption_encrypt_opts_set_algorithm(encrypt_opts,
                                                       MONGOC_AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC);

   reset_failpoints(&ssl_opts);

   // Case 1: createDataKey and encrypt with TCP retry
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, masterkey);
   set_retry_failpoint(&ssl_opts, true, 1);
   res = mongoc_client_encryption_create_datakey(client_encryption, provider, dkopts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   set_retry_failpoint(&ssl_opts, true, 1);
   mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, &keyid);
   res = mongoc_client_encryption_encrypt(client_encryption, &to_encrypt, encrypt_opts, &encrypted_field, &error);
   ASSERT_OR_PRINT(res, error);
   bson_value_destroy(&keyid);
   bson_value_destroy(&encrypted_field);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   // Case 2: createDataKey and encrypt with HTTP retry
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, masterkey);
   set_retry_failpoint(&ssl_opts, false, 1);
   res = mongoc_client_encryption_create_datakey(client_encryption, provider, dkopts, &keyid, &error);
   ASSERT_OR_PRINT(res, error);

   set_retry_failpoint(&ssl_opts, false, 1);
   mongoc_client_encryption_encrypt_opts_set_keyid(encrypt_opts, &keyid);
   res = mongoc_client_encryption_encrypt(client_encryption, &to_encrypt, encrypt_opts, &encrypted_field, &error);
   ASSERT_OR_PRINT(res, error);
   bson_value_destroy(&keyid);
   bson_value_destroy(&encrypted_field);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   // Case 3: createDataKey fails after too many retries
   dkopts = mongoc_client_encryption_datakey_opts_new();
   mongoc_client_encryption_datakey_opts_set_masterkey(dkopts, masterkey);
   set_retry_failpoint(&ssl_opts, true, 4);
   res = mongoc_client_encryption_create_datakey(client_encryption, provider, dkopts, &keyid, &error);
   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_SOCKET, "KMS request failed after");

   bson_value_destroy(&keyid);
   mongoc_client_encryption_datakey_opts_destroy(dkopts);

   bson_free(ca_file);
   bson_free(pem_file);
   mongoc_client_encryption_encrypt_opts_destroy(encrypt_opts);
   mongoc_client_encryption_destroy(client_encryption);
   mongoc_client_destroy(keyvault_client);
}

/* Prose test 23: KMS Retry Tests */
static void
test_kms_retry(void *unused)
{
   BSON_UNUSED(unused);

   bson_t *aws_masterkey = tmp_bson(BSON_STR({"region" : "r", "key" : "k", "endpoint" : "127.0.0.1:9003"}));
   bson_t *azure_masterkey = tmp_bson(BSON_STR({"keyVaultEndpoint" : "127.0.0.1:9003", "keyName" : "foo"}));
   bson_t *gcp_masterkey = tmp_bson(BSON_STR(
      {"projectId" : "foo", "location" : "bar", "keyRing" : "baz", "keyName" : "qux", "endpoint" : "127.0.0.1:9003"}));

   _test_retry_with_masterkey("aws", aws_masterkey);
   _test_retry_with_masterkey("azure", azure_masterkey);
   _test_retry_with_masterkey("gcp", gcp_masterkey);
}

static mongoc_client_t *
create_encrypted_client(void)
{
   mongoc_client_t *client = test_framework_new_default_client();
   bson_error_t error;
   mongoc_auto_encryption_opts_t *ao = mongoc_auto_encryption_opts_new();
   {
      bson_t extra = BSON_INITIALIZER;
      _set_extra_bypass(&extra);
      _set_extra_crypt_shared(&extra);
      mongoc_auto_encryption_opts_set_extra(ao, &extra);
      bson_destroy(&extra);
   }
   bson_t *kms_providers =
      BCON_NEW("local", "{", "key", BCON_BIN(BSON_SUBTYPE_UUID, (uint8_t *)LOCAL_MASTERKEY, 96), "}");
   mongoc_auto_encryption_opts_set_keyvault_namespace(ao, "db", "keyvault");
   mongoc_auto_encryption_opts_set_kms_providers(ao, kms_providers);
   ASSERT_OR_PRINT(mongoc_client_enable_auto_encryption(client, ao, &error), error);
   bson_destroy(kms_providers);
   mongoc_auto_encryption_opts_destroy(ao);
   return client;
}

#define ASSERT_COLL_MATCHES_ONE(coll, expect)                                                         \
   if (1) {                                                                                           \
      mongoc_cursor_t *cursor = mongoc_collection_find_with_opts((coll), tmp_bson("{}"), NULL, NULL); \
      const bson_t *got;                                                                              \
      bool found = mongoc_cursor_next(cursor, &got);                                                  \
      if (!found) {                                                                                   \
         ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);                                \
         test_error("expected 1 document, but got 0");                                                \
      }                                                                                               \
      assert_match_bson(got, expect, false);                                                          \
      ASSERT(!mongoc_cursor_next(cursor, &got)); /* expect exactly one document */                    \
      mongoc_cursor_destroy(cursor);                                                                  \
   } else                                                                                             \
      (void)0

#define ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect)                                          \
   if (1) {                                                                                     \
      mongoc_cursor_t *cursor = mongoc_collection_aggregate((coll), 0, (pipeline), NULL, NULL); \
      const bson_t *got;                                                                        \
      bool found = mongoc_cursor_next(cursor, &got);                                            \
      if (!found) {                                                                             \
         ASSERT_OR_PRINT(!mongoc_cursor_error(cursor, &error), error);                          \
         test_error("expected 1 document, but got 0");                                          \
      }                                                                                         \
      ASSERT_EQUAL_BSON(expect, got);                                                           \
      ASSERT(!mongoc_cursor_next(cursor, &got)); /* expect exactly one document */              \
      mongoc_cursor_destroy(cursor);                                                            \
   } else                                                                                       \
      (void)0

#define ASSERT_AGG_ERROR(coll, pipeline, msg)                                                   \
   if (1) {                                                                                     \
      mongoc_cursor_t *cursor = mongoc_collection_aggregate((coll), 0, (pipeline), NULL, NULL); \
      const bson_t *got;                                                                        \
      bool found = mongoc_cursor_next(cursor, &got);                                            \
      ASSERT(!found);                                                                           \
      ASSERT(mongoc_cursor_error(cursor, &error));                                              \
      ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION, 1, msg);                \
      mongoc_cursor_destroy(cursor);                                                            \
   } else                                                                                       \
      (void)0

#define MAKE_BSON(...) tmp_bson(BSON_STR(__VA_ARGS__))

static void
drop_coll(mongoc_database_t *db, const char *collname)
{
   bson_error_t error;
   mongoc_collection_t *coll = mongoc_database_get_collection(db, collname);
   bool ok = mongoc_collection_drop(coll, &error);
   if (!ok && error.code != MONGOC_SERVER_ERR_NS_NOT_FOUND) {
      test_error("unexpected error dropping %s: %s", collname, error.message);
   }
   mongoc_collection_destroy(coll);
}

server_version_t
get_libmongocrypt_version(void)
{
#ifdef MONGOC_ENABLE_CLIENT_SIDE_ENCRYPTION
   return test_framework_str_to_version(_mongoc_crypt_get_libmongocrypt_version());
#else
   return 0;
#endif
}

static void
test_lookup_setup(void)
{
   bool ok;
   bson_error_t error;

   mongoc_client_t *encrypted_client = create_encrypted_client();
   mongoc_client_t *setup_client = test_framework_new_default_client();


#define TESTDIR "./src/libmongoc/tests/client_side_encryption_prose/lookup/"
   // Insert key into key vault:
   {
      mongoc_collection_t *keyvault = mongoc_client_get_collection(encrypted_client, "db", "keyvault");
      mongoc_collection_drop(keyvault, NULL);
      bson_t *keydoc = get_bson_from_json_file(TESTDIR "key-doc.json");
      bson_t opts = BSON_INITIALIZER;
      // Apply majority write concern.
      {
         mongoc_write_concern_t *wc = mongoc_write_concern_new();
         mongoc_write_concern_set_w(wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
         mongoc_write_concern_append(wc, &opts);
         mongoc_write_concern_destroy(wc);
      }
      ASSERT_OR_PRINT(mongoc_collection_insert_one(keyvault, keydoc, NULL, &opts, &error), error);
      bson_destroy(&opts);
      bson_destroy(keydoc);
      mongoc_collection_destroy(keyvault);
   }

   // Create collections:
   {
      mongoc_database_t *db = mongoc_client_get_database(encrypted_client, "db");
      // Create db.csfle:
      {
         drop_coll(db, "csfle");
         bson_t *schema = get_bson_from_json_file(TESTDIR "schema-csfle.json");
         bson_t *create_opts = BCON_NEW("validator", "{", "$jsonSchema", BCON_DOCUMENT(schema), "}");
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "csfle", create_opts, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
         bson_destroy(create_opts);
         bson_destroy(schema);
      }

      // Create db.csfle2:
      {
         drop_coll(db, "csfle2");
         bson_t *schema = get_bson_from_json_file(TESTDIR "schema-csfle2.json");
         bson_t *create_opts = BCON_NEW("validator", "{", "$jsonSchema", BCON_DOCUMENT(schema), "}");
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "csfle2", create_opts, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
         bson_destroy(create_opts);
         bson_destroy(schema);
      }

      // Create db.qe:
      {
         drop_coll(db, "qe");
         bson_t *schema = get_bson_from_json_file(TESTDIR "schema-qe.json");
         bson_t *create_opts = BCON_NEW("encryptedFields", BCON_DOCUMENT(schema));
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "qe", create_opts, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
         bson_destroy(create_opts);
         bson_destroy(schema);
      }

      // Create db.qe2:
      {
         drop_coll(db, "qe2");
         bson_t *schema = get_bson_from_json_file(TESTDIR "schema-qe2.json");
         bson_t *create_opts = BCON_NEW("encryptedFields", BCON_DOCUMENT(schema));
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "qe2", create_opts, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
         bson_destroy(create_opts);
         bson_destroy(schema);
      }

      // Create db.no_schema:
      {
         drop_coll(db, "no_schema");
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "noschema", NULL, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
      }

      // Create db.no_schema2:
      {
         drop_coll(db, "no_schema2");
         mongoc_collection_t *coll = mongoc_database_create_collection(db, "noschema2", NULL, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
      }

      // Create db.non_csfle_schema:
      {
         drop_coll(db, "non_csfle_schema");
         bson_t *const schema = get_bson_from_json_file(TESTDIR "schema-non-csfle.json");
         bson_t *const create_opts = BCON_NEW("validator", "{", "$jsonSchema", BCON_DOCUMENT(schema), "}");
         mongoc_collection_t *const coll =
            mongoc_database_create_collection(db, "non_csfle_schema", create_opts, &error);
         ASSERT_OR_PRINT(coll, error);
         mongoc_collection_destroy(coll);
         bson_destroy(create_opts);
         bson_destroy(schema);
      }

      mongoc_database_destroy(db);
   }
#undef TESTDIR

   // Insert initial documents:
   {
      mongoc_client_t *client = create_encrypted_client();

      // Insert to db.csfle:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"csfle" : "csfle"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "csfle");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"csfle" : {"$$type" : "binData"}}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.csfle2:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle2");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"csfle2" : "csfle2"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "csfle2");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"csfle2" : {"$$type" : "binData"}}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.qe:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "qe");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"qe" : "qe"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "qe");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"qe" : {"$$type" : "binData"}}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.qe2:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "qe2");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"qe2" : "qe2"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "qe2");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"qe2" : {"$$type" : "binData"}}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.no_schema:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "no_schema");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"no_schema" : "no_schema"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is not encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "no_schema");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"no_schema" : "no_schema"}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.no_schema2:
      {
         mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "no_schema2");
         ok = mongoc_collection_insert_one(coll, MAKE_BSON({"no_schema2" : "no_schema2"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is not encrypted.
         mongoc_collection_t *coll_unencrypted = mongoc_client_get_collection(setup_client, "db", "no_schema2");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"no_schema2" : "no_schema2"}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      // Insert to db.non_csfle_schema
      {
         mongoc_collection_t *const coll = mongoc_client_get_collection(client, "db", "non_csfle_schema");
         ok = mongoc_collection_insert_one(
            coll, MAKE_BSON({"non_csfle_schema" : "non_csfle_schema"}), NULL, NULL, &error);
         ASSERT_OR_PRINT(ok, error);
         mongoc_collection_destroy(coll);
         // Find document with unencrypted client to check it is not encrypted.
         mongoc_collection_t *const coll_unencrypted =
            mongoc_client_get_collection(setup_client, "db", "non_csfle_schema");
         ASSERT_COLL_MATCHES_ONE(coll_unencrypted, MAKE_BSON({"non_csfle_schema" : "non_csfle_schema"}));
         mongoc_collection_destroy(coll_unencrypted);
      }

      mongoc_client_destroy(client);
   }

   mongoc_client_destroy(setup_client);
   mongoc_client_destroy(encrypted_client);
}

static void
test_lookup(void *unused)
{
   BSON_UNUSED(unused);

   test_lookup_setup();
   bson_error_t error;

   // Case 1: db.csfle joins db.no_schema:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"csfle" : "csfle"}},
            {
               "$lookup" : {
                  "from" : "no_schema",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"no_schema" : "no_schema"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"csfle" : "csfle", "matched" : [ {"no_schema" : "no_schema"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 2: db.qe joins db.no_schema.
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "qe");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"qe" : "qe"}},
            {
               "$lookup" : {
                  "from" : "no_schema",
                  "as" : "matched",
                  "pipeline" :
                     [ {"$match" : {"no_schema" : "no_schema"}}, {"$project" : {"_id" : 0, "__safeContent__" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0, "__safeContent__" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"qe" : "qe", "matched" : [ {"no_schema" : "no_schema"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 3: db.no_schema joins db.csfle:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "no_schema");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"no_schema" : "no_schema"}},
            {
               "$lookup" : {
                  "from" : "csfle",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"csfle" : "csfle"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"no_schema" : "no_schema", "matched" : [ {"csfle" : "csfle"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 4: db.no_schema joins db.qe:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "no_schema");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"no_schema" : "no_schema"}},
            {
               "$lookup" : {
                  "from" : "qe",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"qe" : "qe"}}, {"$project" : {"_id" : 0, "__safeContent__" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"no_schema" : "no_schema", "matched" : [ {"qe" : "qe"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 5: db.csfle joins db.csfle2:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"csfle" : "csfle"}},
            {
               "$lookup" : {
                  "from" : "csfle2",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"csfle2" : "csfle2"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"csfle" : "csfle", "matched" : [ {"csfle2" : "csfle2"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 6: qe joins db.qe2:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "qe");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"qe" : "qe"}},
            {
               "$lookup" : {
                  "from" : "qe2",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"qe2" : "qe2"}}, {"$project" : {"_id" : 0, "__safeContent__" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0, "__safeContent__" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"qe" : "qe", "matched" : [ {"qe2" : "qe2"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 7: db.no_schema joins db.no_schema2:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "no_schema");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"no_schema" : "no_schema"}},
            {
               "$lookup" : {
                  "from" : "no_schema2",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"no_schema2" : "no_schema2"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      bson_t *expect = MAKE_BSON({"no_schema" : "no_schema", "matched" : [ {"no_schema2" : "no_schema2"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }

   // Case 8: db.csfle joins db.qe:
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"csfle" : "qe"}},
            {
               "$lookup" : {
                  "from" : "qe",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"qe" : "qe"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      if (test_framework_get_server_version() < test_framework_str_to_version("8.2.0") ||
          get_libmongocrypt_version() < test_framework_str_to_version("1.17.0")) {
         ASSERT_AGG_ERROR(coll, pipeline, "not supported");
      } else {
         // The error domain differs depending on the query analysis component:
         // * `crypt_shared`: `MONGOC_ERROR_CLIENT_SIDE_ENCRYPTION`
         // * `mongocryptd`: `MONGOC_ERROR_QUERY`

         static const char *const expected_error_substring =
            "Cannot specify both encryptionInformation and csfleEncryptionSchemas unless csfleEncryptionSchemas only "
            "contains non-encryption JSON schema validators";

         if (mongoc_client_get_crypt_shared_version(client)) {
            ASSERT_AGG_ERROR(coll, pipeline, expected_error_substring);
         } else {
            mongoc_cursor_t *const cursor = mongoc_collection_aggregate(coll, 0, pipeline, NULL, NULL);
            const bson_t *got;
            ASSERT(!mongoc_cursor_next(cursor, &got));
            ASSERT(mongoc_cursor_error(cursor, &error));
            static const uint32_t expected_error_code = 10026002u;
            ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_QUERY, expected_error_code, expected_error_substring);
            mongoc_cursor_destroy(cursor);
         }
      }

      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }
}

static void
test_lookup_post82(void *unused)
{
   BSON_UNUSED(unused);
   test_lookup_setup();
   bson_error_t error;

   // Case 10: db.qe joins db.non_csfle_schema:
   {
      mongoc_client_t *const client = create_encrypted_client();
      mongoc_collection_t *const coll = mongoc_client_get_collection(client, "db", "qe");

      bson_t *const pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"qe" : "qe"}},
            {
               "$lookup" : {
                  "from" : "non_csfle_schema",
                  "as" : "matched",
                  "pipeline" : [
                     {"$match" : {"non_csfle_schema" : "non_csfle_schema"}},
                     {"$project" : {"_id" : 0, "__safeContent__" : 0}}
                  ]
               }
            },
            {"$project" : {"_id" : 0, "__safeContent__" : 0}}
         ]
      });

      bson_t *const expect = MAKE_BSON({"qe" : "qe", "matched" : [ {"non_csfle_schema" : "non_csfle_schema"} ]});
      ASSERT_AGG_RETURNS_ONE(coll, pipeline, expect);
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }
}

static void
test_lookup_pre81(void *unused)
{
   BSON_UNUSED(unused);
   test_lookup_setup();
   bson_error_t error;

   // Case 9: test error with <8.1
   {
      mongoc_client_t *client = create_encrypted_client(); // Create new client to avoid schema caching.
      mongoc_collection_t *coll = mongoc_client_get_collection(client, "db", "csfle");

      bson_t *pipeline = MAKE_BSON({
         "pipeline" : [
            {"$match" : {"csfle" : "no_schema"}},
            {
               "$lookup" : {
                  "from" : "no_schema",
                  "as" : "matched",
                  "pipeline" : [ {"$match" : {"no_schema" : "no_schema"}}, {"$project" : {"_id" : 0}} ]
               }
            },
            {"$project" : {"_id" : 0}}
         ]
      });

      ASSERT_AGG_ERROR(coll, pipeline, "Upgrade");
      mongoc_collection_destroy(coll);
      mongoc_client_destroy(client);
   }
}

int
skip_if_libmongocrypt_less_than_1_17_0(void)
{
   return get_libmongocrypt_version() >= test_framework_str_to_version("1.17.0");
}

void
test_client_side_encryption_install(TestSuite *suite)
{
   install_json_test_suite_with_check(suite,
                                      JSON_DIR,
                                      "client_side_encryption/legacy",
                                      test_client_side_encryption_cb,
                                      test_framework_skip_if_no_client_side_encryption);
   /* Prose tests from the spec. */
   TestSuite_AddFull(suite,
                     "/client_side_encryption/create_datakey_with_custom_key_material [lock:live-server]",
                     test_create_datakey_with_custom_key_material,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_offline /* requires AWS */);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/datakey_and_double_encryption [lock:live-server]",
                     test_datakey_and_double_encryption,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_offline /* requires AWS */);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/external_key_vault [lock:live-server]",
                     test_external_key_vault,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_no_auth /* requires auth for error check */);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bson_size_limits_and_batch_splitting [lock:live-server]",
                     test_bson_size_limits_and_batch_splitting_no_qe,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bson_size_limits_and_batch_splitting_qe [lock:live-server]",
                     test_bson_size_limits_and_batch_splitting_qe,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_25,
                     test_framework_skip_if_single);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/views_are_prohibited [lock:live-server]",
                     test_views_are_prohibited,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/corpus [lock:live-server]",
                     test_corpus,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_offline /* requires AWS */);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/custom_endpoint [lock:live-server]",
                     test_custom_endpoint,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_offline /* requires AWS, Azure, and GCP */);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bypass_spawning_mongocryptd/"
                     "mongocryptdBypassSpawn [lock:live-server]",
                     test_bypass_spawning_via_mongocryptdBypassSpawn,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bypass_spawning_mongocryptd/"
                     "bypassAutoEncryption [lock:live-server]",
                     test_bypass_spawning_via_bypassAutoEncryption,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bypass_spawning_mongocryptd/"
                     "bypassQueryAnalysis [lock:live-server]",
                     test_bypass_spawning_via_bypassQueryAnalysis,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bypass_spawning_mongocryptd/"
                     "cryptSharedLibLoaded [lock:live-server]",
                     test_bypass_spawning_via_cryptSharedLibLoaded,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     _skip_if_no_crypt_shared);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_tls/valid [lock:live-server]",
                     test_kms_tls_cert_valid,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_tls/expired [lock:live-server]",
                     test_kms_tls_cert_expired,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_tls/wrong_host [lock:live-server]",
                     test_kms_tls_cert_wrong_host,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/unique_index_on_keyaltnames [lock:live-server]",
                     test_unique_index_on_keyaltnames,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/prose_test_16/case1 [lock:live-server][timeout:30]",
                     test_rewrap_with_separate_client_encryption,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_slow);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/prose_test_16/case2 [lock:live-server]",
                     test_rewrap_without_provider,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   /* Other, C driver specific, tests. */
   TestSuite_AddFull(suite,
                     "/client_side_encryption/single_and_pool_mismatches [lock:live-server]",
                     test_invalid_single_and_pool_mismatches,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/multi_threaded [lock:live-server]",
                     test_multi_threaded,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/malformed_explicit [lock:live-server]",
                     test_malformed_explicit,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_tls_options [lock:live-server]",
                     test_kms_tls_options,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     test_framework_skip_if_offline /* requires AWS, Azure, and GCP */,
                     /* Do not run on Windows due to CDRIVER-4181. Tests use a literal IP with
                        a TLS connection. */
                     test_framework_skip_if_windows);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_tls_options/extra_rejected [lock:live-server]",
                     test_kms_tls_options_extra_rejected,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms_retry [lock:live-server]",
                     test_kms_retry,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/explicit_encryption/case1 [lock:live-server]",
                     test_explicit_encryption_case1,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/explicit_encryption/case2 [lock:live-server]",
                     test_explicit_encryption_case2,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/explicit_encryption/case3 [lock:live-server]",
                     test_explicit_encryption_case3,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/explicit_encryption/case4 [lock:live-server]",
                     test_explicit_encryption_case4,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/explicit_encryption/case5 [lock:live-server]",
                     test_explicit_encryption_case5,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/decryption_events/case1 [lock:live-server]",
                     test_decryption_events_case1,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/decryption_events/case2 [lock:live-server]",
                     test_decryption_events_case2,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/decryption_events/case3 [lock:live-server]",
                     test_decryption_events_case3,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);


   TestSuite_AddFull(suite,
                     "/client_side_encryption/decryption_events/case4 [lock:live-server]",
                     test_decryption_events_case4,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/qe_docs_example [lock:live-server]",
                     test_qe_docs_example,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms/callback [lock:live-server]",
                     test_kms_callback,
                     NULL, // dtor
                     NULL, // ctx
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms/auto-aws/fail [lock:live-server]",
                     test_auto_aws_fail,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     _not_have_aws_creds_env);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/kms/auto-aws/succeed [lock:live-server]",
                     test_auto_aws_succeed,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive,
                     _have_aws_creds_env);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/drop_qe_null_error [lock:live-server]",
                     test_drop_qe_null_error,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     TestSuite_CheckLive);

   TestSuite_AddFull(
      suite, "/client_side_encryption/auto_datakeys [lock:live-server]", test_auto_datakeys, NULL, NULL, NULL);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/createEncryptedCollection/simple [lock:live-server]",
                     test_create_encrypted_collection_simple,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);

   TestSuite_AddFull(suite,
                     "/client_side_encryption/createEncryptedCollection/"
                     "missing-encryptedFields [lock:live-server]",
                     test_create_encrypted_collection_no_encryptedFields,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/createEncryptedCollection/"
                     "bad-keyId [lock:live-server]",
                     test_create_encrypted_collection_bad_keyId,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/createEncryptedCollection/insert [lock:live-server]",
                     test_create_encrypted_collection_insert,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_21,
                     test_framework_skip_if_single);
   TestSuite_AddFull(suite,
                     "/client_side_encryption/bypass_mongocryptd_shared_library [lock:live-server]",
                     test_bypass_mongocryptd_shared_library,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_client_side_encryption,
                     test_framework_skip_if_max_wire_version_less_than_17,
                     _skip_if_no_crypt_shared);

   // Add test cases for prose test: 22. Range Explicit Encryption.
   {
      const char *rangeTypes[] = {
         "DecimalNoPrecision",
         "DecimalPrecision",
         "DoubleNoPrecision",
         "DoublePrecision",
         "Date",
         "Int",
         "Long",
      };

      typedef struct {
         const char *name;
         TestFuncWC fn;
      } rangeCase;

      rangeCase rangeCases[] = {
         {"case1", test_range_explicit_encryption_case1},
         {"case2", test_range_explicit_encryption_case2},
         {"case3", test_range_explicit_encryption_case3},
         {"case4", test_range_explicit_encryption_case4},
         {"case5", test_range_explicit_encryption_case5},
         {"case6", test_range_explicit_encryption_case6},
         {"case7", test_range_explicit_encryption_case7},
         {"case8", test_range_explicit_encryption_case8},
      };

      for (size_t i = 0; i < sizeof rangeTypes / sizeof rangeTypes[0]; i++) {
         for (size_t j = 0; j < sizeof rangeCases / sizeof rangeCases[0]; j++) {
            const char *rangeType = rangeTypes[i];
            rangeCase rc = rangeCases[j];

            char *test_name =
               bson_strdup_printf("/client_side_encryption/range_explicit_encryption/%s/%s", rc.name, rangeType);
            mstr name_with_tags = mstr_copy_cstring(test_name);
            mstr_append(&name_with_tags, mstr_cstring(" [lock:live-server]"));

            // Skip DecimalNoPrecision if not a replica set.
            if (0 == strcmp(rangeType, "DecimalNoPrecision")) {
               TestSuite_AddFull(
                  suite,
                  name_with_tags.data,
                  rc.fn,
                  NULL /* dtor */,
                  (void *)rangeTypes[i] /* ctx */,
                  test_framework_skip_if_no_client_side_encryption,
                  test_framework_skip_if_max_wire_version_less_than_25, /* range queries require MongoDB 8.0+ */
                  test_framework_skip_if_not_replset);
            } else {
               TestSuite_AddFull(
                  suite,
                  name_with_tags.data,
                  rc.fn,
                  NULL /* dtor */,
                  (void *)rangeTypes[i] /* ctx */,
                  test_framework_skip_if_no_client_side_encryption,
                  test_framework_skip_if_max_wire_version_less_than_25, /* range queries require MongoDB 8.0+ */
                  test_framework_skip_if_single);
            }
            mstr_destroy(&name_with_tags);

            bson_free(test_name);
         }
      }

      TestSuite_AddFull(suite,
                        "/client_side_encryption/range_explicit_encryption/applies_defaults [lock:live-server]",
                        test_range_explicit_encryption_applies_defaults,
                        NULL,
                        NULL,
                        // No need to test for server version requirements. Test does not contact server.
                        test_framework_skip_if_no_client_side_encryption);

      TestSuite_AddFull(suite,
                        "/client_side_encryption/test_lookup [lock:live-server]",
                        test_lookup,
                        NULL,
                        NULL,
                        test_framework_skip_if_max_wire_version_less_than_26 /* require server 8.1+ */,
                        test_framework_skip_if_single, /* QE not supported on standalone */
                        test_framework_skip_if_no_client_side_encryption);
      TestSuite_AddFull(suite,
                        "/client_side_encryption/test_lookup/post-8.2 [lock:live-server]",
                        test_lookup_post82,
                        NULL,
                        NULL,
                        test_framework_skip_if_max_wire_version_less_than_27, /* require server 8.2+ */
                        skip_if_libmongocrypt_less_than_1_17_0,               /* require libmongocrypt 1.17.0+ */
                        test_framework_skip_if_single,                        /* QE not supported on standalone */
                        test_framework_skip_if_no_client_side_encryption);
      TestSuite_AddFull(suite,
                        "/client_side_encryption/test_lookup/pre-8.1 [lock:live-server]",
                        test_lookup_pre81,
                        NULL,
                        NULL,
                        test_framework_skip_if_max_wire_version_more_than_25 /* require server < 8.1 */,
                        test_framework_skip_if_max_wire_version_less_than_21 /* require server > 7.0 for QE support */,
                        test_framework_skip_if_single, /* QE not supported on standalone */
                        test_framework_skip_if_no_client_side_encryption);
      TestSuite_AddFull(suite,
                        "/client_side_encryption/explicit_encryption/text",
                        test_explicit_encryption_text,
                        NULL,
                        NULL,
                        test_framework_skip_if_max_wire_version_less_than_27 /* require server > 8.2 for QE support */,
                        test_framework_skip_if_single, /* QE not supported on standalone */
                        test_framework_skip_if_no_client_side_encryption);
   }
}
