/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you 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 "P_Cache.h"
#include "P_CacheTest.h"
#include "api/ts/ts.h"
#include <vector>
#include <cmath>

using namespace std;

CacheTestSM::CacheTestSM(RegressionTest *t, const char *name)
  : RegressionSM(t),
    start_memcpy_on_clone(0),
    cache_test_name(name),
    timeout(nullptr),
    cache_action(nullptr),
    start_time(0),
    cache_vc(nullptr),
    cvio(nullptr),
    buffer(nullptr),
    buffer_reader(nullptr),
    total_size(0),
    nbytes(-1),
    repeat_count(0),
    expect_event(EVENT_NONE),
    expect_initial_event(EVENT_NONE),
    initial_event(EVENT_NONE),
    content_salt(0),
    end_memcpy_on_clone(0)
{
  SET_HANDLER(&CacheTestSM::event_handler);
}

CacheTestSM::CacheTestSM(const CacheTestSM &ao) : RegressionSM(ao)
{
  int o = (int)(((char *)&start_memcpy_on_clone) - ((char *)this));
  int s = (int)(((char *)&end_memcpy_on_clone) - ((char *)&start_memcpy_on_clone));
  memcpy(((char *)this) + o, ((char *)&ao) + o, s);
  SET_HANDLER(&CacheTestSM::event_handler);
}

CacheTestSM::~CacheTestSM()
{
  ink_assert(!cache_action);
  ink_assert(!cache_vc);
  if (buffer_reader)
    buffer->dealloc_reader(buffer_reader);
  if (buffer)
    free_MIOBuffer(buffer);
}

int
CacheTestSM::open_read_callout()
{
  cvio = cache_vc->do_io_read(this, nbytes, buffer);
  return 1;
}

int
CacheTestSM::open_write_callout()
{
  cvio = cache_vc->do_io_write(this, nbytes, buffer_reader);
  return 1;
}

int
CacheTestSM::event_handler(int event, void *data)
{
  switch (event) {
  case EVENT_INTERVAL:
  case EVENT_IMMEDIATE:
    cancel_timeout();
    if (cache_action) {
      cache_action->cancel();
      cache_action = nullptr;
    }
    if (cache_vc) {
      cache_vc->do_io_close();
      cache_vc = nullptr;
    }
    cvio = nullptr;
    make_request();
    return EVENT_DONE;

  case CACHE_EVENT_LOOKUP_FAILED:
  case CACHE_EVENT_LOOKUP:
    goto Lcancel_next;

  case CACHE_EVENT_OPEN_READ:
    initial_event = event;
    cancel_timeout();
    cache_action  = nullptr;
    cache_vc      = (CacheVConnection *)data;
    buffer        = new_empty_MIOBuffer();
    buffer_reader = buffer->alloc_reader();
    if (open_read_callout() < 0)
      goto Lclose_error_next;
    else
      return EVENT_DONE;

  case CACHE_EVENT_OPEN_READ_FAILED:
    goto Lcancel_next;

  case VC_EVENT_READ_READY:
    if (!check_buffer())
      goto Lclose_error_next;
    buffer_reader->consume(buffer_reader->read_avail());
    ((VIO *)data)->reenable();
    return EVENT_CONT;

  case VC_EVENT_READ_COMPLETE:
    if (!check_buffer())
      goto Lclose_error_next;
    goto Lclose_next;

  case VC_EVENT_ERROR:
  case VC_EVENT_EOS:
    goto Lclose_error_next;

  case CACHE_EVENT_OPEN_WRITE:
    initial_event = event;
    cancel_timeout();
    cache_action  = nullptr;
    cache_vc      = (CacheVConnection *)data;
    buffer        = new_empty_MIOBuffer();
    buffer_reader = buffer->alloc_reader();
    if (open_write_callout() < 0)
      goto Lclose_error_next;
    else
      return EVENT_DONE;

  case CACHE_EVENT_OPEN_WRITE_FAILED:
    goto Lcancel_next;

  case VC_EVENT_WRITE_READY:
    fill_buffer();
    cvio->reenable();
    return EVENT_CONT;

  case VC_EVENT_WRITE_COMPLETE:
    if (nbytes != cvio->ndone)
      goto Lclose_error_next;
    goto Lclose_next;

  case CACHE_EVENT_REMOVE:
  case CACHE_EVENT_REMOVE_FAILED:
    goto Lcancel_next;

  case CACHE_EVENT_SCAN:
    initial_event = event;
    cache_vc      = (CacheVConnection *)data;
    return EVENT_CONT;

  case CACHE_EVENT_SCAN_OBJECT:
    return CACHE_SCAN_RESULT_CONTINUE;

  case CACHE_EVENT_SCAN_OPERATION_FAILED:
    return CACHE_SCAN_RESULT_CONTINUE;

  case CACHE_EVENT_SCAN_OPERATION_BLOCKED:
    return CACHE_SCAN_RESULT_CONTINUE;

  case CACHE_EVENT_SCAN_DONE:
    return EVENT_CONT;

  case CACHE_EVENT_SCAN_FAILED:
    return EVENT_CONT;

  case AIO_EVENT_DONE:
    goto Lnext;

  default:
    ink_assert(!"case");
    break;
  }
  return EVENT_DONE;

Lcancel_next:
  cancel_timeout();
  cache_action = nullptr;
  goto Lnext;
Lclose_error_next:
  cache_vc->do_io_close(1);
  goto Lclose_next_internal;
Lclose_next:
  cache_vc->do_io_close();
Lclose_next_internal:
  cache_vc = nullptr;
  if (buffer_reader) {
    buffer->dealloc_reader(buffer_reader);
    buffer_reader = nullptr;
  }
  if (buffer) {
    free_MIOBuffer(buffer);
    buffer = nullptr;
  }
Lnext:
  if (check_result(event) && repeat_count) {
    repeat_count--;
    timeout = eventProcessor.schedule_imm(this);
    return EVENT_DONE;
  } else
    return complete(event);
}

void
CacheTestSM::fill_buffer()
{
  int64_t avail = buffer->write_avail();
  CacheKey k    = key;
  k.b[1] += content_salt;
  int64_t sk = (int64_t)sizeof(key);
  while (avail > 0) {
    int64_t l = avail;
    if (l > sk)
      l = sk;

    int64_t pos = cvio->ndone + buffer_reader->read_avail();
    int64_t o   = pos % sk;

    if (l > sk - o)
      l     = sk - o;
    k.b[0]  = pos / sk;
    char *x = ((char *)&k) + o;
    buffer->write(x, l);
    buffer->fill(l);
    avail -= l;
  }
}

int
CacheTestSM::check_buffer()
{
  int64_t avail = buffer_reader->read_avail();
  CacheKey k    = key;
  k.b[1] += content_salt;
  char b[sizeof(key)];
  int64_t sk  = (int64_t)sizeof(key);
  int64_t pos = cvio->ndone - buffer_reader->read_avail();
  while (avail > 0) {
    int64_t l = avail;
    if (l > sk)
      l       = sk;
    int64_t o = pos % sk;
    if (l > sk - o)
      l     = sk - o;
    k.b[0]  = pos / sk;
    char *x = ((char *)&k) + o;
    buffer_reader->read(&b[0], l);
    if (::memcmp(b, x, l))
      return 0;
    buffer_reader->consume(l);
    pos += l;
    avail -= l;
  }
  return 1;
}

int
CacheTestSM::check_result(int event)
{
  return initial_event == expect_initial_event && event == expect_event;
}

int
CacheTestSM::complete(int event)
{
  if (!check_result(event))
    done(REGRESSION_TEST_FAILED);
  else
    done(REGRESSION_TEST_PASSED);
  delete this;
  return EVENT_DONE;
}

EXCLUSIVE_REGRESSION_TEST(cache)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus)
{
  if (cacheProcessor.IsCacheEnabled() != CACHE_INITIALIZED) {
    rprintf(t, "cache not initialized");
    *pstatus = REGRESSION_TEST_FAILED;
    return;
  }

  EThread *thread = this_ethread();

  CACHE_SM(t, write_test, { cacheProcessor.open_write(this, &key, false, CACHE_FRAG_TYPE_NONE, 100, CACHE_WRITE_OPT_SYNC); });
  write_test.expect_initial_event = CACHE_EVENT_OPEN_WRITE;
  write_test.expect_event         = VC_EVENT_WRITE_COMPLETE;
  write_test.nbytes               = 100;
  rand_CacheKey(&write_test.key, thread->mutex);

  CACHE_SM(t, lookup_test, { cacheProcessor.lookup(this, &key, false); });
  lookup_test.expect_event = CACHE_EVENT_LOOKUP;
  lookup_test.key          = write_test.key;

  CACHE_SM(t, read_test, { cacheProcessor.open_read(this, &key, false); });
  read_test.expect_initial_event = CACHE_EVENT_OPEN_READ;
  read_test.expect_event         = VC_EVENT_READ_COMPLETE;
  read_test.nbytes               = 100;
  read_test.key                  = write_test.key;

  CACHE_SM(t, remove_test, { cacheProcessor.remove(this, &key, false); });
  remove_test.expect_event = CACHE_EVENT_REMOVE;
  remove_test.key          = write_test.key;

  CACHE_SM(t, lookup_fail_test, { cacheProcessor.lookup(this, &key, false); });
  lookup_fail_test.expect_event = CACHE_EVENT_LOOKUP_FAILED;
  lookup_fail_test.key          = write_test.key;

  CACHE_SM(t, read_fail_test, { cacheProcessor.open_read(this, &key, false); });
  read_fail_test.expect_event = CACHE_EVENT_OPEN_READ_FAILED;
  read_fail_test.key          = write_test.key;

  CACHE_SM(t, remove_fail_test, { cacheProcessor.remove(this, &key, false); });
  remove_fail_test.expect_event = CACHE_EVENT_REMOVE_FAILED;
  rand_CacheKey(&remove_fail_test.key, thread->mutex);

  CACHE_SM(
    t, replace_write_test,
    { cacheProcessor.open_write(this, &key, false, CACHE_FRAG_TYPE_NONE, 100, CACHE_WRITE_OPT_SYNC); } int open_write_callout() {
      header.serial = 10;
      cache_vc->set_header(&header, sizeof(header));
      cvio = cache_vc->do_io_write(this, nbytes, buffer_reader);
      return 1;
    });
  replace_write_test.expect_initial_event = CACHE_EVENT_OPEN_WRITE;
  replace_write_test.expect_event         = VC_EVENT_WRITE_COMPLETE;
  replace_write_test.nbytes               = 100;
  rand_CacheKey(&replace_write_test.key, thread->mutex);

  CACHE_SM(t, replace_test,
           {
             cacheProcessor.open_write(this, &key, false, CACHE_FRAG_TYPE_NONE, 100, CACHE_WRITE_OPT_OVERWRITE_SYNC);
           } int open_write_callout() {
             CacheTestHeader *h = nullptr;
             int hlen           = 0;
             if (cache_vc->get_header((void **)&h, &hlen) < 0)
               return -1;
             if (h->serial != 10)
               return -1;
             header.serial = 11;
             cache_vc->set_header(&header, sizeof(header));
             cvio = cache_vc->do_io_write(this, nbytes, buffer_reader);
             return 1;
           });
  replace_test.expect_initial_event = CACHE_EVENT_OPEN_WRITE;
  replace_test.expect_event         = VC_EVENT_WRITE_COMPLETE;
  replace_test.nbytes               = 100;
  replace_test.key                  = replace_write_test.key;
  replace_test.content_salt         = 1;

  CACHE_SM(t, replace_read_test, { cacheProcessor.open_read(this, &key, false); } int open_read_callout() {
    CacheTestHeader *h = nullptr;
    int hlen           = 0;
    if (cache_vc->get_header((void **)&h, &hlen) < 0)
      return -1;
    if (h->serial != 11)
      return -1;
    cvio = cache_vc->do_io_read(this, nbytes, buffer);
    return 1;
  });
  replace_read_test.expect_initial_event = CACHE_EVENT_OPEN_READ;
  replace_read_test.expect_event         = VC_EVENT_READ_COMPLETE;
  replace_read_test.nbytes               = 100;
  replace_read_test.key                  = replace_test.key;
  replace_read_test.content_salt         = 1;

  CACHE_SM(t, large_write_test, { cacheProcessor.open_write(this, &key, false, CACHE_FRAG_TYPE_NONE, 100, CACHE_WRITE_OPT_SYNC); });
  large_write_test.expect_initial_event = CACHE_EVENT_OPEN_WRITE;
  large_write_test.expect_event         = VC_EVENT_WRITE_COMPLETE;
  large_write_test.nbytes               = 10000000;
  rand_CacheKey(&large_write_test.key, thread->mutex);

  CACHE_SM(t, pread_test, { cacheProcessor.open_read(this, &key, false); } int open_read_callout() {
    cvio = cache_vc->do_io_pread(this, nbytes, buffer, 7000000);
    return 1;
  });
  pread_test.expect_initial_event = CACHE_EVENT_OPEN_READ;
  pread_test.expect_event         = VC_EVENT_READ_COMPLETE;
  pread_test.nbytes               = 100;
  pread_test.key                  = large_write_test.key;

  // clang-format off
  r_sequential(t,
      write_test.clone(),
      lookup_test.clone(),
      r_sequential(t, 10, read_test.clone()) /* run read_test 10 times */,
      remove_test.clone(),
      lookup_fail_test.clone(),
      read_fail_test.clone(),
      remove_fail_test.clone(),
      replace_write_test.clone(),
      replace_test.clone(),
      replace_read_test.clone(),
      large_write_test.clone(),
      pread_test.clone(),
      nullptr)
  ->run(pstatus);
  // clang-format on

  return;
}

void
force_link_CacheTest()
{
}

// run -R 3 -r cache_disk_replacement_stability

REGRESSION_TEST(cache_disk_replacement_stability)(RegressionTest *t, int level, int *pstatus)
{
  static int const MAX_VOLS           = 26; // maximum values used in any test.
  static uint64_t DEFAULT_SKIP        = 8192;
  static uint64_t DEFAULT_STRIPE_SIZE = 1024ULL * 1024 * 1024 * 911; // 911G
  CacheDisk disk;                                                    // Only need one because it's just checked for failure.
  CacheHostRecord hr1, hr2;
  Vol *sample;
  static int const sample_idx = 16;
  Vol vols[MAX_VOLS];
  Vol *vol_ptrs[MAX_VOLS]; // array of pointers.
  char buff[2048];

  // Only run at the highest levels.
  if (REGRESSION_TEST_EXTENDED > level) {
    *pstatus = REGRESSION_TEST_PASSED;
    return;
  }

  *pstatus = REGRESSION_TEST_INPROGRESS;

  disk.num_errors = 0;

  for (int i = 0; i < MAX_VOLS; ++i) {
    vol_ptrs[i]  = vols + i;
    vols[i].disk = &disk;
    vols[i].len  = DEFAULT_STRIPE_SIZE;
    snprintf(buff, sizeof(buff), "/dev/sd%c %" PRIu64 ":%" PRIu64, 'a' + i, DEFAULT_SKIP, vols[i].len);
    MD5Context().hash_immediate(vols[i].hash_id, buff, strlen(buff));
  }

  hr1.vol_hash_table = nullptr;
  hr1.vols           = vol_ptrs;
  hr1.num_vols       = MAX_VOLS;
  build_vol_hash_table(&hr1);

  hr2.vol_hash_table = nullptr;
  hr2.vols           = vol_ptrs;
  hr2.num_vols       = MAX_VOLS;

  sample      = vols + sample_idx;
  sample->len = 1024ULL * 1024 * 1024 * (1024 + 128); // 1.1 TB
  snprintf(buff, sizeof(buff), "/dev/sd%c %" PRIu64 ":%" PRIu64, 'a' + sample_idx, DEFAULT_SKIP, sample->len);
  MD5Context().hash_immediate(sample->hash_id, buff, strlen(buff));
  build_vol_hash_table(&hr2);

  // See what the difference is
  int to = 0, from = 0;
  int then = 0, now = 0;
  for (int i = 0; i < VOL_HASH_TABLE_SIZE; ++i) {
    if (hr1.vol_hash_table[i] == sample_idx)
      ++then;
    if (hr2.vol_hash_table[i] == sample_idx)
      ++now;
    if (hr1.vol_hash_table[i] != hr2.vol_hash_table[i]) {
      if (hr1.vol_hash_table[i] == sample_idx)
        ++from;
      else
        ++to;
    }
  }
  rprintf(t, "Cache stability difference - "
             "delta = %d of %d : %d to, %d from, originally %d slots, now %d slots (net gain = %d/%d)\n",
          to + from, VOL_HASH_TABLE_SIZE, to, from, then, now, now - then, to - from);
  *pstatus = REGRESSION_TEST_PASSED;

  hr1.vols = nullptr;
  hr2.vols = nullptr;
}

static double zipf_alpha        = 1.2;
static int64_t zipf_bucket_size = 1;

#define ZIPF_SIZE (1 << 20)

static double *zipf_table = nullptr;

static void
build_zipf()
{
  if (zipf_table)
    return;
  zipf_table = (double *)ats_malloc(ZIPF_SIZE * sizeof(double));
  for (int i      = 0; i < ZIPF_SIZE; i++)
    zipf_table[i] = 1.0 / pow(i + 2, zipf_alpha);
  for (int i      = 1; i < ZIPF_SIZE; i++)
    zipf_table[i] = zipf_table[i - 1] + zipf_table[i];
  double x        = zipf_table[ZIPF_SIZE - 1];
  for (int i      = 0; i < ZIPF_SIZE; i++)
    zipf_table[i] = zipf_table[i] / x;
}

static int
get_zipf(double v)
{
  int l = 0, r = ZIPF_SIZE - 1, m;
  do {
    m = (r + l) / 2;
    if (v < zipf_table[m])
      r = m - 1;
    else
      l = m + 1;
  } while (l < r);
  if (zipf_bucket_size == 1)
    return m;
  double x = zipf_table[m], y = zipf_table[m + 1];
  m += static_cast<int>((v - x) / (y - x));
  return m;
}

static bool
test_RamCache(RegressionTest *t, RamCache *cache, const char *name, int64_t cache_size)
{
  bool pass = true;
  CacheKey key;
  Vol *vol = theCache->key_to_vol(&key, "example.com", sizeof("example.com") - 1);
  vector<Ptr<IOBufferData>> data;

  cache->init(cache_size, vol);

  for (int l = 0; l < 10; l++) {
    for (int i = 0; i < 200; i++) {
      IOBufferData *d = THREAD_ALLOC(ioDataAllocator, this_thread());
      INK_MD5 md5;

      d->alloc(BUFFER_SIZE_INDEX_16K);
      data.push_back(make_ptr(d));
      md5.u64[0] = ((uint64_t)i << 32) + i;
      md5.u64[1] = ((uint64_t)i << 32) + i;
      cache->put(&md5, data[i].get(), 1 << 15);
      // More hits for the first 10.
      for (int j = 0; j <= i && j < 10; j++) {
        Ptr<IOBufferData> data;
        INK_MD5 md5;

        md5.u64[0] = ((uint64_t)j << 32) + j;
        md5.u64[1] = ((uint64_t)j << 32) + j;
        cache->get(&md5, &data);
      }
    }
  }

  for (int i = 0; i < 10; i++) {
    INK_MD5 md5;
    Ptr<IOBufferData> data;

    md5.u64[0] = ((uint64_t)i << 32) + i;
    md5.u64[1] = ((uint64_t)i << 32) + i;
    if (!cache->get(&md5, &data)) {
      pass = false;
    }
  }

  int sample_size = cache_size >> 6;
  build_zipf();
  srand48(13);
  int *r = (int *)ats_malloc(sample_size * sizeof(int));
  for (int i = 0; i < sample_size; i++)
    // coverity[dont_call]
    r[i] = get_zipf(drand48());
  data.clear();
  int misses = 0;
  for (int i = 0; i < sample_size; i++) {
    INK_MD5 md5;
    md5.u64[0] = ((uint64_t)r[i] << 32) + r[i];
    md5.u64[1] = ((uint64_t)r[i] << 32) + r[i];
    Ptr<IOBufferData> get_data;
    if (!cache->get(&md5, &get_data)) {
      IOBufferData *d = THREAD_ALLOC(ioDataAllocator, this_thread());
      d->alloc(BUFFER_SIZE_INDEX_16K);
      data.push_back(make_ptr(d));
      cache->put(&md5, data.back().get(), 1 << 15);
      if (i >= sample_size / 2)
        misses++; // Sample last half of the gets.
    }
  }
  double fixed_hit_rate = 1.0 - (((double)(misses)) / (sample_size / 2));
  rprintf(t, "RamCache %s Fixed Size Hit Rate %f\n", name, fixed_hit_rate);

  data.clear();
  misses = 0;
  for (int i = 0; i < sample_size; i++) {
    INK_MD5 md5;
    md5.u64[0] = ((uint64_t)r[i] << 32) + r[i];
    md5.u64[1] = ((uint64_t)r[i] << 32) + r[i];
    Ptr<IOBufferData> get_data;
    if (!cache->get(&md5, &get_data)) {
      IOBufferData *d = THREAD_ALLOC(ioDataAllocator, this_thread());
      d->alloc(BUFFER_SIZE_INDEX_8K + (r[i] % 3));
      data.push_back(make_ptr(d));
      cache->put(&md5, data.back().get(), d->block_size());
      if (i >= sample_size / 2)
        misses++; // Sample last half of the gets.
    }
  }
  double variable_hit_rate = 1.0 - (((double)(misses)) / (sample_size / 2));
  rprintf(t, "RamCache %s Variable Size Hit Rate %f\n", name, variable_hit_rate);

  rprintf(t, "RamCache %s Nominal Size %lld Size %lld\n", name, cache_size, cache->size());

  if (fixed_hit_rate < 0.55 || variable_hit_rate < 0.55)
    return false;
  if (abs(cache_size - cache->size()) > 0.02 * cache_size)
    return false;

  ats_free(r);

  rprintf(t, "RamCache %s Test Done\r", name);

  return pass;
}

REGRESSION_TEST(ram_cache)(RegressionTest *t, int level, int *pstatus)
{
  // Run with -R 3 for now to trigger this check, until we figure out the CI
  if (REGRESSION_TEST_EXTENDED > level) {
    *pstatus = REGRESSION_TEST_PASSED;
    return;
  }

  if (cacheProcessor.IsCacheEnabled() != CACHE_INITIALIZED) {
    rprintf(t, "cache not initialized");
    *pstatus = REGRESSION_TEST_FAILED;
    return;
  }
  for (int s = 20; s <= 28; s += 4) {
    int64_t cache_size = 1LL << s;
    *pstatus           = REGRESSION_TEST_PASSED;
    if (!test_RamCache(t, new_RamCacheLRU(), "LRU", cache_size) || !test_RamCache(t, new_RamCacheCLFUS(), "CLFUS", cache_size))
      *pstatus = REGRESSION_TEST_FAILED;
  }
}
