/* 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 <objectarray.h>
#include <shobjidl.h>
#include <windows.h>
#include <string.h>
#include <propvarutil.h>
#include <propkey.h>

#ifdef __MINGW32__
// MinGW-w64 headers are missing PropVariantToString.
#  include <propsys.h>
PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
#endif

#include "gtest/gtest.h"
#include "gmock/gmock.h"

#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/ToJSValue.h"
#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "JumpListBuilder.h"

using namespace mozilla;
using namespace testing;
using mozilla::dom::AutoJSAPI;
using mozilla::dom::Promise;
using mozilla::dom::PromiseNativeHandler;
using mozilla::dom::ToJSValue;
using mozilla::dom::WindowsJumpListShortcutDescription;
using mozilla::widget::JumpListBackend;
using mozilla::widget::JumpListBuilder;

/**
 * GMock matcher that ensures that two LPCWSTRs match.
 */
MATCHER_P(LPCWSTREq, value, "The equivalent of StrEq for LPCWSTRs") {
  return (wcscmp(arg, value)) == 0;
}

/**
 * GMock matcher that ensures that a IObjectArray* contains nsIShellLinkW's
 * that match an equivalent set of nsTArray<WindowsJumpListShortcutDescriptions>
 */
MATCHER_P(ShellLinksEq, descs,
          "Comparing generated IShellLinkW with "
          "WindowsJumpListShortcutDescription definitions") {
  uint32_t count = 0;
  HRESULT hr = arg->GetCount(&count);
  if (FAILED(hr) || count != descs->Length()) {
    return false;
  }

  for (uint32_t i = 0; i < descs->Length(); ++i) {
    RefPtr<IShellLinkW> link;
    if (FAILED(arg->GetAt(i, IID_IShellLinkW,
                          static_cast<void**>(getter_AddRefs(link))))) {
      return false;
    }

    if (!link) {
      return false;
    }

    const WindowsJumpListShortcutDescription& desc = descs->ElementAt(i);

    // We'll now compare each member of the WindowsJumpListShortcutDescription
    // with what is stored in the IShellLink.

    // WindowsJumpListShortcutDescription.title
    IPropertyStore* propStore = nullptr;
    hr = link->QueryInterface(IID_IPropertyStore, (LPVOID*)&propStore);
    if (FAILED(hr)) {
      return false;
    }

    PROPVARIANT pv;
    hr = propStore->GetValue(PKEY_Title, &pv);
    if (FAILED(hr)) {
      return false;
    }

    wchar_t title[PKEYSTR_MAX];
    hr = PropVariantToString(pv, title, PKEYSTR_MAX);
    if (FAILED(hr)) {
      return false;
    }

    if (!desc.mTitle.Equals(title)) {
      return false;
    }

    // WindowsJumpListShortcutDescription.path
    wchar_t pathBuf[MAX_PATH];
    hr = link->GetPath(pathBuf, MAX_PATH, nullptr, SLGP_SHORTPATH);
    if (FAILED(hr)) {
      return false;
    }

    if (!desc.mPath.Equals(pathBuf)) {
      return false;
    }

    // WindowsJumpListShortcutDescription.arguments (optional)
    wchar_t argsBuf[MAX_PATH];
    hr = link->GetArguments(argsBuf, MAX_PATH);
    if (FAILED(hr)) {
      return false;
    }

    if (desc.mArguments.WasPassed()) {
      if (!desc.mArguments.Value().Equals(argsBuf)) {
        return false;
      }
    } else {
      // Otherwise, the arguments should be empty.
      if (wcsnlen(argsBuf, MAX_PATH) != 0) {
        return false;
      }
    }

    // WindowsJumpListShortcutDescription.description
    wchar_t descBuf[MAX_PATH];
    hr = link->GetDescription(descBuf, MAX_PATH);
    if (FAILED(hr)) {
      return false;
    }

    if (!desc.mDescription.Equals(descBuf)) {
      return false;
    }

    // WindowsJumpListShortcutDescription.iconPath and
    // WindowsJumpListShortcutDescription.fallbackIconIndex
    int iconIdx = 0;
    wchar_t iconPathBuf[MAX_PATH];
    hr = link->GetIconLocation(iconPathBuf, MAX_PATH, &iconIdx);
    if (FAILED(hr)) {
      return false;
    }

    if (desc.mIconPath.WasPassed() && !desc.mIconPath.Value().IsEmpty()) {
      // If the WindowsJumpListShortcutDescription supplied an iconPath,
      // then it should match iconPathBuf and have an icon index of 0.
      if (!desc.mIconPath.Value().Equals(iconPathBuf) || iconIdx != 0) {
        return false;
      }
    } else {
      // Otherwise, the iconPathBuf should equal the
      // WindowsJumpListShortcutDescription path, and the iconIdx should match
      // the fallbackIconIndex.
      if (!desc.mPath.Equals(iconPathBuf) ||
          desc.mFallbackIconIndex != iconIdx) {
        return false;
      }
    }
  }

  return true;
}

/**
 * This is a helper class that allows our tests to wait for a native DOM Promise
 * to resolve, and get the JS::Value that the Promise resolves with. This is
 * expected to run on the main thread.
 */
class WaitForResolver : public PromiseNativeHandler {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaitForResolver, override)

  NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override {
    nsresult rv = NS_ERROR_UNEXPECTED;
    NS_INTERFACE_TABLE0(WaitForResolver)

    return rv;
  }

  WaitForResolver() : mIsDone(false) {}

  void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aError) override {
    mResult = aValue;
    mIsDone = true;
  }

  void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aError) override {
    ASSERT_TRUE(false);  // Should never reach here.
  }

  /**
   * Spins a nested event loop and blocks until the Promise has resolved.
   */
  void SpinUntilResolved() {
    SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns,
                       [&]() { return mIsDone; });
  }

  /**
   * Spins a nested event loop and blocks until the Promise has resolved,
   * after which the JS::Value that the Promise resolves with is returned via
   * the aRetval outparam.
   *
   * @param {JS::MutableHandle<JS::Value>} aRetval
   *   The outparam for the JS::Value that the Promise resolves with.
   */
  void SpinUntilResolvedWithResult(JS::MutableHandle<JS::Value> aRetval) {
    SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns,
                       [&]() { return mIsDone; });
    aRetval.set(mResult);
  }

 private:
  virtual ~WaitForResolver() = default;

  JS::Heap<JS::Value> mResult;
  bool mIsDone;
};

/**
 * An implementation of JumpListBackend that is instrumented using the GMock
 * framework to record calls. Unlike the NativeJumpListBackend, this backend
 * is expected to be instantiated on the main thread and passed as an argument
 * to the JumpListBuilder's worker thread. Testers should wait for the methods
 * that call these functions to resolve their Promises before checking the
 * recorded values.
 */
class TestingJumpListBackend : public JumpListBackend {
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JumpListBackend, override)

  TestingJumpListBackend() : mMonitor("TestingJumpListBackend::mMonitor") {}

  virtual bool IsAvailable() override { return true; }

  MOCK_METHOD(HRESULT, SetAppID, (LPCWSTR));
  MOCK_METHOD(HRESULT, BeginList, (UINT*, REFIID, void**));
  MOCK_METHOD(HRESULT, AddUserTasks, (IObjectArray*));
  MOCK_METHOD(HRESULT, AppendCategory, (LPCWSTR, IObjectArray*));
  MOCK_METHOD(HRESULT, CommitList, ());
  MOCK_METHOD(HRESULT, AbortList, ());
  MOCK_METHOD(HRESULT, DeleteList, (LPCWSTR));

  virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override {
    return 0;
  }

  // In one case (construction), an operation occurs off of the main thread that
  // we must wait for without an associated Promise.
  Monitor& GetMonitor() { return mMonitor; }

 protected:
  virtual ~TestingJumpListBackend() override {};

 private:
  Monitor mMonitor;
};

/**
 * A helper function that creates some fake WindowsJumpListShortcutDescription
 * objects as well as JS::Value representations of those objects. These are
 * returned to the caller through outparams.
 *
 * @param {JSContext*} aCx
 *   The current JSContext in the execution environment.
 * @param {uint32_t} howMany
 *   The number of WindowsJumpListShortcutDescriptions to generate.
 * @param {boolean} longDescription
 *   True if the description should be greater than MAX_PATH (260 characters).
 * @param {nsTArray<WindowsJumpListShortcutDescription>&} aArray
 *   The outparam for the array of generated
 * WindowsJumpListShortcutDescriptions.
 * @param {JS::Handle<JSObject*>} aJSArrayObj
 *   The outparam for the array of JS::Value's representing the generated
 *   WindowsJumpListShortcutDescriptions.
 */
void GenerateWindowsJumpListShortcutDescriptions(
    JSContext* aCx, uint32_t howMany, bool longDescription,
    nsTArray<WindowsJumpListShortcutDescription>& aArray,
    JS::Handle<JSObject*> aJSArrayObj) {
  for (uint32_t i = 0; i < howMany; ++i) {
    WindowsJumpListShortcutDescription desc;
    nsAutoString title(u"Test Task #");
    title.AppendInt(i);
    desc.mTitle = title;

    nsAutoString path(u"C:\\Some\\Test\\Path.exe");
    desc.mPath = path;
    nsAutoString description;

    if (longDescription) {
      description.AppendPrintf(
          "For item #%i, this is a very very very very VERY VERY very very "
          "very very very very very very very very very very VERY VERY very "
          "very very very very very very very very very very very VERY VERY "
          "very very very very very very very very very very very very VERY "
          "VERY very very very very very very very very very very very very "
          "VERY VERY very very very very very very very very very very very "
          "very VERY VERY very very very very very very very very long test "
          "description for an item",
          i);
    } else {
      description.AppendPrintf("This is a test description for an item #%i", i);
    }

    desc.mDescription = description;
    desc.mFallbackIconIndex = 0;

    if (!(i % 2)) {
      nsAutoString arguments(u"-arg1 -arg2 -arg3");
      desc.mArguments.Construct(arguments);
      nsAutoString iconPath(u"C:\\Some\\icon.png");
      desc.mIconPath.Construct(iconPath);
    }

    aArray.AppendElement(desc);
    JS::Rooted<JS::Value> descJSValue(aCx);
    ASSERT_TRUE(ToJSValue(aCx, desc, &descJSValue));

    MOZ_ALWAYS_TRUE(JS_SetElement(aCx, aJSArrayObj, i, descJSValue));
  }
}

/**
 * Tests construction and that the application ID is properly passed to the
 * backend.
 */
TEST(JumpListBuilder, Construction)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  ASSERT_TRUE(testBackend);

  nsAutoString aumid(u"TestApplicationID");
  LPCWSTR passedID = aumid.get();
  // Construction of our class (or any class of that matter) does not return a
  // Promise that we can wait on to ensure that the background thread got the
  // right information. We therefore use a monitor on the testing backend as
  // well as an EXPECT_CALL to block execution of the test until the background
  // work has completed.
  Monitor& mon = testBackend->GetMonitor();
  MonitorAutoLock lock(mon);
  EXPECT_CALL(*testBackend, SetAppID(LPCWSTREq(passedID))).WillOnce([&mon] {
    MonitorAutoLock lock(mon);
    mon.Notify();
    return S_OK;
  });

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  // This is the amount of time that we will wait for the background thread to
  // respond before considering it a timeout failure.
  const int kWaitTimeoutMs = 5000;

  ASSERT_TRUE(mon.Wait(TimeDuration::FromMilliseconds(kWaitTimeoutMs)) !=
              CVStatus::Timeout);
}

/**
 * Tests calling CheckForRemovals and receiving a series of removed jump list
 * entries. Calling CheckForRemovals should call the following methods on the
 * backend, in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AbortList
 */
TEST(JumpListBuilder, CheckForRemovals)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  EXPECT_CALL(*testBackend, AbortList()).Times(2);

  // Let's prepare BeginList to return a two entry collection of IShellLinks.
  // The first IShellLink will have the URL be https://example.com, the second
  // will have the URL be https://mozilla.org.
  EXPECT_CALL(*testBackend, BeginList)
      .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) {
        RefPtr<IObjectCollection> collection;
        HRESULT hr = CoCreateInstance(
            CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER,
            IID_IObjectCollection, getter_AddRefs(collection));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        RefPtr<IShellLinkW> link;
        hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
                              IID_IShellLinkW, getter_AddRefs(link));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        nsAutoString firstLinkHref(u"https://example.com"_ns);
        link->SetArguments(firstLinkHref.get());

        nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns);
        link->SetIconLocation(appPath.get(), 0);

        collection->AddObject(link);

        // Let's re-use the same IShellLink, but change the URL to add our
        // second entry. The values of the IShellLink are ultimately copied
        // over to the items being added to the collection.
        nsAutoString secondLinkHref(u"https://mozilla.org"_ns);
        link->SetArguments(secondLinkHref.get());
        collection->AddObject(link);

        RefPtr<IObjectArray> pArray;
        hr = collection->QueryInterface(IID_IObjectArray,
                                        getter_AddRefs(pArray));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        *ppv = static_cast<IObjectArray*>(pArray);
        (static_cast<IUnknown*>(*ppv))->AddRef();

        // This is the default value to return, according to
        // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
        *pcMinSlots = 10;

        return S_OK;
      });

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;
  nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolvedWithResult(&result);

  ASSERT_TRUE(result.isObject());
  JS::Rooted<JSObject*> obj(cx, result.toObjectOrNull());

  bool isArray;
  ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray));
  ASSERT_TRUE(isArray);

  // We should expect to see 2 URL strings returned in the array.
  uint32_t length = 0;
  ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length));
  ASSERT_EQ(length, 2U);

  // The first one should be https://example.com
  JS::Rooted<JS::Value> firstURLValue(cx);
  ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue));
  JS::Rooted<JSString*> firstURLJSString(cx, firstURLValue.toString());
  nsAutoJSString firstURLAutoString;
  ASSERT_TRUE(firstURLAutoString.init(cx, firstURLJSString));

  ASSERT_TRUE(firstURLAutoString.EqualsLiteral("https://example.com"));

  // The second one should be https://mozilla.org
  JS::Rooted<JS::Value> secondURLValue(cx);
  ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue));
  JS::Rooted<JSString*> secondURLJSString(cx, secondURLValue.toString());
  nsAutoJSString secondURLAutoString;
  ASSERT_TRUE(secondURLAutoString.init(cx, secondURLJSString));

  ASSERT_TRUE(secondURLAutoString.EqualsLiteral("https://mozilla.org"));
}

/**
 * Tests calling CheckForRemovals and receiving a jump list entry with a very
 * long URL doesn't result in JumpListBuilder truncating the URL before handing
 * it back to the caller. Expects the following calls in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AbortList
 */
TEST(JumpListBuilder, CheckForRemovalsLongURL)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  EXPECT_CALL(*testBackend, AbortList()).Times(2);

  constexpr static const nsLiteralString veryLongHref(
      u"https://example.verylongurl.test/first/second/third/fourth/fifth/"
      "sixth/seventh/eighth/ninth/tenth/eleventh/twelfth/thirteenth/"
      "fourteenth/fifteenth-path-item/some/more/junk/after/that/more/more/"
      "more/more/more/more/more/more/more/more/more/more/more/more/more/more/"
      "more/more/more/more/more/more/more/more/more/more/more"_ns);
  // This test ensures that URLs longer than MAX_PATH do not get truncated by
  // JumpListBuilder or one of its utilities, so we must ensure that the static
  // URL we just defined is actually longer than MAX_PATH.
  static_assert(veryLongHref.Length() > MAX_PATH);

  // Let's prepare BeginList to return a one entry collection of IShellLinks.
  // The IShellLink will have the URL be very long - over MAX_PATH characters.
  EXPECT_CALL(*testBackend, BeginList)
      .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) {
        RefPtr<IObjectCollection> collection;
        HRESULT hr = CoCreateInstance(
            CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER,
            IID_IObjectCollection, getter_AddRefs(collection));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        RefPtr<IShellLinkW> link;
        hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
                              IID_IShellLinkW, getter_AddRefs(link));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        link->SetArguments(veryLongHref.get());

        nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns);
        link->SetIconLocation(appPath.get(), 0);

        collection->AddObject(link);

        RefPtr<IObjectArray> pArray;
        hr = collection->QueryInterface(IID_IObjectArray,
                                        getter_AddRefs(pArray));
        MOZ_RELEASE_ASSERT(SUCCEEDED(hr));

        *ppv = static_cast<IObjectArray*>(pArray);
        (static_cast<IUnknown*>(*ppv))->AddRef();

        // This is the default value to return, according to
        // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
        *pcMinSlots = 10;

        return S_OK;
      });

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;
  nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolvedWithResult(&result);

  ASSERT_TRUE(result.isObject());
  JS::Rooted<JSObject*> obj(cx, result.toObjectOrNull());

  bool isArray;
  ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray));
  ASSERT_TRUE(isArray);

  // We should expect to see 1 URL string returned in the array.
  uint32_t length = 0;
  ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length));
  ASSERT_EQ(length, 1U);

  // The URL should match veryLongHref
  JS::Rooted<JS::Value> returnedURLValue(cx);
  ASSERT_TRUE(JS_GetElement(cx, obj, 0, &returnedURLValue));
  JS::Rooted<JSString*> returnedURLValueJSString(cx,
                                                 returnedURLValue.toString());
  nsAutoJSString returnedURLValueAutoString;
  ASSERT_TRUE(returnedURLValueAutoString.init(cx, returnedURLValueJSString));

  ASSERT_TRUE(returnedURLValueAutoString.Equals(veryLongHref));
}

/**
 * Tests calling PopulateJumpList with empty arguments, which should call the
 * following methods on the backend, in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - CommitList
 *
 * This should result in an empty jump list for the user.
 */
TEST(JumpListBuilder, PopulateJumpListEmpty)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx, JS::ObjectValue(*taskDescsObj));

  nsAutoString customTitle(u"");

  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Tests calling PopulateJumpList with only tasks, and no custom items.
 * This should call the following methods on the backend, in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AddUserTasks
 * - CommitList
 *
 * This should result in a jump list with just tasks shown to the user, and
 * no custom section.
 */
TEST(JumpListBuilder, PopulateJumpListOnlyTasks)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx, JS::ObjectValue(*taskDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> taskDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
                                              taskDescsObj);

  nsAutoString customTitle(u"");
  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);

  EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0);
  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Tests calling PopulateJumpList with only custom items, and no tasks.
 * This should call the following methods on the backend, in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AppendCategory
 * - CommitList
 *
 * This should result in a jump list with just custom items shown to the user,
 * and no tasks.
 */
TEST(JumpListBuilder, PopulateJumpListOnlyCustomItems)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx);
  taskDescsJSVal.setObject(*taskDescsObj);

  nsAutoString customTitle(u"My custom title");

  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> descs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs,
                                              customDescsObj);

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0);

  EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
                                           ShellLinksEq(&descs)))
      .Times(1);
  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Tests calling PopulateJumpList with tasks and custom items.
 * This should call the following methods on the backend, in order:
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AddUserTasks
 * - AppendCategory
 * - CommitList
 *
 * This should result in a jump list with both built-in tasks as well as
 * custom tasks with a custom label.
 */
TEST(JumpListBuilder, PopulateJumpList)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx, JS::ObjectValue(*taskDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> taskDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
                                              taskDescsObj);

  nsAutoString customTitle(u"My custom title");

  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> customDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs,
                                              customDescsObj);

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);

  EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
                                           ShellLinksEq(&customDescs)))
      .Times(1);
  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Tests calling PopulateJumpList with tasks and custom items, but makes it so
 * that AppendCategory returns E_ACCESSDENIED, which can occur if Windows is
 * configured to not show recently opened items. The PopulateJumpList Promise
 * should still resolve.
 *
 * - SetAppID
 * - AbortList
 * - BeginList
 * - AddUserTasks
 * - AppendCategory
 * - CommitList
 *
 * This should result in a jump list with just built-in tasks.
 */
TEST(JumpListBuilder, PopulateJumpListNoOpenedItems)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx, JS::ObjectValue(*taskDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> taskDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs,
                                              taskDescsObj);

  nsAutoString customTitle(u"My custom title");

  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> customDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs,
                                              customDescsObj);

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);

  EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
                                           ShellLinksEq(&customDescs)))
      .WillOnce([] { return E_ACCESSDENIED; });

  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Tests calling ClearJumpList calls the following:
 *
 * - SetAppID
 * - DeleteList (passing the aumid)
 *
 * This results in an empty jump list for the user.
 */
TEST(JumpListBuilder, ClearJumpList)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  EXPECT_CALL(*testBackend, AbortList()).Times(0);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(0);
  EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0);

  EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0);
  EXPECT_CALL(*testBackend, CommitList()).Times(0);
  EXPECT_CALL(*testBackend, DeleteList(LPCWSTREq(aumid.get()))).Times(1);

  nsresult rv = builder->ClearJumpList(cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}

/**
 * Test that a WindowsJumpListShortcutDescription with a description
 * longer than MAX_PATH gets truncated to MAX_PATH. This is because a
 * description longer than MAX_PATH will cause CommitList to fail.
 */
TEST(JumpListBuilder, TruncateDescription)
{
  RefPtr<StrictMock<TestingJumpListBackend>> testBackend =
      new StrictMock<TestingJumpListBackend>();
  nsAutoString aumid(u"TestApplicationID");
  // We set up this expectation here because SetAppID will be called soon
  // after construction of the JumpListBuilder via the background thread.
  EXPECT_CALL(*testBackend, SetAppID(_)).Times(1);

  nsCOMPtr<nsIJumpListBuilder> builder =
      new JumpListBuilder(aumid, testBackend);
  ASSERT_TRUE(builder);

  AutoJSAPI jsapi;
  MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
  JSContext* cx = jsapi.cx();
  RefPtr<Promise> promise;

  JS::Rooted<JSObject*> taskDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> taskDescsJSVal(cx, JS::ObjectValue(*taskDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> taskDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs,
                                              taskDescsObj);

  nsAutoString customTitle(u"My custom title");

  JS::Rooted<JSObject*> customDescsObj(cx, JS::NewArrayObject(cx, 0));
  JS::Rooted<JS::Value> customDescsJSVal(cx, JS::ObjectValue(*customDescsObj));
  nsTArray<WindowsJumpListShortcutDescription> customDescs;
  GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, customDescs,
                                              customDescsObj);

  // We expect the long descriptions to be truncated to 260 characters, so
  // we'll truncate the descriptions here ourselves.
  for (auto& taskDesc : taskDescs) {
    taskDesc.mDescription.SetLength(MAX_PATH - 1);
  }
  for (auto& customDesc : customDescs) {
    customDesc.mDescription.SetLength(MAX_PATH - 1);
  }

  EXPECT_CALL(*testBackend, AbortList()).Times(1);
  EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1);
  EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1);

  EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()),
                                           ShellLinksEq(&customDescs)))
      .Times(1);
  EXPECT_CALL(*testBackend, CommitList()).Times(1);
  EXPECT_CALL(*testBackend, DeleteList(_)).Times(0);

  nsresult rv =
      builder->PopulateJumpList(taskDescsJSVal, customTitle, customDescsJSVal,
                                cx, getter_AddRefs(promise));
  ASSERT_TRUE(NS_SUCCEEDED(rv));
  ASSERT_TRUE(promise);

  RefPtr<WaitForResolver> resolver = new WaitForResolver();
  promise->AppendNativeHandler(resolver);
  JS::Rooted<JS::Value> result(cx);
  resolver->SpinUntilResolved();
}
