/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

import { Assert } from "resource://testing-common/Assert.sys.mjs";
import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
});

async function promiseAnimationFrame(window) {
  await new Promise(resolve => window.requestAnimationFrame(resolve));

  let { tm } = Services;
  return new Promise(resolve => tm.dispatchToMainThread(resolve));
}

function makeWidgetId(id) {
  id = id.toLowerCase();
  return id.replace(/[^a-z0-9_-]/g, "_");
}

async function getPageActionButtonId(window, extensionId) {
  // This would normally be set automatically on navigation, and cleared
  // when the user types a value into the URL bar, to show and hide page
  // identity info and icons such as page action buttons.
  //
  // Unfortunately, that doesn't happen automatically in browser chrome
  // tests.
  window.gURLBar.setPageProxyState("valid");

  let { gIdentityHandler } = window.gBrowser.ownerGlobal;
  // If the current tab is blank and the previously selected tab was an internal
  // page, the urlbar will now be showing the internal identity box due to the
  // setPageProxyState call above.  The page action button is hidden in that
  // case, so make sure we're not showing the internal identity box.
  gIdentityHandler._identityBox.classList.remove("chromeUI");

  await promiseAnimationFrame(window);

  return window.BrowserPageActions.urlbarButtonNodeIDForActionID(
    makeWidgetId(extensionId)
  );
}

async function getPageActionButton(window, extensionId) {
  let pageActionId = await getPageActionButtonId(window, extensionId);
  return window.document.getElementById(pageActionId);
}

async function clickPageAction(window, extensionId, modifiers = {}) {
  let pageActionId = await getPageActionButtonId(window, extensionId);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    `#${pageActionId}`,
    modifiers,
    window.browsingContext
  );
  return new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
}

function getBrowserActionWidgetId(extensionId) {
  return makeWidgetId(extensionId) + "-browser-action";
}

function getBrowserActionWidget(extensionId) {
  return lazy.CustomizableUI.getWidget(getBrowserActionWidgetId(extensionId));
}

async function showBrowserAction(window, extensionId) {
  let group = getBrowserActionWidget(extensionId);
  let widget = group.forWindow(window);
  if (!widget.node) {
    return;
  }

  let navbar = window.document.getElementById("nav-bar");
  if (group.areaType == lazy.CustomizableUI.TYPE_TOOLBAR) {
    Assert.equal(
      widget.overflowed,
      navbar.hasAttribute("overflowing"),
      "Expect widget overflow state to match toolbar"
    );
  } else if (group.areaType == lazy.CustomizableUI.TYPE_PANEL) {
    let panel = window.gUnifiedExtensions.panel;
    let shown = BrowserTestUtils.waitForPopupEvent(panel, "shown");
    window.gUnifiedExtensions.togglePanel();
    await shown;
  }
}

async function clickBrowserAction(window, extensionId, modifiers) {
  await promiseAnimationFrame(window);
  await showBrowserAction(window, extensionId);

  if (modifiers) {
    let widgetId = getBrowserActionWidgetId(extensionId);
    BrowserTestUtils.synthesizeMouseAtCenter(
      `#${widgetId}`,
      modifiers,
      window.browsingContext
    );
  } else {
    let widget = getBrowserActionWidget(extensionId).forWindow(window);
    widget.node.firstElementChild.click();
  }
}

async function promisePopupShown(popup) {
  return new Promise(resolve => {
    if (popup.state == "open") {
      resolve();
    } else {
      let onPopupShown = event => {
        popup.removeEventListener("popupshown", onPopupShown);
        resolve();
      };
      popup.addEventListener("popupshown", onPopupShown);
    }
  });
}

function awaitBrowserLoaded(browser) {
  if (
    browser.ownerGlobal.document.readyState === "complete" &&
    browser.currentURI.spec !== "about:blank"
  ) {
    return Promise.resolve();
  }
  return new Promise(resolve => {
    const listener = ev => {
      if (browser.currentURI.spec === "about:blank") {
        return;
      }
      browser.removeEventListener("AppTestDelegate:load", listener);
      resolve();
    };
    browser.addEventListener("AppTestDelegate:load", listener);
  });
}

function getPanelForNode(node) {
  return node.closest("panel");
}

async function awaitExtensionPanel(window, extensionId, awaitLoad = true) {
  let { originalTarget: browser } = await BrowserTestUtils.waitForEvent(
    window.document,
    "WebExtPopupLoaded",
    true,
    event => event.detail.extension.id === extensionId
  );

  await Promise.all([
    promisePopupShown(getPanelForNode(browser)),

    awaitLoad && awaitBrowserLoaded(browser),
  ]);

  return browser;
}

function closeBrowserAction(window, extensionId) {
  let group = getBrowserActionWidget(extensionId);

  let node = window.document.getElementById(group.viewId);
  lazy.CustomizableUI.hidePanelForNode(node);

  return Promise.resolve();
}

function getPageActionPopup(window, extensionId) {
  let panelId = makeWidgetId(extensionId) + "-panel";
  return window.document.getElementById(panelId);
}

function closePageAction(window, extensionId) {
  let node = getPageActionPopup(window, extensionId);
  if (node) {
    return promisePopupShown(node).then(() => {
      node.hidePopup();
    });
  }

  return Promise.resolve();
}

function openNewForegroundTab(window, url, waitForLoad = true) {
  return BrowserTestUtils.openNewForegroundTab(
    window.gBrowser,
    url,
    waitForLoad
  );
}

async function removeTab(tab) {
  BrowserTestUtils.removeTab(tab);
}

// These methods are exported so that they can be used in head.js but are
// *not* part of the AppUiTestDelegate API.
export var AppUiTestInternals = {
  awaitBrowserLoaded,
  getBrowserActionWidget,
  getBrowserActionWidgetId,
  getPageActionButton,
  getPageActionPopup,
  getPanelForNode,
  makeWidgetId,
  promiseAnimationFrame,
  promisePopupShown,
  showBrowserAction,
};

// These methods are part of the AppUiTestDelegate API. All implementations need
// to be kept in sync. For details, see:
// testing/specialpowers/content/AppTestDelegateParent.sys.mjs
export var AppUiTestDelegate = {
  awaitExtensionPanel,
  clickBrowserAction,
  clickPageAction,
  closeBrowserAction,
  closePageAction,
  openNewForegroundTab,
  removeTab,
};
