/* 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 { mailTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/mailnews/MailTestUtils.sys.mjs"
);
const { MessageGenerator } = ChromeUtils.importESModule(
  "resource://testing-common/mailnews/MessageGenerator.sys.mjs"
);
const { PromiseTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/mailnews/PromiseTestUtils.sys.mjs"
);

const tabmail = document.getElementById("tabmail");
let rootFolder, testFolder, otherFolder;

add_setup(async function () {
  const generator = new MessageGenerator();

  const account = MailServices.accounts.createLocalMailAccount();
  account.addIdentity(MailServices.accounts.createIdentity());
  rootFolder = account.incomingServer.rootFolder;
  rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
  testFolder = rootFolder.createLocalSubfolder("searchMessagesFolder");
  testFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
  const messageStrings = generator
    .makeMessages({ count: 20 })
    .map(message => message.toMessageString());
  testFolder.addMessageBatch(messageStrings);
  otherFolder = rootFolder.createLocalSubfolder("searchMessagesOtherFolder");

  tabmail.currentAbout3Pane.paneLayout.messagePaneVisible = true;
  Services.xulStore.removeDocument(
    "chrome://messenger/content/SearchDialog.xhtml"
  );

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

add_task(async function () {
  const windowOpenedPromise = BrowserTestUtils.domWindowOpenedAndLoaded(
    null,
    w =>
      w.document.documentURI == "chrome://messenger/content/SearchDialog.xhtml"
  );
  window.searchAllMessages();
  const win = await windowOpenedPromise;
  const doc = win.document;

  await SimpleTest.promiseFocus(win);

  const searchButton = doc.getElementById("search-button");
  const clearButton = doc.querySelector(
    "#searchTerms > vbox > hbox:nth-child(2) > button"
  );
  const searchTermList = doc.getElementById("searchTermList");
  const threadTree = doc.getElementById("threadTree");
  const columns = threadTree.columns;
  const picker = threadTree.querySelector("treecolpicker");
  const popup = picker.querySelector("menupopup");
  const openButton = doc.getElementById("openButton");
  const deleteButton = doc.getElementById("deleteButton");
  const fileMessageButton = doc.getElementById("fileMessageButton");
  const fileMessagePopup = fileMessageButton.querySelector("menupopup");
  const openInFolderButton = doc.getElementById("openInFolderButton");
  const saveAsVFButton = doc.getElementById("saveAsVFButton");
  const statusText = doc.getElementById("statusText");

  const treeClick = mailTestUtils.treeClick.bind(
    null,
    EventUtils,
    win,
    threadTree
  );

  // Test search criteria. The search results are deterministic unless
  // MessageGenerator is changed.

  await TestUtils.waitForCondition(
    () => searchTermList.itemCount == 1,
    "waiting for a search term to exist"
  );
  const searchTerm0 = searchTermList.getItemAtIndex(0);
  const input0 = searchTerm0.querySelector("search-value input");
  const button0 = searchTerm0.querySelector("button.small-button:first-child");

  // Row 0 will look for subjects including "hovercraft".
  Assert.equal(input0.value, "");
  input0.focus();
  EventUtils.sendString("hovercraft", win);

  // Add another row.
  EventUtils.synthesizeMouseAtCenter(button0, {}, win);
  await TestUtils.waitForCondition(
    () => searchTermList.itemCount == 2,
    "waiting for a second search term to exist"
  );

  const searchTerm1 = searchTermList.getItemAtIndex(1);
  const menulist = searchTerm1.querySelector("search-attribute menulist");
  const menuitem = menulist.querySelector(`menuitem[value="1"]`);
  const input1 = searchTerm1.querySelector("search-value input");

  // Change row 1's search attribute.
  EventUtils.synthesizeMouseAtCenter(menulist, {}, win);
  await BrowserTestUtils.waitForPopupEvent(menulist, "shown");
  menulist.menupopup.activateItem(menuitem);
  await BrowserTestUtils.waitForPopupEvent(menulist, "hidden");

  // Row 1 will look for the sender Emily Ekberg.
  Assert.equal(input1.value, "");
  EventUtils.synthesizeMouseAtCenter(input1, {}, win);
  EventUtils.sendString("emily@ekberg.invalid", win);

  // Search. Emily didn't send a message about hovercraft, so no results.
  EventUtils.synthesizeMouseAtCenter(searchButton, {}, win);
  // Allows 5 seconds for expected statusText to appear.
  await TestUtils.waitForCondition(
    () => statusText.value == "No matches found",
    "waiting for status text to update"
  );

  // Change the search from AND to OR.
  EventUtils.synthesizeMouseAtCenter(
    doc.querySelector(`#booleanAndGroup > radio[value="or"]`),
    {},
    win
  );
  // Change the subject search to something more common.
  input0.select();
  EventUtils.sendString("in", win);

  // Search. 10 messages should be found.
  EventUtils.synthesizeMouseAtCenter(searchButton, {}, win);
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 10,
    "waiting for tree view to be filled"
  );
  // statusText changes on 500 ms time base.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 500));
  await TestUtils.waitForCondition(
    () => statusText.value == "10 matches found",
    "waiting for status text to update"
  );

  // Test tree sort column and direction.

  EventUtils.synthesizeMouseAtCenter(columns.subjectCol.element, {}, win);
  Assert.equal(
    columns.subjectCol.element.getAttribute("sortDirection"),
    "ascending"
  );
  EventUtils.synthesizeMouseAtCenter(columns.flaggedCol.element, {}, win);
  Assert.equal(
    columns.flaggedCol.element.getAttribute("sortDirection"),
    "ascending"
  );
  EventUtils.synthesizeMouseAtCenter(columns.dateCol.element, {}, win);
  Assert.equal(
    columns.dateCol.element.getAttribute("sortDirection"),
    "ascending"
  );
  EventUtils.synthesizeMouseAtCenter(columns.dateCol.element, {}, win);
  Assert.equal(
    columns.dateCol.element.getAttribute("sortDirection"),
    "descending"
  );

  // Test tree column visibility and order.

  checkTreeColumnsInOrder(threadTree, [
    "flaggedCol",
    "attachmentCol",
    "subjectCol",
    "unreadButtonColHeader",
    "correspondentCol",
    "junkStatusCol",
    "dateCol",
    "locationCol",
  ]);
  EventUtils.synthesizeMouseAtCenter(picker, {}, win);
  await BrowserTestUtils.waitForPopupEvent(popup, "shown");
  popup.activateItem(
    popup.querySelector(`[colindex="${columns.selectCol.index}"]`)
  );
  popup.activateItem(
    popup.querySelector(`[colindex="${columns.deleteCol.index}"]`)
  );
  popup.hidePopup();
  await BrowserTestUtils.waitForPopupEvent(popup, "hidden");
  // Wait for macOS to catch up.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 500));
  checkTreeColumnsInOrder(threadTree, [
    "selectCol",
    "flaggedCol",
    "attachmentCol",
    "subjectCol",
    "unreadButtonColHeader",
    "correspondentCol",
    "junkStatusCol",
    "dateCol",
    "locationCol",
    "deleteCol",
  ]);

  threadTree._reorderColumn(
    columns.deleteCol.element,
    columns.selectCol.element,
    false
  );
  threadTree.invalidate();
  // Wait for macOS to catch up.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 500));
  checkTreeColumnsInOrder(threadTree, [
    "selectCol",
    "deleteCol",
    "flaggedCol",
    "attachmentCol",
    "subjectCol",
    "unreadButtonColHeader",
    "correspondentCol",
    "junkStatusCol",
    "dateCol",
    "locationCol",
  ]);

  // Test message selection with the select column.

  treeClick(0, "subjectCol", {});
  await TestUtils.waitForCondition(
    () => threadTree.view.selection.count == 1,
    "waiting for first message to be selected"
  );
  Assert.ok(!openButton.disabled);
  Assert.ok(!deleteButton.disabled);
  Assert.ok(!fileMessageButton.disabled);
  Assert.ok(!openInFolderButton.disabled);
  treeClick(1, "selectCol", {});
  await TestUtils.waitForCondition(
    () => threadTree.view.selection.count == 2,
    "waiting for second message to be selected"
  );
  Assert.ok(!openButton.disabled);
  Assert.ok(!deleteButton.disabled);
  Assert.ok(!fileMessageButton.disabled);
  Assert.ok(openInFolderButton.disabled);
  treeClick(1, "selectCol", {});
  await TestUtils.waitForCondition(
    () => threadTree.view.selection.count == 1,
    "waiting for second message to be unselected"
  );
  Assert.ok(!openButton.disabled);
  Assert.ok(!deleteButton.disabled);
  Assert.ok(!fileMessageButton.disabled);
  Assert.ok(!openInFolderButton.disabled);
  treeClick(0, "selectCol", {});
  await TestUtils.waitForCondition(
    () => threadTree.view.selection.count == 0,
    "waiting for first message to be selected"
  );
  Assert.ok(openButton.disabled);
  Assert.ok(deleteButton.disabled);
  Assert.ok(fileMessageButton.disabled);
  Assert.ok(openInFolderButton.disabled);

  // Opening messages.

  // Test opening a message with the "Open" button.
  treeClick(0, "subjectCol", {});
  let tabOpenPromise = BrowserTestUtils.waitForEvent(window, "TabOpen");
  EventUtils.synthesizeMouseAtCenter(openButton, {}, win);
  const {
    detail: { tabInfo: tab1 },
  } = await tabOpenPromise;
  await BrowserTestUtils.waitForEvent(tab1.chromeBrowser, "MsgLoaded");
  Assert.equal(tab1.mode.name, "mailMessageTab");

  await SimpleTest.promiseFocus(win);

  // Test opening a message with a double click.
  tabOpenPromise = BrowserTestUtils.waitForEvent(window, "TabOpen");
  treeClick(0, "subjectCol", { clickCount: 2 });
  const {
    detail: { tabInfo: tab2 },
  } = await tabOpenPromise;
  await BrowserTestUtils.waitForEvent(tab2.chromeBrowser, "MsgLoaded");
  Assert.equal(tab2.mode.name, "mailMessageTab");

  await SimpleTest.promiseFocus(win);

  // Test opening a message with the keyboard.
  tabOpenPromise = BrowserTestUtils.waitForEvent(window, "TabOpen");
  threadTree.focus();
  EventUtils.synthesizeKey("VK_RETURN", {}, win);
  const {
    detail: { tabInfo: tab3 },
  } = await tabOpenPromise;
  await BrowserTestUtils.waitForEvent(tab3.chromeBrowser, "MsgLoaded");
  Assert.equal(tab3.mode.name, "mailMessageTab");

  await SimpleTest.promiseFocus(win);

  // Test opening a message with the "Open in Folder" button.
  const tabSelectPromise = BrowserTestUtils.waitForEvent(window, "TabSelect");
  EventUtils.synthesizeMouseAtCenter(openInFolderButton, {}, win);
  const {
    detail: { tabInfo: tab0 },
  } = await tabSelectPromise;
  await BrowserTestUtils.waitForEvent(tab0.chromeBrowser, "MsgLoaded");
  Assert.equal(tab0, tabmail.tabInfo[0]);

  tabmail.closeOtherTabs(tab0);

  await SimpleTest.promiseFocus(win);

  // Deleting messages.

  // Test deleting a message with the delete column.
  let deletePromise = PromiseTestUtils.promiseFolderEvent(
    testFolder,
    "DeleteOrMoveMsgCompleted"
  );
  treeClick(0, "deleteCol", {});
  await deletePromise;
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 9,
    "waiting for row to be removed from tree view"
  );

  // Test deleting a message with the "Delete" button.
  deletePromise = PromiseTestUtils.promiseFolderEvent(
    testFolder,
    "DeleteOrMoveMsgCompleted"
  );
  EventUtils.synthesizeMouseAtCenter(deleteButton, {}, win);
  await deletePromise;
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 8,
    "waiting for row to be removed from tree view"
  );

  // Test deleting a message with the keyboard.
  treeClick(0, "subjectCol", {});
  deletePromise = PromiseTestUtils.promiseFolderEvent(
    testFolder,
    "DeleteOrMoveMsgCompleted"
  );
  const dialogPromise = BrowserTestUtils.promiseAlertDialog("accept");
  EventUtils.synthesizeKey("VK_DELETE", { shiftKey: true }, win);
  await dialogPromise;
  await deletePromise;
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 7,
    "waiting for row to be removed from tree view"
  );

  // Moving messages.

  // Test moving a message to another folder with the "Move To" button.
  treeClick(0, "subjectCol", {});
  const movePromise = PromiseTestUtils.promiseFolderEvent(
    testFolder,
    "DeleteOrMoveMsgCompleted"
  );

  EventUtils.synthesizeMouseAtCenter(fileMessageButton, {}, win);
  await BrowserTestUtils.waitForPopupEvent(fileMessagePopup, "shown");
  const rootFolderMenu = [...fileMessagePopup.children].find(
    i => i._folder == rootFolder
  );
  rootFolderMenu.openMenu(true);
  await BrowserTestUtils.waitForPopupEvent(rootFolderMenu.menupopup, "shown");
  const otherFolderItem = [...rootFolderMenu.menupopup.children].find(
    i => i._folder == otherFolder
  );
  rootFolderMenu.menupopup.activateItem(otherFolderItem);
  await BrowserTestUtils.waitForPopupEvent(fileMessagePopup, "hidden");

  await movePromise;
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 6,
    "waiting for row to be removed from tree view"
  );

  // TODO: Test dragging a message to another folder.

  // Test the "Save as Search Folder" button.

  const virtualFolderDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
    undefined,
    "chrome://messenger/content/virtualFolderProperties.xhtml",
    {
      async callback(vfWin) {
        await SimpleTest.promiseFocus(vfWin);
        await BrowserTestUtils.closeWindow(vfWin);
      },
    }
  );
  EventUtils.synthesizeMouseAtCenter(saveAsVFButton, {}, win);
  await virtualFolderDialogPromise;

  await SimpleTest.promiseFocus(win);

  // Test clearing the search.

  EventUtils.synthesizeMouseAtCenter(clearButton, {}, win);
  await TestUtils.waitForCondition(
    () => searchTermList.itemCount == 1,
    "waiting for search term list to be cleared"
  );
  await TestUtils.waitForCondition(
    () => threadTree.view.rowCount == 0,
    "waiting for tree view to be cleared"
  );

  const newSearchTerm0 = searchTermList.getItemAtIndex(0);
  Assert.notEqual(newSearchTerm0, searchTerm0);
  const newInput0 = newSearchTerm0.querySelector("search-value input");
  Assert.equal(newInput0.value, "");

  await BrowserTestUtils.closeWindow(win);

  // Open the window again, and check the tree columns are as we left them.

  const window2OpenedPromise = BrowserTestUtils.domWindowOpenedAndLoaded(
    null,
    w =>
      w.document.documentURI == "chrome://messenger/content/SearchDialog.xhtml"
  );
  window.searchAllMessages();
  const win2 = await window2OpenedPromise;
  const doc2 = win.document;
  await SimpleTest.promiseFocus(win2);

  const threadTree2 = doc2.getElementById("threadTree");

  checkTreeColumnsInOrder(threadTree2, [
    "selectCol",
    "deleteCol",
    "flaggedCol",
    "attachmentCol",
    "subjectCol",
    "unreadButtonColHeader",
    "correspondentCol",
    "junkStatusCol",
    "dateCol",
    "locationCol",
  ]);

  await BrowserTestUtils.closeWindow(win2);
});

function checkTreeColumnsInOrder(tree, expectedOrder) {
  Assert.deepEqual(
    Array.from(tree.querySelectorAll("treecol:not([hidden])"))
      .sort((a, b) => a.ordinal - b.ordinal)
      .map(c => c.id),
    expectedOrder
  );
}
