/* 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/. */

const { MessageGenerator } = ChromeUtils.importESModule(
  "resource://testing-common/mailnews/MessageGenerator.sys.mjs"
);
const { PromiseTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/mailnews/PromiseTestUtils.sys.mjs"
);
const { ensure_table_view } = ChromeUtils.importESModule(
  "resource://testing-common/MailViewHelpers.sys.mjs"
);

const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
  Ci.nsIDragService
);
const mailboxService = MailServices.messageServiceFromURI("mailbox:");

const generator = new MessageGenerator();
const tabmail = document.getElementById("tabmail");
const about3Pane = tabmail.currentAbout3Pane;
const { folderPane, threadTree } = about3Pane;
let rootFolder, sourceFolder, destFolder;

add_setup(async function () {
  const account = MailServices.accounts.createLocalMailAccount();

  rootFolder = account.incomingServer.rootFolder.QueryInterface(
    Ci.nsIMsgLocalMailFolder
  );
  sourceFolder = rootFolder
    .createLocalSubfolder("dragDropSource")
    .QueryInterface(Ci.nsIMsgLocalMailFolder);
  destFolder = rootFolder
    .createLocalSubfolder("dragDropDest")
    .QueryInterface(Ci.nsIMsgLocalMailFolder);

  sourceFolder.addMessageBatch(
    generator
      .makeMessages({ count: 15 })
      .map(message => message.toMessageString())
  );
  sourceFolder.addMessageBatch(
    generator
      .makeMessages({ count: 3, msgsPerThread: 3 })
      .map(message => message.toMessageString())
  );

  registerCleanupFunction(() => {
    MailServices.accounts.removeAccount(account, false);
  });
});

/**
 * Tests dragging messages from the thread tree correctly sets up the
 * dataTransfer object. Also checks the flavorDataProvider for exporting
 * messages to the filesystem.
 */
add_task(async function testDragMessageSource() {
  function beginDrag(index) {
    dragService.startDragSessionForTests(
      about3Pane,
      Ci.nsIDragService.DRAGDROP_ACTION_NONE
    );

    return EventUtils.synthesizeDragOver(
      threadTree.getRowAtIndex(index),
      destination,
      null,
      null,
      about3Pane,
      about3Pane,
      {}
    );
  }

  async function checkDataTransfer(headers) {
    Assert.equal(
      dataTransfer.mozItemCount,
      headers.length,
      "item count should match selection count"
    );

    for (const [index, header] of Object.entries(headers)) {
      const messageURI = sourceFolder.getUriForMsg(header);
      const messageURL = mailboxService.getUrlForUri(messageURI).spec;

      Assert.equal(
        dataTransfer.mozGetDataAt("text/plain", index),
        messageURI,
        `text/plain of item ${index}`
      );
      Assert.equal(
        dataTransfer.mozGetDataAt("text/x-moz-message", index),
        messageURI,
        `text/x-moz-message of item ${index}`
      );
      Assert.equal(
        dataTransfer.mozGetDataAt("text/x-moz-url", index),
        messageURL,
        `text/x-moz-url of item ${index}`
      );
      Assert.equal(
        dataTransfer.mozGetDataAt("application/x-moz-file-promise-url", index),
        messageURL,
        `application/x-moz-file-promise-url of item ${index}`
      );
      Assert.stringMatches(
        dataTransfer.mozGetDataAt(
          "application/x-moz-file-promise-dest-filename",
          index
        ),
        `^${header.flags & Ci.nsMsgMessageFlags.HasRe ? "Re[:_ ]? " : ""}${
          header.subject
        }(-\\d)?\.eml$`,
        `application/x-moz-file-promise-dest-filename of item ${index}`
      );
      const flavorDataProvider = dataTransfer.mozGetDataAt(
        "application/x-moz-file-promise",
        index
      );
      Assert.ok(
        flavorDataProvider.QueryInterface(Ci.nsIFlavorDataProvider),
        "nsIFlavorDataProvider exists"
      );

      // Create a fake nsITransferable, mimicking what happens when a dragged
      // message is dropped in a filesystem window.

      const transferable = Cc[
        "@mozilla.org/widget/transferable;1"
      ].createInstance(Ci.nsITransferable);
      transferable.init(window.docShell);

      const supportsURI = Cc["@mozilla.org/supports-string;1"].createInstance(
        Ci.nsISupportsString
      );
      supportsURI.data = messageURI;
      transferable.setTransferData("text/plain", supportsURI);

      const tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
      tempFile.append(`${header.subject}.eml`);
      if (tempFile.exists()) {
        tempFile.remove(false);
      }

      const supportsLeafName = Cc[
        "@mozilla.org/supports-string;1"
      ].createInstance(Ci.nsISupportsString);
      supportsLeafName.data = tempFile.leafName;
      transferable.setTransferData(
        "application/x-moz-file-promise-dest-filename",
        supportsLeafName
      );
      transferable.setTransferData(
        "application/x-moz-file-promise-dir",
        tempFile.parent
      );

      const writePromise = TestUtils.topicObserved("message-saved");
      flavorDataProvider.getFlavorData(
        transferable,
        "application/x-moz-file-promise"
      );
      await writePromise;
      Assert.ok(tempFile.exists(), ".eml file created");

      const fileContent = await IOUtils.readUTF8(tempFile.path);
      Assert.stringContains(
        fileContent,
        `Subject: ${header.flags & Ci.nsMsgMessageFlags.HasRe ? "Re: " : ""}${
          header.subject
        }\r\n`,
        "message written to file"
      );
    }

    Assert.equal(
      dataTransfer.effectAllowed,
      "copyMove",
      "effectAllowed of drag operation"
    );
    Assert.equal(
      dataTransfer.dropEffect,
      "move",
      "dropEffect of drag operation"
    );
  }

  function endDrag() {
    EventUtils.synthesizeDropAfterDragOver(
      result,
      dataTransfer,
      destination,
      about3Pane,
      {}
    );

    dragService.getCurrentSession().endDragSession(true);
  }

  function checkMove(
    [isMove, , destFolder2, destMessages],
    expectedMessageIDs
  ) {
    Assert.ok(isMove, "message(s) should be moved not copied");
    Assert.equal(
      destFolder2,
      destFolder,
      "message(s) should be moved to the right folder"
    );
    Assert.equal(
      destMessages.length,
      expectedMessageIDs.length,
      "number of messages moved"
    );
    for (let i = 0; i < expectedMessageIDs.length; i++) {
      Assert.equal(
        destMessages[i].messageId,
        expectedMessageIDs[i],
        "moved message"
      );
    }
  }

  about3Pane.restoreState({
    folderURI: sourceFolder.URI,
    messagePaneVisible: false,
  });
  await ensure_table_view(document);

  const sourceMessages = [...sourceFolder.messages];
  const sourceMessageIDs = sourceMessages.map(m => m.messageId);
  let result, dataTransfer;
  const destination = folderPane.getRowForFolder(destFolder);

  info("Dragging a single selection");

  let movePromise = new PromiseTestUtils.promiseFolderNotification(
    destFolder,
    "msgsMoveCopyCompleted"
  );
  threadTree.selectedIndex = 0;
  [result, dataTransfer] = beginDrag(0);
  await checkDataTransfer([sourceMessages[0]]);
  endDrag();
  checkMove(await movePromise, [sourceMessageIDs[0]]);

  info("Dragging a contiguous multiple selection");

  movePromise = new PromiseTestUtils.promiseFolderNotification(
    destFolder,
    "msgsMoveCopyCompleted"
  );
  threadTree.selectedIndices = [1, 2];
  [result, dataTransfer] = beginDrag(1);
  await checkDataTransfer([sourceMessages[2], sourceMessages[3]]);
  endDrag();
  checkMove(await movePromise, [sourceMessageIDs[2], sourceMessageIDs[3]]);

  info("Dragging a non-contiguous multiple selection");

  movePromise = new PromiseTestUtils.promiseFolderNotification(
    destFolder,
    "msgsMoveCopyCompleted"
  );
  threadTree.selectedIndices = [3, 5, 7];
  [, dataTransfer] = beginDrag(3);
  await checkDataTransfer([
    sourceMessages[6],
    sourceMessages[8],
    sourceMessages[10],
  ]);
  endDrag();
  checkMove(await movePromise, [
    sourceMessageIDs[6],
    sourceMessageIDs[8],
    sourceMessageIDs[10],
  ]);

  info("Dragging an unselected message with ctrl key already pressed");

  // Since EventUtils.synthesizeDragOver() does not correctly emulate a drag
  // operation with a modifier key already pressed, we'll call the relevant
  // parts of this function directly.
  const srcElement = threadTree.getRowAtIndex(1);
  EventUtils.synthesizeMouseAtCenter(
    srcElement,
    { type: "mousedown", ctrlKey: true },
    about3Pane
  );
  const rect = srcElement.getBoundingClientRect();
  const x = rect.width / 2;
  const y = rect.height / 2;
  EventUtils.synthesizeMouse(
    srcElement,
    x,
    y,
    { type: "mousemove", ctrlKey: true },
    about3Pane
  );
  EventUtils.synthesizeMouse(
    srcElement,
    x + 10,
    y + 10,
    { type: "mousemove", ctrlKey: true },
    about3Pane
  );

  Assert.equal(
    threadTree.selectedIndex,
    5,
    "The selection should not have changed"
  );

  info("Dragging an unselected message");

  movePromise = new PromiseTestUtils.promiseFolderNotification(
    destFolder,
    "msgsMoveCopyCompleted"
  );
  [, dataTransfer] = beginDrag(1);
  Assert.equal(
    threadTree.selectedIndex,
    1,
    "Source message should now be selected"
  );
  await checkDataTransfer([sourceMessages[4]]);
  endDrag();
  checkMove(await movePromise, [sourceMessageIDs[4]]);

  info("Dragging an unselected collapsed thread");

  movePromise = new PromiseTestUtils.promiseFolderNotification(
    destFolder,
    "msgsMoveCopyCompleted"
  );
  [, dataTransfer] = beginDrag(8);
  Assert.equal(
    threadTree.selectedIndex,
    8,
    "Source messages should now be selected"
  );
  await checkDataTransfer([
    sourceMessages[15],
    sourceMessages[16],
    sourceMessages[17],
  ]);
  endDrag();
  checkMove(await movePromise, [
    sourceMessageIDs[15],
    sourceMessageIDs[16],
    sourceMessageIDs[17],
  ]);
});

/**
 * Tests dragging messages over the folder tree allows the right drag action.
 */
add_task(async function testDragMessageDestination() {
  about3Pane.restoreState({
    folderURI: sourceFolder.URI,
    messagePaneVisible: false,
  });
  await ensure_table_view(document);

  dragService.startDragSessionForTests(
    about3Pane,
    Ci.nsIDragService.DRAGDROP_ACTION_NONE
  );
  let result, dataTransfer;

  function dragOver(folder, modifiers, expectedEffect) {
    [result, dataTransfer] = EventUtils.synthesizeDragOver(
      threadTree.getRowAtIndex(0),
      folderPane.getRowForFolder(folder),
      null,
      expectedEffect,
      about3Pane,
      about3Pane,
      modifiers
    );

    Assert.equal(
      dataTransfer.effectAllowed,
      "copyMove",
      "effectAllowed of drag operation"
    );
    Assert.equal(
      dataTransfer.dropEffect,
      expectedEffect,
      "dropEffect of drag operation"
    );
  }

  dragOver(rootFolder, {}, "none");
  dragOver(destFolder, {}, "move");
  dragOver(
    destFolder,
    {
      altKey: AppConstants.platform == "macosx",
      ctrlKey: AppConstants.platform != "macosx",
    },
    "copy"
  );
  dragOver(sourceFolder, {}, "none");
  dragOver(rootFolder, {}, "none");

  EventUtils.synthesizeDropAfterDragOver(
    result,
    dataTransfer,
    folderPane.getRowForFolder(rootFolder),
    about3Pane,
    { type: "dragend" }
  );

  dragService.getCurrentSession().endDragSession(true);
});

/**
 * Tests dragging a message from the filesystem to the window.
 */
add_task(async function testDragImportFileMessage() {
  function doDrag(file, dragTarget) {
    const dragData = [[{ type: "application/x-moz-file", data: file }]];
    const dataTransfer = new DataTransfer();
    dataTransfer.dropEffect = "move";
    for (let i = 0; i < dragData.length; i++) {
      const item = dragData[i];
      for (let j = 0; j < item.length; j++) {
        dataTransfer.mozSetDataAt(item[j].type, item[j].data, i);
      }
    }
    dragService.startDragSessionForTests(
      about3Pane,
      Ci.nsIDragService.DRAGDROP_ACTION_MOVE
    );
    const session = dragService.getCurrentSession();
    session.dataTransfer = dataTransfer;

    // EventUtils needs a "from" element but it must be somewhere that can't
    // be dragged from or the dataTransfer will get overwritten.
    EventUtils.synthesizeDragOver(
      document.getElementById("spacesToolbar"),
      dragTarget,
      dragData,
      "move",
      about3Pane
    );

    // This makes sure that the fake dataTransfer has still
    // the expected drop effect after the synthesizeDragOver call.
    session.dataTransfer.dropEffect = "move";

    const movePromise = new PromiseTestUtils.promiseFolderNotification(
      sourceFolder,
      "msgAdded"
    );
    EventUtils.synthesizeDropAfterDragOver(
      null,
      session.dataTransfer,
      dragTarget,
      about3Pane,
      {}
    );
    dragService.getCurrentSession().endDragSession(true);
    return movePromise;
  }

  // Drag a message to the folder tree. It should be added to the target
  // folder, not the current folder.

  about3Pane.restoreState({
    folderURI: sourceFolder.URI,
    messagePaneVisible: false,
  });
  let [droppedMessage] = await doDrag(
    new FileUtils.File(getTestFilePath("files/sampleContent.eml")),
    folderPane.getRowForFolder(destFolder)
  );
  Assert.equal(
    droppedMessage.folder,
    destFolder,
    "the message should be added to the right folder"
  );
  Assert.equal(
    droppedMessage.messageId,
    "sample.content@made.up.invalid",
    "the right message should be added"
  );

  // Drag a message to the thread tree. It should be added to the current folder.

  about3Pane.restoreState({
    folderURI: destFolder.URI,
    messagePaneVisible: false,
  });
  [droppedMessage] = await doDrag(
    new FileUtils.File(getTestFilePath("files/sampleContent.eml")),
    threadTree
  );
  Assert.equal(
    droppedMessage.folder,
    destFolder,
    "the message should be added to the right folder"
  );
  Assert.equal(
    droppedMessage.messageId,
    "sample.content@made.up.invalid",
    "the right message should be added"
  );
});
