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

#include "gtest/gtest.h"
#include "common/Formatter.h"
#include "common/WeightedPriorityQueue.h"

#include <numeric>
#include <vector>
#include <map>
#include <list>
#include <tuple>

#define CEPH_OP_CLASS_STRICT	0
#define CEPH_OP_CLASS_NORMAL	0
#define CEPH_OP_QUEUE_BACK	0
#define CEPH_OP_QUEUE_FRONT	0

class WeightedPriorityQueueTest : public testing::Test
{
protected:
  typedef unsigned Klass;
  // tuple<Prio, Klass, OpID> so that we can verfiy the op
  typedef std::tuple<unsigned, unsigned, unsigned> Item;
  typedef unsigned Prio;
  typedef unsigned Kost;
  typedef WeightedPriorityQueue<Item, Klass> WQ;
  // Simulate queue structure
  typedef std::list<std::pair<Kost, Item> > ItemList;
  typedef std::map<Klass, ItemList> KlassItem;
  typedef std::map<Prio, KlassItem> LQ;
  typedef std::list<Item> Removed;
  const unsigned max_prios = 5; // (0-4) * 64
  const unsigned klasses = 37;  // Make prime to help get good coverage

  void fill_queue(WQ &wq, LQ &strictq, LQ &normq,
      unsigned item_size, bool randomize = false) {
    unsigned p, k, c, o, op_queue, fob;
    for (unsigned i = 1; i <= item_size; ++i) {
      // Choose priority, class, cost and 'op' for this op.
      if (randomize) {
        p = (rand() % max_prios) * 64;
        k = rand() % klasses;
        c = rand() % (1<<22);  // 4M cost
        // Make some of the costs 0, but make sure small costs
        // still work ok.
        if (c > (1<<19) && c < (1<<20)) {
          c = 0;
	}
        op_queue = rand() % 10;
        fob = rand() % 10;
      } else {
        p = (i % max_prios) * 64;
        k = i % klasses;
        c = (i % 8 == 0 || i % 16 == 0) ? 0 : 1 << (i % 23);
        op_queue = i % 7; // Use prime numbers to
        fob = i % 11;     // get better coverage
      }
      o = rand() % (1<<16);
      // Choose how to enqueue this op.
      switch (op_queue) {
      case 6 :
        // Strict Queue
        if (fob == 4) {
	  // Queue to the front.
	  strictq[p][k].push_front(std::make_pair(
  	    c, std::make_tuple(p, k, o)));
	  wq.enqueue_strict_front(Klass(k), p, std::make_tuple(p, k, o));
        } else {
	  //Queue to the back.
	  strictq[p][k].push_back(std::make_pair(
	    c, std::make_tuple(p, k, o)));
	  wq.enqueue_strict(Klass(k), p, std::make_tuple(p, k, o));
        }
        break;
      default:
        // Normal queue
        if (fob == 4) {
	  // Queue to the front.
	  normq[p][k].push_front(std::make_pair(
	    c, std::make_tuple(p, k, o)));
	  wq.enqueue_front(Klass(k), p, c, std::make_tuple(p, k, o));
        } else {
	  //Queue to the back.
	  normq[p][k].push_back(std::make_pair(
	    c, std::make_tuple(p, k, o)));
	  wq.enqueue(Klass(k), p, c, std::make_tuple(p, k, o));
        }
        break;
      }
    }
  }
  void test_queue(unsigned item_size, bool randomize = false) {
    // Due to the WRR queue having a lot of probabilistic logic
    // we can't determine the exact order OPs will be dequeued.
    // However, the queue should not dequeue a priority out of
    // order. It should also dequeue the strict priority queue
    // first and in order. In both the strict and normal queues
    // push front and back should be respected. Here we keep
    // track of the ops queued and make sure they dequeue
    // correctly.
  
    // Set up local tracking queues
    WQ wq(0, 0);
    LQ strictq, normq;
    fill_queue(wq, strictq, normq, item_size, randomize);
    // Test that the queue is dequeuing properly.
    typedef std::map<unsigned, unsigned> LastKlass;
    LastKlass last_strict, last_norm;
    while (!(wq.empty())) {
      Item r = wq.dequeue();
      if (!(strictq.empty())) {
        // Check that there are no higher priorities
        // in the strict queue.
        LQ::reverse_iterator ri = strictq.rbegin();
        EXPECT_EQ(std::get<0>(r), ri->first);
        // Check that if there are multiple classes in a priority
        // that it is not dequeueing the same class each time.
        LastKlass::iterator si = last_strict.find(std::get<0>(r));
        if (strictq[std::get<0>(r)].size() > 1 && si != last_strict.end()) {
	  EXPECT_NE(std::get<1>(r), si->second);
	}
        last_strict[std::get<0>(r)] = std::get<1>(r);

	Item t = strictq[std::get<0>(r)][std::get<1>(r)].front().second;
        EXPECT_EQ(std::get<2>(r), std::get<2>(t));
        strictq[std::get<0>(r)][std::get<1>(r)].pop_front();
        if (strictq[std::get<0>(r)][std::get<1>(r)].empty()) {
	  strictq[std::get<0>(r)].erase(std::get<1>(r));
	}
        if (strictq[std::get<0>(r)].empty()) {
	  strictq.erase(std::get<0>(r));
	}
      } else {
        // Check that if there are multiple classes in a priority
        // that it is not dequeueing the same class each time.
        LastKlass::iterator si = last_norm.find(std::get<0>(r));
        if (normq[std::get<0>(r)].size() > 1 && si != last_norm.end()) {
	  EXPECT_NE(std::get<1>(r), si->second);
	}
        last_norm[std::get<0>(r)] = std::get<1>(r);

	Item t = normq[std::get<0>(r)][std::get<1>(r)].front().second;
        EXPECT_EQ(std::get<2>(r), std::get<2>(t));
        normq[std::get<0>(r)][std::get<1>(r)].pop_front();
        if (normq[std::get<0>(r)][std::get<1>(r)].empty()) {
	  normq[std::get<0>(r)].erase(std::get<1>(r));
	}
        if (normq[std::get<0>(r)].empty()) {
	  normq.erase(std::get<0>(r));
	}
      }
    }
  }

  void SetUp() override {
    srand(time(0));
  }
  void TearDown() override {
  }
};

TEST_F(WeightedPriorityQueueTest, wpq_size){
  WQ wq(0, 0);
  EXPECT_TRUE(wq.empty());
  EXPECT_EQ(0u, wq.get_size_slow());

  // Test the strict queue size.
  for (unsigned i = 1; i < 5; ++i) {
    wq.enqueue_strict(Klass(i),i, std::make_tuple(i, i, i));
    EXPECT_FALSE(wq.empty());
    EXPECT_EQ(i, wq.get_size_slow());
  }
  // Test the normal queue size.
  for (unsigned i = 5; i < 10; ++i) {
    wq.enqueue(Klass(i), i, i, std::make_tuple(i, i, i));
    EXPECT_FALSE(wq.empty());
    EXPECT_EQ(i, wq.get_size_slow());
  }
  // Test that as both queues are emptied
  // the size is correct.
  for (unsigned i = 8; i >0; --i) {
    wq.dequeue();
    EXPECT_FALSE(wq.empty());
    EXPECT_EQ(i, wq.get_size_slow());
  }
  wq.dequeue();
  EXPECT_TRUE(wq.empty());
  EXPECT_EQ(0u, wq.get_size_slow());
}

TEST_F(WeightedPriorityQueueTest, wpq_test_static) {
  test_queue(1000);
} 

TEST_F(WeightedPriorityQueueTest, wpq_test_random) {
  test_queue(rand() % 500 + 500, true);
} 

TEST_F(WeightedPriorityQueueTest, wpq_test_remove_by_class_null) {
  WQ wq(0, 0);
  LQ strictq, normq;
  unsigned num_items = 10;
  fill_queue(wq, strictq, normq, num_items);
  Removed wq_removed;
  // Pick a klass that was not enqueued
  wq.remove_by_class(klasses + 1, &wq_removed);
  EXPECT_EQ(0u, wq_removed.size());
}

TEST_F(WeightedPriorityQueueTest, wpq_test_remove_by_class) {
  WQ wq(0, 0);
  LQ strictq, normq;
  unsigned num_items = 1000;
  fill_queue(wq, strictq, normq, num_items);
  unsigned num_to_remove = 0;
  const Klass k = 5;
  // Find how many ops are in the class
  for (LQ::iterator it = strictq.begin();
       it != strictq.end(); ++it) {
    num_to_remove += it->second[k].size();
  }
  for (LQ::iterator it = normq.begin();
       it != normq.end(); ++it) {
    num_to_remove += it->second[k].size();
  }
  Removed wq_removed;
  wq.remove_by_class(k, &wq_removed);
  // Check that the right ops were removed.
  EXPECT_EQ(num_to_remove, wq_removed.size());
  EXPECT_EQ(num_items - num_to_remove, wq.get_size_slow());
  for (Removed::iterator it = wq_removed.begin();
       it != wq_removed.end(); ++it) {
    EXPECT_EQ(k, std::get<1>(*it));
  }
  // Check that none were missed
  while (!(wq.empty())) {
    EXPECT_NE(k, std::get<1>(wq.dequeue()));
  }
}
