/* 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 "Common.h"

#define EXPIRED_TIME_SEC     (PR_Now() / PR_USEC_PER_SEC - 3600)
#define NOTEXPIRED_TIME_SEC  (PR_Now() / PR_USEC_PER_SEC + 3600)

static void
SetupCacheEntry(LookupCacheV2* aLookupCache,
                const nsCString& aCompletion,
                bool aNegExpired = false,
                bool aPosExpired = false)
{
  AddCompleteArray completes;
  AddCompleteArray emptyCompletes;
  MissPrefixArray misses;
  MissPrefixArray emptyMisses;

  AddComplete* add = completes.AppendElement(fallible);
  add->complete.FromPlaintext(aCompletion);

  Prefix* prefix = misses.AppendElement(fallible);
  prefix->FromPlaintext(aCompletion);

  // Setup positive cache first otherwise negative cache expiry will be
  // overwritten.
  int64_t posExpirySec = aPosExpired ? EXPIRED_TIME_SEC : NOTEXPIRED_TIME_SEC;
  aLookupCache->AddGethashResultToCache(completes, emptyMisses, posExpirySec);

  int64_t negExpirySec = aNegExpired ? EXPIRED_TIME_SEC : NOTEXPIRED_TIME_SEC;
  aLookupCache->AddGethashResultToCache(emptyCompletes, misses, negExpirySec);
}

static void
SetupCacheEntry(LookupCacheV4* aLookupCache,
                const nsCString& aCompletion,
                bool aNegExpired = false,
                bool aPosExpired = false)
{
  FullHashResponseMap map;

  Prefix prefix;
  prefix.FromPlaintext(aCompletion);

  CachedFullHashResponse* response = map.LookupOrAdd(prefix.ToUint32());

  response->negativeCacheExpirySec =
    aNegExpired ? EXPIRED_TIME_SEC : NOTEXPIRED_TIME_SEC;
  response->fullHashes.Put(GeneratePrefix(aCompletion, COMPLETE_SIZE),
                           aPosExpired ? EXPIRED_TIME_SEC : NOTEXPIRED_TIME_SEC);

  aLookupCache->AddFullHashResponseToCache(map);
}

template<typename T>
void
TestCache(const Completion aCompletion,
          bool aExpectedHas,
          bool aExpectedConfirmed,
          bool aExpectedInCache,
          T* aCache = nullptr)
{
  bool has, inCache, confirmed;
  uint32_t matchLength;

  if (aCache) {
    aCache->Has(aCompletion, &has, &matchLength, &confirmed);
    inCache = aCache->IsInCache(aCompletion.ToUint32());
  } else {
    _PrefixArray array = { GeneratePrefix(_Fragment("cache.notexpired.com/"), 10),
                           GeneratePrefix(_Fragment("cache.expired.com/"), 8),
                           GeneratePrefix(_Fragment("gound.com/"), 5),
                           GeneratePrefix(_Fragment("small.com/"), 4)
                         };

    UniquePtr<T> cache = SetupLookupCache<T>(array);

    // Create an expired entry and a non-expired entry
    SetupCacheEntry(cache.get(), _Fragment("cache.notexpired.com/"));
    SetupCacheEntry(cache.get(), _Fragment("cache.expired.com/"), true, true);

    cache->Has(aCompletion, &has, &matchLength, &confirmed);
    inCache = cache->IsInCache(aCompletion.ToUint32());
  }

  EXPECT_EQ(has, aExpectedHas);
  EXPECT_EQ(confirmed, aExpectedConfirmed);
  EXPECT_EQ(inCache, aExpectedInCache);
}

template<typename T>
void
TestCache(const _Fragment& aFragment,
          bool aExpectedHas,
          bool aExpectedConfirmed,
          bool aExpectedInCache,
          T* aCache = nullptr)
{
  Completion lookupHash;
  lookupHash.FromPlaintext(aFragment);

  TestCache<T>(lookupHash, aExpectedHas, aExpectedConfirmed, aExpectedInCache, aCache);
}

// This testcase check the returned result of |Has| API if fullhash cannot match
// any prefix in the local database.
TEST(UrlClassifierCaching, NotFound)
{
  TestCache<LookupCacheV2>(_Fragment("nomatch.com/"), false, false, false);
  TestCache<LookupCacheV4>(_Fragment("nomatch.com/"), false, false, false);
}

// This testcase check the returned result of |Has| API if fullhash find a match
// in the local database but not in the cache.
TEST(UrlClassifierCaching, NotInCache)
{
  TestCache<LookupCacheV2>(_Fragment("gound.com/"), true, false, false);
  TestCache<LookupCacheV4>(_Fragment("gound.com/"), true, false, false);
}

// This testcase check the returned result of |Has| API if fullhash matches
// a cache entry in positive cache.
TEST(UrlClassifierCaching, InPositiveCacheNotExpired)
{
  TestCache<LookupCacheV2>(_Fragment("cache.notexpired.com/"), true, true, true);
  TestCache<LookupCacheV4>(_Fragment("cache.notexpired.com/"), true, true, true);
}

// This testcase check the returned result of |Has| API if fullhash matches
// a cache entry in positive cache but that it is expired.
TEST(UrlClassifierCaching, InPositiveCacheExpired)
{
  TestCache<LookupCacheV2>(_Fragment("cache.expired.com/"), true, false, true);
  TestCache<LookupCacheV4>(_Fragment("cache.expired.com/"), true, false, true);
}

// This testcase check the returned result of |Has| API if fullhash matches
// a cache entry in negative cache.
TEST(UrlClassifierCaching, InNegativeCacheNotExpired)
{
  // Create a fullhash whose prefix matches the prefix in negative cache
  // but completion doesn't match any fullhash in positive cache.

  Completion prefix;
  prefix.FromPlaintext(_Fragment("cache.notexpired.com/"));

  Completion fullhash;
  fullhash.FromPlaintext(_Fragment("firefox.com/"));

  // Overwrite the 4-byte prefix of `fullhash` so that it conflicts with `prefix`.
  // Since "cache.notexpired.com" is added to database in TestCache as a
  // 10-byte prefix, we should copy more than 10 bytes to fullhash to ensure
  // it can match the prefix in database.
  memcpy(fullhash.buf, prefix.buf, 10);

  TestCache<LookupCacheV2>(fullhash, false, false, true);
  TestCache<LookupCacheV4>(fullhash, false, false, true);
}

// This testcase check the returned result of |Has| API if fullhash matches
// a cache entry in negative cache but that entry is expired.
TEST(UrlClassifierCaching, InNegativeCacheExpired)
{
  // Create a fullhash whose prefix is in the cache.

  Completion prefix;
  prefix.FromPlaintext(_Fragment("cache.expired.com/"));

  Completion fullhash;
  fullhash.FromPlaintext(_Fragment("firefox.com/"));

  memcpy(fullhash.buf, prefix.buf, 10);

  TestCache<LookupCacheV2>(fullhash, true, false, true);
  TestCache<LookupCacheV4>(fullhash, true, false, true);
}

#define CACHED_URL              _Fragment("cache.com/")
#define NEG_CACHE_EXPIRED_URL   _Fragment("cache.negExpired.com/")
#define POS_CACHE_EXPIRED_URL   _Fragment("cache.posExpired.com/")
#define BOTH_CACHE_EXPIRED_URL  _Fragment("cache.negAndposExpired.com/")

// This testcase create 4 cache entries.
// 1. unexpired entry.
// 2. an entry whose negative cache time is expired but whose positive cache
//    is not expired.
// 3. an entry whose positive cache time is expired
// 4. an entry whose negative cache time and positive cache time are expired
// After calling |InvalidateExpiredCacheEntry| API, entries with expired
// negative time should be removed from cache(2 & 4)
template<typename T>
void TestInvalidateExpiredCacheEntry()
{
  _PrefixArray array = { GeneratePrefix(CACHED_URL, 10),
                         GeneratePrefix(NEG_CACHE_EXPIRED_URL, 8),
                         GeneratePrefix(POS_CACHE_EXPIRED_URL, 5),
                         GeneratePrefix(BOTH_CACHE_EXPIRED_URL, 4)
                       };
  UniquePtr<T> cache = SetupLookupCache<T>(array);

  SetupCacheEntry(cache.get(), CACHED_URL, false, false);
  SetupCacheEntry(cache.get(), NEG_CACHE_EXPIRED_URL, true, false);
  SetupCacheEntry(cache.get(), POS_CACHE_EXPIRED_URL, false, true);
  SetupCacheEntry(cache.get(), BOTH_CACHE_EXPIRED_URL, true, true);

  // Before invalidate
  TestCache<T>(CACHED_URL, true, true, true, cache.get());
  TestCache<T>(NEG_CACHE_EXPIRED_URL, true, true, true, cache.get());
  TestCache<T>(POS_CACHE_EXPIRED_URL, true, false, true, cache.get());
  TestCache<T>(BOTH_CACHE_EXPIRED_URL, true, false, true, cache.get());

  // Call InvalidateExpiredCacheEntry to remove cache entries whose negative cache
  // time is expired
  cache->InvalidateExpiredCacheEntries();

  // After invalidate, NEG_CACHE_EXPIRED_URL & BOTH_CACHE_EXPIRED_URL should
  // not be found in cache.
  TestCache<T>(NEG_CACHE_EXPIRED_URL, true, false, false, cache.get());
  TestCache<T>(BOTH_CACHE_EXPIRED_URL, true, false, false, cache.get());

  // Other entries should remain the same result.
  TestCache<T>(CACHED_URL, true, true, true, cache.get());
  TestCache<T>(POS_CACHE_EXPIRED_URL, true, false, true, cache.get());
}

TEST(UrlClassifierCaching, InvalidateExpiredCacheEntryV2)
{
  TestInvalidateExpiredCacheEntry<LookupCacheV2>();
}

TEST(UrlClassifierCaching, InvalidateExpiredCacheEntryV4)
{
  TestInvalidateExpiredCacheEntry<LookupCacheV4>();
}

// This testcase check if an cache entry whose negative cache time is expired
// and it doesn't have any postive cache entries in it, it should be removed
// from cache after calling |Has|.
TEST(UrlClassifierCaching, NegativeCacheExpireV2)
{
  _PrefixArray array = { GeneratePrefix(NEG_CACHE_EXPIRED_URL, 8) };
  UniquePtr<LookupCacheV2> cache = SetupLookupCache<LookupCacheV2>(array);

  nsCOMPtr<nsICryptoHash> cryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID);

  MissPrefixArray misses;
  Prefix* prefix = misses.AppendElement(fallible);
  prefix->FromPlaintext(NEG_CACHE_EXPIRED_URL);

  AddCompleteArray dummy;
  cache->AddGethashResultToCache(dummy, misses, EXPIRED_TIME_SEC);

  // Ensure it is in cache in the first place.
  EXPECT_EQ(cache->IsInCache(prefix->ToUint32()), true);

  // It should be removed after calling Has API.
  TestCache<LookupCacheV2>(NEG_CACHE_EXPIRED_URL, true, false, false, cache.get());
}

TEST(UrlClassifierCaching, NegativeCacheExpireV4)
{
  _PrefixArray array = { GeneratePrefix(NEG_CACHE_EXPIRED_URL, 8) };
  UniquePtr<LookupCacheV4> cache = SetupLookupCache<LookupCacheV4>(array);

  FullHashResponseMap map;
  Prefix prefix;
  nsCOMPtr<nsICryptoHash> cryptoHash = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID);
  prefix.FromPlaintext(NEG_CACHE_EXPIRED_URL);
  CachedFullHashResponse* response = map.LookupOrAdd(prefix.ToUint32());

  response->negativeCacheExpirySec = EXPIRED_TIME_SEC;

  cache->AddFullHashResponseToCache(map);

  // Ensure it is in cache in the first place.
  EXPECT_EQ(cache->IsInCache(prefix.ToUint32()), true);

  // It should be removed after calling Has API.
  TestCache<LookupCacheV4>(NEG_CACHE_EXPIRED_URL, true, false, false, cache.get());
}
