#include <mongoc/mongoc.h>
#include <bson/bson-types.h>

#include "mongoc/mongoc-util-private.h"
#include "mongoc/mongoc-write-concern-private.h"
#include "mongoc/mongoc-read-concern-private.h"
#include "mongoc/mongoc-opts-helpers-private.h"
#include "json-test.h"
#include "test-libmongoc.h"


static void
compare_write_concern (const mongoc_write_concern_t *wc_correct,
                       const mongoc_write_concern_t *wc)
{
   ASSERT_CMPINT32 (wc_correct->w, ==, wc->w);
   ASSERT_CMPINT32 (wc_correct->wtimeout, ==, wc->wtimeout);
   ASSERT_CMPINT (wc_correct->journal, ==, wc->journal);
}


static void
compare_read_concern (const mongoc_read_concern_t *rc_correct,
                      const mongoc_read_concern_t *rc)
{
   ASSERT_CMPSTR (rc_correct->level, rc->level);
}

mongoc_write_concern_t *
convert_write_concern (const bson_t *wc_doc)
{
   mongoc_write_concern_t *wc;
   bson_iter_t iter;
   const char *key;

   wc = mongoc_write_concern_new ();
   BSON_ASSERT (bson_iter_init (&iter, wc_doc));

   while (bson_iter_next (&iter)) {
      key = bson_iter_key (&iter);

      if (strcmp (key, "w") == 0) {
         if (BSON_ITER_HOLDS_UTF8 (&iter)) {
            if (strcmp (bson_lookup_utf8 (wc_doc, "w"), "majority") == 0) {
               mongoc_write_concern_set_w (wc, MONGOC_WRITE_CONCERN_W_MAJORITY);
            } else {
               mongoc_write_concern_set_wtag (wc,
                                              bson_lookup_utf8 (wc_doc, "w"));
            }
         } else {
            if (bson_lookup_int32 (wc_doc, "w") < 0) {
               goto invalid;
            }
            mongoc_write_concern_set_w (wc, bson_lookup_int32 (wc_doc, "w"));
         }
      } else if (strcmp (key, "wtimeoutMS") == 0) {
         if (bson_lookup_int32 (wc_doc, "wtimeoutMS") < 0) {
            goto invalid;
         }
         mongoc_write_concern_set_wtimeout_int64 (
            wc, bson_lookup_int32 (wc_doc, "wtimeoutMS"));
      } else if (strcmp (key, "journal") == 0) {
         mongoc_write_concern_set_journal (
            wc, bson_iter_value (&iter)->value.v_bool);
      }
   }

   if (wc->w == 0 && wc->journal == 1) {
      goto invalid;
   }

   return wc;

invalid:
   mongoc_write_concern_destroy (wc);
   return NULL;
}

mongoc_read_concern_t *
convert_read_concern (const bson_t *rc_doc)
{
   mongoc_read_concern_t *rc;
   bson_iter_t iter;
   const char *key;

   rc = mongoc_read_concern_new ();
   BSON_ASSERT (bson_iter_init (&iter, rc_doc));

   while (bson_iter_next (&iter)) {
      key = bson_iter_key (&iter);

      if (strcmp (key, "level") == 0) {
         if (BSON_ITER_HOLDS_UTF8 (&iter)) {
            mongoc_read_concern_set_level (rc,
                                           bson_lookup_utf8 (rc_doc, "level"));
         } else {
            goto invalid;
         }
      }
   }

   return rc;

invalid:
   mongoc_read_concern_destroy (rc);
   return NULL;
}


static void
test_rw_concern_uri (bson_t *scenario)
{
   bson_iter_t scenario_iter;
   bson_iter_t test_iter;
   bson_t test;
   const char *description;
   const char *uri_str;
   bool valid;
   mongoc_uri_t *uri;
   bson_t rc_doc;
   bson_t wc_doc;
   const mongoc_read_concern_t *rc;
   const mongoc_write_concern_t *wc;
   mongoc_write_concern_t *wc_correct;
   mongoc_read_concern_t *rc_correct;

   /* initialize tests with the scenario */
   BSON_ASSERT (bson_iter_init_find (&scenario_iter, scenario, "tests"));
   BSON_ASSERT (bson_iter_recurse (&scenario_iter, &test_iter));

   while (bson_iter_next (&test_iter)) {
      bson_iter_bson (&test_iter, &test);

      description = bson_lookup_utf8 (&test, "description");
      uri_str = bson_lookup_utf8 (&test, "uri");
      valid = _mongoc_lookup_bool (&test, "valid", true);

      if (_mongoc_lookup_bool (&test, "warning", false)) {
         MONGOC_ERROR ("update the \"%s\" test to handle warning: true",
                       description);
         abort ();
      }

      uri = mongoc_uri_new_with_error (uri_str, NULL);
      if (!valid) {
         BSON_ASSERT (!uri);
         continue;
      }

      BSON_ASSERT (uri);

      if (bson_has_field (&test, "readConcern")) {
         rc = mongoc_uri_get_read_concern (uri);
         bson_lookup_doc (&test, "readConcern", &rc_doc);
         rc_correct = convert_read_concern (&rc_doc);
         compare_read_concern (rc_correct, rc);
         mongoc_read_concern_destroy (rc_correct);
      }

      if (bson_has_field (&test, "writeConcern")) {
         wc = mongoc_uri_get_write_concern (uri);
         bson_lookup_doc (&test, "writeConcern", &wc_doc);
         wc_correct = convert_write_concern (&wc_doc);
         compare_write_concern (wc_correct, wc);
         mongoc_write_concern_destroy (wc_correct);
      }

      mongoc_uri_destroy (uri);
   }
}

static void
test_rw_concern_document (bson_t *scenario)
{
   bson_iter_t scenario_iter;
   bson_iter_t test_iter;
   bson_t test;
   bool valid;
   bson_t rc_doc;
   bson_t wc_doc;
   mongoc_write_concern_t *wc;
   mongoc_read_concern_t *rc;
   const bson_t *wc_doc_result;
   const bson_t *rc_doc_result;
   bson_t rc_doc_correct;
   bson_t wc_doc_correct;

   BSON_ASSERT (bson_iter_init_find (&scenario_iter, scenario, "tests"));
   BSON_ASSERT (bson_iter_recurse (&scenario_iter, &test_iter));

   while (bson_iter_next (&test_iter)) {
      bson_iter_bson (&test_iter, &test);
      valid = _mongoc_lookup_bool (&test, "valid", true);

      if (bson_has_field (&test, "readConcern")) {
         bson_lookup_doc (&test, "readConcern", &rc_doc);
         rc = convert_read_concern (&rc_doc);
      } else {
         rc = mongoc_read_concern_new ();
      }

      if (bson_has_field (&test, "writeConcern")) {
         bson_lookup_doc (&test, "writeConcern", &wc_doc);
         wc = convert_write_concern (&wc_doc);
      } else {
         wc = mongoc_write_concern_new ();
      }

      if (!valid) {
         BSON_ASSERT (rc == NULL || wc == NULL);
         mongoc_write_concern_destroy (wc);
         mongoc_read_concern_destroy (rc);
         continue;
      }

      if (bson_has_field (&test, "readConcernDocument")) {
         bson_lookup_doc (&test, "readConcernDocument", &rc_doc_correct);
         rc_doc_result = _mongoc_read_concern_get_bson (rc);
         match_bson (&rc_doc_correct, rc_doc_result, false /* is_command */);
      }

      if (bson_has_field (&test, "writeConcernDocument")) {
         bson_lookup_doc (&test, "writeConcernDocument", &wc_doc_correct);
         wc_doc_result = _mongoc_write_concern_get_bson (wc);
         match_bson (&wc_doc_correct, wc_doc_result, false);
      }

      mongoc_write_concern_destroy (wc);
      mongoc_read_concern_destroy (rc);
   }
}

static void
errinfo_propagated (void *unused)
{
   mongoc_client_t *client;
   bool ret;
   bson_error_t error;
   mongoc_collection_t *coll;
   bson_t reply;
   const char *failpoint =
      "{'configureFailPoint':'failCommand','data':{'failCommands':['insert'],'"
      "writeConcernError':{'code':100,'codeName':'UnsatisfiableWriteConcern','"
      "errmsg':'Not enough data-bearing "
      "nodes','errInfo':{'writeConcern':{'w':2,'wtimeout':0,'provenance':'"
      "clientSupplied'}}}},'mode':{'times':1}}";


   client = test_framework_client_new ();
   ret = mongoc_client_command_simple (client,
                                       "admin",
                                       tmp_bson (failpoint),
                                       NULL /* read prefs */,
                                       NULL /* reply */,
                                       &error);
   ASSERT_OR_PRINT (ret, error);
   coll = get_test_collection (client, "rwc_errinfo");
   ret = mongoc_collection_insert_one (
      coll, tmp_bson ("{'x': 1}"), NULL /* opts */, &reply, &error);
   BSON_ASSERT (!ret);
   assert_match_bson (&reply,
                      tmp_bson ("{'writeConcernErrors': [ { 'errInfo' : { "
                                "'writeConcern' : { 'w' : 2, 'wtimeout' : 0, "
                                "'provenance' : 'clientSupplied' } } } ] }"),
                      false);

   bson_destroy (&reply);
   mongoc_collection_destroy (coll);
   mongoc_client_destroy (client);
}

void
test_read_write_concern_install (TestSuite *suite)
{
   char resolved[PATH_MAX];

   ASSERT (
      realpath (JSON_DIR "/read_write_concern/connection-string", resolved));
   install_json_test_suite (suite, resolved, &test_rw_concern_uri);

   test_framework_resolve_path (JSON_DIR "/read_write_concern/document",
                                resolved);
   install_json_test_suite (suite, resolved, &test_rw_concern_document);
   TestSuite_AddFull (suite,
                      "/read_write_concern/errinfo_propagated",
                      errinfo_propagated,
                      NULL /* dtor */,
                      NULL /* ctx */,
                      test_framework_skip_if_max_wire_version_less_than_7,
                      test_framework_skip_if_no_failpoint);
}
