#include <common-oid-private.h>
#include <mongoc/mongoc-client-private.h>
#include <mongoc/mongoc-host-list-private.h>
#include <mongoc/mongoc-socket-private.h>
#include <mongoc/mongoc-stream-private.h>
#include <mongoc/mongoc-util-private.h>

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
#include <mongoc/mongoc-openssl-private.h>
#endif

#include <mongoc/mongoc.h>
#include <mongoc/utlist.h>

#include <mlib/time_point.h>

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

#define TIMEOUT 20000 /* milliseconds */
#define NSERVERS 10

static void
test_topology_scanner_helper(
   uint32_t id, const bson_t *bson, int64_t rtt_msec, void *data, const bson_error_t *error /* IN */)
{
   bson_iter_t iter;
   int *finished = (int *)data;
   uint32_t max_wire_version;

   BSON_UNUSED(rtt_msec);

   if (error->code) {
      test_error("scanner error: %s", error->message);
   }

   /* mock servers are configured to return distinct wire versions */
   BSON_ASSERT(bson);
   BSON_ASSERT(bson_iter_init_find(&iter, bson, "maxWireVersion"));
   BSON_ASSERT(BSON_ITER_HOLDS_INT32(&iter));
   max_wire_version = (uint32_t)bson_iter_int32(&iter);
   ASSERT_CMPINT(max_wire_version, ==, id + WIRE_VERSION_MIN);

   (*finished)--;
}

static void
_test_topology_scanner(bool with_ssl)
{
   mock_server_t *servers[NSERVERS];
   int i;
   bson_t q = BSON_INITIALIZER;
   int finished = NSERVERS * 3;

#ifdef MONGOC_ENABLE_SSL
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
#endif

   bson_oid_t topology_id;
   mcommon_oid_set_zero(&topology_id);
   mongoc_log_and_monitor_instance_t log_and_monitor;
   mongoc_log_and_monitor_instance_init(&log_and_monitor);
   mongoc_topology_scanner_t *topology_scanner = mongoc_topology_scanner_new(
      NULL, &topology_id, &log_and_monitor, NULL, &test_topology_scanner_helper, &finished, TIMEOUT);

#ifdef MONGOC_ENABLE_SSL
   if (with_ssl) {
      copt.ca_file = CERT_CA;
      copt.weak_cert_validation = 1;

      mongoc_topology_scanner_set_ssl_opts(topology_scanner, &copt);
#if defined(MONGOC_ENABLE_SSL_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10100000L
      topology_scanner->openssl_ctx = _mongoc_openssl_ctx_new(&copt);
#endif
   }
#endif

   for (i = 0; i < NSERVERS; i++) {
      /* use max wire versions just to distinguish among responses */
      servers[i] = mock_server_with_auto_hello(i + WIRE_VERSION_MIN);
      mock_server_set_rand_delay(servers[i], true);

#ifdef MONGOC_ENABLE_SSL
      if (with_ssl) {
         sopt.ca_file = CERT_CA;
         sopt.pem_file = CERT_SERVER;

         mock_server_set_ssl_opts(servers[i], &sopt);
      }
#endif

      mock_server_run(servers[i]);

      mongoc_topology_scanner_add(
         topology_scanner, mongoc_uri_get_hosts(mock_server_get_uri(servers[i])), (uint32_t)i, false);
   }

   for (i = 0; i < 3; i++) {
      mongoc_topology_scanner_start(topology_scanner, false);
      mongoc_topology_scanner_work(topology_scanner);
   }

   BSON_ASSERT(finished == 0);

   mongoc_topology_scanner_destroy(topology_scanner);
   mongoc_log_and_monitor_instance_destroy_contents(&log_and_monitor);

   bson_destroy(&q);

   for (i = 0; i < NSERVERS; i++) {
      mock_server_destroy(servers[i]);
   }
}


void
test_topology_scanner(void)
{
   _test_topology_scanner(false);
}


#ifdef MONGOC_ENABLE_SSL_OPENSSL
void
test_topology_scanner_ssl(void)
{
   _test_topology_scanner(true);
}
#endif


/*
 * Servers discovered by a scan should be checked during that scan, CDRIVER-751.
 */
void
test_topology_scanner_discovery(void)
{
   mock_server_t *primary;
   mock_server_t *secondary;
   char *primary_response;
   char *secondary_response;
   mongoc_client_t *client;
   char *uri_str;
   mongoc_read_prefs_t *secondary_pref;
   bson_error_t error;
   future_t *future;
   request_t *request;
   mongoc_server_description_t *sd;

   primary = mock_server_new();
   secondary = mock_server_new();
   mock_server_run(primary);
   mock_server_run(secondary);

   primary_response = bson_strdup_printf("{'ok': 1, "
                                         " 'isWritablePrimary': true,"
                                         " 'setName': 'rs',"
                                         " 'minWireVersion': %d,"
                                         " 'maxWireVersion': %d,"
                                         " 'hosts': ['%s', '%s']}",
                                         WIRE_VERSION_MIN,
                                         WIRE_VERSION_MAX,
                                         mock_server_get_host_and_port(primary),
                                         mock_server_get_host_and_port(secondary));

   secondary_response = bson_strdup_printf("{'ok': 1, "
                                           " 'isWritablePrimary': false,"
                                           " 'secondary': true,"
                                           " 'setName': 'rs',"
                                           " 'minWireVersion': %d,"
                                           " 'maxWireVersion': %d,"
                                           " 'hosts': ['%s', '%s']}",
                                           WIRE_VERSION_MIN,
                                           WIRE_VERSION_MAX,
                                           mock_server_get_host_and_port(primary),
                                           mock_server_get_host_and_port(secondary));

   uri_str = bson_strdup_printf("mongodb://%s/?" MONGOC_URI_REPLICASET "=rs", mock_server_get_host_and_port(primary));
   client = test_framework_client_new(uri_str, NULL);
   secondary_pref = mongoc_read_prefs_new(MONGOC_READ_SECONDARY_PREFERRED);

   future = future_topology_select(client->topology, MONGOC_SS_READ, TEST_SS_LOG_CONTEXT, secondary_pref, NULL, &error);

   /* a single scan discovers *and* checks the secondary */
   request = mock_server_receives_any_hello(primary);
   reply_to_request_simple(request, primary_response);
   request_destroy(request);

   /* let client process that response */
   mlib_sleep_for(250, ms);

   /* a check of the secondary is scheduled in this scan */
   request = mock_server_receives_any_hello(secondary);
   reply_to_request_simple(request, secondary_response);

   /* scan completes */
   ASSERT_OR_PRINT((sd = future_get_mongoc_server_description_ptr(future)), error);

   ASSERT_CMPSTR(sd->host.host_and_port, mock_server_get_host_and_port(secondary));

   mongoc_server_description_destroy(sd);
   future_destroy(future);
   request_destroy(request);
   mongoc_read_prefs_destroy(secondary_pref);
   bson_free(secondary_response);
   bson_free(primary_response);
   bson_free(uri_str);
   mongoc_client_destroy(client);
   mock_server_destroy(secondary);
   mock_server_destroy(primary);
}


/* scanner shouldn't spin if two primaries point at each other */
void
test_topology_scanner_oscillate(void)
{
   mock_server_t *server0;
   mock_server_t *server1;
   char *server0_response;
   char *server1_response;
   mongoc_client_t *client;
   mongoc_topology_scanner_t *scanner;
   char *uri_str;
   mongoc_read_prefs_t *primary_pref;
   bson_error_t error;
   future_t *future;
   request_t *request;

   server0 = mock_server_new();
   server1 = mock_server_new();
   mock_server_run(server0);
   mock_server_run(server1);

   /* server 0 says it's primary, but only server 1 is in the set */
   server0_response = bson_strdup_printf("{'ok': 1, "
                                         " 'isWritablePrimary': true,"
                                         " 'setName': 'rs',"
                                         " 'hosts': ['%s']}",
                                         mock_server_get_host_and_port(server1));

   /* the opposite */
   server1_response = bson_strdup_printf("{'ok': 1, "
                                         " 'isWritablePrimary': true,"
                                         " 'setName': 'rs',"
                                         " 'hosts': ['%s']}",
                                         mock_server_get_host_and_port(server0));

   /* start with server 0 */
   uri_str = bson_strdup_printf("mongodb://%s/?" MONGOC_URI_REPLICASET "=rs", mock_server_get_host_and_port(server0));
   client = test_framework_client_new(uri_str, NULL);
   scanner = client->topology->scanner;
   primary_pref = mongoc_read_prefs_new(MONGOC_READ_PRIMARY);

   BSON_ASSERT(!scanner->async->ncmds);
   future = future_topology_select(client->topology, MONGOC_SS_READ, TEST_SS_LOG_CONTEXT, primary_pref, NULL, &error);

   /* a single scan discovers servers 0 and 1 */
   request = mock_server_receives_any_hello(server0);
   reply_to_request_simple(request, server0_response);
   request_destroy(request);

   /* let client process that response */
   mlib_sleep_for(250, ms);

   request = mock_server_receives_any_hello(server1);
   reply_to_request_simple(request, server1_response);

   /* we don't schedule another check of server0 */
   mlib_sleep_for(250, ms);

   BSON_ASSERT(!future_get_mongoc_server_description_ptr(future));
   BSON_ASSERT(scanner->async->ncmds == 0);

   future_destroy(future);
   request_destroy(request);
   mongoc_read_prefs_destroy(primary_pref);
   bson_free(server1_response);
   bson_free(server0_response);
   bson_free(uri_str);
   mongoc_client_destroy(client);
   mock_server_destroy(server1);
   mock_server_destroy(server0);
}


void
test_topology_scanner_connection_error(void)
{
   mongoc_client_t *client;
   bson_error_t error;

   /* assuming nothing is listening on this port */
   client = test_framework_client_new("mongodb://localhost:9876", NULL);

   ASSERT(!mongoc_client_command_simple(client, "db", tmp_bson("{'foo': 1}"), NULL, NULL, &error));

   ASSERT_ERROR_CONTAINS(error,
                         MONGOC_ERROR_SERVER_SELECTION,
                         MONGOC_ERROR_SERVER_SELECTION_FAILURE,
                         "connection refused calling hello on "
                         "'localhost:9876'");

   mongoc_client_destroy(client);
}


void
test_topology_scanner_socket_timeout(void)
{
   mock_server_t *server;
   mongoc_client_t *client;
   mongoc_uri_t *uri;
   bson_error_t error;
   char *expected_msg;

   server = mock_server_new();
   mock_server_run(server);

   uri = mongoc_uri_copy(mock_server_get_uri(server));
   mongoc_uri_set_option_as_int32(uri, MONGOC_URI_CONNECTTIMEOUTMS, 10);
   client = test_framework_client_new_from_uri(uri, NULL);

   ASSERT(!mongoc_client_command_simple(client, "db", tmp_bson("{'foo': 1}"), NULL, NULL, &error));

   /* the mock server did accept connection, but never replied */
   expected_msg = bson_strdup_printf("socket timeout calling hello on '%s'", mongoc_uri_get_hosts(uri)->host_and_port);

   ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER_SELECTION, MONGOC_ERROR_SERVER_SELECTION_FAILURE, expected_msg);

   bson_free(expected_msg);
   mongoc_client_destroy(client);
   mongoc_uri_destroy(uri);
   mock_server_destroy(server);
}


typedef struct {
   uint16_t slow_port;
   mongoc_client_t *client;
} initiator_data_t;


static mongoc_stream_t *
slow_initiator(const mongoc_uri_t *uri, const mongoc_host_list_t *host, void *user_data, bson_error_t *err)
{
   initiator_data_t *data;

   data = (initiator_data_t *)user_data;

   if (host->port == data->slow_port) {
      mlib_sleep_for(500, ms); /* 500 ms is longer than connectTimeoutMS */
   }

   return mongoc_client_default_stream_initiator(uri, host, data->client, err);
}


static void
test_topology_scanner_blocking_initiator(void)
{
   mock_rs_t *rs;
   mongoc_uri_t *uri;
   mongoc_client_t *client;
   initiator_data_t data;
   bson_error_t error;

   rs = mock_rs_with_auto_hello(WIRE_VERSION_MIN, /* wire version   */
                                true,             /* has primary    */
                                1,                /* n_secondaries  */
                                0 /* n_arbiters     */);

   mock_rs_run(rs);
   uri = mongoc_uri_copy(mock_rs_get_uri(rs));
   mongoc_uri_set_option_as_int32(uri, MONGOC_URI_CONNECTTIMEOUTMS, 100);
   client = test_framework_client_new_from_uri(uri, NULL);

   /* pretend last host in linked list is slow */
   data.slow_port = mongoc_uri_get_hosts(uri)->next->port;
   data.client = client;
   mongoc_client_set_stream_initiator(client, slow_initiator, &data);

   ASSERT_OR_PRINT(mongoc_client_command_simple(
                      client, "admin", tmp_bson("{'" HANDSHAKE_CMD_LEGACY_HELLO "': 1}"), NULL, NULL, &error),
                   error);

   mongoc_client_destroy(client);
   mongoc_uri_destroy(uri);
   mock_rs_destroy(rs);
}

static mock_server_t *
_mock_server_listening_on(char *server_bind_to)
{
   mock_server_t *mock_server;
   mock_server_bind_opts_t opts = {0};
   struct sockaddr_in ipv4_addr = {0};
   struct sockaddr_in6 ipv6_addr = {0};

   if (strcmp("both", server_bind_to) == 0) {
      opts.bind_addr_len = sizeof(ipv6_addr);
      opts.family = AF_INET6;
      opts.ipv6_only = 0;
      ipv6_addr.sin6_family = AF_INET6;
      ipv6_addr.sin6_port = htons(0);    /* any port */
      ipv6_addr.sin6_addr = in6addr_any; /* either IPv4 or IPv6 */
      opts.bind_addr = (struct sockaddr_in *)&ipv6_addr;
   } else if (strcmp("ipv4", server_bind_to) == 0) {
      opts.bind_addr_len = sizeof(ipv4_addr);
      opts.family = AF_INET;
      opts.ipv6_only = 0;
      ipv4_addr.sin_family = AF_INET;
      ipv4_addr.sin_port = htons(0);
      BSON_ASSERT(inet_pton(AF_INET, "127.0.0.1", &ipv4_addr.sin_addr));
      opts.bind_addr = &ipv4_addr;
   } else if (strcmp("ipv6", server_bind_to) == 0) {
      opts.bind_addr_len = sizeof(ipv6_addr);
      opts.family = AF_INET6;
      opts.ipv6_only = 1;
      ipv6_addr.sin6_family = AF_INET6;
      ipv6_addr.sin6_port = htons(0);
      BSON_ASSERT(inet_pton(AF_INET6, "::1", &ipv6_addr.sin6_addr));
      opts.bind_addr = (struct sockaddr_in *)&ipv6_addr;
   } else {
      fprintf(stderr, "bad value of server_bind_to=%s\n", server_bind_to);
      ASSERT(false);
   }
   mock_server = mock_server_with_auto_hello(WIRE_VERSION_MAX);
   mock_server_set_bind_opts(mock_server, &opts);
   mock_server_run(mock_server);
   return mock_server;
}

typedef struct dns_testcase {
   char *server_bind_to;  /* ipv4, ipv6, or both */
   char *client_hostname; /* 127.0.0.1, [::1], or localhost */
   bool should_succeed;
   int expected_ncmds;
   char *expected_client_bind_to; /* ipv4, ipv6, or either */
} dns_testcase_t;

static void
_test_topology_scanner_dns_helper(
   uint32_t id, const bson_t *bson, int64_t rtt_msec, void *data, const bson_error_t *error /* IN */)
{
   dns_testcase_t *testcase = (dns_testcase_t *)data;

   BSON_UNUSED(id);
   BSON_UNUSED(bson);
   BSON_UNUSED(rtt_msec);

   if (testcase->should_succeed) {
      ASSERT_OR_PRINT(!error->code, (*error));
   } else {
      ASSERT(error->code);
      ASSERT_ERROR_CONTAINS((*error), MONGOC_ERROR_STREAM, MONGOC_ERROR_STREAM_CONNECT, "connection refused");
   }
}

static void
test_topology_scanner_dns_testcase(dns_testcase_t *testcase)
{
   mongoc_host_list_t host;
   mock_server_t *server;
   mongoc_topology_scanner_t *ts;
   char *host_str;
   mongoc_socket_t *sock;
   mongoc_topology_scanner_node_t *node;

   bson_oid_t topology_id;
   mcommon_oid_set_zero(&topology_id);
   mongoc_log_and_monitor_instance_t log_and_monitor;
   mongoc_log_and_monitor_instance_init(&log_and_monitor);

   server = _mock_server_listening_on(testcase->server_bind_to);
   ts = mongoc_topology_scanner_new(
      NULL, &topology_id, &log_and_monitor, NULL, &_test_topology_scanner_dns_helper, testcase, TIMEOUT);
   host_str = bson_strdup_printf("%s:%d", testcase->client_hostname, mock_server_get_port(server));
   BSON_ASSERT(_mongoc_host_list_from_string(&host, host_str));
   /* we should only have one host. */
   BSON_ASSERT(!host.next);
   bson_free(host_str);

   mongoc_topology_scanner_add(ts, &host, 1, false);
   mongoc_topology_scanner_scan(ts, 1 /* any server id is ok. */);
   ASSERT_CMPINT((int)(ts->async->ncmds), ==, testcase->expected_ncmds);
   mongoc_topology_scanner_work(ts);
   node = mongoc_topology_scanner_get_node(ts, 1);

   /* check the socket that the scanner found. */
   if (testcase->should_succeed) {
      ASSERT(node->stream->type == MONGOC_STREAM_SOCKET);
      sock = mongoc_stream_socket_get_socket((mongoc_stream_socket_t *)node->stream);
      if (strcmp("ipv4", testcase->expected_client_bind_to) == 0) {
         ASSERT(sock->domain == AF_INET);
      } else if (strcmp("ipv6", testcase->expected_client_bind_to) == 0) {
         ASSERT(sock->domain == AF_INET6);
      } else if (strcmp("either", testcase->expected_client_bind_to) != 0) {
         fprintf(stderr, "bad value for testcase->expected_client_bind_to=%s\n", testcase->expected_client_bind_to);
         ASSERT(false);
      }
   }

   mongoc_topology_scanner_destroy(ts);
   mock_server_destroy(server);
   mongoc_log_and_monitor_instance_destroy_contents(&log_and_monitor);
}

/* test when clients try connecting to servers varying the DNS results of the
 * clients and the socket binding of the server. */
static void
test_topology_scanner_dns(void)
{
   /* server can bind to: {ipv4 only, ipv6 only, both}
    * client can connect to: {127.0.0.1, ::1, localhost}
    * there are 9 combinations. */
   int ntests, i;
   dns_testcase_t tests[] = {{"ipv4", "127.0.0.1", true, 1, "ipv4"},
                             {"ipv4", "[::1]", false, 1, "n/a"},
                             {"ipv6", "127.0.0.1", false, 1, "n/a"},
                             {"ipv6", "[::1]", true, 1, "ipv6"},
                             {"both", "127.0.0.1", true, 1, "ipv4"},
                             {"both", "[::1]", true, 1, "ipv6"}};
   /* these tests require a hostname mapping to both IPv4 and IPv6 local.
    * this can be localhost normally, but some configurations may have localhost
    * only mapping to 127.0.0.1, not ::1. */
   dns_testcase_t tests_with_ipv4_and_ipv6_uri[] = {{"ipv4", "<placeholder>", true, 2, "ipv4"},
                                                    {"ipv6", "<placeholder>", true, 2, "ipv6"},
                                                    {"both", "<placeholder>", true, 2, "either"}};
   char *ipv4_and_ipv6_host = test_framework_getenv("MONGOC_TEST_IPV4_AND_IPV6_HOST");

   ntests = sizeof(tests) / sizeof(dns_testcase_t);
   for (i = 0; i < ntests; ++i) {
      test_topology_scanner_dns_testcase(tests + i);
   }

   if (ipv4_and_ipv6_host) {
      ntests = sizeof(tests_with_ipv4_and_ipv6_uri) / sizeof(dns_testcase_t);
      for (i = 0; i < ntests; ++i) {
         tests_with_ipv4_and_ipv6_uri[i].client_hostname = ipv4_and_ipv6_host;
         test_topology_scanner_dns_testcase(tests_with_ipv4_and_ipv6_uri + i);
      }
      bson_free(ipv4_and_ipv6_host);
   }
}

static void
_retired_fails_to_initiate_cb(
   uint32_t id, const bson_t *bson, int64_t rtt_msec, void *data, const bson_error_t *error /* IN */)
{
   BSON_UNUSED(id);
   BSON_UNUSED(bson);
   BSON_UNUSED(rtt_msec);
   BSON_UNUSED(data);
   BSON_UNUSED(error);
   /* this should never get called. */
   BSON_ASSERT(false);
}

static mongoc_stream_t *
null_connect(mongoc_async_cmd_t *acmd)
{
   BSON_UNUSED(acmd);

   return NULL;
}

/* test when a retired node fails to initiate a stream. CDRIVER-1972 introduced
 * a bug in which the topology callback would be incorrectly called when a
 * retired node failed to establish a connection.
 */
static void
test_topology_retired_fails_to_initiate(void)
{
   mock_server_t *server;
   mongoc_topology_scanner_t *scanner;
   mongoc_async_cmd_t *acmd;
   mongoc_host_list_t host_list;

   server = mock_server_with_auto_hello(WIRE_VERSION_MAX);
   mock_server_run(server);

   bson_oid_t topology_id;
   mcommon_oid_set_zero(&topology_id);
   mongoc_log_and_monitor_instance_t log_and_monitor;
   mongoc_log_and_monitor_instance_init(&log_and_monitor);

   scanner = mongoc_topology_scanner_new(
      NULL, &topology_id, &log_and_monitor, NULL, &_retired_fails_to_initiate_cb, NULL, TIMEOUT);

   BSON_ASSERT(_mongoc_host_list_from_string(&host_list, mock_server_get_host_and_port(server)));

   mongoc_topology_scanner_add(scanner, &host_list, 1, false);
   mongoc_topology_scanner_start(scanner, false);
   BSON_ASSERT(scanner->async->ncmds > 0);
   /* retire the node */
   scanner->nodes->retired = true;
   /* override the stream initiator of every async command, simulating
    * a failed mongoc_socket_new or mongoc_stream_connect. */
   DL_FOREACH(scanner->async->cmds, acmd)
   {
      scanner->async->cmds->_stream_connect = null_connect;
   }

   mongoc_topology_scanner_work(scanner);
   /* we expect the scanner callback not to get called. */

   mongoc_topology_scanner_destroy(scanner);
   mock_server_destroy(server);
   mongoc_log_and_monitor_instance_destroy_contents(&log_and_monitor);
}

static void
heartbeat_failed(const mongoc_apm_server_heartbeat_failed_t *event)
{
   bson_error_t error;
   bool *failed = (bool *)mongoc_apm_server_heartbeat_failed_get_context(event);

   mongoc_apm_server_heartbeat_failed_get_error(event, &error);

   fprintf(stderr, "heartbeat failed: %s\n", error.message);
   fflush(stderr);

   *failed = true;
}

/* CDRIVER-2624: due to a bug, we repeated the TLS handshake on each heartbeat,
 * causing some MongoDB versions to hang up */
static void
_test_topology_scanner_does_not_renegotiate(bool pooled)
{
   mongoc_uri_t *uri;
   mongoc_apm_callbacks_t *callbacks;
   mongoc_client_pool_t *pool = NULL;
   mongoc_client_t *client;
   bool failed = false;
   bool r;
   bson_error_t error;

   uri = test_framework_get_uri();
   mongoc_uri_set_option_as_int32(uri, "heartbeatFrequencyMS", 500);
   /* faster pool shutdown to make the test quick */
   mongoc_uri_set_option_as_int32(uri, "connectTimeoutMS", 1000);

   callbacks = mongoc_apm_callbacks_new();
   mongoc_apm_set_server_heartbeat_failed_cb(callbacks, heartbeat_failed);

   if (pooled) {
      pool = test_framework_client_pool_new_from_uri(uri, NULL);
      test_framework_set_pool_ssl_opts(pool);
      mongoc_client_pool_set_apm_callbacks(pool, callbacks, &failed);
      client = mongoc_client_pool_pop(pool);
   } else {
      client = test_framework_client_new_from_uri(uri, NULL);
      mongoc_client_set_apm_callbacks(client, callbacks, &failed);
      test_framework_set_ssl_opts(client);
   }

   /* ensure connection */
   r = mongoc_client_command_simple(client, "admin", tmp_bson("{'ping': 1}"), NULL, NULL, &error);
   ASSERT_OR_PRINT(r, error);

   mlib_sleep_for(1500, ms);

   r = mongoc_client_command_simple(client, "admin", tmp_bson("{'ping': 1}"), NULL, NULL, &error);
   ASSERT_OR_PRINT(r, error);

   /* no heartbeats failed */
   BSON_ASSERT(!failed);

   if (pooled) {
      mongoc_client_pool_push(pool, client);
      mongoc_client_pool_destroy(pool);
   } else {
      mongoc_client_destroy(client);
   }

   mongoc_uri_destroy(uri);
   mongoc_apm_callbacks_destroy(callbacks);
}

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

   _test_topology_scanner_does_not_renegotiate(false);
}

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

   _test_topology_scanner_does_not_renegotiate(true);
}

void
test_topology_scanner_install(TestSuite *suite)
{
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/scanner", test_topology_scanner);
#ifdef MONGOC_ENABLE_SSL_OPENSSL
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/scanner_ssl", test_topology_scanner_ssl);
#endif
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/scanner_discovery", test_topology_scanner_discovery);
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/scanner_oscillate", test_topology_scanner_oscillate);
   TestSuite_Add(suite, "/TOPOLOGY/scanner_connection_error", test_topology_scanner_connection_error);
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/scanner_socket_timeout", test_topology_scanner_socket_timeout);
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/blocking_initiator", test_topology_scanner_blocking_initiator);
   TestSuite_AddMockServerTest(
      suite, "/TOPOLOGY/dns", test_topology_scanner_dns, test_framework_skip_if_no_dual_ip_hostname);
   TestSuite_AddMockServerTest(suite, "/TOPOLOGY/retired_fails_to_initiate", test_topology_retired_fails_to_initiate);
   TestSuite_AddFull(suite,
                     "/TOPOLOGY/scanner/renegotiate/single [lock:live-server][timeout:30]",
                     test_topology_scanner_does_not_renegotiate_single,
                     NULL,
                     NULL,
                     test_framework_skip_if_slow_or_live);
   TestSuite_AddFull(suite,
                     "/TOPOLOGY/scanner/renegotiate/pooled [lock:live-server][timeout:30]",
                     test_topology_scanner_does_not_renegotiate_pooled,
                     NULL,
                     NULL,
                     test_framework_skip_if_slow_or_live);
}
