#include <mongoc/mongoc.h>

#include "test-libmongoc.h"
#include "test-conveniences.h"
#include <mongoc/mongoc-array-private.h>
#include "TestSuite.h"

static void
stored_log_handler (const mongoc_structured_log_entry_t *entry, void *user_data)
{
   mongoc_array_t *log_array = (mongoc_array_t *) user_data;
   bson_t *doc = mongoc_structured_log_entry_message_as_bson (entry);
   MONGOC_DEBUG ("stored log: %s", tmp_json (doc));
   _mongoc_array_append_val (log_array, doc);
}

static void
stored_log_clear (mongoc_array_t *log_array)
{
   for (size_t i = 0; i < log_array->len; i++) {
      bson_t *doc = _mongoc_array_index (log_array, bson_t *, i);
      bson_destroy (doc);
   }
   _mongoc_array_clear (log_array);
}

/* specifications/source/command-logging-and-monitoring/tests/README.md
 * Test 1: Default truncation limit */
static void
prose_test_1 (void)
{
   // 1. Configure logging with a minimum severity level of "debug" for the "command" component. Do not explicitly
   // configure the max document length.
   mongoc_client_t *client = test_framework_new_default_client ();
   mongoc_array_t stored_log;
   _mongoc_array_init (&stored_log, sizeof (bson_t *));
   {
      mongoc_structured_log_opts_t *log_opts = mongoc_structured_log_opts_new ();

      ASSERT (mongoc_structured_log_opts_set_max_level_for_component (
         log_opts, MONGOC_STRUCTURED_LOG_COMPONENT_COMMAND, MONGOC_STRUCTURED_LOG_LEVEL_DEBUG));

      mongoc_structured_log_opts_set_handler (log_opts, stored_log_handler, &stored_log);

      ASSERT (mongoc_client_set_structured_log_opts (client, log_opts));
      mongoc_structured_log_opts_destroy (log_opts);
   }
   mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll");

   // 2. Construct an array docs containing the document {"x" : "y"} repeated 100 times.
   bson_t *docs[100];
   for (unsigned i = 0; i < sizeof docs / sizeof docs[0]; i++) {
      docs[i] = tmp_bson (BSON_STR ({"x" : "y"}));
   }

   // 3. Insert docs to a collection via insertMany.
   bson_error_t error;
   ASSERT_OR_PRINT (
      mongoc_collection_insert_many (coll, (const bson_t **) docs, sizeof docs / sizeof docs[0], NULL, NULL, &error),
      error);

   // 4. Inspect the resulting "command started" log message and assert that the "command" value is a string of length
   // 1000 + (length of trailing ellipsis).
   {
      ASSERT (stored_log.len >= 1);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 0);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command started");
      ASSERT (bson_iter_find (&iter, "command"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t command_len;
      const char *command = bson_iter_utf8 (&iter, &command_len);
      ASSERT (command);
      ASSERT_CMPUINT32 (command_len, ==, 1003);
   }

   // 5. Inspect the resulting "command succeeded" log message and assert that the "reply" value is a string of length
   // <= 1000 + (length of trailing ellipsis).
   {
      ASSERT (stored_log.len == 2);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 1);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command succeeded");
      ASSERT (bson_iter_find (&iter, "reply"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t reply_len;
      const char *reply = bson_iter_utf8 (&iter, &reply_len);
      ASSERT (reply);
      ASSERT_CMPUINT32 (reply_len, <=, 1003);
   }

   // 6. Run find() on the collection where the document was inserted.
   stored_log_clear (&stored_log);
   mongoc_cursor_t *cursor = mongoc_collection_find_with_opts (coll, tmp_bson ("{}"), NULL, NULL);
   ASSERT (cursor);
   {
      const bson_t *doc;
      ASSERT (mongoc_cursor_next (cursor, &doc));
   }

   // 7. Inspect the resulting "command succeeded" log message and assert that the reply is a string of length 1000 +
   // (length of trailing ellipsis).
   {
      ASSERT (stored_log.len >= 1);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 0);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command started");
   }
   {
      ASSERT (stored_log.len == 2);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 1);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command succeeded");
      ASSERT (bson_iter_find (&iter, "reply"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t reply_len;
      const char *reply = bson_iter_utf8 (&iter, &reply_len);
      ASSERT (reply);
      ASSERT_CMPUINT32 (reply_len, ==, 1003);
   }

   mongoc_cursor_destroy (cursor);
   mongoc_collection_destroy (coll);
   mongoc_client_destroy (client);
   stored_log_clear (&stored_log);
   _mongoc_array_destroy (&stored_log);
}

/* Test 2: Explicitly configured truncation limit */
static void
prose_test_2 (void)
{
   // 1. Configure logging with a minimum severity level of "debug" for the "command" component. Set the max document
   // length to 5.
   mongoc_client_t *client = test_framework_new_default_client ();
   mongoc_array_t stored_log;
   _mongoc_array_init (&stored_log, sizeof (bson_t *));
   {
      mongoc_structured_log_opts_t *log_opts = mongoc_structured_log_opts_new ();

      ASSERT (mongoc_structured_log_opts_set_max_document_length (log_opts, 5));
      ASSERT (mongoc_structured_log_opts_set_max_level_for_component (
         log_opts, MONGOC_STRUCTURED_LOG_COMPONENT_COMMAND, MONGOC_STRUCTURED_LOG_LEVEL_DEBUG));

      mongoc_structured_log_opts_set_handler (log_opts, stored_log_handler, &stored_log);

      ASSERT (mongoc_client_set_structured_log_opts (client, log_opts));
      mongoc_structured_log_opts_destroy (log_opts);
   }

   // 2. Run the command {"hello": true}.
   {
      bson_error_t error;
      ASSERT_OR_PRINT (
         mongoc_client_command_simple (client, "db", tmp_bson (BSON_STR ({"hello" : true})), NULL, NULL, &error),
         error);
   }

   // 3. Inspect the resulting "command started" log message and assert that the "command" value is a string of length 5
   // + (length of trailing ellipsis).
   {
      ASSERT (stored_log.len >= 1);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 0);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command started");
      ASSERT (bson_iter_find (&iter, "command"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t command_len;
      const char *command = bson_iter_utf8 (&iter, &command_len);
      ASSERT (command);
      ASSERT_CMPUINT32 (command_len, ==, 5 + 3);
      ASSERT_CMPSTR (command, "{ \"he...");
   }

   // 4. Inspect the resulting "command succeeded" log message and assert that the "reply" value is a string of length 5
   // + (length of trailing ellipsis).
   {
      ASSERT (stored_log.len == 2);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 1);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command succeeded");
      ASSERT (bson_iter_find (&iter, "reply"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t reply_len;
      const char *reply = bson_iter_utf8 (&iter, &reply_len);
      ASSERT (reply);
      ASSERT_CMPUINT32 (reply_len, ==, 5 + 3);
   }

   // 5. If the driver attaches raw server responses to failures and can access these via log messages to assert on, run
   // the command {"notARealCommand": true}. Inspect the resulting "command failed" log message and confirm that the
   // server error is a string of length 5 + (length of trailing ellipsis).
   //
   // This is not applicable to libmongoc. The spec allows flexible data type for "failure", and here we chose a
   // document rather than a string. The document is not subject to truncation.
   //
   // While we're here, test that the proposed fake command itself is truncated as expected, and the "failure" is a
   // document.
   stored_log_clear (&stored_log);
   ASSERT (
      !mongoc_client_command_simple (client, "db", tmp_bson (BSON_STR ({"notARealCommand" : true})), NULL, NULL, NULL));
   {
      ASSERT (stored_log.len >= 1);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 0);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command started");
      ASSERT (bson_iter_find (&iter, "command"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t command_len;
      const char *command = bson_iter_utf8 (&iter, &command_len);
      ASSERT (command);
      ASSERT_CMPUINT32 (command_len, ==, 5 + 3);
      ASSERT_CMPSTR (command, "{ \"no...");
   }
   {
      ASSERT (stored_log.len == 2);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 1);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command failed");
      ASSERT (bson_iter_find (&iter, "failure"));
      ASSERT (BSON_ITER_HOLDS_DOCUMENT (&iter));
   }

   mongoc_client_destroy (client);
   stored_log_clear (&stored_log);
   _mongoc_array_destroy (&stored_log);
}

/* Test 3: Truncation with multi-byte code points */
static void
prose_test_3 (void)
{
   // "Drivers MUST write language-specific tests that confirm truncation of commands, replies, and (if applicable)
   // server responses included in error messages work as expected when the data being truncated includes multi-byte
   // Unicode codepoints." "If the driver uses anything other than Unicode codepoints as the unit for max document
   // length, there also MUST be tests confirming that cases where the max length falls in the middle of a multi-byte
   // codepoint are handled gracefully."
   //
   // For libmongoc, our max length is in bytes and truncation will round lengths down if necessary to avoid splitting a
   // valid UTF-8 sequence. This test repeatedly sends a fake command to the server using every possible maximum
   // length, checking for the expected truncations.

   bson_t command = BSON_INITIALIZER;
   BSON_APPEND_BOOL (&command, "notARealCommand", true);
   BSON_APPEND_UTF8 (&command, "twoByteUtf8", "\xc2\xa9");
   BSON_APPEND_UTF8 (&command, "threeByteUtf8", "\xef\xbf\xbd");
   BSON_APPEND_UTF8 (&command, "fourByteUtf8", "\xf4\x8f\xbf\xbf");

   // Stop testing after $db, before we reach lsid. The result will always be truncated.
   const char *expected_json = "{ \"notARealCommand\" : true, \"twoByteUtf8\" : \"\xc2\xa9\", \"threeByteUtf8\" : "
                               "\"\xef\xbf\xbd\", \"fourByteUtf8\" : \"\xf4\x8f\xbf\xbf\", \"$db\" : \"db\"";
   const int max_expected_length = strlen (expected_json);

   // List of lengths we expect not to see when trying every max_expected_length
   static const int expect_missing_lengths[] = {46, 70, 71, 94, 95, 96};

   mongoc_client_t *client = test_framework_new_default_client ();

   int expected_length = 0;
   for (int test_length = 0; test_length <= max_expected_length; test_length++) {
      MONGOC_DEBUG ("testing length %d of %d", test_length, max_expected_length);

      // Track the expected length of a serialized string with the max_document_length set to 'test_length'.
      // When a length is mentioned in expect_missing_lengths, we let the expected_length lag behind the test_length.
      // At this point, the ellipsis length is not included.
      bool expect_missing = false;
      if (test_length > max_expected_length) {
         expect_missing = true;
      } else {
         for (int missing = 0; missing < sizeof expect_missing_lengths / sizeof expect_missing_lengths[0]; missing++) {
            if (expect_missing_lengths[missing] == test_length) {
               expect_missing = true;
               break;
            }
         }
      }
      if (!expect_missing) {
         expected_length = test_length;
      }

      // Set up the log options for each command, to test this new max_document_length
      mongoc_structured_log_opts_t *log_opts = mongoc_structured_log_opts_new ();
      ASSERT (mongoc_structured_log_opts_set_max_document_length (log_opts, test_length));
      ASSERT (mongoc_structured_log_opts_set_max_level_for_component (
         log_opts, MONGOC_STRUCTURED_LOG_COMPONENT_COMMAND, MONGOC_STRUCTURED_LOG_LEVEL_DEBUG));

      mongoc_array_t stored_log;
      _mongoc_array_init (&stored_log, sizeof (bson_t *));
      mongoc_structured_log_opts_set_handler (log_opts, stored_log_handler, &stored_log);
      ASSERT (mongoc_client_set_structured_log_opts (client, log_opts));
      mongoc_structured_log_opts_destroy (log_opts);

      ASSERT (!mongoc_client_command_simple (client, "db", &command, NULL, NULL, NULL));

      ASSERT (stored_log.len >= 1);
      bson_t *log = _mongoc_array_index (&stored_log, bson_t *, 0);
      bson_iter_t iter;
      ASSERT (bson_iter_init (&iter, log));
      ASSERT (bson_iter_find (&iter, "message"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      ASSERT_CMPSTR (bson_iter_utf8 (&iter, NULL), "Command started");
      ASSERT (bson_iter_find (&iter, "command"));
      ASSERT (BSON_ITER_HOLDS_UTF8 (&iter));
      uint32_t logged_command_len;
      const char *logged_command_str = bson_iter_utf8 (&iter, &logged_command_len);
      ASSERT (logged_command_str);
      ASSERT_CMPUINT32 (logged_command_len, ==, expected_length + 3);

      // Note that here we do not use mcommon_string to truncate, just as a convenient way to represent the
      // expected string with ellipsis for ASSERT_CMPSTR. (The code under test internally uses mcommon_string_append_t
      // also.)
      mcommon_string_append_t expected_json_truncated;
      mcommon_string_new_as_append (&expected_json_truncated);
      mcommon_string_append_bytes (&expected_json_truncated, expected_json, expected_length);
      mcommon_string_append (&expected_json_truncated, "...");
      ASSERT_CMPSTR (logged_command_str, mcommon_str_from_append (&expected_json_truncated));
      mcommon_string_from_append_destroy (&expected_json_truncated);

      ASSERT (mongoc_client_set_structured_log_opts (client, NULL));
      stored_log_clear (&stored_log);
      _mongoc_array_destroy (&stored_log);
   }

   mongoc_client_destroy (client);
   bson_destroy (&command);
}

void
test_command_logging_and_monitoring_install (TestSuite *suite)
{
   TestSuite_AddLive (suite, "/command-logging-and-monitoring/logging/prose_test_1", prose_test_1);
   TestSuite_AddLive (suite, "/command-logging-and-monitoring/logging/prose_test_2", prose_test_2);
   TestSuite_AddLive (suite, "/command-logging-and-monitoring/logging/prose_test_3", prose_test_3);
}
