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

/**
 * Test the spaces toolbar features.
 */

/* globals gSpacesToolbar */

var folderA;
var folderB;
var testAccount;

add_setup(function () {
  // Set up two folders.
  testAccount = MailServices.accounts.createLocalMailAccount();
  const rootFolder = testAccount.incomingServer.rootFolder.QueryInterface(
    Ci.nsIMsgLocalMailFolder
  );
  folderA = rootFolder.createLocalSubfolder("spacesToolbarA");
  folderB = rootFolder.createLocalSubfolder("spacesToolbarB");
});

registerCleanupFunction(async () => {
  window.MailServices.accounts.removeAccount(testAccount, true);
  // Close all opened tabs.
  const tabmail = document.getElementById("tabmail");
  tabmail.closeOtherTabs(tabmail.tabInfo[0]);
  // Reset the spaces toolbar to its default visible state.
  window.gSpacesToolbar.toggleToolbar(false);
  // Reset the menubar visibility.
  const menubar = document.getElementById("toolbar-menubar");
  menubar.removeAttribute("autohide");
  menubar.removeAttribute("inactive");
  await new Promise(resolve => requestAnimationFrame(resolve));
});

async function assertMailShown(win = window) {
  await TestUtils.waitForCondition(
    () =>
      win.document.getElementById("tabmail").currentTabInfo.mode.name ==
      "mail3PaneTab",
    "The mail tab should be visible"
  );
}

async function assertAddressBookShown(win = window) {
  await TestUtils.waitForCondition(() => {
    const panel = win.document.querySelector(
      // addressBookTabWrapper0, addressBookTabWrapper1, etc
      "#tabpanelcontainer > [id^=addressBookTabWrapper][selected]"
    );
    if (!panel) {
      return false;
    }
    const browser = panel.querySelector("[id^=addressBookTabBrowser]");
    return browser.contentDocument.readyState == "complete";
  }, "The address book tab should be visible and loaded");
}

async function assertChatShown(win = window) {
  await TestUtils.waitForCondition(
    () => win.document.getElementById("chatTabPanel").hasAttribute("selected"),
    "The chat tab should be visible"
  );
}

async function assertCalendarShown(win = window) {
  await TestUtils.waitForCondition(() => {
    return (
      win.document
        .getElementById("calendarTabPanel")
        .hasAttribute("selected") &&
      !win.document.getElementById("calendar-view-box").collapsed
    );
  }, "The calendar view should be visible");
}

async function assertTasksShown(win = window) {
  await TestUtils.waitForCondition(() => {
    return (
      win.document
        .getElementById("calendarTabPanel")
        .hasAttribute("selected") &&
      !win.document.getElementById("calendar-task-box").collapsed
    );
  }, "The task view should be visible");
}

async function assertSettingsShown(win = window) {
  await TestUtils.waitForCondition(() => {
    const panel = win.document.querySelector(
      // preferencesTabWrapper0, preferencesTabWrapper1, etc
      "#tabpanelcontainer > [id^=preferencesTabWrapper][selected]"
    );
    if (!panel) {
      return false;
    }
    const browser = panel.querySelector("[id^=preferencesTabBrowser]");
    return browser.contentDocument.readyState == "complete";
  }, "The settings tab should be visible and loaded");
}

async function assertContentShown(url, win = window) {
  await TestUtils.waitForCondition(() => {
    const panel = win.document.querySelector(
      // contentTabWrapper0, contentTabWrapper1, etc
      "#tabpanelcontainer > [id^=contentTabWrapper][selected]"
    );
    if (!panel) {
      return false;
    }
    const doc = panel.querySelector("[id^=contentTabBrowser]").contentDocument;
    return doc.URL == url && doc.readyState == "complete";
  }, `The selected content tab should show ${url}`);
}

async function sub_test_cycle_through_primary_tabs() {
  // We can't really cycle through all buttons and tabs with a simple for loop
  // since some tabs are actual collapsing views and other tabs are separate
  // pages. We can improve this once the new 3pane tab is actually a standalone
  // tab.

  // Switch to address book.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("addressBookButton"),
    {},
    window
  );
  await assertAddressBookShown();

  // Switch to calendar.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("calendarButton"),
    {},
    window
  );
  await assertCalendarShown();

  // Switch to Mail.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("mailButton"),
    {},
    window
  );
  await assertMailShown();

  // Switch to Tasks.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("tasksButton"),
    {},
    window
  );
  await assertTasksShown();

  // Switch to chat.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("chatButton"),
    {},
    window
  );
  await assertChatShown();

  // Switch to Settings.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("settingsButton"),
    {},
    window
  );
  await assertSettingsShown();

  // Switch to Mail.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("mailButton"),
    {},
    window
  );
  await assertMailShown();

  window.tabmail.closeOtherTabs(window.tabmail.tabInfo[0]);
}

add_task(async function testSpacesToolbarVisibility() {
  const spacesToolbar = document.getElementById("spacesToolbar");
  const toggleButton = document.getElementById("spacesToolbarReveal");
  const pinnedButton = document.getElementById("spacesPinnedButton");
  Assert.ok(spacesToolbar, "The spaces toolbar exists");

  const assertVisibility = async function (isHidden, msg) {
    await TestUtils.waitForCondition(
      () => spacesToolbar.hidden == !isHidden,
      `The spaces toolbar should be ${!isHidden ? "visible" : "hidden"}: ${msg}`
    );

    await TestUtils.waitForCondition(
      () => toggleButton.hidden == isHidden,
      `The toggle button should be ${isHidden ? "hidden" : "visible"}: ${msg}`
    );

    await TestUtils.waitForCondition(
      () => pinnedButton.hidden == isHidden,
      `The pinned button should be ${isHidden ? "hidden" : "visible"}: ${msg}`
    );
  };

  async function toggleVisibilityWithAppMenu(expectChecked) {
    const appMenu = document.getElementById("appMenu-popup");
    const menuShownPromise = BrowserTestUtils.waitForEvent(
      appMenu,
      "popupshown"
    );
    EventUtils.synthesizeMouseAtCenter(
      document.getElementById("button-appmenu"),
      {},
      window
    );
    await menuShownPromise;

    const viewShownPromise = BrowserTestUtils.waitForEvent(
      appMenu.querySelector("#appMenu-viewView"),
      "ViewShown"
    );
    EventUtils.synthesizeMouseAtCenter(
      appMenu.querySelector("#appmenu_View"),
      {},
      window
    );
    await viewShownPromise;

    const toolbarShownPromise = BrowserTestUtils.waitForEvent(
      appMenu.querySelector("#appMenu-toolbarsView"),
      "ViewShown"
    );
    EventUtils.synthesizeMouseAtCenter(
      appMenu.querySelector("#appmenu_Toolbars"),
      {},
      window
    );
    await toolbarShownPromise;

    const appMenuButton = document.getElementById("appmenu_spacesToolbar");
    Assert.equal(
      appMenuButton.checked,
      expectChecked,
      `The app menu item should ${expectChecked ? "not " : ""}be checked`
    );

    EventUtils.synthesizeMouseAtCenter(appMenuButton, {}, window);

    // Close the appmenu.
    EventUtils.synthesizeMouseAtCenter(
      document.getElementById("button-appmenu"),
      {},
      window
    );
  }
  await assertVisibility(true, "on initial load");

  // Collapse with a mouse click.
  let activeElement = document.activeElement;
  const collapseButton = document.getElementById("collapseButton");
  EventUtils.synthesizeMouseAtCenter(collapseButton, {}, window);
  await assertVisibility(false, "after clicking collapse button");

  await toggleVisibilityWithAppMenu(false);
  await assertVisibility(true, "after revealing with the app menu");

  // We already clicked the collapse button, so it should already be the
  // focusButton for the gSpacesToolbar, and thus focusable.
  collapseButton.focus();
  Assert.ok(
    collapseButton.matches(":focus"),
    "Collapse button should be focusable"
  );

  // Hide the spaces toolbar using the collapse button, which already has focus.
  EventUtils.synthesizeKey(" ", {}, window);
  await assertVisibility(false, "after closing with space key press");
  Assert.ok(
    pinnedButton.matches(":focus"),
    "Pinned button should be focused after closing with a key press"
  );

  // Show using the pinned button menu.
  const pinnedMenu = document.getElementById("spacesButtonMenuPopup");
  const pinnedMenuShown = BrowserTestUtils.waitForEvent(
    pinnedMenu,
    "popupshown"
  );
  EventUtils.synthesizeKey("KEY_Enter", {}, window);
  await pinnedMenuShown;
  pinnedMenu.activateItem(document.getElementById("spacesPopupButtonReveal"));

  await assertVisibility(true, "after opening with pinned menu");
  Assert.ok(
    collapseButton.matches(":focus"),
    "Collapse button should be focused again after showing with the pinned menu"
  );

  // Move focus to the mail button.
  const mailButton = document.getElementById("mailButton");
  // Loop around from the collapse button to the mailButton.
  EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);

  Assert.ok(
    mailButton.matches(":focus"),
    "Mail button should become focused after pressing key down"
  );
  Assert.ok(
    spacesToolbar.matches(":focus-within"),
    "Spaces toolbar should contain the focus"
  );

  // Now move focus elsewhere.
  EventUtils.synthesizeKey("KEY_Tab", {}, window);
  activeElement = document.activeElement;
  Assert.ok(
    !mailButton.matches(":focus"),
    "Mail button should no longer be focused"
  );
  Assert.ok(
    !spacesToolbar.matches(":focus-within"),
    "Spaces toolbar should no longer contain the focus"
  );

  // Hide the spaces toolbar using the app menu.
  await toggleVisibilityWithAppMenu(true);
  await assertVisibility(false, "after hiding with the app menu");

  // macOS by default doesn't move the focus when clicking on toolbar buttons.
  if (AppConstants.platform != "macosx") {
    Assert.notEqual(
      document.activeElement,
      activeElement,
      "The focus moved from the previous element"
    );
    // Focus should be on the main app menu since we used the mouse to toggle the
    // spaces toolbar.
    Assert.equal(
      document.activeElement,
      document.getElementById("button-appmenu"),
      "Active element is on the app menu"
    );
  } else {
    Assert.equal(
      document.activeElement,
      activeElement,
      "The focus didn't move from the previous element"
    );
  }

  // Now click the status bar toggle button to reveal the toolbar again.
  toggleButton.focus();
  Assert.ok(
    toggleButton.matches(":focus"),
    "Toggle button should be focusable"
  );
  EventUtils.synthesizeKey("KEY_Enter", {}, window);
  await assertVisibility(true, "after showing with the toggle button");
  // Focus is restored to the mailButton.
  Assert.ok(
    mailButton.matches(":focus"),
    "Mail button should become focused again"
  );

  // Clicked buttons open or move to the correct tab, starting with just one tab
  // open.
  await sub_test_cycle_through_primary_tabs();
});

add_task(async function testSpacesToolbarContextMenu() {
  const tabmail = document.getElementById("tabmail");
  const firstMailTabInfo = tabmail.currentTabInfo;
  firstMailTabInfo.folder = folderB;

  // Fetch context menu elements.
  const contextMenu = document.getElementById("spacesContextMenu");
  const newTabItem = document.getElementById("spacesContextNewTabItem");
  const newWindowItem = document.getElementById("spacesContextNewWindowItem");

  const settingsMenu = document.getElementById("settingsContextMenu");
  const settingsItem = document.getElementById(
    "settingsContextOpenSettingsItem"
  );
  const accountItem = document.getElementById(
    "settingsContextOpenAccountSettingsItem"
  );
  const addonsItem = document.getElementById("settingsContextOpenAddonsItem");

  /**
   * Open the context menu, test its state, select an action and wait for it to
   * close.
   *
   * @param {object} input - Input data.
   * @param {Element} input.button - The button whose context menu should be
   *   opened.
   * @param {Element} [input.item] - The context menu item to select. Either
   *   this or switchItem must be given.
   * @param {number} [input.switchItem] - The nth switch-to-tab item to select.
   * @param {object} expect - The expected state of the context menu when
   *   opened.
   * @param {boolean} [expect.settings=false] - Whether we expect the settings
   *   context menu. If this is true, the other values are ignored.
   * @param {boolean} [expect.newTab=false] - Whether we expect the "Open in new
   *   tab" item to be visible.
   * @param {boolean} [expect.newWindow=false] - Whether we expect the "Open in
   *   new window" item to be visible.
   * @param {number} [expect.numSwitch=0] - The expected number of switch-to-tab
   *   items.
   * @param {string} msg - A message to use in tests.
   */
  async function useContextMenu(input, expect, msg) {
    const menu = expect.settings ? settingsMenu : contextMenu;
    const shownPromise = BrowserTestUtils.waitForEvent(menu, "popupshown");
    EventUtils.synthesizeMouseAtCenter(
      input.button,
      { type: "contextmenu" },
      window
    );
    await shownPromise;
    let item = input.item;
    if (!expect.settings) {
      Assert.equal(
        BrowserTestUtils.isVisible(newTabItem),
        expect.newTab || false,
        `Open in new tab item visibility: ${msg}`
      );
      Assert.equal(
        BrowserTestUtils.isVisible(newWindowItem),
        expect.newWindow || false,
        `Open in new window item visibility: ${msg}`
      );
      const switchItems = menu.querySelectorAll(".switch-to-tab");
      Assert.equal(
        switchItems.length,
        expect.numSwitch || 0,
        `Should have the expected number of switch items: ${msg}`
      );
      if (!item) {
        item = switchItems[input.switchItem];
      }
    }
    const hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
    menu.activateItem(item);
    await hiddenPromise;
  }

  const tabScroll = document.getElementById("tabmail-arrowscrollbox").scrollbox;
  /**
   * Ensure the tab is scrolled into view.
   *
   * @param {MozTabmailTab} tab - The tab to scroll into view.
   */
  async function scrollToTab(tab) {
    function tabInView() {
      const tabRect = tab.getBoundingClientRect();
      const scrollRect = tabScroll.getBoundingClientRect();
      return (
        tabRect.left >= scrollRect.left && tabRect.right <= scrollRect.right
      );
    }
    if (tabInView()) {
      info(`Tab ${tab.label} already in view`);
      return;
    }
    tab.scrollIntoView({ block: "start", behavior: "instant" });
    await TestUtils.waitForCondition(
      tabInView,
      "Tab should be scrolled into view: " + tab.label
    );
    info(`Tab ${tab.label} was scrolled into view`);
  }

  let numTabs = 0;
  /**
   * Wait for and return the latest tab.
   *
   * This should be called every time a tab is created so the test can keep
   * track of the expected number of tabs.
   *
   * @returns {MozTabmailTab} - The last tab.
   */
  async function waitForNewTab() {
    numTabs++;
    let tabs;
    await TestUtils.waitForCondition(() => {
      tabs = document.querySelectorAll("tab.tabmail-tab");
      return tabs.length == numTabs;
    }, `Waiting for ${numTabs} tabs`);
    return tabs[numTabs - 1];
  }

  /**
   * Close a tab and wait for it to close.
   *
   * This should be used alongside waitForNewTab so the test can keep track of
   * the expected number of tabs.
   *
   * @param {MozTabmailTab} tab - The tab to close.
   */
  async function closeTab(tab) {
    numTabs--;
    await scrollToTab(tab);
    EventUtils.synthesizeMouseAtCenter(
      tab.querySelector(".tab-close-button"),
      {},
      window
    );
    await TestUtils.waitForCondition(
      () => document.querySelectorAll("tab.tabmail-tab").length == numTabs,
      "Waiting for tab to close"
    );
  }

  const toolbar = document.getElementById("spacesToolbar");
  /**
   * Verify the current tab and space match.
   *
   * @param {MozTabmailTab} tab - The expected tab.
   * @param {Element} spaceButton - The expected button to be shown as the
   *   current space in the spaces toolbar.
   * @param {string} msg - A message to use in tests.
   */
  async function assertTab(tab, spaceButton, msg) {
    await TestUtils.waitForCondition(
      () => tab.selected,
      `Tab should be selected: ${msg}`
    );
    const current = toolbar.querySelectorAll("button.current");
    Assert.equal(current.length, 1, `Should have one current space: ${msg}`);
    Assert.equal(
      current[0],
      spaceButton,
      `Current button ${current[0].id} should match: ${msg}`
    );
  }

  /**
   * Click on a tab and verify we have switched tabs and spaces.
   *
   * @param {MozTabmailTab} tab - The tab to click.
   * @param {Element} spaceButton - The expected button to be shown as the
   *   current space after clicking the tab.
   * @param {string} msg - A message to use in tests.
   */
  async function switchTab(tab, spaceButton, msg) {
    await scrollToTab(tab);
    EventUtils.synthesizeMouseAtCenter(tab, {}, window);
    await assertTab(tab, spaceButton, msg);
  }

  // -- Test initial tab --

  const mailButton = document.getElementById("mailButton");
  const firstTab = await waitForNewTab();
  await assertTab(firstTab, mailButton, "First tab is mail tab");
  await assertMailShown();

  // -- Test spaces that only open one tab --

  let calendarTab;
  const calendarButton = document.getElementById("calendarButton");
  for (const { name, button, assertShown } of [
    {
      name: "address book",
      button: document.getElementById("addressBookButton"),
      assertShown: assertAddressBookShown,
    },
    {
      name: "calendar",
      button: calendarButton,
      assertShown: assertCalendarShown,
    },
    {
      name: "tasks",
      button: document.getElementById("tasksButton"),
      assertShown: assertTasksShown,
    },
    {
      name: "chat",
      button: document.getElementById("chatButton"),
      assertShown: assertChatShown,
    },
  ]) {
    info(`Testing ${name} space`);
    // Only have option to open in new tab.
    await useContextMenu(
      { button, item: newTabItem },
      { newTab: true },
      `Opening ${name} tab`
    );
    const newTab = await waitForNewTab();
    if (name == "calendar") {
      calendarTab = newTab;
    }
    await assertTab(newTab, button, `Opened ${name} tab`);
    await assertShown();
    // Only have option to switch tabs.
    // Doing this from the same tab does nothing.
    await useContextMenu(
      { button, switchItem: 0 },
      { numSwitch: 1 },
      `When ${name} tab is open`
    );
    // Wait one tick to allow tabs to potentially change.
    await TestUtils.waitForTick();
    // However, the same tab should remain shown.
    await assertShown();

    // Switch to first tab and back.
    await switchTab(firstTab, mailButton, `${name} to first tab`);
    await assertMailShown();
    await useContextMenu(
      { button, switchItem: 0 },
      { numSwitch: 1 },
      `Switching from first tab to ${name}`
    );
    await assertTab(newTab, button, `Switched from first tab to ${name}`);
    await assertShown();
  }

  // -- Test opening mail space in a new tab --

  // Open new mail tabs whilst we are still in a non-mail tab.
  await useContextMenu(
    { button: mailButton, item: newTabItem },
    { newWindow: true, newTab: true, numSwitch: 1 },
    "Opening the second mail tab"
  );
  const secondMailTab = await waitForNewTab();
  await assertTab(secondMailTab, mailButton, "Opened second mail tab");
  await assertMailShown();
  // Displayed folder should be the same as in the first mail tab.
  const [, secondMailTabInfo] =
    tabmail._getTabContextForTabbyThing(secondMailTab);
  await TestUtils.waitForCondition(
    () => secondMailTabInfo.folder?.URI == folderB.URI,
    "Should display folder B in the second mail tab"
  );

  secondMailTabInfo.folder = folderA;

  // Open a new mail tab whilst in a mail tab.
  await useContextMenu(
    { button: mailButton, item: newTabItem },
    { newWindow: true, newTab: true, numSwitch: 2 },
    "Opening the third mail tab"
  );
  const thirdMailTab = await waitForNewTab();
  await assertTab(thirdMailTab, mailButton, "Opened third mail tab");
  await assertMailShown();
  // Displayed folder should be the same as in the mail tab that was in view
  // when the context menu was opened, rather than the folder in the first tab.
  const [, thirdMailTabInfo] =
    tabmail._getTabContextForTabbyThing(thirdMailTab);
  await TestUtils.waitForCondition(
    () => thirdMailTabInfo.folder?.URI == folderA.URI,
    "Should display folder A in the third mail tab"
  );

  // -- Test switching between the multiple mail tabs --

  await useContextMenu(
    { button: mailButton, switchItem: 1 },
    { newWindow: true, newTab: true, numSwitch: 3 },
    "Switching to second mail tab"
  );
  await assertTab(secondMailTab, mailButton, "Switch to second mail tab");
  await assertMailShown();
  await useContextMenu(
    { button: mailButton, switchItem: 0 },
    { newWindow: true, newTab: true, numSwitch: 3 },
    "Switching to first mail tab"
  );
  await assertTab(firstTab, mailButton, "Switch to first mail tab");
  await assertMailShown();

  await switchTab(calendarTab, calendarButton, "First mail to calendar tab");
  await useContextMenu(
    { button: mailButton, switchItem: 2 },
    { newWindow: true, newTab: true, numSwitch: 3 },
    "Switching to third mail tab"
  );
  await assertTab(thirdMailTab, mailButton, "Switch to third mail tab");
  await assertMailShown();

  // -- Test the mail button with multiple mail tabs --

  // Clicking the mail button whilst in the mail space does nothing.
  // Specifically, we do not want it to take us to the first tab.
  EventUtils.synthesizeMouseAtCenter(mailButton, {}, window);
  // Wait one cycle to see if the tab would change.
  await TestUtils.waitForTick();
  await assertTab(thirdMailTab, mailButton, "Remain in third tab");
  await assertMailShown();
  Assert.equal(
    thirdMailTabInfo.folder.URI,
    folderA.URI,
    "Still display folder A in the third mail tab"
  );

  // Clicking the mail button whilst in a different space takes us to the first
  // mail tab.
  await switchTab(calendarTab, calendarButton, "Third mail to calendar tab");
  EventUtils.synthesizeMouseAtCenter(mailButton, {}, window);
  await assertTab(firstTab, mailButton, "Switch to the first mail tab");
  await assertMailShown();
  Assert.equal(
    firstMailTabInfo.folder.URI,
    folderB.URI,
    "Still display folder B in the first mail tab"
  );

  // -- Test opening the mail space in a new window --

  const windowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
  await useContextMenu(
    { button: mailButton, item: newWindowItem },
    { newWindow: true, newTab: true, numSwitch: 3 },
    "Opening mail tab in new window"
  );
  const newMailWindow = await windowPromise;
  const newTabmail = newMailWindow.document.getElementById("tabmail");
  // Expect the same folder as the previously focused tab.
  await TestUtils.waitForCondition(
    () => newTabmail.currentTabInfo.folder?.URI == folderB.URI,
    "Waiting for folder B to be displayed in the new window"
  );
  Assert.equal(
    newMailWindow.document.querySelectorAll("tab.tabmail-tab").length,
    1,
    "Should only have one tab in the new window"
  );
  await assertMailShown(newMailWindow);

  // -- Test opening different tabs that belong to the settings space --

  const settingsButton = document.getElementById("settingsButton");
  await useContextMenu(
    { button: settingsButton, item: accountItem },
    { settings: true },
    "Opening account settings"
  );
  const accountTab = await waitForNewTab();
  // Shown as part of the settings space.
  await assertTab(accountTab, settingsButton, "Opened account settings tab");
  await assertContentShown("about:accountsettings");

  await useContextMenu(
    { button: settingsButton, item: settingsItem },
    { settings: true },
    "Opening settings"
  );
  let settingsTab = await waitForNewTab();
  // Shown as part of the settings space.
  await assertTab(settingsTab, settingsButton, "Opened settings tab");
  await assertSettingsShown();

  await useContextMenu(
    { button: settingsButton, item: addonsItem },
    { settings: true },
    "Opening add-ons"
  );
  const addonsTab = await waitForNewTab();
  // Shown as part of the settings space.
  await assertTab(addonsTab, settingsButton, "Opened add-ons tab");
  await assertContentShown("about:addons");

  // -- Test the settings button with multiple settings tabs --

  // Clicking the settings button whilst in the settings space does nothing.
  EventUtils.synthesizeMouseAtCenter(settingsButton, {}, window);
  // Wait one cycle to see if the tab would change.
  await TestUtils.waitForTick();
  await assertTab(addonsTab, settingsButton, "Remain in add-ons tab");
  await assertContentShown("about:addons");

  // Clicking the settings button whilst in a different space takes us to the
  // settings tab, rather than the first tab, since this is the primary tab for
  // the space.
  await switchTab(calendarTab, calendarButton, "Add-ons to calendar tab");
  EventUtils.synthesizeMouseAtCenter(settingsButton, {}, window);
  await assertTab(settingsTab, settingsButton, "Switch to the settings tab");
  await assertSettingsShown();

  // Clicking the settings button whilst in a different space and no settings
  // tab will open a new settings tab, rather than switch to another tab in the
  // settings space because they are not the primary tab for the space.
  await closeTab(settingsTab);
  await switchTab(calendarTab, calendarButton, "Settings to calendar tab");
  EventUtils.synthesizeMouseAtCenter(settingsButton, {}, window);
  settingsTab = await waitForNewTab();
  await assertTab(settingsTab, settingsButton, "Re-opened settings tab");
  await assertSettingsShown();

  // -- Test opening different settings tabs when they already exist --

  await useContextMenu(
    { button: settingsButton, item: addonsItem },
    { settings: true },
    "Switching to add-ons"
  );
  await assertTab(addonsTab, settingsButton, "Switched to add-ons");
  await assertContentShown("about:addons");

  await useContextMenu(
    { button: settingsButton, item: accountItem },
    { settings: true },
    "Switching to account settings"
  );
  await assertTab(accountTab, settingsButton, "Switched to account settings");
  await assertContentShown("about:accountsettings");

  await useContextMenu(
    { button: settingsButton, item: settingsItem },
    { settings: true },
    "Switching to settings"
  );
  await assertTab(settingsTab, settingsButton, "Switched to settings");
  await assertSettingsShown();

  // -- Test clicking the spaces buttons when all the tabs are already open.

  await sub_test_cycle_through_primary_tabs();

  // Tidy up the opened window.
  // FIXME: Closing the window earlier in the test causes a test failure on the
  // osx build on the try server.
  await BrowserTestUtils.closeWindow(newMailWindow);
});

add_task(async function testSpacesToolbarMenubar() {
  document.getElementById("toolbar-menubar").removeAttribute("autohide");
  const spacesToolbar = document.getElementById("spacesToolbar");

  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("collapseButton"),
    {},
    window
  );
  Assert.ok(spacesToolbar.hidden, "The spaces toolbar is hidden");
  Assert.ok(
    !document.getElementById("spacesToolbarReveal").hidden,
    "The status bar toggle button is visible"
  );

  // Test the menubar button.
  const viewShownPromise = BrowserTestUtils.waitForEvent(
    document.getElementById("menu_View_Popup"),
    "popupshown"
  );
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("menu_View"),
    {},
    window
  );
  await viewShownPromise;

  const toolbarsShownPromise = BrowserTestUtils.waitForEvent(
    document.getElementById("view_toolbars_popup"),
    "popupshown"
  );
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("menu_Toolbars"),
    {},
    window
  );
  await toolbarsShownPromise;

  const menuButton = document.getElementById("viewToolbarsPopupSpacesToolbar");
  Assert.ok(
    menuButton.getAttribute("checked") != "true",
    "The menu item is not checked"
  );

  const viewHiddenPromise = BrowserTestUtils.waitForEvent(
    document.getElementById("menu_View_Popup"),
    "popuphidden"
  );
  EventUtils.synthesizeMouseAtCenter(menuButton, {}, window);
  await viewHiddenPromise;

  Assert.ok(
    menuButton.getAttribute("checked") == "true",
    "The menu item is checked"
  );
}).skip(AppConstants.platform == "macosx"); // Can't click menu bar on Mac.

add_task(async function testSpacesToolbarOSX() {
  const size = document
    .getElementById("spacesToolbar")
    .getBoundingClientRect().width;

  // By default, macOS shouldn't need any custom styling.
  Assert.ok(
    !document.getElementById("titlebar").hasAttribute("style"),
    "The custom styling was cleared from all toolbars"
  );

  const styleAppliedPromise = BrowserTestUtils.waitForCondition(
    () =>
      document.getElementById("tabmail-tabs").getAttribute("style") ==
      `margin-inline-start: ${size}px;`,
    "The correct style was applied to the tabmail"
  );

  // Force full screen.
  window.fullScreen = true;
  await new Promise(resolve => requestAnimationFrame(resolve));
  await styleAppliedPromise;

  const styleRemovedPromise = BrowserTestUtils.waitForCondition(
    () => !document.getElementById("tabmail-tabs").hasAttribute("style"),
    "The custom styling was cleared from all toolbars"
  );
  // Restore original window size.
  window.fullScreen = false;
  await new Promise(resolve => requestAnimationFrame(resolve));
  await styleRemovedPromise;
}).skip(AppConstants.platform != "macosx");

add_task(async function testSpacesToolbarClearedAlignment() {
  // Hide the spaces toolbar to check if the style it's cleared.
  window.gSpacesToolbar.toggleToolbar(true);
  Assert.ok(
    !document.getElementById("titlebar").hasAttribute("style") &&
      !document.getElementById("navigation-toolbox").hasAttribute("style"),
    "The custom styling was cleared from all toolbars"
  );
});

add_task(async function testSpacesToolbarExtension() {
  window.gSpacesToolbar.toggleToolbar(false);

  for (let i = 0; i < 6; i++) {
    await window.gSpacesToolbar.createToolbarButton(
      `testButton${i}`,
      {
        url: `https://test.invalid/${i}`,
      },
      {
        title: `Title ${i}`,
        iconStyles: new Map([
          [
            "--webextension-toolbar-image",
            'url("chrome://messenger/content/extension.svg")',
          ],
        ]),
      }
    );
    const button = document.getElementById(`testButton${i}`);
    Assert.ok(button);
    Assert.equal(button.title, `Title ${i}`);

    const img = button.querySelector("img");
    Assert.equal(
      img.style.getPropertyValue("--webextension-toolbar-image"),
      `url("chrome://messenger/content/extension.svg")`,
      `Button image should have the correct icon.`
    );

    const menuitem = document.getElementById(`testButton${i}-menuitem`);
    Assert.ok(menuitem);
    Assert.equal(menuitem.label, `Title ${i}`);
    Assert.equal(
      menuitem.style.getPropertyValue("--webextension-toolbar-image"),
      `url("chrome://messenger/content/extension.svg")`,
      `Menuitem should have the correct icon.`
    );

    const space = window.gSpacesToolbar.spaces.find(
      s => s.name == `testButton${i}`
    );
    Assert.ok(space);
    Assert.equal(
      space.tabProperties.url,
      `https://test.invalid/${i}`,
      "Added url should be correct."
    );
  }

  for (let i = 0; i < 6; i++) {
    await window.gSpacesToolbar.updateToolbarButton(
      `testButton${i}`,
      {
        url: `https://test.invalid/${i + 1}`,
      },
      {
        title: `Modified Title ${i}`,
        iconStyles: new Map([
          [
            "--webextension-toolbar-image",
            'url("chrome://messenger/skin/icons/new-addressbook.svg")',
          ],
        ]),
      }
    );
    const button = document.getElementById(`testButton${i}`);
    Assert.ok(button);
    Assert.equal(button.title, `Modified Title ${i}`);

    const img = button.querySelector("img");
    Assert.equal(
      img.style.getPropertyValue("--webextension-toolbar-image"),
      `url("chrome://messenger/skin/icons/new-addressbook.svg")`,
      `Button image should have the correct icon.`
    );

    const menuitem = document.getElementById(`testButton${i}-menuitem`);
    Assert.ok(menuitem);
    Assert.equal(
      menuitem.label,
      `Modified Title ${i}`,
      "Updated title should be correct."
    );
    Assert.equal(
      menuitem.style.getPropertyValue("--webextension-toolbar-image"),
      `url("chrome://messenger/skin/icons/new-addressbook.svg")`,
      `Menuitem should have the correct icon.`
    );

    const space = window.gSpacesToolbar.spaces.find(
      s => s.name == `testButton${i}`
    );
    Assert.ok(space);
    Assert.equal(
      space.tabProperties.url,
      `https://test.invalid/${i + 1}`,
      "Updated url should be correct."
    );
  }

  const overflowButton = document.getElementById(
    "spacesToolbarAddonsOverflowButton"
  );

  const originalHeight = window.outerHeight;
  // Set a ridiculous tiny height to be sure all add-on buttons are hidden.
  window.resizeTo(window.outerWidth, 300);
  await new Promise(resolve => requestAnimationFrame(resolve));
  await BrowserTestUtils.waitForCondition(
    () => !overflowButton.hidden,
    "The overflow button is visible"
  );

  const overflowPopup = document.getElementById("spacesToolbarAddonsPopup");
  const popupshown = BrowserTestUtils.waitForEvent(overflowPopup, "popupshown");
  overflowButton.click();
  await popupshown;

  Assert.ok(overflowPopup.hasChildNodes());

  const popuphidden = BrowserTestUtils.waitForEvent(
    overflowPopup,
    "popuphidden"
  );
  // Restore the original height.
  window.resizeTo(window.outerWidth, originalHeight);
  await new Promise(resolve => requestAnimationFrame(resolve));

  await popuphidden;
  await BrowserTestUtils.waitForCondition(
    () => overflowButton.hidden,
    "The overflow button is hidden"
  );

  // Remove all previously added toolbar buttons and make sure all previously
  // generate elements are properly cleared.
  for (let i = 0; i < 6; i++) {
    await window.gSpacesToolbar.removeToolbarButton(`testButton${i}`);
    const space = window.gSpacesToolbar.spaces.find(
      s => s.name == `testButton${i}`
    );
    Assert.ok(!space);

    const button = document.getElementById(`testButton${i}`);
    Assert.ok(!button);

    const menuitem = document.getElementById(`testButton${i}-menuitem`);
    Assert.ok(!menuitem);
  }
});

add_task(function testPinnedSpacesBadge() {
  window.gSpacesToolbar.toggleToolbar(true);
  const spacesPinnedButton = document.getElementById("spacesPinnedButton");
  const spacesPopupButtonChat = document.getElementById(
    "spacesPopupButtonChat"
  );

  window.gSpacesToolbar.updatePinnedBadgeState();

  Assert.ok(
    !spacesPinnedButton.classList.contains("has-badge"),
    "Pinned button does not indicate badged items without any"
  );

  spacesPopupButtonChat.classList.add("has-badge");
  window.gSpacesToolbar.updatePinnedBadgeState();

  Assert.ok(
    spacesPinnedButton.classList.contains("has-badge"),
    "Pinned button indicates it has badged items"
  );

  spacesPopupButtonChat.classList.remove("has-badge");
  window.gSpacesToolbar.updatePinnedBadgeState();

  Assert.ok(
    !spacesPinnedButton.classList.contains("has-badge"),
    "Badge state is reset from pinned button"
  );
});

add_task(async function testSpacesToolbarFocusRing() {
  // Make sure the spaces toolbar is visible.
  window.gSpacesToolbar.toggleToolbar(false);
  // Move the focus ring on the mail toolbar button.
  document.getElementById("mailButton").focus();

  // Collect an array of all currently visible buttons.
  const buttons = [
    ...document.querySelectorAll(".spaces-toolbar-button:not([hidden])"),
  ];

  // Simulate the Arrow Down keypress to make sure the correct button gets the
  // focus.
  for (let i = 1; i < buttons.length; i++) {
    const previousElement = document.activeElement;
    EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
    Assert.equal(
      document.activeElement.id,
      buttons[i].id,
      "The next button is focused"
    );
    Assert.ok(
      document.activeElement.tabIndex == 0 && previousElement.tabIndex == -1,
      "The roving tab index was updated"
    );
  }

  // Do the same with the Arrow Up key press but reversing the array.
  buttons.reverse();
  for (let i = 1; i < buttons.length; i++) {
    const previousElement = document.activeElement;
    EventUtils.synthesizeKey("KEY_ArrowUp", {}, window);
    Assert.equal(
      document.activeElement.id,
      buttons[i].id,
      "The previous button is focused"
    );
    Assert.ok(
      document.activeElement.tabIndex == 0 && previousElement.tabIndex == -1,
      "The roving tab index was updated"
    );
  }

  // Pressing the END key should move the focus down to the last available
  // button.
  EventUtils.synthesizeKey("KEY_End", {}, window);
  Assert.equal(
    document.activeElement.id,
    "collapseButton",
    "The last button is focused"
  );

  // Pressing the HOME key should move the focus up to the first available
  // button.
  EventUtils.synthesizeKey("KEY_Home", {}, window);
  Assert.equal(
    document.activeElement.id,
    "mailButton",
    "The first button is focused"
  );

  // Focus follows the mouse click.
  EventUtils.synthesizeMouseAtCenter(
    document.getElementById("calendarButton"),
    {},
    window
  );
  Assert.equal(
    document.activeElement.id,
    "calendarButton",
    "Focus should move to the clicked calendar button"
  );

  // Now press a key to make sure roving index was updated with the click.
  EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
  Assert.equal(
    document.activeElement.id,
    "tasksButton",
    "Focus should move to the tasks button"
  );
});
