/*
 * Copyright 2022-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 "test-mongocrypt.h"

#include "mc-check-conversions-private.h"
#include "mc-optional-private.h"
#include "mc-range-mincover-private.h"
#include "mc-array-private.h"


enum {
   /// Why this number? The Decimal128 tests generate thousands of test strings,
   /// but we can't set this arbitrarily high, since we'll bump up on stack
   /// overflow on MSVC. This is large enough to capture all strings in all
   /// Decimal128 tests without overflowing the stack.
   MAX_MINCOVER_STRINGS = 4500
};

typedef struct {
   int32_t lowerBound;
   bool includeLowerBound;
   int32_t upperBound;
   bool includeUpperBound;
   mc_optional_int32_t min;
   mc_optional_int32_t max;
   size_t sparsity;
   const char *expectMincoverStrings[MAX_MINCOVER_STRINGS];
   const char *expectError;
} Int32Test;

typedef struct {
   int64_t lowerBound;
   bool includeLowerBound;
   int64_t upperBound;
   bool includeUpperBound;
   mc_optional_int64_t min;
   mc_optional_int64_t max;
   size_t sparsity;
   const char *expectMincoverStrings[MAX_MINCOVER_STRINGS];
   const char *expectError;
} Int64Test;

typedef struct {
   double lowerBound;
   bool includeLowerBound;
   double upperBound;
   bool includeUpperBound;
   size_t sparsity;
   mc_optional_double_t min;
   mc_optional_double_t max;
   mc_optional_uint32_t precision;
   const char *expectMincoverStrings[MAX_MINCOVER_STRINGS];
   const char *expectError;
} DoubleTest;

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
typedef struct {
   mc_dec128 lowerBound;
   bool includeLowerBound;
   mc_dec128 upperBound;
   bool includeUpperBound;
   size_t sparsity;
   mc_optional_dec128_t min;
   mc_optional_dec128_t max;
   mc_optional_uint32_t precision;
   const char *expectMincoverStrings[MAX_MINCOVER_STRINGS];
   const char *expectError;
} Decimal128Test;
#endif

typedef struct _test_getMincover_args {
   mc_mincover_t *(*getMincover) (void *tests,
                                  size_t idx,
                                  mongocrypt_status_t *status);
   const char *(*expectError) (void *tests, size_t idx);
   const char *const *(*expectMincoverStrings) (void *tests, size_t idx);
   void (*dump) (void *tests, size_t idx, mc_mincover_t *got);
} _test_getMincover_args;

static mc_mincover_t *
_test_getMincover32 (void *tests, size_t idx, mongocrypt_status_t *status)
{
   BSON_ASSERT_PARAM (tests);

   Int32Test *test = (Int32Test *) tests + idx;

   return mc_getMincoverInt32 (
      (mc_getMincoverInt32_args_t){.lowerBound = test->lowerBound,
                                   .includeLowerBound = test->includeLowerBound,
                                   .upperBound = test->upperBound,
                                   .includeUpperBound = test->includeUpperBound,
                                   .min = test->min,
                                   .max = test->max,
                                   .sparsity = test->sparsity},
      status);
}

static mc_mincover_t *
_test_getMincover64 (void *tests, size_t idx, mongocrypt_status_t *status)
{
   BSON_ASSERT_PARAM (tests);

   Int64Test *const test = (Int64Test *) tests + idx;

   return mc_getMincoverInt64 (
      (mc_getMincoverInt64_args_t){.lowerBound = test->lowerBound,
                                   .includeLowerBound = test->includeLowerBound,
                                   .upperBound = test->upperBound,
                                   .includeUpperBound = test->includeUpperBound,
                                   .min = test->min,
                                   .max = test->max,
                                   .sparsity = test->sparsity},
      status);
}

static mc_mincover_t *
_test_getMincoverDouble_helper (void *tests,
                                size_t idx,
                                mongocrypt_status_t *status)
{
   BSON_ASSERT_PARAM (tests);

   DoubleTest *const test = (DoubleTest *) tests + idx;

   return mc_getMincoverDouble (
      (mc_getMincoverDouble_args_t){
         .lowerBound = test->lowerBound,
         .includeLowerBound = test->includeLowerBound,
         .upperBound = test->upperBound,
         .includeUpperBound = test->includeUpperBound,
         .sparsity = test->sparsity,
         .min = test->precision.set ? test->min : (mc_optional_double_t){0},
         .max = test->precision.set ? test->max : (mc_optional_double_t){0},
         .precision = test->precision},
      status);
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
static mc_mincover_t *
_test_getMincoverDecimal128_helper (void *tests,
                                    size_t idx,
                                    mongocrypt_status_t *status)
{
   BSON_ASSERT_PARAM (tests);

   Decimal128Test *const test = (Decimal128Test *) tests + idx;

   return mc_getMincoverDecimal128 (
      (mc_getMincoverDecimal128_args_t){
         .lowerBound = test->lowerBound,
         .includeLowerBound = test->includeLowerBound,
         .upperBound = test->upperBound,
         .includeUpperBound = test->includeUpperBound,
         .sparsity = test->sparsity,
         .min = test->precision.set ? test->min : (mc_optional_dec128_t){0},
         .max = test->precision.set ? test->max : (mc_optional_dec128_t){0},
         .precision = test->precision},
      status);
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT

static const char *
_test_expectError32 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Int32Test *) tests + idx)->expectError;
}

static const char *
_test_expectError64 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Int64Test *) tests + idx)->expectError;
}

static const char *
_test_expectErrorDouble (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((DoubleTest *) tests + idx)->expectError;
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
static const char *
_test_expectErrorDecimal128 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Decimal128Test *) tests + idx)->expectError;
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT

static const char *const *
_test_expectMincover32 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Int32Test *) tests + idx)->expectMincoverStrings;
}

static const char *const *
_test_expectMincover64 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Int64Test *) tests + idx)->expectMincoverStrings;
}

static const char *const *
_test_expectMincoverDouble (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((DoubleTest *) tests + idx)->expectMincoverStrings;
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
static const char *const *
_test_expectMincoverDecimal128 (void *tests, size_t idx)
{
   BSON_ASSERT_PARAM (tests);
   return ((Decimal128Test *) tests + idx)->expectMincoverStrings;
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT

static void
_test_dump_32 (void *tests, size_t idx, mc_mincover_t *got)
{
   BSON_ASSERT_PARAM (tests);
   Int32Test *const test = (Int32Test *) tests + idx;
   fflush (stdout); // Avoid incomplete stdout output from prior tests on error
   fprintf (stderr,
            "testcase: lowerBound=%" PRId32 " (%s) upperBound=%" PRId32 " (%s)",
            test->lowerBound,
            test->includeLowerBound ? "inclusive" : "exclusive",
            test->upperBound,
            test->includeUpperBound ? "inclusive" : "exclusive");
   if (test->min.set) {
      fprintf (stderr, " min=%" PRId32, test->min.value);
   }
   if (test->max.set) {
      fprintf (stderr, " max=%" PRId32, test->max.value);
   }
   fprintf (stderr, " sparsity=%zu\n", test->sparsity);
   fprintf (stderr, "mincover expected ... begin\n");
   for (const char **p = test->expectMincoverStrings; *p; ++p) {
      fprintf (stderr, "  %s\n", *p);
   }
   fprintf (stderr, "mincover expected ... end\n");
   fprintf (stderr, "mincover got ... begin\n");
   for (size_t i = 0; i < mc_mincover_len (got); i++) {
      fprintf (stderr, "  %s\n", mc_mincover_get (got, i));
   }
   fprintf (stderr, "mincover got ... end\n");
}

static void
_test_dump_64 (void *tests, size_t idx, mc_mincover_t *got)
{
   BSON_ASSERT_PARAM (tests);
   Int64Test *const test = (Int64Test *) tests + idx;
   fflush (stdout); // Avoid incomplete stdout output from prior tests on error
   fprintf (stderr,
            "testcase: lowerBound=%" PRId64 " (%s) upperBound=%" PRId64 " (%s)",
            test->lowerBound,
            test->includeLowerBound ? "inclusive" : "exclusive",
            test->upperBound,
            test->includeUpperBound ? "inclusive" : "exclusive");
   if (test->min.set) {
      fprintf (stderr, " min=%" PRId64, test->min.value);
   }
   if (test->max.set) {
      fprintf (stderr, " max=%" PRId64, test->max.value);
   }
   fprintf (stderr, " sparsity=%zu\n", test->sparsity);
   fprintf (stderr, "mincover expected ... begin\n");
   for (const char **p = test->expectMincoverStrings; *p; ++p) {
      fprintf (stderr, "  %s\n", *p);
   }
   fprintf (stderr, "mincover expected ... end\n");
   fprintf (stderr, "mincover got ... begin\n");
   for (size_t i = 0; i < mc_mincover_len (got); i++) {
      fprintf (stderr, "  %s\n", mc_mincover_get (got, i));
   }
   fprintf (stderr, "mincover got ... end\n");
}

static void
_test_dump_Double (void *tests, size_t idx, mc_mincover_t *got)
{
   BSON_ASSERT_PARAM (tests);
   DoubleTest *const test = (DoubleTest *) tests + idx;
   fflush (stdout); // Avoid incomplete stdout output from prior tests on error
   fprintf (stderr,
            "testcase: lowerBound=%f (%s) upperBound=%f (%s)",
            test->lowerBound,
            test->includeLowerBound ? "inclusive" : "exclusive",
            test->upperBound,
            test->includeUpperBound ? "inclusive" : "exclusive");
   if (test->min.set) {
      fprintf (stderr, " min=%f", test->min.value);
   }
   if (test->max.set) {
      fprintf (stderr, " max=%f", test->max.value);
   }
   if (test->precision.set) {
      fprintf (stderr, " precision=%" PRIu32, test->precision.value);
   }
   fprintf (stderr, " sparsity=%zu\n", test->sparsity);
   fprintf (stderr, "mincover expected ... begin\n");
   for (const char **p = test->expectMincoverStrings; *p; ++p) {
      fprintf (stderr, "  %s\n", *p);
   }
   fprintf (stderr, "mincover expected ... end\n");
   fprintf (stderr, "mincover got ... begin\n");
   for (size_t i = 0; i < mc_mincover_len (got); i++) {
      fprintf (stderr, "  %s\n", mc_mincover_get (got, i));
   }
   fprintf (stderr, "mincover got ... end\n");
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
static void
_test_dump_Decimal128 (void *tests, size_t idx, mc_mincover_t *got)
{
   BSON_ASSERT_PARAM (tests);
   Decimal128Test *const test = (Decimal128Test *) tests + idx;
   fflush (stdout); // Avoid incomplete stdout output from prior tests on error
   fprintf (stderr,
            "testcase: lowerBound=%s (%s) upperBound=%s (%s)",
            mc_dec128_to_string (test->lowerBound).str,
            test->includeLowerBound ? "inclusive" : "exclusive",
            mc_dec128_to_string (test->upperBound).str,
            test->includeUpperBound ? "inclusive" : "exclusive");
   if (test->min.set) {
      fprintf (stderr, " min=%s", mc_dec128_to_string (test->min.value).str);
   }
   if (test->max.set) {
      fprintf (stderr, " max=%s", mc_dec128_to_string (test->max.value).str);
   }
   if (test->precision.set) {
      fprintf (stderr, " precision=%" PRIu32, test->precision.value);
   }
   fprintf (stderr, " sparsity=%zu\n", test->sparsity);
   fprintf (stderr, "mincover expected ... begin\n");
   for (const char **p = test->expectMincoverStrings; *p; ++p) {
      fprintf (stderr, "  %s\n", *p);
   }
   fprintf (stderr, "mincover expected ... end\n");
   fprintf (stderr, "mincover got ... begin\n");
   for (size_t i = 0; i < mc_mincover_len (got); i++) {
      fprintf (stderr, "  %s\n", mc_mincover_get (got, i));
   }
   fprintf (stderr, "mincover got ... end\n");
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT

static void
_test_getMincover_impl (void *tests,
                        size_t num_tests,
                        _test_getMincover_args args)
{
   BSON_ASSERT_PARAM (tests);

   for (size_t i = 0; i < num_tests; i++) {
      mongocrypt_status_t *const status = mongocrypt_status_new ();
      mc_mincover_t *got = args.getMincover (tests, i, status);
      const char *expectError = args.expectError (tests, i);
      if (expectError) {
         ASSERT_OR_PRINT_MSG (NULL == got, "expected error, got success");
         ASSERT_STATUS_CONTAINS (status, expectError);
         mongocrypt_status_destroy (status);
         continue;
      }
      ASSERT_OK_STATUS (got != NULL, status);

      size_t numGot = mc_mincover_len (got);
      const char *const *expectStrings = args.expectMincoverStrings (tests, i);

      const char *const *exp_iter = expectStrings;
      size_t nthItem = 0;
      for (; *exp_iter; ++nthItem, ++exp_iter) {
         if (nthItem > numGot) {
            // List length mismatch. Keep scanning, though. We'll use the
            // numbers later
            continue;
         }
         const char *gotItem = mc_mincover_get (got, nthItem);
         const char *expectItem = *exp_iter;

         if (0 == strcmp (gotItem, expectItem)) {
            // This one matches, Keep going.
            continue;
         }
         args.dump (tests, nthItem, got);
         TEST_ERROR ("test %zu: mincover mismatch at index %zu:\n"
                     "      Got: %s\n"
                     " Expected: %s\n",
                     i,
                     nthItem,
                     gotItem,
                     expectItem);
      }

      if (nthItem != numGot) {
         args.dump (tests, i, got);
         TEST_ERROR (
            "test %zu: Got the wrong number of mincover items. Expected %zu "
            "items, but got %zu\n",
            i,
            nthItem,
            numGot);
      }

      mc_mincover_destroy (got);
      mongocrypt_status_destroy (status);
   }
}

static void
_test_getMincoverInt32 (_mongocrypt_tester_t *tester)
{
   static Int32Test tests[] = {
      {.lowerBound = 1,
       .includeLowerBound = false,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"01"}},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = false,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"001", "010"}},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"001", "01"}},
      {.lowerBound = 3,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"011"}},
      {.lowerBound = 4,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectError = "must be less than or equal to"},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 8,
       .includeUpperBound = true,
       .min = OPT_I32_C (0),
       .max = OPT_I32_C (7),
       .sparsity = 1,
       .expectError = "less than or equal to the maximum value"},

#include "./data/range-min-cover/mincover_int32.cstruct"

   };

   _test_getMincover_impl (
      tests,
      sizeof (tests) / sizeof (tests[0]),
      (_test_getMincover_args){.getMincover = _test_getMincover32,
                               .expectMincoverStrings = _test_expectMincover32,
                               .expectError = _test_expectError32,
                               .dump = _test_dump_32});
}

static void
_test_getMincoverInt64 (_mongocrypt_tester_t *tester)
{
   static Int64Test tests[] = {
      {.lowerBound = 1,
       .includeLowerBound = false,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"01"}},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = false,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"001", "010"}},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"001", "01"}},
      {.lowerBound = 3,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectMincoverStrings = {"011"}},
      {.lowerBound = 4,
       .includeLowerBound = true,
       .upperBound = 3,
       .includeUpperBound = true,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectError = "must be less than or equal to"},
      {.lowerBound = 1,
       .includeLowerBound = true,
       .upperBound = 8,
       .includeUpperBound = true,
       .min = OPT_I64_C (0),
       .max = OPT_I64_C (7),
       .sparsity = 1,
       .expectError = "less than or equal to the maximum value"},

#include "./data/range-min-cover/mincover_int64.cstruct"

   };

   _test_getMincover_impl (
      tests,
      sizeof (tests) / sizeof (tests[0]),
      (_test_getMincover_args){.getMincover = _test_getMincover64,
                               .expectMincoverStrings = _test_expectMincover64,
                               .expectError = _test_expectError64,
                               .dump = _test_dump_64});
}

static void
_test_getMincoverDouble (_mongocrypt_tester_t *tester)
{
   static DoubleTest tests[] = {
#include "./data/range-min-cover/mincover_double.cstruct"
#include "./data/range-min-cover/mincover_double_precision.cstruct"
   };

   _test_getMincover_impl (
      tests,
      sizeof (tests) / sizeof (tests[0]),
      (_test_getMincover_args){.getMincover = _test_getMincoverDouble_helper,
                               .expectMincoverStrings =
                                  _test_expectMincoverDouble,
                               .expectError = _test_expectErrorDouble,
                               .dump = _test_dump_Double});
}

#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
static void
_test_getMincoverDecimal128 (_mongocrypt_tester_t *tester)
{
   Decimal128Test tests[] = {
#include "./data/range-min-cover/mincover_decimal128.cstruct"
#include "./data/range-min-cover/mincover_decimal128_precision.cstruct"
   };

   _test_getMincover_impl (
      tests,
      sizeof (tests) / sizeof (tests[0]),
      (_test_getMincover_args){
         .getMincover = _test_getMincoverDecimal128_helper,
         .expectMincoverStrings = _test_expectMincoverDecimal128,
         .expectError = _test_expectErrorDecimal128,
         .dump = _test_dump_Decimal128});
}
#endif // MONGOCRYPT_HAVE_DECIMAL128_SUPPORT

void
_mongocrypt_tester_install_range_mincover (_mongocrypt_tester_t *tester)
{
   INSTALL_TEST (_test_getMincoverInt32);
   INSTALL_TEST (_test_getMincoverInt64);
   INSTALL_TEST (_test_getMincoverDouble);
#if MONGOCRYPT_HAVE_DECIMAL128_SUPPORT
   INSTALL_TEST (_test_getMincoverDecimal128);
#endif
}
