#include "iwkv.h"
#include "iwlog.h"
#include "iwutils.h"
#include "iwcfg.h"
#include "iwkv_tests.h"
#include "iwkv_internal.h"

uint32_t g_seed;
uint32_t g_rnd_data_pos;
#define RND_DATA_SZ (10 * 1048576)
char RND_DATA[RND_DATA_SZ];

static void *rndbuf_next(uint32_t len) {
  assert(len <= RND_DATA_SZ);
  if (g_rnd_data_pos + len > RND_DATA_SZ) {
    g_rnd_data_pos = 0;
  }
  const char *ret = RND_DATA + g_rnd_data_pos;
  g_rnd_data_pos += len;
  return (void*) ret;
}

int init_suite(void) {
  iwrc rc = iwkv_init();
  RCRET(rc);
  uint64_t ts;
  iwp_current_time_ms(&ts, false);
  ts = IW_SWAB64(ts);
  ts >>= 32;
  g_seed = ts;

  fprintf(stderr, "\nRandom seed: %u\n", g_seed);
  iwu_rand_seed(g_seed);
  for (int i = 0; i < RND_DATA_SZ; ++i) {
    RND_DATA[i] = ' ' + iwu_rand_range(95); // ascii space ... ~
  }
  return rc;
}

int clean_suite(void) {
  return 0;
}

static void iwkv_test4_4(void) {
  char *path = "iwkv_test4_4.db";
  IWKV iwkv;
  IWDB db1;
  IWKV_val key = { 0 };
  IWKV_val val = { 0 };
  IWKV_OPTS opts = {
    .path                     = path,
    .oflags                   = IWKV_TRUNC,
    .random_seed              = g_seed,
    .wal                      = {
      .enabled                = true,
      .savepoint_timeout_sec  = 2,
      .checkpoint_timeout_sec = 300
    }
  };
  iwrc rc = iwkv_open(&opts, &iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
  rc = iwkv_db(iwkv, 1, 0, &db1);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "key00001";
  key.size = strlen(key.data);
  val.data = "value00001";
  val.size = strlen(val.data);
  rc = iwkv_put(db1, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  CU_ASSERT_FALSE(iwal_synched(iwkv));

  sleep(4);

  CU_ASSERT_TRUE(iwal_synched(iwkv));

  rc = iwkv_close(&iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
}

static void iwkv_test4_3_impl(int fmt_version) {
  char *path = "iwkv_test4_3.db";
  IWKV iwkv;
  IWDB db1;
  IWKV_val key = { 0 };
  IWKV_val val = { 0 };
  IWKV_OPTS opts = {
    .path                      = path,
    .oflags                    = IWKV_TRUNC | IWKV_NO_TRIM_ON_CLOSE,
    .random_seed               = g_seed,
    .fmt_version               = fmt_version,
    .wal                       = {
      .enabled                 = true,
      .check_crc_on_checkpoint = true,
      .savepoint_timeout_sec   = UINT32_MAX
    }
  };
  iwrc rc = iwkv_open(&opts, &iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
  rc = iwkv_db(iwkv, 1, 0, &db1);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "key00001";
  key.size = strlen(key.data);
  val.data = "value00001";
  val.size = strlen(val.data);
  rc = iwkv_put(db1, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_sync(iwkv, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "key00002";
  key.size = strlen(key.data);
  val.data = "value00002";
  val.size = strlen(val.data);
  rc = iwkv_put(db1, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_sync(iwkv, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "key00003";
  key.size = strlen(key.data);
  val.data = "value00003";
  val.size = strlen(val.data);
  rc = iwkv_put(db1, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  iwkvd_trigger_xor(IWKVD_WAL_NO_CHECKPOINT_ON_CLOSE);

  rc = iwkv_close(&iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  // Now reopen and roll WAL log

  iwkvd_trigger_xor(IWKVD_WAL_NO_CHECKPOINT_ON_CLOSE);

  opts.oflags &= ~IWKV_TRUNC;
  rc = iwkv_open(&opts, &iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
  rc = iwkv_db(iwkv, 1, 0, &db1);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "key00001";
  key.size = strlen(key.data);
  rc = iwkv_get(db1, &key, &val);
  CU_ASSERT_EQUAL_FATAL(rc, 0); // !!!!
  CU_ASSERT_NSTRING_EQUAL(val.data, "value00001", val.size);
  iwkv_kv_dispose(0, &val);

  key.data = "key00002";
  key.size = strlen(key.data);
  rc = iwkv_get(db1, &key, &val);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
  CU_ASSERT_NSTRING_EQUAL(val.data, "value00002", val.size);
  iwkv_kv_dispose(0, &val);

  key.data = "key00003";
  key.size = strlen(key.data);
  rc = iwkv_get(db1, &key, &val);
  CU_ASSERT_EQUAL_FATAL(rc, IWKV_ERROR_NOTFOUND);

  rc = iwkv_close(&iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
}

static void iwkv_test4_3_v1() {
  iwkv_test4_3_impl(1);
}

static void iwkv_test4_3_v2() {
  iwkv_test4_3_impl(2);
}

static void iwkv_test2_impl(char *path, const char *walpath, uint32_t num, uint32_t vrange) {
  g_rnd_data_pos = 0;
  char kbuf[100];
  iwrc rc;
  IWKV iwkv;
  IWDB db1;
  if (walpath) {
    unlink(walpath);
  }
  IWKV_val key = { 0 };
  IWKV_val val = { 0 };
  IWKV_OPTS opts = {
    .path                      = path,
    .oflags                    = IWKV_TRUNC,
    .random_seed               = g_seed,
    .wal                       = {
      .enabled                 = (walpath != NULL),
      .check_crc_on_checkpoint = true,
      .savepoint_timeout_sec   = UINT32_MAX,
      .wal_buffer_sz           = 64 * 1024,
      .checkpoint_buffer_sz    = 32 * 1024 * 1024
    }
  };
  rc = iwkv_open(&opts, &iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_db(iwkv, 1, 0, &db1);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = kbuf;

  for (int i = 0; i < num; ++i) {
    int k = iwu_rand_range(num);
    snprintf(key.data, sizeof(kbuf), "%016d", k);
    key.size = strlen(key.data);
    uint32_t value_size = iwu_rand_range(vrange + 1);
    if (value_size == 0) {
      value_size = 1;
    }
    val.data = rndbuf_next(value_size);
    val.size = value_size;
    rc = iwkv_put(db1, &key, &val, 0);
    CU_ASSERT_EQUAL_FATAL(rc, 0);
  }
  rc = iwkv_close(&iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
}

static void iwkv_test4_2(void) {
  uint32_t num = 1000;
  uint32_t vrange = 100000;
  iwkv_test2_impl("iwkv_test4_2.db", NULL, num, vrange);
  iwkv_test2_impl("iwkv_test4_2wal.db", "iwkv_test4_2wal.db-wal", num, vrange);
  FILE *iw1 = fopen("iwkv_test4_2.db", "rb");
  CU_ASSERT_PTR_NOT_NULL_FATAL(iw1);
  FILE *iw2 = fopen("iwkv_test4_2wal.db", "rb");
  CU_ASSERT_PTR_NOT_NULL_FATAL(iw2);
  int ret = cmp_files(iw1, iw2);
  CU_ASSERT_FALSE(ret);
  fclose(iw1);
  fclose(iw2);
}

static void iwkv_test1_impl(char *path, const char *walpath) {
  iwrc rc;
  IWKV iwkv;
  IWDB db1, db2;
  if (walpath) {
    unlink(walpath);
  }
  IWKV_val key = { 0 };
  IWKV_val val = { 0 };
  IWKV_OPTS opts = {
    .path                    = path,
    .oflags                  = IWKV_TRUNC,
    .wal                     = {
      .enabled               = (walpath != NULL),
      .savepoint_timeout_sec = UINT32_MAX
    }
  };
  rc = iwkv_open(&opts, &iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_db(iwkv, 1, 0, &db1);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_db(iwkv, 2, 0, &db2);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "foo";
  key.size = strlen(key.data);
  val.data = "bar";
  val.size = strlen(val.data);
  rc = iwkv_put(db1, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "foozz";
  key.size = strlen(key.data);
  val.data = "bazz";
  val.size = strlen(val.data);
  rc = iwkv_put(db2, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "foozz2";
  key.size = strlen(key.data);
  val.data = "bazzbazzbazzbazz";
  val.size = strlen(val.data);
  rc = iwkv_put(db2, &key, &val, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  key.data = "foozz";
  key.size = strlen(key.data);
  rc = iwkv_del(db2, &key, 0);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_db_destroy(&db2);
  CU_ASSERT_EQUAL_FATAL(rc, 0);

  rc = iwkv_close(&iwkv);
  CU_ASSERT_EQUAL_FATAL(rc, 0);
}

static void iwkv_test4_1(void) {
  iwkv_test1_impl("iwkv_test4_1.db", NULL);
  iwkv_test1_impl("iwkv_test4_1wal.db", "iwkv_test4_1wal.db-wal");
  FILE *iw1 = fopen("iwkv_test4_1.db", "rb");
  CU_ASSERT_PTR_NOT_NULL_FATAL(iw1);
  FILE *iw2 = fopen("iwkv_test4_1wal.db", "rb");
  CU_ASSERT_PTR_NOT_NULL_FATAL(iw2);
  int ret = cmp_files(iw1, iw2);
  CU_ASSERT_FALSE(ret);
  fclose(iw1);
  fclose(iw2);
}

int main() {
  CU_pSuite pSuite = NULL;

  /* Initialize the CUnit test registry */
  if (CUE_SUCCESS != CU_initialize_registry()) {
    return CU_get_error();
  }

  /* Add a suite to the registry */
  pSuite = CU_add_suite("iwkv_test4", init_suite, clean_suite);

  if (NULL == pSuite) {
    CU_cleanup_registry();
    return CU_get_error();
  }
  /* Add the tests to the suite */
  if (  (NULL == CU_add_test(pSuite, "iwkv_test4_1", iwkv_test4_1))
     || (NULL == CU_add_test(pSuite, "iwkv_test4_2", iwkv_test4_2))
     || (NULL == CU_add_test(pSuite, "iwkv_test4_3_v1", iwkv_test4_3_v1))
     || (NULL == CU_add_test(pSuite, "iwkv_test4_3_v2", iwkv_test4_3_v2))
     || (NULL == CU_add_test(pSuite, "iwkv_test4_4", iwkv_test4_4))) {
    CU_cleanup_registry();
    return CU_get_error();
  }

  /* Run all tests using the CUnit Basic interface */
  CU_basic_set_mode(CU_BRM_VERBOSE);
  CU_basic_run_tests();
  int ret = CU_get_error() || CU_get_number_of_failures();
  CU_cleanup_registry();
  return ret;
}
