/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 <string>

#include "Cache.h"
#include "common.h"
#include "Registry.h"
#include "UtfConvert.h"

#include "mozilla/Result.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/WinHeaderOnlyUtils.h"

class WDBACacheTest : public ::testing::Test {
 protected:
  std::wstring mCacheRegKey;

  void SetUp() override {
    // Create a unique registry key to put the cache in for each test.
    const ::testing::TestInfo* const testInfo =
        ::testing::UnitTest::GetInstance()->current_test_info();
    Utf8ToUtf16Result testCaseResult = Utf8ToUtf16(testInfo->test_case_name());
    ASSERT_TRUE(testCaseResult.isOk());
    mCacheRegKey = testCaseResult.unwrap();

    Utf8ToUtf16Result testNameResult = Utf8ToUtf16(testInfo->name());
    ASSERT_TRUE(testNameResult.isOk());
    mCacheRegKey += L'.';
    mCacheRegKey += testNameResult.unwrap();

    FilePathResult uuidResult = GenerateUUIDStr();
    ASSERT_TRUE(uuidResult.isOk());
    mCacheRegKey += L'.';
    mCacheRegKey += uuidResult.unwrap();
  }

  void TearDown() override {
    // It seems like the TearDown probably doesn't run if SetUp doesn't
    // succeed, but I can't find any documentation saying that. And we don't
    // want to accidentally clobber the entirety of AGENT_REGKEY_NAME.
    if (!mCacheRegKey.empty()) {
      std::wstring regKey = AGENT_REGKEY_NAME;
      regKey += L'\\';
      regKey += mCacheRegKey;
      RegDeleteTreeW(HKEY_CURRENT_USER, regKey.c_str());
    }
  }
};

TEST_F(WDBACacheTest, BasicFunctionality) {
  Cache cache(mCacheRegKey.c_str());
  VoidResult result = cache.Init();
  ASSERT_TRUE(result.isOk());

  // Test that the cache starts empty
  Cache::MaybeEntryResult entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  Cache::MaybeEntry entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isNothing());

  // Test that the cache stops accepting items when it is full.
  ASSERT_EQ(Cache::kDefaultCapacity, 2U);
  Cache::Entry toWrite = Cache::Entry{
      .notificationType = "string1",
      .notificationShown = "string2",
      .notificationAction = "string3",
      .prevNotificationAction = "string4",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isOk());
  toWrite = Cache::Entry{
      .notificationType = "string5",
      .notificationShown = "string6",
      .notificationAction = "string7",
      .prevNotificationAction = "string8",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isOk());
  toWrite = Cache::Entry{
      .notificationType = "string9",
      .notificationShown = "string10",
      .notificationAction = "string11",
      .prevNotificationAction = "string12",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isErr());

  // Read the two cache entries back out and test that they match the expected
  // values.
  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 2U);
  ASSERT_EQ(entry.value().notificationType, "string1");
  ASSERT_EQ(entry.value().notificationShown, "string2");
  ASSERT_EQ(entry.value().notificationAction, "string3");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 2U);
  ASSERT_EQ(entry.value().notificationType, "string5");
  ASSERT_EQ(entry.value().notificationShown, "string6");
  ASSERT_EQ(entry.value().notificationAction, "string7");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string8");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isNothing());
}

TEST_F(WDBACacheTest, Version1Migration) {
  // Set up 2 version 1 cache entries
  VoidResult result = RegistrySetValueString(
      IsPrefixed::Unprefixed, L"PingCacheNotificationType0", "string1");
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  L"PingCacheNotificationShown0", "string2");
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  L"PingCacheNotificationAction0", "string3");
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  L"PingCacheNotificationType1", "string4");
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  L"PingCacheNotificationShown1", "string5");
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  L"PingCacheNotificationAction1", "string6");
  ASSERT_TRUE(result.isOk());

  Cache cache(mCacheRegKey.c_str());
  result = cache.Init();
  ASSERT_TRUE(result.isOk());

  Cache::MaybeEntryResult entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  Cache::MaybeEntry entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 1U);
  ASSERT_EQ(entry.value().notificationType, "string1");
  ASSERT_EQ(entry.value().notificationShown, "string2");
  ASSERT_EQ(entry.value().notificationAction, "string3");
  ASSERT_TRUE(entry.value().prevNotificationAction.isNothing());

  // Insert a new item to test coexistence of different versions
  Cache::Entry toWrite = Cache::Entry{
      .notificationType = "string7",
      .notificationShown = "string8",
      .notificationAction = "string9",
      .prevNotificationAction = "string10",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isOk());

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 1U);
  ASSERT_EQ(entry.value().notificationType, "string4");
  ASSERT_EQ(entry.value().notificationShown, "string5");
  ASSERT_EQ(entry.value().notificationAction, "string6");
  ASSERT_TRUE(entry.value().prevNotificationAction.isNothing());

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 2U);
  ASSERT_EQ(entry.value().notificationType, "string7");
  ASSERT_EQ(entry.value().notificationShown, "string8");
  ASSERT_EQ(entry.value().notificationAction, "string9");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string10");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isNothing());
}

TEST_F(WDBACacheTest, ForwardsCompatibility) {
  // Set up a cache that might have been made by a future version with a larger
  // capacity set and more keys per entry.
  std::wstring settingsKey = mCacheRegKey + L"\\version2";
  VoidResult result = RegistrySetValueDword(
      IsPrefixed::Unprefixed, Cache::kCapacityRegName, 8, settingsKey.c_str());
  ASSERT_TRUE(result.isOk());
  // We're going to insert the future version's entry at index 6 so there's
  // space for 1 more before we loop back to index 0. Then we are going to
  // enqueue 2 new values to test that this works properly.
  result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName,
                                 6, settingsKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName, 1,
                                 settingsKey.c_str());
  ASSERT_TRUE(result.isOk());

  // Insert an entry as if it was inserted by a future version
  std::wstring entryRegKey = settingsKey + L"\\6";
  result =
      RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey,
                            9999, entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  Cache::kNotificationTypeKey, "string1",
                                  entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  Cache::kNotificationShownKey, "string2",
                                  entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  Cache::kNotificationActionKey, "string3",
                                  entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed,
                                  Cache::kPrevNotificationActionKey, "string4",
                                  entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());
  result = RegistrySetValueString(IsPrefixed::Unprefixed, L"UnknownFutureKey",
                                  "string5", entryRegKey.c_str());
  ASSERT_TRUE(result.isOk());

  Cache cache(mCacheRegKey.c_str());
  result = cache.Init();
  ASSERT_TRUE(result.isOk());

  // Insert 2 new items to test that these features work with a different
  // capacity.
  Cache::Entry toWrite = Cache::Entry{
      .notificationType = "string6",
      .notificationShown = "string7",
      .notificationAction = "string8",
      .prevNotificationAction = "string9",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isOk());
  toWrite = Cache::Entry{
      .notificationType = "string10",
      .notificationShown = "string11",
      .notificationAction = "string12",
      .prevNotificationAction = "string13",
  };
  result = cache.Enqueue(toWrite);
  ASSERT_TRUE(result.isOk());

  // Read cache and verify the output
  Cache::MaybeEntryResult entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  Cache::MaybeEntry entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 9999U);
  ASSERT_EQ(entry.value().notificationType, "string1");
  ASSERT_EQ(entry.value().notificationShown, "string2");
  ASSERT_EQ(entry.value().notificationAction, "string3");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 2U);
  ASSERT_EQ(entry.value().notificationType, "string6");
  ASSERT_EQ(entry.value().notificationShown, "string7");
  ASSERT_EQ(entry.value().notificationAction, "string8");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string9");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isSome());
  ASSERT_EQ(entry.value().entryVersion, 2U);
  ASSERT_EQ(entry.value().notificationType, "string10");
  ASSERT_EQ(entry.value().notificationShown, "string11");
  ASSERT_EQ(entry.value().notificationAction, "string12");
  ASSERT_TRUE(entry.value().prevNotificationAction.isSome());
  ASSERT_EQ(entry.value().prevNotificationAction.value(), "string13");

  entryResult = cache.Dequeue();
  ASSERT_TRUE(entryResult.isOk());
  entry = entryResult.unwrap();
  ASSERT_TRUE(entry.isNothing());
}
