// Copyright (c) 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "hash_table_base.h"

#include "util/test/test.h"

#include <algorithm>
#include <vector>

// This unit-test is also used to illustrate how to use HashTableBase<>
// in a concrete way. Here, each node is a simple pointer to a Int class
// that wraps a simple integer, but also keeps tracks of
// construction/destruction steps in global counters. This is used by the
// test to verify that operations like copies or moves do not miss or
// create allocations/deallocations.
//
// Because the derived table HashTableTest owns all pointer objects, it
// needs to manually create/deallocate them in its destructor, copy/move
// constructors and operators, as well as insert()/erase()/clear() methods.
//
// Finally, iteration support is provided through const_iterator and
// begin(), end() methods, which are enough for range-based for loops.
//

// A simple int wrapper class that can also count creation/destruction.
class Int {
 public:
  explicit Int(int x) : x_(x) { creation_counter++; }

  Int(const Int& other) : x_(other.x_) { creation_counter++; }

  ~Int() { destruction_counter++; }

  int& x() { return x_; }
  const int& x() const { return x_; }

  size_t hash() const { return static_cast<size_t>(x_); }

  static void ResetCounters() {
    creation_counter = 0;
    destruction_counter = 0;
  }

  int x_;

  static size_t creation_counter;
  static size_t destruction_counter;
};

size_t Int::creation_counter;
size_t Int::destruction_counter;

// A HashTableBase<> node type that contains a simple pointer to a Int value.
struct TestHashNode {
  Int* int_ptr;

  bool is_null() const { return !int_ptr; }
  bool is_tombstone() const { return int_ptr == &kTombstone; }
  bool is_valid() const { return !is_null() && !is_tombstone(); }
  size_t hash_value() const { return int_ptr->hash(); }

  static Int kTombstone;
};

// Value is irrelevant since only its address is used.
Int TestHashNode::kTombstone(-1);

// TestHashTable derives a HashTableBase<> instantiation to demonstrate
// full uses of the template. This includes:
//
//  - Storing a pointer in each node, and managing ownership of pointed
//    objects explicitly in the destructor, copy/move constructor and
//    operator, as well as insert() and erase() methods.
//
//  - The internal pointed objects are Int instance, but the TestHashTable
//    API masks that entirely, instead implementing a simple set of integers,
//    including iteration support.
//
//  Note that placing the integers directly in the nodes would be much easier,
//  but would not allow demonstrating how to manage ownership in thedestructor
class TestHashTable : public HashTableBase<TestHashNode> {
 public:
  using BaseType = HashTableBase<TestHashNode>;
  using Node = BaseType::Node;

  static_assert(std::is_same<Node, TestHashNode>::value,
                "HashTableBase<>::Node is not defined properly!");

  BaseType& asBaseType() { return *this; }
  const BaseType& asBaseType() const { return *this; }

  TestHashTable() = default;

  // IMPORTANT NOTE: Because the table contains bare owning pointers, we
  // have to use explicit copy and move constructor/operators for things
  // to work as expected. This is yet another reason why HashTableBase<>
  // should only be used with care (preferably with non-owning pointers).
  //
  TestHashTable(const TestHashTable& other) : BaseType(other) {
    // Only node (i.e. pointers) are copied by the base type.
    for (Node& node : ValidNodesRange()) {
      node.int_ptr = new Int(*node.int_ptr);
    }
  }

  TestHashTable& operator=(const TestHashTable& other) {
    if (this != &other) {
      this->~TestHashTable();
      new (this) TestHashTable(other);
    }
    return *this;
  }

  TestHashTable(TestHashTable&& other) noexcept : BaseType(std::move(other)) {}
  TestHashTable& operator=(TestHashTable&& other) noexcept {
    if (this != &other) {
      this->~TestHashTable();
      new (this) TestHashTable(std::move(other));
    }
    return *this;
  }

  ~TestHashTable() {
    // Discard all valid Int pointers in the hash table.
    for (Node& node : ValidNodesRange())
      delete node.int_ptr;
  }

  // Return true iff the table contains |x|.
  bool contains(int x) const {
    size_t hash = static_cast<size_t>(x);
    Node* node = NodeLookup(
        hash, [&](const Node* node) { return node->int_ptr->x() == x; });
    return node->is_valid();
  }

  // Try to insert |x| in the table. Returns true on success, or false if
  // the value was already in it.
  bool insert(int x) {
    size_t hash = static_cast<size_t>(x);
    Node* node = NodeLookup(
        hash, [&](const Node* node) { return node->int_ptr->x() == x; });
    if (node->is_valid())
      return false;

    bool was_tombstone = node->is_tombstone();
    node->int_ptr = new Int(x);
    UpdateAfterInsert(was_tombstone);
    return true;
  }

  // Try to remove |x| from the table. Return true on success, or false
  // if the value was not in it.
  bool erase(int x) {
    size_t hash = static_cast<size_t>(x);
    Node* node = NodeLookup(
        hash, [&](const Node* node) { return node->int_ptr->x() == x; });
    if (!node->is_valid())
      return false;

    delete node->int_ptr;
    node->int_ptr = &TestHashNode::kTombstone;
    UpdateAfterRemoval();
    return true;
  }

  // Remove all items
  void clear() {
    // Remove all pointed objects, since NodeClear() will not do it.
    for (Node& node : ValidNodesRange())
      delete node.int_ptr;

    NodeClear();
  }

  // Define const_iterator that point to the integer value instead of the node
  // of the Int instance to completely hide these from this class' API.
  struct const_iterator : public BaseType::NodeIterator {
    const int& operator*() const {
      return (this->BaseType::NodeIterator::operator*()).int_ptr->x();
    }
    const int* operator->() const { return &(this->operator*()); }
  };

  const_iterator begin() const { return {BaseType::NodeBegin()}; }

  const_iterator end() const { return {BaseType::NodeEnd()}; }
};

TEST(HashTableBaseTest, Construction) {
  Int::ResetCounters();
  {
    TestHashTable table;
    EXPECT_TRUE(table.empty());
    EXPECT_EQ(table.size(), 0u);
    EXPECT_EQ(table.begin(), table.end());
  }

  // No item was created or destroyed.
  EXPECT_EQ(Int::creation_counter, 0u);
  EXPECT_EQ(Int::destruction_counter, 0u);
}

TEST(HashTableBaseTest, InsertionsAndLookups) {
  Int::ResetCounters();
  {
    TestHashTable table;
    table.insert(1);
    table.insert(5);
    table.insert(7);

    EXPECT_FALSE(table.empty());
    EXPECT_EQ(table.size(), 3u);
    EXPECT_NE(table.begin(), table.end());

    EXPECT_EQ(Int::creation_counter, 3u);
    EXPECT_EQ(Int::destruction_counter, 0u);

    EXPECT_FALSE(table.contains(0));
    EXPECT_TRUE(table.contains(1));
    EXPECT_FALSE(table.contains(2));
    EXPECT_FALSE(table.contains(3));
    EXPECT_TRUE(table.contains(5));
    EXPECT_FALSE(table.contains(6));
    EXPECT_TRUE(table.contains(7));
    EXPECT_FALSE(table.contains(8));
  }

  EXPECT_EQ(Int::creation_counter, 3u);
  EXPECT_EQ(Int::destruction_counter, 3u);
}

TEST(HashTableBaseTest, CopyAssignment) {
  Int::ResetCounters();
  {
    TestHashTable table;
    table.insert(1);
    table.insert(5);
    table.insert(7);

    EXPECT_FALSE(table.empty());
    EXPECT_EQ(table.size(), 3u);

    TestHashTable table2;
    EXPECT_TRUE(table2.empty());
    table2 = table;
    EXPECT_FALSE(table2.empty());
    EXPECT_EQ(table2.size(), 3u);
    EXPECT_FALSE(table.empty());
    EXPECT_EQ(table.size(), 3u);

    EXPECT_EQ(Int::creation_counter, 6u);
    EXPECT_EQ(Int::destruction_counter, 0u);

    EXPECT_FALSE(table.contains(0));
    EXPECT_TRUE(table.contains(1));
    EXPECT_FALSE(table.contains(2));
    EXPECT_FALSE(table.contains(3));
    EXPECT_TRUE(table.contains(5));
    EXPECT_FALSE(table.contains(6));
    EXPECT_TRUE(table.contains(7));
    EXPECT_FALSE(table.contains(8));

    EXPECT_FALSE(table2.contains(0));
    EXPECT_TRUE(table2.contains(1));
    EXPECT_FALSE(table2.contains(2));
    EXPECT_FALSE(table2.contains(3));
    EXPECT_TRUE(table2.contains(5));
    EXPECT_FALSE(table2.contains(6));
    EXPECT_TRUE(table2.contains(7));
    EXPECT_FALSE(table2.contains(8));
  }

  EXPECT_EQ(Int::creation_counter, 6u);
  EXPECT_EQ(Int::destruction_counter, 6u);
}

TEST(HashTableBaseTest, MoveAssignment) {
  Int::ResetCounters();
  {
    TestHashTable table;
    table.insert(1);
    table.insert(5);
    table.insert(7);

    EXPECT_FALSE(table.empty());
    EXPECT_EQ(table.size(), 3u);

    TestHashTable table2;
    EXPECT_TRUE(table2.empty());
    table2 = std::move(table);
    EXPECT_FALSE(table2.empty());
    EXPECT_EQ(table2.size(), 3u);
    EXPECT_TRUE(table.empty());
    EXPECT_EQ(table.size(), 0u);

    EXPECT_EQ(Int::creation_counter, 3u);
    EXPECT_EQ(Int::destruction_counter, 0u);

    EXPECT_FALSE(table2.contains(0));
    EXPECT_TRUE(table2.contains(1));
    EXPECT_FALSE(table2.contains(2));
    EXPECT_FALSE(table2.contains(3));
    EXPECT_TRUE(table2.contains(5));
    EXPECT_FALSE(table2.contains(6));
    EXPECT_TRUE(table2.contains(7));
    EXPECT_FALSE(table2.contains(8));
  }

  EXPECT_EQ(Int::creation_counter, 3u);
  EXPECT_EQ(Int::destruction_counter, 3u);
}

TEST(HashTableBaseTest, Clear) {
  Int::ResetCounters();
  {
    TestHashTable table;
    table.insert(1);
    table.insert(5);
    table.insert(7);

    EXPECT_FALSE(table.empty());
    EXPECT_EQ(table.size(), 3u);

    table.clear();
    EXPECT_TRUE(table.empty());

    EXPECT_EQ(Int::creation_counter, 3u);
    EXPECT_EQ(Int::destruction_counter, 3u);
  }

  EXPECT_EQ(Int::creation_counter, 3u);
  EXPECT_EQ(Int::destruction_counter, 3u);
}

TEST(HashTableBaseTest, Iteration) {
  TestHashTable table;
  table.insert(1);
  table.insert(5);
  table.insert(7);

  EXPECT_FALSE(table.empty());
  EXPECT_EQ(table.size(), 3u);

  std::vector<int> values;

  for (const int& x : table)
    values.push_back(x);

  std::sort(values.begin(), values.end());
  EXPECT_EQ(values.size(), 3u);
  EXPECT_EQ(values[0], 1);
  EXPECT_EQ(values[1], 5);
  EXPECT_EQ(values[2], 7);
}

TEST(HashTableBaseTest, Erase) {
  TestHashTable table;
  // Use sequention hashes to test that tombstone_count_ grows the hash table.
  for (int i = 0; i < 16; ++i) {
    table.insert(i);
    table.erase(i);
    EXPECT_EQ(table.size(), 0u);
  }
}