/** @file
  Test file for AcidPtr
  @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 REQUIRE 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 "catch.hpp"

#include "tscore/AcidPtr.h"
#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <ctime>
#include <thread>
#include <atomic>
#include <condition_variable>
#include <chrono>
#include <stdlib.h>

using namespace std;

TEST_CASE("AcidPtr Atomicity")
{
  // fail if skew is detected.
  constexpr int        N = 1000; // Number of threads
  constexpr int        K = 50;   // Size of data sample.
  AcidPtr<vector<int>> ptr(new vector<int>(K));
  atomic<int>          errors{0};
  atomic<unsigned>     count{0};
  condition_variable   gate;
  mutex                gate_mutex;

  auto job_read_write = [&]() {
    {
      unique_lock<mutex> gate_lock(gate_mutex);
      gate.wait(gate_lock);
    }
    int                        r = random();
    AcidCommitPtr<vector<int>> cptr(ptr);
    int                        old = (*cptr)[0];
    for (int &i : *cptr) {
      if (i != old) {
        errors++;
      }
      i = r;
    }
    ++count;
  };

  auto job_read = [&]() {
    {
      unique_lock<mutex> gate_lock(gate_mutex);
      gate.wait(gate_lock);
    }
    auto sptr = ptr.getPtr();
    int  old  = (*sptr)[0];
    for (int const &i : *sptr) {
      if (i != old) {
        errors++;
      }
    }
    ++count;
  };

  array<thread, N> writers;
  array<thread, N> readers;

  // Use a for loop so the threads start in pairs.
  for (int i = 0; i < N; i++) {
    writers[i] = thread(job_read_write);
    readers[i] = thread(job_read);
    gate.notify_all();
  }

  while (count < 2 * N) {
    gate.notify_all();
    this_thread::sleep_for(chrono::milliseconds{10});
  }

  for (auto &t : readers) {
    t.join();
  }

  for (auto &t : writers) {
    t.join();
  }

  REQUIRE(errors == 0); // skew detected
}

TEST_CASE("AcidPtr Isolation")
{
  AcidPtr<int> p;
  REQUIRE(p.getPtr() != nullptr);
  REQUIRE(p.getPtr().get() != nullptr);
  {
    AcidCommitPtr<int> w(p);
    *w = 40;
  }
  CHECK(*p.getPtr() == 40);
  {
    AcidCommitPtr<int> w  = p;
    *w                   += 1;
    CHECK(*p.getPtr() == 40); // new value not committed until end of scope
  }
  CHECK(*p.getPtr() == 41);
  {
    *AcidCommitPtr<int>(p) += 1; // leaves scope immediately if not named.
    CHECK(*p.getPtr() == 42);
  }
  CHECK(*p.getPtr() == 42);
}

TEST_CASE("AcidPtr persistence")
{
  AcidPtr<int>               p(new int(40));
  std::shared_ptr<const int> r1, r2, r3, r4;
  REQUIRE(p.getPtr() != nullptr);
  r1 = p.getPtr();
  {
    AcidCommitPtr<int> w  = p;
    r2                    = p.getPtr();
    *w                   += 1; // update p at end of scope
  }
  r3 = p.getPtr();
  {
    *AcidCommitPtr<int>(p) += 1; // leaves scope immediately if not named.
    r4                      = p.getPtr();
  }
  CHECK(*r1 == 40); // references to data are still valid, but inconsistent. (todo: rename AcidPtr to AiPtr?)
  CHECK(*r2 == 40);
  CHECK(*r3 == 41);
  CHECK(*r4 == 42);
}

TEST_CASE("AcidPtr Abort")
{
  AcidPtr<int> p;
  {
    AcidCommitPtr<int> w(p);
    *w = 40;
  }
  CHECK(*p.getPtr() == 40);
  {
    AcidCommitPtr<int> w  = p;
    *w                   += 1;
    w.abort();
    CHECK(w == nullptr);
  }
  CHECK(*p.getPtr() == 40);
}
