// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab

#include "journal/ObjectPlayer.h"
#include "journal/Entry.h"
#include "include/stringify.h"
#include "common/Timer.h"
#include "gtest/gtest.h"
#include "test/librados/test.h"
#include "test/journal/RadosTestFixture.h"

template <typename T>
class TestObjectPlayer : public RadosTestFixture, public T {
public:
  auto create_object(const std::string &oid, uint8_t order) {
    auto object = ceph::make_ref<journal::ObjectPlayer>(
      m_ioctx, oid + ".", 0, *m_timer, m_timer_lock, order,
      T::max_fetch_bytes);
    return object;
  }

  int fetch(const ceph::ref_t<journal::ObjectPlayer>& object_player) {
    while (true) {
      C_SaferCond ctx;
      object_player->set_refetch_state(
        journal::ObjectPlayer::REFETCH_STATE_NONE);
      object_player->fetch(&ctx);
      int r = ctx.wait();
      if (r < 0 || !object_player->refetch_required()) {
        return r;
      }
    }
    return 0;
  }

  int watch_and_wait_for_entries(const ceph::ref_t<journal::ObjectPlayer>& object_player,
                                 journal::ObjectPlayer::Entries *entries,
                                 size_t count) {
    for (size_t i = 0; i < 50; ++i) {
      object_player->get_entries(entries);
      if (entries->size() == count) {
        break;
      }

      C_SaferCond ctx;
      object_player->watch(&ctx, 0.1);

      int r = ctx.wait();
      if (r < 0) {
        return r;
      }
    }
    return 0;
  }

  std::string get_object_name(const std::string &oid) {
    return oid + ".0";
  }
};

template <uint32_t _max_fetch_bytes>
struct TestObjectPlayerParams {
  static inline const uint32_t max_fetch_bytes = _max_fetch_bytes;
};

typedef ::testing::Types<TestObjectPlayerParams<0>,
                         TestObjectPlayerParams<10> > TestObjectPlayerTypes;
TYPED_TEST_SUITE(TestObjectPlayer, TestObjectPlayerTypes);

TYPED_TEST(TestObjectPlayer, Fetch) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 124, this->create_payload(std::string(24, '1')));

  bufferlist bl;
  encode(entry1, bl);
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);
  ASSERT_LE(0, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(2U, entries.size());

  journal::ObjectPlayer::Entries expected_entries = {entry1, entry2};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, FetchLarge) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123,
                        this->create_payload(std::string(8192 - 32, '1')));
  journal::Entry entry2(234, 124, this->create_payload(""));

  bufferlist bl;
  encode(entry1, bl);
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 12);
  ASSERT_LE(0, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(2U, entries.size());

  journal::ObjectPlayer::Entries expected_entries = {entry1, entry2};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, FetchDeDup) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 123, this->create_payload(std::string(24, '2')));

  bufferlist bl;
  encode(entry1, bl);
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);
  ASSERT_LE(0, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(1U, entries.size());

  journal::ObjectPlayer::Entries expected_entries = {entry2};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, FetchEmpty) {
  std::string oid = this->get_temp_oid();

  bufferlist bl;
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);

  ASSERT_EQ(0, this->fetch(object));
  ASSERT_TRUE(object->empty());
}

TYPED_TEST(TestObjectPlayer, FetchCorrupt) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 124, this->create_payload(std::string(24, '2')));

  bufferlist bl;
  encode(entry1, bl);
  encode(this->create_payload("corruption" + std::string(1024, 'X')), bl);
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);
  ASSERT_EQ(-EBADMSG, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(1U, entries.size());

  journal::ObjectPlayer::Entries expected_entries = {entry1};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, FetchAppend) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 124, this->create_payload(std::string(24, '2')));

  bufferlist bl;
  encode(entry1, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);
  ASSERT_LE(0, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(1U, entries.size());

  journal::ObjectPlayer::Entries expected_entries = {entry1};
  ASSERT_EQ(expected_entries, entries);

  bl.clear();
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));
  ASSERT_LE(0, this->fetch(object));

  object->get_entries(&entries);
  ASSERT_EQ(2U, entries.size());

  expected_entries = {entry1, entry2};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, PopEntry) {
  std::string oid = this->get_temp_oid();

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 124, this->create_payload(std::string(24, '1')));

  bufferlist bl;
  encode(entry1, bl);
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));

  auto object = this->create_object(oid, 14);
  ASSERT_LE(0, this->fetch(object));

  journal::ObjectPlayer::Entries entries;
  object->get_entries(&entries);
  ASSERT_EQ(2U, entries.size());

  journal::Entry entry;
  object->front(&entry);
  object->pop_front();
  ASSERT_EQ(entry1, entry);
  object->front(&entry);
  object->pop_front();
  ASSERT_EQ(entry2, entry);
  ASSERT_TRUE(object->empty());
}

TYPED_TEST(TestObjectPlayer, Watch) {
  std::string oid = this->get_temp_oid();
  auto object = this->create_object(oid, 14);

  C_SaferCond cond1;
  object->watch(&cond1, 0.1);

  journal::Entry entry1(234, 123, this->create_payload(std::string(24, '1')));
  journal::Entry entry2(234, 124, this->create_payload(std::string(24, '1')));

  bufferlist bl;
  encode(entry1, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));
  ASSERT_LE(0, cond1.wait());

  journal::ObjectPlayer::Entries entries;
  ASSERT_EQ(0, this->watch_and_wait_for_entries(object, &entries, 1U));
  ASSERT_EQ(1U, entries.size());

  journal::ObjectPlayer::Entries expected_entries;
  expected_entries = {entry1};
  ASSERT_EQ(expected_entries, entries);

  C_SaferCond cond2;
  object->watch(&cond2, 0.1);

  bl.clear();
  encode(entry2, bl);
  ASSERT_EQ(0, this->append(this->get_object_name(oid), bl));
  ASSERT_LE(0, cond2.wait());

  ASSERT_EQ(0, this->watch_and_wait_for_entries(object, &entries, 2U));
  ASSERT_EQ(2U, entries.size());

  expected_entries = {entry1, entry2};
  ASSERT_EQ(expected_entries, entries);
}

TYPED_TEST(TestObjectPlayer, Unwatch) {
  std::string oid = this->get_temp_oid();
  auto object = this->create_object(oid, 14);

  C_SaferCond watch_ctx;
  object->watch(&watch_ctx, 600);

  usleep(200000);

  object->unwatch();
  ASSERT_EQ(-ECANCELED, watch_ctx.wait());
}
