/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"

#include "mozilla/SmallArrayLRUCache.h"

#include <algorithm>
#include <cstring>
#include <utility>

using Key = unsigned;

struct Value {
  Value() : m(unsigned(-1)) {}
  explicit Value(unsigned a) : m(a) {}

  bool operator==(const Value& aOther) const { return m == aOther.m; }
  bool operator!=(const Value& aOther) const { return m != aOther.m; }

  unsigned m;
};

constexpr static unsigned CacheSize = 8;

using TestCache = mozilla::SmallArrayLRUCache<Key, Value, CacheSize>;

// This struct embeds a given object type between two "guard" objects, to check
// if anything is written out of bounds.
template <typename T>
struct Boxed {
  constexpr static size_t GuardSize = std::max(sizeof(T), size_t(256));

  // A Guard is a character array with a pre-set content that can be checked for
  // unwanted changes.
  struct Guard {
    char mGuard[GuardSize];
    explicit Guard(char aValue) { memset(&mGuard, aValue, GuardSize); }
    void Check(char aValue) {
      for (const char& c : mGuard) {
        ASSERT_EQ(c, aValue);
      }
    }
  };

  Guard mGuardBefore;
  T mObject;
  Guard mGuardAfter;

  template <typename... Ts>
  explicit Boxed(Ts&&... aTs)
      : mGuardBefore(0x5a),
        mObject(std::forward<Ts>(aTs)...),
        mGuardAfter(0xa5) {
    Check();
  }

  ~Boxed() { Check(); }

  T& Object() { return mObject; }
  const T& Object() const { return mObject; }

  void Check() {
    mGuardBefore.Check(0x5a);
    mGuardAfter.Check(0xa5);
  }
};

TEST(SmallArrayLRUCache, FetchOrAdd_KeysFitInCache)
{
  // We're going to add-or-fetch between 1 and CacheSize keys, so they all fit
  // in the cache.
  for (Key keys = 1; keys <= CacheSize; ++keys) {
    Boxed<TestCache> boxedCache;
    TestCache& cache = boxedCache.Object();
    for (Key i = 0; i < keys; ++i) {
      bool valueFunctionCalled = false;
      Value v = cache.FetchOrAdd(i, [&]() {
        valueFunctionCalled = true;
        return Value{i};
      });
      ASSERT_EQ(v, Value{i});
      ASSERT_TRUE(valueFunctionCalled);
      boxedCache.Check();
    }

    // Fetching any key should never call the value function.
    for (Key i = 0; i < CacheSize * 3; ++i) {
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
      // Fetching the same key again will never call the function value.
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
    }
  }
}

TEST(SmallArrayLRUCache, Add_FetchOrAdd_KeysFitInCache)
{
  // We're going to add between 1 and CacheSize keys, so they all fit in the
  // cache.
  for (Key keys = 1; keys <= CacheSize; ++keys) {
    Boxed<TestCache> boxedCache;
    TestCache& cache = boxedCache.Object();
    for (Key i = 0; i < keys; ++i) {
      cache.Add(i, Value{i});
      boxedCache.Check();
    }

    // Fetching any key should never call the value function.
    for (Key i = 0; i < CacheSize * 3; ++i) {
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
      // Fetching the same key again will never call the function value.
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
    }
  }
}

TEST(SmallArrayLRUCache, FetchOrAdd_KeysDoNotFitInCache)
{
  // We're going to add-or-fetch strictly more than CacheSize keys, so they
  // cannot fit in the cache, only the last `CacheSize` ones are kept.
  for (Key keys = CacheSize + 1; keys <= CacheSize * 2; ++keys) {
    Boxed<TestCache> boxedCache;
    TestCache& cache = boxedCache.Object();
    for (Key i = 0; i < keys; ++i) {
      bool valueFunctionCalled = false;
      Value v = cache.FetchOrAdd(i, [&]() {
        valueFunctionCalled = true;
        return Value{i};
      });
      ASSERT_EQ(v, Value{i});
      ASSERT_TRUE(valueFunctionCalled);
      boxedCache.Check();
    }

    // Fetching keys from 0 should always call the function value:
    // - 0 is the oldest key, it must have been pushed out when `CacheSize`
    //   was added.
    // - Once we've fetched 0, it's pushed out the old (smallest) key.
    // Etc.
    for (Key i = 0; i < CacheSize * 3; ++i) {
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_TRUE(valueFunctionCalled);
        boxedCache.Check();
      }
      // Fetching the same key again will never call the function value.
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
    }
  }
}

TEST(SmallArrayLRUCache, Add_FetchOrAdd_KeysDoNotFitInCache)
{
  // We're going to add strictly more than CacheSize keys, so they cannot fit in
  // the cache, only the last `CacheSize` ones are kept.
  for (Key keys = CacheSize + 1; keys <= CacheSize * 2; ++keys) {
    Boxed<TestCache> boxedCache;
    TestCache& cache = boxedCache.Object();
    for (Key i = 0; i < keys; ++i) {
      cache.Add(i, Value{i});
      boxedCache.Check();
    }

    // Fetching keys from 0 should always call the function value:
    // - 0 is the oldest key, it must have been pushed out when `CacheSize`
    //   was added.
    // - Once we've fetched 0, it's pushed out the old (smallest) key.
    // Etc.
    for (Key i = 0; i < CacheSize * 3; ++i) {
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_TRUE(valueFunctionCalled);
        boxedCache.Check();
      }
      // Fetching the same key again will never call the function value.
      {
        bool valueFunctionCalled = false;
        Value v = cache.FetchOrAdd(i % keys, [&]() {
          valueFunctionCalled = true;
          return Value{i % keys};
        });
        ASSERT_EQ(v, Value{i % keys});
        ASSERT_FALSE(valueFunctionCalled);
        boxedCache.Check();
      }
    }
  }
}

TEST(SmallArrayLRUCache, Clear)
{
  Boxed<TestCache> boxedCache;
  TestCache& cache = boxedCache.Object();

  // First fetch will always call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }

  // Second fetch will never call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_FALSE(valueFunctionCalled);
    boxedCache.Check();
  }

  cache.Clear();

  // After Clear(), first fetch will always call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }

  // Next fetch will never call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_FALSE(valueFunctionCalled);
    boxedCache.Check();
  }
}

TEST(SmallArrayLRUCache, Shutdown)
{
  Boxed<TestCache> boxedCache;
  TestCache& cache = boxedCache.Object();

  // First fetch will always call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }

  // Second fetch will never call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_FALSE(valueFunctionCalled);
    boxedCache.Check();
  }

  cache.Shutdown();

  // After Shutdown(), any fetch will always call the function value.
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }
  cache.Add(42, Value{4242});
  boxedCache.Check();
  {
    bool valueFunctionCalled = false;
    Value v = cache.FetchOrAdd(42, [&]() {
      valueFunctionCalled = true;
      return Value{4242};
    });
    ASSERT_EQ(v, Value{4242});
    ASSERT_TRUE(valueFunctionCalled);
    boxedCache.Check();
  }
}
