/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

/* exported
     assertDevToolsExtensionEnabled,
     closeToolboxForTab,
     navigateToWithDevToolsOpen
     openToolboxForTab,
     registerBlankToolboxPanel,
     TOOLBOX_BLANK_PANEL_ID,
*/

ChromeUtils.defineESModuleGetters(this, {
  loader: "resource://devtools/shared/loader/Loader.sys.mjs",
  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
});
XPCOMUtils.defineLazyGetter(this, "gDevTools", () => {
  const { gDevTools } = loader.require("devtools/client/framework/devtools");
  return gDevTools;
});

const TOOLBOX_BLANK_PANEL_ID = "testBlankPanel";

// Register a blank custom tool so that we don't need to wait the webconsole
// to be fully loaded/unloaded to prevent intermittent failures (related
// to a webconsole that is still loading when the test has been completed).
async function registerBlankToolboxPanel() {
  const testBlankPanel = {
    id: TOOLBOX_BLANK_PANEL_ID,
    url: "about:blank",
    label: "Blank Tool",
    isToolSupported() {
      return true;
    },
    build(iframeWindow, toolbox) {
      return Promise.resolve({
        target: toolbox.target,
        toolbox: toolbox,
        isReady: true,
        panelDoc: iframeWindow.document,
        destroy() {},
      });
    },
  };

  registerCleanupFunction(() => {
    gDevTools.unregisterTool(testBlankPanel.id);
  });

  gDevTools.registerTool(testBlankPanel);
}

async function openToolboxForTab(tab, panelId = TOOLBOX_BLANK_PANEL_ID) {
  if (
    panelId == TOOLBOX_BLANK_PANEL_ID &&
    !gDevTools.getToolDefinition(panelId)
  ) {
    info(`Registering ${TOOLBOX_BLANK_PANEL_ID} tool to the developer tools`);
    registerBlankToolboxPanel();
  }

  const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: panelId });
  const { url, outerWindowID } = toolbox.target.form;
  info(
    `Developer toolbox opened on panel "${panelId}" for target ${JSON.stringify(
      { url, outerWindowID }
    )}`
  );
  return toolbox;
}

async function closeToolboxForTab(tab) {
  await gDevTools.closeToolboxForTab(tab);
  const tabUrl = tab.linkedBrowser.currentURI.spec;
  info(`Developer toolbox closed for tab "${tabUrl}"`);
}

function assertDevToolsExtensionEnabled(uuid, enabled) {
  for (let toolbox of DevToolsShim.getToolboxes()) {
    is(
      enabled,
      !!toolbox.isWebExtensionEnabled(uuid),
      `extension is ${enabled ? "enabled" : "disabled"} on toolbox`
    );
  }
}

/**
 * Navigate the currently selected tab to a new URL and wait for it to load.
 * Also wait for the toolbox to attach to the new target, if we navigated
 * to a new process.
 *
 * @param {object} tab The tab to redirect.
 * @param {string} uri The url to be loaded in the current tab.
 * @param {boolean} isErrorPage You may pass `true` is the URL is an error
 *                    page. Otherwise BrowserTestUtils.browserLoaded will wait
 *                    for 'load' event, which never fires for error pages.
 *
 * @returns {Promise} A promise that resolves when the page has fully loaded.
 */
async function navigateToWithDevToolsOpen(tab, uri, isErrorPage = false) {
  const toolbox = await gDevTools.getToolboxForTab(tab);
  const target = toolbox.target;

  // If we're switching origins, we need to wait for the 'switched-target'
  // event to make sure everything is ready.
  // Navigating from/to pages loaded in the parent process, like about:robots,
  // also spawn new targets.
  // (If target switching is disabled, the toolbox will reboot)
  const onTargetSwitched =
    toolbox.commands.targetCommand.once("switched-target");
  // Otherwise, if we don't switch target, it is safe to wait for navigate event.
  const onNavigate = target.once("navigate");

  // If the current top-level target follows the window global lifecycle, a
  // target switch will occur regardless of process changes.
  const targetFollowsWindowLifecycle =
    target.targetForm.followWindowGlobalLifeCycle;

  info(`Load document "${uri}"`);
  const browser = gBrowser.selectedBrowser;
  const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
  const currentBrowsingContextID = browser.browsingContext.id;
  const onBrowserLoaded = BrowserTestUtils.browserLoaded(
    browser,
    false,
    null,
    isErrorPage
  );
  BrowserTestUtils.loadURIString(browser, uri);

  info(`Waiting for page to be loaded…`);
  await onBrowserLoaded;
  info(`→ page loaded`);

  // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
  // while target may be updated slightly later.
  const switchedToAnotherProcess =
    currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
  const switchedToAnotherBrowsingContext =
    currentBrowsingContextID !== browser.browsingContext.id;

  // If:
  // - the tab navigated to another process, or,
  // - the tab navigated to another browsing context, or,
  // - if the old target follows the window lifecycle
  // then, expect a target switching.
  if (
    switchedToAnotherProcess ||
    targetFollowsWindowLifecycle ||
    switchedToAnotherBrowsingContext
  ) {
    info(`Waiting for target switch…`);
    await onTargetSwitched;
    info(`→ switched-target emitted`);
  } else {
    info(`Waiting for target 'navigate' event…`);
    await onNavigate;
    info(`→ 'navigate' emitted`);
  }
}
