// Copyright 2022, DragonflyDB authors.  All rights reserved.
// See LICENSE for licensing terms.
//

#include "core/top_keys.h"

#include <absl/strings/str_cat.h>
#include <gmock/gmock.h>

#include "base/gtest.h"
#include "base/logging.h"

using ::testing::Pair;
using ::testing::UnorderedElementsAre;

namespace dfly {

TEST(TopKeysTest, Basic) {
  TopKeys top_keys({.min_key_count_to_record = 2});
  top_keys.Touch("key1");
  top_keys.Touch("key1");
  top_keys.Touch("key2");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 2)));
}

TEST(TopKeysTest, MultiTouch) {
  TopKeys top_keys({.min_key_count_to_record = 2});
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre());
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 2)));
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 3)));
}

TEST(TopKeysTest, MinKeyCountToRecord) {
  TopKeys top_keys({.min_key_count_to_record = 3});
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre());
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre());
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 3)));
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 4)));
  top_keys.Touch("key1");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 5)));
}

TEST(TopKeysTest, MultiKeys) {
  TopKeys top_keys({.min_key_count_to_record = 2});
  for (int i = 0; i < 2; ++i) {
    top_keys.Touch("key1");
    top_keys.Touch("key2");
  }
  top_keys.Touch("key3");
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 2), Pair("key2", 2)));
}

TEST(TopKeysTest, BucketCollision) {
  TopKeys top_keys({.buckets = 1, .min_key_count_to_record = 1});
  for (int i = 0; i < 5; ++i) {
    top_keys.Touch("key1");
  }
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 5)));

  for (int i = 0; i < 100; ++i) {
    top_keys.Touch("key2");
  }

  auto top_keys_table = top_keys.GetTopKeys();
  EXPECT_EQ(top_keys_table.size(), 1);
  EXPECT_LE(top_keys_table["key2"], 100);
  EXPECT_GE(top_keys_table["key2"], 50);

  // Touching "key1" should *not* replace "key2".
  top_keys.Touch("key1");
  EXPECT_FALSE(top_keys.GetTopKeys().contains("key1"));
}

TEST(TopKeysTest, BucketCollisionAggressiveDecay) {
  TopKeys top_keys({.buckets = 1, .min_key_count_to_record = 2, .decay_base = 1.0});
  for (int i = 0; i < 5; ++i) {
    top_keys.Touch("key1");
  }
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 5)));

  for (int i = 0; i < 100; ++i) {
    top_keys.Touch("key2");
  }
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key2", 96)));
}

TEST(TopKeysTest, BucketCollisionHesitantDecay) {
  TopKeys top_keys({.buckets = 1, .min_key_count_to_record = 2, .decay_base = 1000.0});
  for (int i = 0; i < 5; ++i) {
    top_keys.Touch("key1");
  }
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 5)));

  for (int i = 0; i < 100; ++i) {
    top_keys.Touch("key2");
  }
  // "key2" will never replace "key1", as the decay practically never happens (1000^-5)
  EXPECT_THAT(top_keys.GetTopKeys(), UnorderedElementsAre(Pair("key1", 5)));
}

TEST(TopKeysTest, SavedByMultipleArrays) {
  // This test is not trivial. It tests that having multiple arrays inside TopKeys saves keys in
  // case of collision. The way it does it is by inserting an arbitrary key (= "key"), and then (at
  // runtime) finding another key which *does* collide with that key.
  //
  // Once we've found such a key, we create another TopKeys instance, but this time with 10 arrays
  // which should mean that for some hash value, the keys won't be present in the same bucket.

  std::string collision_key;

  TopKeys::Options options(
      {.buckets = 2, .depth = 1, .min_key_count_to_record = 2, .decay_base = 1});
  {
    TopKeys top_keys(options);

    // Insert some key
    top_keys.Touch("key");
    top_keys.Touch("key");

    // Find a key with a collision
    int i = 0;
    while (true) {
      collision_key = absl::StrCat("key", i);
      top_keys.Touch(collision_key);
      if (!top_keys.GetTopKeys().contains(collision_key)) {
        break;
      }
      ++i;
    }
  }

  options.depth = 10;
  {
    TopKeys top_keys(options);

    // Insert some key
    top_keys.Touch("key");
    top_keys.Touch("key");

    // Insert collision key, expect result to be present
    top_keys.Touch(collision_key);
    top_keys.Touch(collision_key);
    EXPECT_THAT(top_keys.GetTopKeys(),
                UnorderedElementsAre(Pair("key", 2), Pair(collision_key, 2)));
  }
}

}  // end of namespace dfly
