#include <mongoc/mongoc-crypto-private.h>
#include <mongoc/mongoc-scram-private.h>

#include <mongoc/mongoc.h>

#include <TestSuite.h>
#include <mock_server/future-functions.h>
#include <mock_server/future.h>
#include <mock_server/mock-server.h>
#include <test-conveniences.h>
#include <test-libmongoc.h>

#ifdef MONGOC_ENABLE_SSL
static void
test_mongoc_scram_step_username_not_set(void)
{
   mongoc_scram_t scram;
   bool success;
   uint8_t buf[4096] = {0};
   uint32_t buflen = 0;
   bson_error_t error;

   _mongoc_scram_init(&scram, MONGOC_CRYPTO_ALGORITHM_SHA_1);
   _mongoc_scram_set_pass(&scram, "password");

   success = _mongoc_scram_step(&scram, buf, buflen, buf, sizeof buf, &buflen, &error);

   ASSERT(!success);
   ASSERT_ERROR_CONTAINS(
      error, MONGOC_ERROR_SCRAM, MONGOC_ERROR_SCRAM_PROTOCOL_ERROR, "SCRAM Failure: username is not set");

   _mongoc_scram_destroy(&scram);
}

typedef struct {
   const char *original;
   const char *normalized;
   bool should_be_required;
   bool should_succeed;
} sasl_prep_testcase_t;


/* test that an error is reported if the server responds with an iteration
 * count that is less than 4096 */
static void
test_iteration_count(int count, bool should_succeed)
{
   mongoc_scram_t scram;
   uint8_t buf[4096] = {0};
   uint32_t buflen = 0;
   bson_error_t error;
   const char *client_nonce = "YWJjZA==";
   char *server_response;
   bool success;

   server_response = bson_strdup_printf("r=YWJjZA==YWJjZA==,s=r6+P1iLmSJvhrRyuFi6Wsg==,i=%d", count);
   /* set up the scram state to immediately test step 2. */
   _mongoc_scram_init(&scram, MONGOC_CRYPTO_ALGORITHM_SHA_1);
   _mongoc_scram_set_pass(&scram, "password");
   bson_strncpy(scram.encoded_nonce, client_nonce, sizeof(scram.encoded_nonce));
   scram.encoded_nonce_len = (int32_t)strlen(client_nonce);
   scram.auth_message = bson_malloc0(4096);
   scram.auth_messagemax = 4096;
   /* prepare the server's "response" from step 1 as the input for step 2. */
   memcpy(buf, server_response, strlen(server_response) + 1);
   buflen = (int32_t)strlen(server_response);
   scram.step = 1;
   success = _mongoc_scram_step(&scram, buf, buflen, buf, sizeof buf, &buflen, &error);
   if (should_succeed) {
      ASSERT_OR_PRINT(success, error);
   } else {
      BSON_ASSERT(!success);
      ASSERT_ERROR_CONTAINS(error,
                            MONGOC_ERROR_SCRAM,
                            MONGOC_ERROR_SCRAM_PROTOCOL_ERROR,
                            "SCRAM Failure: iterations must be at least 4096");
   }
   bson_free(server_response);
   _mongoc_scram_destroy(&scram);
}

static void
test_mongoc_scram_iteration_count(void)
{
   test_iteration_count(1000, false);
   test_iteration_count(4095, false);
   test_iteration_count(4096, true);
   test_iteration_count(10000, true);
}

/* Regression test for CDRIVER-6315. */
static void
test_mongoc_scram_step_nonce_mismatch(void)
{
   mongoc_scram_t scram;
   uint8_t buf[4096] = {0};
   uint32_t buflen = 0;
   bson_error_t error;
   const char *client_nonce = "YWJjZA==";
   /* server-first message whose r= field does not begin with the client's nonce */
   const char *server_response = "r=ZZZZZZZZserver-nonce,s=r6+P1iLmSJvhrRyuFi6Wsg==,i=4096";
   bool success;

   _mongoc_scram_init(&scram, MONGOC_CRYPTO_ALGORITHM_SHA_1);
   _mongoc_scram_set_pass(&scram, "password");
   bson_strncpy(scram.encoded_nonce, client_nonce, sizeof(scram.encoded_nonce));
   scram.encoded_nonce_len = (int32_t)strlen(client_nonce);
   scram.auth_message = bson_malloc0(4096);
   scram.auth_messagemax = 4096;
   memcpy(buf, server_response, strlen(server_response) + 1);
   buflen = (int32_t)strlen(server_response);
   scram.step = 1;

   success = _mongoc_scram_step(&scram, buf, buflen, buf, sizeof buf, &buflen, &error);

   BSON_ASSERT(!success);
   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_SCRAM,
                         MONGOC_ERROR_SCRAM_PROTOCOL_ERROR,
                         "SCRAM Failure: client nonce not repeated in sasl step 2");

   _mongoc_scram_destroy(&scram);
}

static void
test_mongoc_scram_step2_invalid_parse_state(void)
{
   const char *responses[] = {
      "r",                                                /* bare key at end of buffer */
      "s",                                                /* bare key at end of buffer */
      "i",                                                /* bare key at end of buffer */
      "r=YWJjZA==YWJjZA==,s=r6+P1iLmSJvhrRyuFi6Wsg==,i",  /* valid fields then bare trailing key */
      "r?",                                               /* no '=' after key */
      "s+",                                               /* no '=' after key */
      "i~",                                               /* no '=' after key */
      "r=YWJjZA==YWJjZA==,s=r6+P1iLmSJvhrRyuFi6Wsg==,i-", /* valid fields then no '=' after key */
   };

   for (size_t idx = 0; idx < sizeof(responses) / sizeof(responses[0]); idx++) {
      const size_t inbuflen = strlen(responses[idx]);
      uint8_t *const inbuf = bson_malloc(inbuflen);
      memcpy(inbuf, responses[idx], inbuflen);

      mongoc_scram_t scram;
      _mongoc_scram_init(&scram, MONGOC_CRYPTO_ALGORITHM_SHA_1);
      _mongoc_scram_set_pass(&scram, "password");

      const char *const client_nonce = "YWJjZA==";
      bson_strncpy(scram.encoded_nonce, client_nonce, sizeof(scram.encoded_nonce));
      scram.encoded_nonce_len = (int32_t)strlen(client_nonce);
      scram.auth_message = bson_malloc0(4096);
      scram.auth_messagemax = 4096;
      /* Advance to step 2 (step is incremented in _mongoc_scram_step). */
      scram.step = 1;

      uint8_t outbuf[4096] = {0};
      uint32_t outbuflen = 0;
      bson_error_t error;
      ASSERT(!_mongoc_scram_step(&scram, inbuf, (uint32_t)inbuflen, outbuf, sizeof(outbuf), &outbuflen, &error));
      ASSERT_ERROR_CONTAINS(error,
                            MONGOC_ERROR_SCRAM,
                            MONGOC_ERROR_SCRAM_PROTOCOL_ERROR,
                            "SCRAM Failure: invalid parse state in sasl step 2");

      _mongoc_scram_destroy(&scram);
      bson_free(inbuf);
   }
}

static void
test_mongoc_scram_step3_trailing_key(void)
{
   const char *responses[] = {
      "e",        /* bare key at end of buffer */
      "v",        /* bare key at end of buffer */
      "v=abc,e",  /* valid field followed by bare trailing key */
      "e?",       /* no '=' after key */
      "v?",       /* no '=' after key */
      "v=abc,e?", /* valid field followed by no '=' after key */
   };

   for (size_t i = 0; i < sizeof(responses) / sizeof(responses[0]); i++) {
      const size_t inbuflen = strlen(responses[i]);
      uint8_t *const inbuf = bson_malloc(inbuflen);
      memcpy(inbuf, responses[i], inbuflen);

      mongoc_scram_t scram;
      _mongoc_scram_init(&scram, MONGOC_CRYPTO_ALGORITHM_SHA_1);
      _mongoc_scram_set_pass(&scram, "password");
      /* Advance to step 3 (step is incremented in _mongoc_scram_step). */
      scram.step = 2;

      uint8_t outbuf[4096] = {0};
      uint32_t outbuflen = 0;
      bson_error_t error;
      ASSERT(!_mongoc_scram_step(&scram, inbuf, (uint32_t)inbuflen, outbuf, sizeof(outbuf), &outbuflen, &error));
      ASSERT_ERROR_CONTAINS(error,
                            MONGOC_ERROR_SCRAM,
                            MONGOC_ERROR_SCRAM_PROTOCOL_ERROR,
                            "SCRAM Failure: invalid parse state in sasl step 3");

      _mongoc_scram_destroy(&scram);
      bson_free(inbuf);
   }
}

static void
test_mongoc_scram_sasl_prep(void)
{
   int i, ntests;
   char *normalized;
   bson_error_t err;
   /* examples from RFC 4013 section 3. */
   sasl_prep_testcase_t tests[] = {// normalization
                                   {"\x65\xCC\x81", "\xC3\xA9", true, true},
                                   {"\xC2\xAA", "a", true, true},
                                   {"Henry \xE2\x85\xA3", "Henry IV", true, true},
                                   {"A\xEF\xAC\x83n", "Affin", true, true},
                                   // mapped to nothing character (Table B.1)
                                   {"I\xC2\xADX", "IX", true, true},
                                   // mapped to nothing character (Table C.1.2)
                                   {"I\xE2\x80\x80\xC2\xA0X", "I  X", true, true},
                                   // prohibited character
                                   {"banana \x07 apple", "(invalid)", true, false},
                                   // unassigned codepoint (Table A.1)
                                   {"banana \xe0\xAA\xBA apple", "(invalid)", true, false},
                                   // bidi: RandALCat but not RandALCat at beginning and end
                                   {"\xD8\xA7\x31", "(invalid)", true, false},
                                   // bidi: RandALCat and LCat characters
                                   {"\xFB\x1D apple \x09\xA8", "(invalid)", true, false},
                                   // bidi: RandALCat with RandALCat at beginning and end
                                   {"\xD8\xA1 \xDC\x92", "\xD8\xA1 \xDC\x92", true, true},
                                   // normalization and mapped to nothing
                                   {"I\xE2\x80\x80\xC2\xA0X \xE2\x85\xA3", "I  X IV", true, true},
                                   {"user", "user", false, true},
                                   {"USER", "USER", false, true}};
   ntests = sizeof(tests) / sizeof(sasl_prep_testcase_t);
   for (i = 0; i < ntests; i++) {
      ASSERT_CMPINT(tests[i].should_be_required, ==, _mongoc_sasl_prep_required(tests[i].original));
      memset(&err, 0, sizeof(err));
      normalized = _mongoc_sasl_prep(tests[i].original, &err);
      if (tests[i].should_succeed) {
         ASSERT_CMPSTR(tests[i].normalized, normalized);
         ASSERT_CMPINT(err.code, ==, 0);
         bson_free(normalized);
      } else {
         ASSERT_CMPINT(err.code, ==, MONGOC_ERROR_SCRAM_PROTOCOL_ERROR);
         ASSERT_CMPINT(err.domain, ==, MONGOC_ERROR_SCRAM);
         BSON_ASSERT(normalized == NULL);
      }
   }
}

static void
test_mongoc_utf8_char_length(void)
{
   ASSERT_CMPSIZE_T(_mongoc_utf8_char_length(","), ==, 1u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_char_length("ɶ"), ==, 2u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_char_length("ྡྷ"), ==, 3u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_char_length("🌂"), ==, 4u);
}

static void
test_mongoc_utf8_string_length(void)
{
   ASSERT_CMPSIZE_T(_mongoc_utf8_string_length(",ase"), ==, 4u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_string_length("ɸɴ"), ==, 2u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_string_length("ྡྷ🌂e4🌕"), ==, 5u);
   ASSERT_CMPSIZE_T(_mongoc_utf8_string_length("no special characters"), ==, 21u);
}

static void
test_mongoc_utf8_to_unicode(void)
{
   ASSERT_CMPUINT32(_mongoc_utf8_get_first_code_point(",", 1), ==, 0x002C);
   ASSERT_CMPUINT32(_mongoc_utf8_get_first_code_point("ɶ", 2), ==, 0x0276);
   ASSERT_CMPUINT32(_mongoc_utf8_get_first_code_point("ྡྷ", 3), ==, 0x0FA2);
   ASSERT_CMPUINT32(_mongoc_utf8_get_first_code_point("🌂", 4), ==, 0x1F302);
}

#endif

enum {
   // ensure there are more users than slots in cache to test cache invalidation
   NUM_CACHE_TEST_USERS = 10 + MONGOC_SCRAM_CACHE_SIZE,

   // ensure that there are several times that the cache needs to be invalidated
   NUM_CACHE_TEST_THREADS = 3 * NUM_CACHE_TEST_USERS,
};

static char *_scram_cache_invalidation_uri_str = NULL;

static BSON_THREAD_FUN(_scram_cache_invalidation_thread, username_number_ptr)
{
   bson_error_t error;

   const char *password = "mypass";
   char *username = bson_strdup_printf("cachetestuser%dX", *(int *)username_number_ptr);
   bson_free(username_number_ptr);

   const char *uri_str = _scram_cache_invalidation_uri_str;
   char *cache_test_user_uri = test_framework_add_user_password(uri_str, username, password);
   BSON_ASSERT(cache_test_user_uri);

   mongoc_uri_t *cache_test_uri = mongoc_uri_new(cache_test_user_uri);
   BSON_ASSERT(cache_test_uri);

   // Set serverSelectionTryOnce=false so a single failed connection attempt
   // does not result in an error.
   mongoc_uri_set_option_as_bool(cache_test_uri, MONGOC_URI_SERVERSELECTIONTRYONCE, false);

   mongoc_client_t *client = test_framework_client_new_from_uri(cache_test_uri, NULL /* api */);
   BSON_ASSERT(client);

   test_framework_set_ssl_opts(client);
   BSON_ASSERT(client);

   mongoc_collection_t *collection = mongoc_client_get_collection(client, "admin", "testcache");
   BSON_ASSERT(collection);

   bson_t insert = BSON_INITIALIZER;
   bool ok = mongoc_collection_insert_one(collection, &insert, NULL, NULL, &error);
   ASSERT_OR_PRINT(ok, error);

   mongoc_collection_destroy(collection);
   mongoc_client_destroy(client);
   mongoc_uri_destroy(cache_test_uri);
   bson_free(cache_test_user_uri);
   bson_free(username);

   BSON_THREAD_RETURN;
}

static void
test_mongoc_scram_cache_invalidation(void *ctx)
{
   BSON_UNUSED(ctx);

   bson_error_t error;
   mongoc_uri_t *const uri = test_framework_get_uri();
   BSON_ASSERT(uri);

   mongoc_client_t *client = test_framework_new_default_client();
   BSON_ASSERT(client);

   mongoc_database_t *const db = mongoc_client_get_database(client, "admin");
   BSON_ASSERT(db);

   bson_t *roles = tmp_bson("[{'role': 'readWrite', 'db': 'admin'}]");

   _scram_cache_invalidation_uri_str = test_framework_get_uri_str_no_auth("admin");

   /* Remove cache test users if they already exist.
    * Create more test users than could exist in cache. */
   for (int i = 0; i < NUM_CACHE_TEST_USERS; i++) {
      const char *password = "mypass";
      char *username = bson_strdup_printf("cachetestuser%dX", i);

      mongoc_database_remove_user(db, username, &error);
      bool ok = mongoc_database_add_user(db, username, password, roles, NULL, &error);
      ASSERT_OR_PRINT(ok, error);
      bson_free(username);
   }

   bson_thread_t threads[NUM_CACHE_TEST_THREADS];
   for (int i = 0; i < NUM_CACHE_TEST_THREADS; i++) {
      int *username_number_ptr = bson_malloc(sizeof(*username_number_ptr));
      *username_number_ptr = i % NUM_CACHE_TEST_USERS;
      int rc = mcommon_thread_create(&threads[i], _scram_cache_invalidation_thread, username_number_ptr);
      BSON_ASSERT(rc == 0);
   }

   for (int i = 0; i < NUM_CACHE_TEST_THREADS; i++) {
      int rc = mcommon_thread_join(threads[i]);
      BSON_ASSERT(rc == 0);
   }

   bson_free(_scram_cache_invalidation_uri_str);
   _scram_cache_invalidation_uri_str = NULL;
   mongoc_database_destroy(db);
   mongoc_client_destroy(client);
   mongoc_uri_destroy(uri);
}

static void
_clear_scram_users(void)
{
   mongoc_client_t *const client = test_framework_new_default_client();
   ASSERT(client);
   mongoc_database_t *const db = mongoc_client_get_database(client, "admin");
   ASSERT(db);
   (void)mongoc_database_remove_user(db, "sha1", NULL);
   (void)mongoc_database_remove_user(db, "sha256", NULL);
   (void)mongoc_database_remove_user(db, "both", NULL);
   mongoc_database_destroy(db);
   mongoc_client_destroy(client);
}

static void
_create_scram_users(void)
{
   mongoc_client_t *client;
   bool res;
   bson_error_t error;
   client = test_framework_new_default_client();
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': 'sha1', 'pwd': 'sha1', 'roles': ['root'], "
                                               "'mechanisms': ['SCRAM-SHA-1']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': 'sha256', 'pwd': 'sha256', 'roles': ['root'], "
                                               "'mechanisms': ['SCRAM-SHA-256']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': 'both', 'pwd': 'both', 'roles': ['root'], "
                                               "'mechanisms': ['SCRAM-SHA-1', 'SCRAM-SHA-256']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(res, error);
   mongoc_client_destroy(client);
}

static void
_drop_scram_users(void)
{
   mongoc_client_t *client;
   mongoc_database_t *db;
   bool res;
   bson_error_t error;
   client = test_framework_new_default_client();
   db = mongoc_client_get_database(client, "admin");
   res = mongoc_database_remove_user(db, "sha1", &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_database_remove_user(db, "sha256", &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_database_remove_user(db, "both", &error);
   ASSERT_OR_PRINT(res, error);
   mongoc_database_destroy(db);
   mongoc_client_destroy(client);
}

static void
_check_mechanism(bool pooled, const char *client_mech, const char *server_mechs, const char *expected_used_mech)
{
   mock_server_t *server;
   mongoc_client_pool_t *client_pool = NULL;
   mongoc_client_t *client = NULL;
   mongoc_uri_t *uri;
   future_t *future;
   request_t *request;
   const bson_t *sasl_doc;
   const char *used_mech;

   server = mock_server_new();
   mock_server_auto_hello(server,
                          "{'ok': 1,"
                          " 'minWireVersion': %d,"
                          " 'maxWireVersion': %d,"
                          " 'isWritablePrimary': true,"
                          " 'saslSupportedMechs': [%s]}",
                          WIRE_VERSION_MIN,
                          WIRE_VERSION_MAX,
                          server_mechs ? server_mechs : "");

   mock_server_run(server);
   uri = mongoc_uri_copy(mock_server_get_uri(server));
   mongoc_uri_set_username(uri, "user");
   mongoc_uri_set_password(uri, "password");
   if (client_mech) {
      mongoc_uri_set_auth_mechanism(uri, client_mech);
   }

   if (pooled) {
      client_pool = test_framework_client_pool_new_from_uri(uri, NULL);
      client = mongoc_client_pool_pop(client_pool);
      /* suppress the auth failure logs from pooled clients. */
      capture_logs(true);
   } else {
      client = test_framework_client_new_from_uri(uri, NULL);
   }
   future = future_client_command_simple(
      client, "admin", tmp_bson("{'dbstats': 1}"), NULL /* read_prefs. */, NULL /* reply. */, NULL /* error. */);
   request = mock_server_receives_msg(server, MONGOC_QUERY_NONE, tmp_bson("{}"));
   sasl_doc = request_get_doc(request, 0);
   used_mech = bson_lookup_utf8(sasl_doc, "mechanism");
   ASSERT_CMPSTR(used_mech, expected_used_mech);
   /* we're not actually going to auth, just hang up. */
   reply_to_request_with_hang_up(request);
   future_wait(future);
   future_destroy(future);
   request_destroy(request);
   mongoc_uri_destroy(uri);
   if (pooled) {
      mongoc_client_pool_push(client_pool, client);
      mongoc_client_pool_destroy(client_pool);
      capture_logs(false);
   } else {
      mongoc_client_destroy(client);
   }
   mock_server_destroy(server);
}

typedef enum { MONGOC_TEST_NO_ERROR, MONGOC_TEST_USER_NOT_FOUND_ERROR, MONGOC_TEST_AUTH_ERROR } test_error_t;

void
_check_error(const bson_error_t *error, test_error_t expected_error)
{
   uint32_t domain = 0;
   uint32_t code = 0;
   const char *message = "";

   switch (expected_error) {
   case MONGOC_TEST_AUTH_ERROR: {
      domain = MONGOC_ERROR_CLIENT;
      code = MONGOC_ERROR_CLIENT_AUTHENTICATE;
      ASSERT_CMPUINT32(error->domain, ==, domain);
      ASSERT_CMPUINT32(error->code, ==, code);
      const char *const a = "Authentication failed";
      const char *const b = "Unable to use";
      const bool found = strstr(error->message, a) || strstr(error->message, b);
      ASSERT_WITH_MSG(found, "[%s] does not contain [%s] or [%s]", error->message, a, b);
      break;
   }
   case MONGOC_TEST_USER_NOT_FOUND_ERROR:
      domain = MONGOC_ERROR_CLIENT;
      code = MONGOC_ERROR_CLIENT_AUTHENTICATE;
      message = "Could not find user";
      break;
   case MONGOC_TEST_NO_ERROR:
   default:
      return;
   }

   ASSERT_ERROR_CONTAINS((*error), domain, code, message);
}

static void
_try_auth_from_uri(bool pooled, mongoc_uri_t *uri, test_error_t expected_error)
{
   mongoc_client_pool_t *client_pool = NULL;
   mongoc_client_t *client = NULL;
   mongoc_collection_t *coll;
   bson_error_t error;
   bson_t reply;
   bool res;

   if (pooled) {
      client_pool = test_framework_client_pool_new_from_uri(uri, NULL);
      test_framework_set_pool_ssl_opts(client_pool);
      mongoc_client_pool_set_error_api(client_pool, 2);
      client = mongoc_client_pool_pop(client_pool);
      /* suppress the auth failure logs from pooled clients. */
      capture_logs(true);
   } else {
      client = test_framework_client_new_from_uri(uri, NULL);
      mongoc_client_set_error_api(client, 2);
      test_framework_set_ssl_opts(client);
   }
   coll = get_test_collection(client, "try_auth");
   res = mongoc_collection_insert_one(coll, tmp_bson("{'x': 1}"), NULL /* opts */, &reply, &error);

   if (expected_error == MONGOC_TEST_NO_ERROR) {
      ASSERT_OR_PRINT(res, error);
      ASSERT_MATCH(&reply, "{'insertedCount': 1 }");
   } else {
      ASSERT(!res);
      _check_error(&error, expected_error);
   }
   bson_destroy(&reply);
   mongoc_collection_destroy(coll);
   if (pooled) {
      mongoc_client_pool_push(client_pool, client);
      mongoc_client_pool_destroy(client_pool);
      capture_logs(false);
   } else {
      mongoc_client_destroy(client);
   }
}

/* if auth is expected to succeed, expected_error is zero'd out. */
static void
_try_auth(bool pooled, const char *user, const char *pwd, const char *mechanism, test_error_t expected_error)
{
   mongoc_uri_t *uri;

   uri = test_framework_get_uri();
   mongoc_uri_set_username(uri, user);
   mongoc_uri_set_password(uri, pwd);
   if (mechanism) {
      mongoc_uri_set_auth_mechanism(uri, mechanism);
   }
   _try_auth_from_uri(pooled, uri, expected_error);
   mongoc_uri_destroy(uri);
}


static void
_test_mongoc_scram_auth(bool pooled)
{
   /* Auth spec: "For each test user, verify that you can connect and run a
   command requiring authentication for the following cases:
   - Explicitly specifying each mechanism the user supports.
   - Specifying no mechanism and relying on mechanism negotiation." */
   _try_auth(pooled, "sha1", "sha1", NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "sha1", "sha1", "SCRAM-SHA-1", MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "sha256", "sha256", NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "sha256", "sha256", "SCRAM-SHA-256", MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "both", "both", NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "both", "both", "SCRAM-SHA-1", MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "both", "both", "SCRAM-SHA-256", MONGOC_TEST_NO_ERROR);

   _check_mechanism(pooled, NULL, NULL, "SCRAM-SHA-1");
   _check_mechanism(pooled, NULL, "'SCRAM-SHA-1'", "SCRAM-SHA-1");
   _check_mechanism(pooled, NULL, "'SCRAM-SHA-256'", "SCRAM-SHA-256");
   _check_mechanism(pooled, NULL, "'SCRAM-SHA-1','SCRAM-SHA-256'", "SCRAM-SHA-256");

   _check_mechanism(pooled, "SCRAM-SHA-1", NULL, "SCRAM-SHA-1");
   _check_mechanism(pooled, "SCRAM-SHA-1", "'SCRAM-SHA-1'", "SCRAM-SHA-1");
   _check_mechanism(pooled, "SCRAM-SHA-1", "'SCRAM-SHA-256'", "SCRAM-SHA-1");
   _check_mechanism(pooled, "SCRAM-SHA-1", "'SCRAM-SHA-1','SCRAM-SHA-256'", "SCRAM-SHA-1");

   _check_mechanism(pooled, "SCRAM-SHA-256", NULL, "SCRAM-SHA-256");
   _check_mechanism(pooled, "SCRAM-SHA-256", "'SCRAM-SHA-1'", "SCRAM-SHA-256");
   _check_mechanism(pooled, "SCRAM-SHA-256", "'SCRAM-SHA-256'", "SCRAM-SHA-256");
   _check_mechanism(pooled, "SCRAM-SHA-256", "'SCRAM-SHA-1','SCRAM-SHA-256'", "SCRAM-SHA-256");

   /* Test some failure auths. */
   _try_auth(pooled, "sha1", "bad", NULL, MONGOC_TEST_AUTH_ERROR);
   _try_auth(pooled, "sha256", "bad", NULL, MONGOC_TEST_AUTH_ERROR);
   _try_auth(pooled, "both", "bad", NULL, MONGOC_TEST_AUTH_ERROR);
   _try_auth(pooled, "sha1", "bad", "SCRAM-SHA-256", MONGOC_TEST_AUTH_ERROR);
   _try_auth(pooled, "sha256", "bad", "SCRAM-SHA-1", MONGOC_TEST_AUTH_ERROR);

   /* Auth spec: "For a non-existent username, verify that not specifying a
    * mechanism when connecting fails with the same error type that would occur
    * with a correct username but incorrect password or mechanism." */
   _try_auth(pooled, "unknown_user", "bad", NULL, MONGOC_TEST_AUTH_ERROR);
}

/* test the auth tests described in the auth spec. */
static void
test_mongoc_scram_auth(void *ctx)
{
   BSON_UNUSED(ctx);

   _clear_scram_users();

   /* Auth spec: "Create three test users, one with only SHA-1, one with only
    * SHA-256 and one with both" */
   _create_scram_users();
   _test_mongoc_scram_auth(false);
   _test_mongoc_scram_auth(true);
   _drop_scram_users();
}

static int
_skip_if_no_sha256(void)
{
   mongoc_client_t *client;
   bool res;
   bson_error_t error;

   client = test_framework_new_default_client();

   /* Check if SCRAM-SHA-256 is a supported auth mechanism by attempting to
    * create a new user with it. */
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': 'temp', 'pwd': 'sha256', 'roles': ['root'], "
                                               "'mechanisms': ['SCRAM-SHA-256']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);

   if (res) {
      mongoc_database_t *db;

      db = mongoc_client_get_database(client, "admin");
      ASSERT_OR_PRINT(mongoc_database_remove_user(db, "temp", &error), error);
      mongoc_database_destroy(db);
   }

   mongoc_client_destroy(client);
   return res ? 1 : 0;
}

#define ROMAN_NUMERAL_NINE "\xE2\x85\xA8"
#define ROMAN_NUMERAL_FOUR "\xE2\x85\xA3"

static void
_clear_saslprep_users(void)
{
   mongoc_client_t *const client = test_framework_new_default_client();
   ASSERT(client);
   mongoc_database_t *const db = mongoc_client_get_database(client, "admin");
   ASSERT(db);
   (void)mongoc_database_remove_user(db, "IX", NULL);
   (void)mongoc_database_remove_user(db, ROMAN_NUMERAL_NINE, NULL);
   mongoc_database_destroy(db);
   mongoc_client_destroy(client);
}

static void
_create_saslprep_users(void)
{
   mongoc_client_t *client;
   bool res;
   bson_error_t error;
   client = test_framework_new_default_client();
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': 'IX', 'pwd': 'IX', 'roles': ['root'], "
                                               "'mechanisms': ['SCRAM-SHA-256']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_client_command_simple(client,
                                      "admin",
                                      tmp_bson("{'createUser': '" ROMAN_NUMERAL_NINE "', 'pwd': '" ROMAN_NUMERAL_FOUR
                                               "', 'roles': ['root'], 'mechanisms': ['SCRAM-SHA-256']}"),
                                      NULL /* read_prefs */,
                                      NULL /* reply */,
                                      &error);
   ASSERT_OR_PRINT(res, error);
   mongoc_client_destroy(client);
}

static void
_drop_saslprep_users(void)
{
   mongoc_client_t *client;
   mongoc_database_t *db;
   bool res;
   bson_error_t error;
   client = test_framework_new_default_client();
   db = mongoc_client_get_database(client, "admin");
   res = mongoc_database_remove_user(db, "IX", &error);
   ASSERT_OR_PRINT(res, error);
   res = mongoc_database_remove_user(db, ROMAN_NUMERAL_NINE, &error);
   ASSERT_OR_PRINT(res, error);
   mongoc_database_destroy(db);
   mongoc_client_destroy(client);
}

static void
_make_uri(const char *username, const char *password, mongoc_uri_t **out)
{
   char *uri_str;
   char *tmp;

   tmp = test_framework_get_uri_str_no_auth("admin");
   uri_str = test_framework_add_user_password(tmp, username, password);
   mongoc_uri_destroy(*out);
   *out = mongoc_uri_new(uri_str);
   bson_free(tmp);
   bson_free(uri_str);
}

static void
_test_mongoc_scram_saslprep_auth(bool pooled)
{
   mongoc_uri_t *uri = NULL;

   /* Test URIs of the form in the auth spec test plan for SASLPrep.
      - mongodb://IX:IX@mongodb.example.com/admin
      - mongodb://IX:I%C2%ADX@mongodb.example.com/admin
      - mongodb://%E2%85%A8:IV@mongodb.example.com/admin
      - mongodb://%E2%85%A8:I%C2%ADV@mongodb.example.com/admin

      Test in three ways.
      1. By embedding the multi-byte UTF-8 characters directly into the
      connection string.
      2. By percent escaping the multi-byte UTF-8 characters.
      3. By using the setters, mongoc_uri_set_username/mongoc_uri_set_password
      and embedding the UTF-8 characters (percent unescaping does not occur for
      the setters)
   */

   /* Way 1: embedding multi-byte UTF-8 characters directly */
   _make_uri("IX", "IX", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri("IX", ROMAN_NUMERAL_NINE, &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri(ROMAN_NUMERAL_NINE, "IV", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri(ROMAN_NUMERAL_NINE, ROMAN_NUMERAL_FOUR, &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   /* Way 2: Percent escaping */
   _make_uri("IX", "IX", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri("IX", "I%C2%ADX", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri("%E2%85%A8", "IV", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);

   _make_uri("%E2%85%A8", "I%C2%ADV", &uri);
   _try_auth_from_uri(pooled, uri, MONGOC_TEST_NO_ERROR);
   mongoc_uri_destroy(uri);

   /* Way 3: with username/password setters. */
   _try_auth(pooled, "IX", "IX", NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, "IX", ROMAN_NUMERAL_NINE, NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, ROMAN_NUMERAL_NINE, "IV", NULL, MONGOC_TEST_NO_ERROR);
   _try_auth(pooled, ROMAN_NUMERAL_NINE, ROMAN_NUMERAL_FOUR, NULL, MONGOC_TEST_NO_ERROR);
}


static void
test_mongoc_saslprep_auth(void *ctx)
{
   BSON_UNUSED(ctx);

   _clear_saslprep_users();
   _create_saslprep_users();
   _test_mongoc_scram_saslprep_auth(false);
   _test_mongoc_scram_saslprep_auth(true);
   _drop_saslprep_users();
}

// `test_mongoc_scram_empty_password` is a regression test for CDRIVER-5550.
static void
test_mongoc_scram_empty_password(void *ctx)
{
   BSON_UNUSED(ctx);
   char *user = test_framework_get_admin_user();
   char *uri_str = test_framework_get_uri_str_no_auth("admin");
   mongoc_uri_t *uri = mongoc_uri_new(uri_str);
   mongoc_uri_set_username(uri, user);

   // Expect an auth failure (not a crash):
   _try_auth_from_uri(false /* pooled */, uri, MONGOC_TEST_AUTH_ERROR);
   _try_auth_from_uri(true /* pooled */, uri, MONGOC_TEST_AUTH_ERROR);

   mongoc_uri_destroy(uri);
   bson_free(uri_str);
   bson_free(user);
}

void
test_scram_install(TestSuite *suite)
{
#ifdef MONGOC_ENABLE_SSL
   TestSuite_Add(suite, "/scram/username_not_set", test_mongoc_scram_step_username_not_set);
   TestSuite_Add(suite, "/scram/sasl_prep", test_mongoc_scram_sasl_prep);
   TestSuite_Add(suite, "/scram/iteration_count", test_mongoc_scram_iteration_count);
   TestSuite_Add(suite, "/scram/nonce_mismatch", test_mongoc_scram_step_nonce_mismatch);
   TestSuite_Add(suite, "/scram/step2_invalid_parse_state", test_mongoc_scram_step2_invalid_parse_state);
   TestSuite_Add(suite, "/scram/step3_invalid_parse_state", test_mongoc_scram_step3_trailing_key);
   TestSuite_Add(suite, "/scram/utf8_char_length", test_mongoc_utf8_char_length);
   TestSuite_Add(suite, "/scram/utf8_string_length", test_mongoc_utf8_string_length);
   TestSuite_Add(suite, "/scram/utf8_to_unicode", test_mongoc_utf8_to_unicode);
#endif
   TestSuite_AddFull(suite,
                     "/scram/cache_invalidation [lock:live-server]",
                     test_mongoc_scram_cache_invalidation,
                     NULL,
                     NULL,
                     test_framework_skip_if_no_auth,
                     test_framework_skip_if_macos); // CDRIVER-6079
   TestSuite_AddFull(suite,
                     "/scram/auth_tests [lock:live-server]",
                     test_mongoc_scram_auth,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_auth,
                     _skip_if_no_sha256,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/scram/saslprep_auth [lock:live-server]",
                     test_mongoc_saslprep_auth,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_auth,
                     _skip_if_no_sha256,
                     TestSuite_CheckLive);
   TestSuite_AddFull(suite,
                     "/scram/empty_password [lock:live-server]",
                     test_mongoc_scram_empty_password,
                     NULL /* dtor */,
                     NULL /* ctx */,
                     test_framework_skip_if_no_auth,
                     _skip_if_no_sha256,
                     TestSuite_CheckLive);
}
