/* 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 <windows.h>
#include <ole2.h>
#include <shlobj.h>

#include <cstddef>
#include <cstdint>
#include <cstring>

#include "nsArray.h"
#include "nsArrayUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIFile.h"
#include "nsNetUtil.h"
#include "nsISupportsPrimitives.h"
#include "nsITransferable.h"

#include "nsClipboard.h"
#include "nsDataObjCollection.h"

#include "gtest/gtest.h"

// shims for conversion from cppunittest to gtest
template <size_t N>
void fail(const char (&msg)[N]) {
  ADD_FAILURE() << "TEST-UNEXPECTED-FAIL | " << msg;
}
template <size_t N>
void passed(const char (&msg)[N]) {
  GTEST_SUCCEED() << "TEST-PASS | " << msg;
}

nsIFile* xferFile;

nsresult CheckValidHDROP(STGMEDIUM* pSTG) {
  if (pSTG->tymed != TYMED_HGLOBAL) {
    fail("Received data is not in an HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  HGLOBAL hGlobal = pSTG->hGlobal;
  DROPFILES* pDropFiles;
  pDropFiles = (DROPFILES*)GlobalLock(hGlobal);
  if (!pDropFiles) {
    fail("There is no data at the given HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  if (pDropFiles->pFiles != sizeof(DROPFILES)) {
    fail("DROPFILES struct has wrong size");
  }

  if (!pDropFiles->fWide) {
    fail("Received data is not Unicode");
    return NS_ERROR_UNEXPECTED;
  }
  nsString s;
  unsigned long offset = 0;
  while (true) {
    s = (char16_t*)((char*)pDropFiles + pDropFiles->pFiles + offset);
    if (s.IsEmpty()) break;
    nsresult rv;
    nsCOMPtr<nsIFile> localFile;
    rv = NS_NewLocalFile(s, getter_AddRefs(localFile));
    if (NS_FAILED(rv)) {
      fail("File could not be opened");
      return NS_ERROR_UNEXPECTED;
    }
    offset += sizeof(char16_t) * (s.Length() + 1);
  }
  return NS_OK;
}

nsresult CheckValidTEXT(STGMEDIUM* pSTG) {
  if (pSTG->tymed != TYMED_HGLOBAL) {
    fail("Received data is not in an HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  HGLOBAL hGlobal = pSTG->hGlobal;
  char* pText;
  pText = (char*)GlobalLock(hGlobal);
  if (!pText) {
    fail("There is no data at the given HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  nsCString string;
  string = pText;

  if (!string.EqualsLiteral("Mozilla can drag and drop")) {
    fail("Text passed through drop object wrong");
    return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

nsresult CheckValidTEXTTwo(STGMEDIUM* pSTG) {
  if (pSTG->tymed != TYMED_HGLOBAL) {
    fail("Received data is not in an HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  HGLOBAL hGlobal = pSTG->hGlobal;
  char* pText;
  pText = (char*)GlobalLock(hGlobal);
  if (!pText) {
    fail("There is no data at the given HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  nsCString string;
  string = pText;

  if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
    fail("Text passed through drop object wrong");
    return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

nsresult CheckValidUNICODE(STGMEDIUM* pSTG) {
  if (pSTG->tymed != TYMED_HGLOBAL) {
    fail("Received data is not in an HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  HGLOBAL hGlobal = pSTG->hGlobal;
  char16_t* pText;
  pText = (char16_t*)GlobalLock(hGlobal);
  if (!pText) {
    fail("There is no data at the given HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  nsString string;
  string = pText;

  if (!string.EqualsLiteral("Mozilla can drag and drop")) {
    fail("Text passed through drop object wrong");
    return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

nsresult CheckValidUNICODETwo(STGMEDIUM* pSTG) {
  if (pSTG->tymed != TYMED_HGLOBAL) {
    fail("Received data is not in an HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  HGLOBAL hGlobal = pSTG->hGlobal;
  char16_t* pText;
  pText = (char16_t*)GlobalLock(hGlobal);
  if (!pText) {
    fail("There is no data at the given HGLOBAL");
    return NS_ERROR_UNEXPECTED;
  }

  nsString string;
  string = pText;

  if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) {
    fail("Text passed through drop object wrong");
    return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

nsresult GetTransferableFile(nsCOMPtr<nsITransferable>& pTransferable) {
  nsresult rv;

  nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferFile);

  pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
  pTransferable->Init(nullptr);
  rv = pTransferable->SetTransferData("application/x-moz-file", genericWrapper);
  return rv;
}

nsresult GetTransferableText(nsCOMPtr<nsITransferable>& pTransferable) {
  nsresult rv;
  constexpr auto mozString = u"Mozilla can drag and drop"_ns;
  nsCOMPtr<nsISupportsString> xferString =
      do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
  rv = xferString->SetData(mozString);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);

  pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
  pTransferable->Init(nullptr);
  rv = pTransferable->SetTransferData("text/plain", genericWrapper);
  return rv;
}

nsresult GetTransferableTextTwo(nsCOMPtr<nsITransferable>& pTransferable) {
  nsresult rv;
  constexpr auto mozString = u" twice over"_ns;
  nsCOMPtr<nsISupportsString> xferString =
      do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID);
  rv = xferString->SetData(mozString);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString);

  pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
  pTransferable->Init(nullptr);
  rv = pTransferable->SetTransferData("text/plain", genericWrapper);
  return rv;
}

nsresult GetTransferableURI(nsCOMPtr<nsITransferable>& pTransferable) {
  nsresult rv;

  nsCOMPtr<nsIURI> xferURI;

  rv = NS_NewURI(getter_AddRefs(xferURI), "http://www.mozilla.org");
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferURI);

  pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1");
  pTransferable->Init(nullptr);
  rv = pTransferable->SetTransferData("text/x-moz-url", genericWrapper);
  return rv;
}

nsresult MakeDataObject(nsIArray* transferableArray,
                        RefPtr<IDataObject>& itemToDrag) {
  nsresult rv;
  uint32_t itemCount = 0;

  nsCOMPtr<nsIURI> uri;
  rv = NS_NewURI(getter_AddRefs(uri), "http://www.mozilla.org");
  NS_ENSURE_SUCCESS(rv, rv);

  rv = transferableArray->GetLength(&itemCount);
  NS_ENSURE_SUCCESS(rv, rv);

  // Copied more or less exactly from nsDragService::InvokeDragSession
  // This is what lets us play fake Drag Service for the test
  if (itemCount > 1) {
    nsDataObjCollection* dataObjCollection = new nsDataObjCollection();
    if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY;
    itemToDrag = dataObjCollection;
    for (uint32_t i = 0; i < itemCount; ++i) {
      nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, i);
      if (trans) {
        RefPtr<IDataObject> dataObj;
        rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj),
                                                 uri);
        NS_ENSURE_SUCCESS(rv, rv);
        // Add the flavors to the collection object too
        rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection);
        NS_ENSURE_SUCCESS(rv, rv);

        dataObjCollection->AddDataObject(dataObj);
      }
    }
  }  // if dragging multiple items
  else {
    nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, 0);
    if (trans) {
      rv = nsClipboard::CreateNativeDataObject(trans,
                                               getter_AddRefs(itemToDrag), uri);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }  // else dragging a single object
  return rv;
}

nsresult Do_CheckOneFile() {
  nsresult rv;
  nsCOMPtr<nsITransferable> transferable;
  nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
  nsCOMPtr<nsISupports> genericWrapper;
  RefPtr<IDataObject> dataObj;

  rv = GetTransferableFile(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = MakeDataObject(transferableArray, dataObj);
  if (NS_FAILED(rv)) {
    fail("Could not create data object");
    return rv;
  }

  FORMATETC fe;
  SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("File data object does not support the file data type!");
    return NS_ERROR_UNEXPECTED;
  }

  STGMEDIUM* stg;
  stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("File data object did not provide data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidHDROP(stg);
  if (NS_FAILED(rv)) {
    fail("HDROP was invalid");
    return rv;
  }

  ReleaseStgMedium(stg);

  return NS_OK;
}

nsresult Do_CheckTwoFiles() {
  nsresult rv;
  nsCOMPtr<nsITransferable> transferable;
  nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
  nsCOMPtr<nsISupports> genericWrapper;
  RefPtr<IDataObject> dataObj;

  rv = GetTransferableFile(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = GetTransferableFile(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = MakeDataObject(transferableArray, dataObj);
  if (NS_FAILED(rv)) {
    fail("Could not create data object");
    return rv;
  }

  FORMATETC fe;
  SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("File data object does not support the file data type!");
    return NS_ERROR_UNEXPECTED;
  }

  STGMEDIUM* stg;
  stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("File data object did not provide data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidHDROP(stg);
  if (NS_FAILED(rv)) {
    fail("HDROP was invalid");
    return rv;
  }

  ReleaseStgMedium(stg);

  return NS_OK;
}

nsresult Do_CheckOneString() {
  nsresult rv;
  nsCOMPtr<nsITransferable> transferable;
  nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
  nsCOMPtr<nsISupports> genericWrapper;
  RefPtr<IDataObject> dataObj;

  rv = GetTransferableText(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = MakeDataObject(transferableArray, dataObj);
  if (NS_FAILED(rv)) {
    fail("Could not create data object");
    return rv;
  }

  FORMATETC fe;
  SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("String data object does not support the ASCII text data type!");
    return NS_ERROR_UNEXPECTED;
  }

  STGMEDIUM* stg;
  stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("String data object did not provide ASCII data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidTEXT(stg);
  if (NS_FAILED(rv)) {
    fail("TEXT was invalid");
    return rv;
  }

  ReleaseStgMedium(stg);

  SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("String data object does not support the wide text data type!");
    return NS_ERROR_UNEXPECTED;
  }

  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("String data object did not provide wide data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidUNICODE(stg);
  if (NS_FAILED(rv)) {
    fail("UNICODE was invalid");
    return rv;
  }

  return NS_OK;
}

nsresult Do_CheckTwoStrings() {
  nsresult rv;
  nsCOMPtr<nsITransferable> transferable;
  nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
  nsCOMPtr<nsISupports> genericWrapper;
  RefPtr<IDataObject> dataObj;

  rv = GetTransferableText(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = GetTransferableTextTwo(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  rv = MakeDataObject(transferableArray, dataObj);
  if (NS_FAILED(rv)) {
    fail("Could not create data object");
    return rv;
  }

  FORMATETC fe;
  SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("String data object does not support the ASCII text data type!");
    return NS_ERROR_UNEXPECTED;
  }

  STGMEDIUM* stg;
  stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("String data object did not provide ASCII data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidTEXTTwo(stg);
  if (NS_FAILED(rv)) {
    fail("TEXT was invalid");
    return rv;
  }

  ReleaseStgMedium(stg);

  SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);
  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("String data object does not support the wide text data type!");
    return NS_ERROR_UNEXPECTED;
  }

  if (dataObj->GetData(&fe, stg) != S_OK) {
    fail("String data object did not provide wide data on request");
    return NS_ERROR_UNEXPECTED;
  }

  rv = CheckValidUNICODETwo(stg);
  if (NS_FAILED(rv)) {
    fail("UNICODE was invalid");
    return rv;
  }

  return NS_OK;
}

nsresult Do_CheckSetArbitraryData(bool aMultiple) {
  nsresult rv;
  nsCOMPtr<nsITransferable> transferable;
  nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create();
  nsCOMPtr<nsISupports> genericWrapper;
  RefPtr<IDataObject> dataObj;

  rv = GetTransferableText(transferable);
  if (NS_FAILED(rv)) {
    fail("Could not create the proper nsITransferable!");
    return rv;
  }
  genericWrapper = do_QueryInterface(transferable);
  rv = transferableArray->AppendElement(genericWrapper);
  if (NS_FAILED(rv)) {
    fail("Could not append element to transferable array");
    return rv;
  }

  if (aMultiple) {
    rv = GetTransferableText(transferable);
    if (NS_FAILED(rv)) {
      fail("Could not create the proper nsITransferable!");
      return rv;
    }
    genericWrapper = do_QueryInterface(transferable);
    rv = transferableArray->AppendElement(genericWrapper);
    if (NS_FAILED(rv)) {
      fail("Could not append element to transferable array");
      return rv;
    }
  }

  rv = MakeDataObject(transferableArray, dataObj);
  if (NS_FAILED(rv)) {
    fail("Could not create data object");
    return rv;
  }

  static CLIPFORMAT mozArbitraryFormat =
      ::RegisterClipboardFormatW(L"MozillaTestFormat");
  FORMATETC fe;
  STGMEDIUM stg;
  SET_FORMATETC(fe, mozArbitraryFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL);

  HGLOBAL hg = GlobalAlloc(GPTR, 1024);
  stg.tymed = TYMED_HGLOBAL;
  stg.hGlobal = hg;
  stg.pUnkForRelease = nullptr;

  if (dataObj->SetData(&fe, &stg, true) != S_OK) {
    if (aMultiple) {
      fail("Unable to set arbitrary data type on data object collection!");
    } else {
      fail("Unable to set arbitrary data type on data object!");
    }
    return NS_ERROR_UNEXPECTED;
  }

  if (dataObj->QueryGetData(&fe) != S_OK) {
    fail("Arbitrary data set on data object is not advertised!");
    return NS_ERROR_UNEXPECTED;
  }

  STGMEDIUM* stg2;
  stg2 = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM));
  if (dataObj->GetData(&fe, stg2) != S_OK) {
    fail("Data object did not provide arbitrary data upon request!");
    return NS_ERROR_UNEXPECTED;
  }

  if (stg2->hGlobal != hg) {
    fail("Arbitrary data was not returned properly!");
    return rv;
  }
  ReleaseStgMedium(stg2);

  return NS_OK;
}

// This function performs basic drop tests, testing a data object consisting
// of one transferable
nsresult Do_Test1() {
  nsresult rv = NS_OK;
  nsresult workingrv;

  workingrv = Do_CheckOneFile();
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on a single file");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully created a working file drag object!");
  }

  workingrv = Do_CheckOneString();
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on a single string");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully created a working string drag object!");
  }

  workingrv = Do_CheckSetArbitraryData(false);
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on setting arbitrary data");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully set arbitrary data on a drag object");
  }

  return rv;
}

// This function performs basic drop tests, testing a data object consisting of
// two transferables.
nsresult Do_Test2() {
  nsresult rv = NS_OK;
  nsresult workingrv;

  workingrv = Do_CheckTwoFiles();
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on multiple files");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully created a working multiple file drag object!");
  }

  workingrv = Do_CheckTwoStrings();
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on multiple strings");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully created a working multiple string drag object!");
  }

  workingrv = Do_CheckSetArbitraryData(true);
  if (NS_FAILED(workingrv)) {
    fail("Drag object tests failed on setting arbitrary data");
    rv = NS_ERROR_UNEXPECTED;
  } else {
    passed("Successfully set arbitrary data on a drag object");
  }

  return rv;
}

// This function performs advanced drag and drop tests, testing a data object
// consisting of multiple transferables that have different data types
nsresult Do_Test3() {
  nsresult rv = NS_OK;
  // nsresult workingrv;

  // XXX TODO Write more advanced tests in Bug 535860
  return rv;
}

nsCOMPtr<nsIFile> GetTemporaryDirectory() {
  nsCOMPtr<nsIFile> tmpdir;

#define ENSURE(expr) NS_ENSURE_SUCCESS(expr, nullptr);

  ENSURE(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpdir)));
  MOZ_RELEASE_ASSERT(tmpdir);

  ENSURE(tmpdir->AppendNative("TestWinDND"_ns));
  ENSURE(tmpdir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777));

#undef ENSURE

  return tmpdir;
}

TEST(TestWinDND, All)
{
  nsCOMPtr<nsIFile> file = GetTemporaryDirectory();
  if (!file) {
    fail("could not create temporary directory!");
    return;
  }
  xferFile = file;

  if (NS_SUCCEEDED(Do_Test1())) {
    passed(
        "Basic Drag and Drop data type tests (single transferable) succeeded!");
  }

  if (NS_SUCCEEDED(Do_Test2())) {
    passed(
        "Basic Drag and Drop data type tests (multiple transferables) "
        "succeeded!");
  }

  // if (NS_SUCCEEDED(Do_Test3()))
  //  passed("Advanced Drag and Drop data type tests succeeded!");
}

// A drag source that claims more descriptor entries (cItems) than its
// allocation holds.
class TruncatedFileGroupDescriptorDataObject final : public IDataObject {
 public:
  explicit TruncatedFileGroupDescriptorDataObject(HGLOBAL aDescriptor)
      : mRefCnt(1), mDescriptor(aDescriptor) {}

  HRESULT STDMETHODCALLTYPE QueryInterface(REFIID aIid, void** aOut) override {
    if (!aOut) {
      return E_POINTER;
    }
    if (aIid == IID_IUnknown || aIid == IID_IDataObject) {
      *aOut = static_cast<IDataObject*>(this);
      AddRef();
      return S_OK;
    }
    *aOut = nullptr;
    return E_NOINTERFACE;
  }

  ULONG STDMETHODCALLTYPE AddRef() override {
    return InterlockedIncrement(&mRefCnt);
  }

  ULONG STDMETHODCALLTYPE Release() override {
    ULONG count = InterlockedDecrement(&mRefCnt);
    if (!count) {
      delete this;
    }
    return count;
  }

  HRESULT STDMETHODCALLTYPE GetData(FORMATETC* aFormat,
                                    STGMEDIUM* aMedium) override {
    if (!aFormat || !aMedium || !MatchesDescriptorW(*aFormat)) {
      return DV_E_FORMATETC;
    }
    // Hand back a fresh copy; the caller owns it via ReleaseStgMedium.
    const SIZE_T size = ::GlobalSize(mDescriptor);
    HGLOBAL copy = ::GlobalAlloc(GMEM_MOVEABLE, size);
    if (!copy) {
      return E_OUTOFMEMORY;
    }
    void* dst = ::GlobalLock(copy);
    void* src = ::GlobalLock(mDescriptor);
    memcpy(dst, src, size);
    ::GlobalUnlock(mDescriptor);
    ::GlobalUnlock(copy);

    memset(aMedium, 0, sizeof(*aMedium));
    aMedium->tymed = TYMED_HGLOBAL;
    aMedium->hGlobal = copy;
    return S_OK;
  }

  HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC* aFormat) override {
    if (!aFormat) {
      return E_INVALIDARG;
    }
    return MatchesDescriptorW(*aFormat) ? S_OK : DV_E_FORMATETC;
  }

  HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC*, STGMEDIUM*) override {
    return E_NOTIMPL;
  }
  HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC*,
                                                  FORMATETC* aOut) override {
    if (aOut) {
      aOut->ptd = nullptr;
    }
    return E_NOTIMPL;
  }
  HRESULT STDMETHODCALLTYPE SetData(FORMATETC*, STGMEDIUM*, BOOL) override {
    return E_NOTIMPL;
  }
  HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD, IEnumFORMATETC**) override {
    return E_NOTIMPL;
  }
  HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC*, DWORD, IAdviseSink*,
                                    DWORD*) override {
    return OLE_E_ADVISENOTSUPPORTED;
  }
  HRESULT STDMETHODCALLTYPE DUnadvise(DWORD) override {
    return OLE_E_ADVISENOTSUPPORTED;
  }
  HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA**) override {
    return OLE_E_ADVISENOTSUPPORTED;
  }

 private:
  ~TruncatedFileGroupDescriptorDataObject() {
    if (mDescriptor) {
      ::GlobalFree(mDescriptor);
    }
  }

  static bool MatchesDescriptorW(const FORMATETC& aFormat) {
    static CLIPFORMAT sFileDescriptorW =
        ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW);
    return aFormat.cfFormat == sFileDescriptorW &&
           (aFormat.tymed & TYMED_HGLOBAL) &&
           aFormat.dwAspect == DVASPECT_CONTENT;
  }

  volatile LONG mRefCnt;
  HGLOBAL mDescriptor;
};

// Reject a group descriptor whose backing HGLOBAL is too small to hold the
// requested number of entries.
TEST(TestWinDND, FileGroupDescriptorHasItems)
{
  EXPECT_FALSE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      nullptr, 1));

  // Header only, room for zero descriptor entries.
  HGLOBAL headerOnly = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(UINT));
  ASSERT_NE(headerOnly, nullptr);
  EXPECT_TRUE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      headerOnly, 0));
  EXPECT_FALSE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      headerOnly, 1));
  EXPECT_FALSE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORA>(
      headerOnly, 1));
  // Overflowing the size computation must fail.
  EXPECT_FALSE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      headerOnly, UINT64_MAX));
  ::GlobalFree(headerOnly);

  // Room for exactly two wide descriptor entries, but not 3.
  HGLOBAL twoEntries =
      ::GlobalAlloc(GMEM_MOVEABLE, offsetof(FILEGROUPDESCRIPTORW, fgd) +
                                       2 * sizeof(FILEDESCRIPTORW));
  ASSERT_NE(twoEntries, nullptr);
  EXPECT_TRUE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      twoEntries, 2));
  EXPECT_FALSE(nsClipboard::FileGroupDescriptorHasItems<FILEGROUPDESCRIPTORW>(
      twoEntries, 3));
  ::GlobalFree(twoEntries);
}

TEST(TestWinDND, TruncatedFileGroupDescriptorIsRejected)
{
  // Allocate room for the cItems header only, but claim two entries.
  HGLOBAL descriptor = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(UINT));
  ASSERT_NE(descriptor, nullptr);
  auto* group = static_cast<FILEGROUPDESCRIPTORW*>(::GlobalLock(descriptor));
  ASSERT_NE(group, nullptr);
  group->cItems = 2;
  ::GlobalUnlock(descriptor);

  // mRefCnt is initialized to 1.
  RefPtr<IDataObject> dataObj = dont_AddRef(static_cast<IDataObject*>(
      new TruncatedFileGroupDescriptorDataObject(descriptor)));

  void* data = nullptr;
  uint32_t len = 0;
  nsresult rv = nsClipboard::GetNativeDataOffClipboard(
      dataObj, /* aIndex */ 1, CF_HDROP, /* aMIMEImageFormat */ nullptr, &data,
      &len);

  EXPECT_EQ(rv, NS_ERROR_INVALID_ARG);
  EXPECT_EQ(data, nullptr);
  EXPECT_EQ(len, 0u);
}
