/**
 * Tests for Thrift server for leveldb
 * @author Dhruba Borthakur (dhruba@gmail.com)
 * Copyright 2012 Facebook
 */
#include <protocol/TBinaryProtocol.h>
#include <transport/TSocket.h>
#include <transport/TBufferTransports.h>
#include <util/testharness.h>
#include <DB.h>
#include <AssocService.h>
#include <leveldb_types.h>
#include "server_options.h"

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using boost::shared_ptr;
using namespace Tleveldb;

extern "C" void startServer(int argc, char**argv);
extern "C" void stopServer(int port);
extern  ServerOptions server_options;

static DBHandle dbhandle;
static DBClient* dbclient;
static AssocServiceClient* aclient;
static const Text dbname = "test-dhruba";
static int ARGC;
static char** ARGV;

static void cleanupDir(std::string dir) {
  // remove old data, if any
  char* cleanup = new char[100];
  snprintf(cleanup, 100, "rm -rf %s", dir.c_str());
  system(cleanup);
}

static void createDatabase() {
  DBOptions options;
  options.create_if_missing = true; // create
  options.error_if_exists = false;  // overwrite
  options.write_buffer_size = (4<<20); // 4 MB
  options.max_open_files = 1000;
  options.block_size = 4096;
  options.block_restart_interval = 16;
  options.compression = kSnappyCompression;

  cleanupDir(server_options.getDataDirectory(dbname));

  // create the database
  dbclient->Open(dbhandle, dbname, options);
}

static void initialize(int port) {
  boost::shared_ptr<TSocket> socket1(new TSocket("localhost", port));
  boost::shared_ptr<TTransport> transport1(new TBufferedTransport(socket1));
  boost::shared_ptr<TProtocol> protocol1(new TBinaryProtocol(transport1));

  // open database
  dbclient = new DBClient(protocol1);
  transport1->open();

  boost::shared_ptr<TSocket> socket2(new TSocket("localhost", port+1));
  boost::shared_ptr<TTransport> transport2(new TBufferedTransport(socket2));
  boost::shared_ptr<TProtocol> protocol2(new TBinaryProtocol(transport2));
  aclient = new AssocServiceClient(protocol2);
  transport2->open();

  createDatabase();
  printf("Database created.\n");
}

//
// Run base leveldb thrift server get/put/iter/scan tests
//
static void testClient() {
  WriteOptions writeOptions;
  printf("Running base leveldb operations .................\n");

  // insert record into leveldb
  Slice key;
  key.data = "Key1";
  key.size = 4;
  Slice value;
  value.data = "value1";
  value.size = 6;
  kv keyvalue;
  keyvalue.key = key;
  keyvalue.value = value;
  int ret = dbclient->Put(dbhandle, keyvalue, writeOptions);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Put Key1 suceeded.\n");

  //read it back
  ReadOptions readOptions;
  ResultItem rValue;
  dbclient->Get(rValue, dbhandle, key, readOptions);
  ASSERT_TRUE(rValue.status == Code::kOk);
  ASSERT_TRUE(value.data.compare(rValue.value.data) == 0);
  ASSERT_TRUE(value.size == rValue.value.size);
  printf("Get suceeded.\n");

  // get a snapshot
  ResultSnapshot rsnap;
  dbclient->GetSnapshot(rsnap, dbhandle);
  ASSERT_TRUE(rsnap.status == Code::kOk);
  ASSERT_TRUE(rsnap.snapshot.snapshotid > 0);
  printf("Snapshot suceeded.\n");

  // insert a new record into leveldb
  Slice key2;
  key2.data = "Key2";
  key2.size = 4;
  Slice value2;
  value2.data = "value2";
  value2.size = 6;
  keyvalue.key = key2;
  keyvalue.value = value2;
  ret = dbclient->Put(dbhandle, keyvalue, writeOptions);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Put Key2 suceeded.\n");

  // verify that a get done with a previous snapshot does not find Key2
  readOptions.snapshot = rsnap.snapshot;
  dbclient->Get(rValue, dbhandle, key2, readOptions);
  ASSERT_TRUE(rValue.status == Code::kNotFound);
  printf("Get with snapshot succeeded.\n");

  // release snapshot
  ret = dbclient->ReleaseSnapshot(dbhandle, rsnap.snapshot);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Snapshot released.\n");

  // if we try to re-release the same snapshot, it should fail
  ret = dbclient->ReleaseSnapshot(dbhandle, rsnap.snapshot);
  ASSERT_TRUE(ret == Code::kNotFound);
  
  // compact whole database
  Slice range;
  range.size = 0;
  ret = dbclient->CompactRange(dbhandle, range, range);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Compaction trigger suceeded.\n");

  // create a new iterator to scan all keys from the start
  Slice target;
  ResultIterator ri;
  readOptions.snapshot.snapshotid = 0;
  dbclient->NewIterator(ri, dbhandle, readOptions,
                        IteratorType::seekToFirst, target);
  ASSERT_TRUE(ri.status == Code::kOk);
  int foundPairs = 0;
  while (true) {
    ResultPair pair;
    dbclient->GetNext(pair, dbhandle, ri.iterator);
    if (pair.status == Code::kOk) {
      foundPairs++;
    } else {
      break;
    }
  }
  ASSERT_TRUE(foundPairs == 2);
  ret = dbclient->DeleteIterator(dbhandle, ri.iterator);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Iterator scan-all forward passes.\n");

  // create a new iterator, position at end and scan backwards
  readOptions.snapshot.snapshotid = 0;
  dbclient->NewIterator(ri, dbhandle, readOptions,
                        IteratorType::seekToLast, target);
  ASSERT_TRUE(ri.status == Code::kOk);
  foundPairs = 0;
  while (true) {
    ResultPair pair;
    dbclient->GetPrev(pair, dbhandle, ri.iterator);
    if (pair.status == Code::kOk) {
      foundPairs++;
    } else {
      break;
    }
  }
  ASSERT_TRUE(foundPairs == 2);
  ret = dbclient->DeleteIterator(dbhandle, ri.iterator);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Iterator scan-all backward passes.\n");
  
  // create a new iterator, position at middle
  readOptions.snapshot.snapshotid = 0;
  target = key;
  dbclient->NewIterator(ri, dbhandle, readOptions,
                        IteratorType::seekToKey, target);
  ASSERT_TRUE(ri.status == Code::kOk);
  foundPairs = 0;
  while (true) {
    ResultPair pair;
    dbclient->GetPrev(pair, dbhandle, ri.iterator);
    if (pair.status == Code::kOk) {
      foundPairs++;
    } else {
      break;
    }
  }
  ASSERT_TRUE(foundPairs == 1);
  ret = dbclient->DeleteIterator(dbhandle, ri.iterator);
  ASSERT_TRUE(ret == Code::kOk);
  printf("Iterator scan-selective backward passes.\n");
}

// clean up all data that we inserted as part of this test
void clearDatabase() {
  WriteOptions writeOptions;
  ReadOptions readOptions;
  readOptions.snapshot.snapshotid = 0;
  printf("Clearing entire database.\n");

  ResultIterator ri;
  Slice dummy;
  dbclient->NewIterator(ri, dbhandle, readOptions,
                        IteratorType::seekToFirst, dummy);
  ASSERT_TRUE(ri.status == Code::kOk);
  while (true) {
    ResultPair pair;
    dbclient->GetNext(pair, dbhandle, ri.iterator);
    if (pair.status == Code::kOk) {
      Slice key = pair.keyvalue.key;
      Code code = dbclient->Delete(dbhandle, key, writeOptions); 
      ASSERT_EQ(code, Code::kOk);
    } else {
      break;
    }
  }
  // no need to invoke DeleteIterator because we scanned
  // till the end using the iterator and ist is auto-deleted
  // by the server.
}

//
// Run assoc tests
//
static void testAssocs() {
  WriteOptions writeOptions;
  printf("Running assoc leveldb operations ................\n");

  // insert record into leveldb
  int64_t assocType = 100;
  int64_t id1 = 1;
  int64_t id2 = 2;
  int64_t id1Type = 101;
  int64_t id2Type = 102;
  int64_t ts =3333;
  AssocVisibility vis = AssocVisibility::VISIBLE;
  bool update_count = true;
  int64_t dataVersion = 5;
  const Text data = "data......";
  const Text wormhole_comments = "wormhole...";
  int64_t count = aclient->taoAssocPut(dbname, assocType,
           id1, id2, id1Type, id2Type, 
           ts, vis, update_count,
           dataVersion, data, wormhole_comments);
  ASSERT_GE(count, 0);
  printf("AssocPut first record suceeded.\n");

  // verify assoc counts.
  int64_t cnt = aclient->taoAssocCount(dbname, assocType, id1); 
  ASSERT_EQ(cnt, 1);
  printf("AssocCount suceeded.\n");

  // verify that we can read back what we inserted earlier
  std::vector<int64_t> id2list(1);
  id2list[0] = id2;
  std::vector<TaoAssocGetResult> readback(1);
  aclient->taoAssocGet(readback, dbname,
                       assocType, id1, id2list);
  printf("AssocGet suceeded.\n");
  ASSERT_EQ((unsigned int)1, readback.size());
  ASSERT_EQ(id1Type, readback[0].id1Type);
  ASSERT_EQ(id2Type, readback[0].id2Type);
  ASSERT_EQ(ts, readback[0].time);
  ASSERT_EQ(dataVersion, readback[0].dataVersion);
  ASSERT_EQ(readback[0].data.compare(data), 0);

  // add one more assoc
  const Text data1 = "data1......";
  count = aclient->taoAssocPut(dbname, assocType,
           id1, id2+2, id1Type+1, id2Type+1, 
           ts+1, vis, update_count,
           dataVersion+1, data1, wormhole_comments);
  ASSERT_EQ(count, 2);
  printf("AssocPut second record suceeded.\n");

  // verify assoc count is 2
  cnt = aclient->taoAssocCount(dbname, assocType, id1); 
  ASSERT_EQ(cnt, 2);

  // do a range get for id1+type and verify that there
  // are two assocs.
  readback.clear();
  int64_t offset = 0;
  int64_t limit = 1000;
  aclient->taoAssocRangeGet(readback, dbname, assocType,
                            id1, LONG_MAX, 0,
                            offset, limit);
  ASSERT_EQ((unsigned int)2, readback.size());

  // Delete the most recent assoc
  int c = aclient->taoAssocDelete(dbname, assocType,
            id1, id2+2, AssocVisibility::HIDDEN, true, "");
  ASSERT_EQ(c, 1);

  // verify assoc falls back to 1.
  cnt = aclient->taoAssocCount(dbname, assocType, id1); 
  ASSERT_EQ(cnt, 1);
  printf("AssocCount suceeded.\n");
}

//
// close all resources
//
static void close() {
  // close database
  dbclient->Close(dbhandle, dbname);
  // transport->close();
}


int main(int argc, char **argv) {

  ARGC = argc;
  ARGV = argv;

  // create a server
  startServer(argc, argv);
  printf("Server thread created.\n");

  // give some time to the server to initialize itself
  while (server_options.getPort() == 0) {
    sleep(1);
  }

  // test client
  initialize(server_options.getPort());

  // run all tests
  testClient();
  clearDatabase();

  testAssocs();
  clearDatabase();

  // done all tests
  close();

  return 0;
}