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

"use strict";

/* import-globals-from head.js */

// Helpers for the remote debugging backward compatibility tests, which connect
// this Firefox to a second Firefox provisioned by ./mach devtools-compat-test.
// Documentation in /documentation/TESTS_BACKWARD_COMPAT.md

const COMPAT_CONFIG_ENV_VAR = "DEVTOOLS_COMPAT_CONFIG";

// The fixture pages are served by the regular mochitest HTTP server, but the
// server Firefox runs with its own profile and has no proxy configured, so it
// cannot use the example.com aliases the other URL_ROOT constants rely on.
// 127.0.0.1 is served directly (it is registered in build/pgo/server-locations.txt)
// and, unlike mochi.test, it is a secure context, which the service worker
// fixture needs. The port matches the one declared in server-locations.txt.
const FIXTURE_ROOT =
  CHROME_URL_ROOT.replace(
    "chrome://mochitests/content/",
    "http://127.0.0.1:8888/"
  ) + "resources/backward-compat/";

/**
 * Read the configuration written by ./mach devtools-compat-test.
 *
 * @returns {object|null}
 *          The parsed configuration, or null when no server was provisioned,
 *          which is the case on regular pushes and for a plain `mach mochitest`
 *          run. The configuration holds the name of the `server` recipe, the
 *          `host` of the server as a "host:port" string, the `control` channel
 *          base URL, and a `runtime` object describing the server application
 *          with its brandName, version and channel.
 */
async function getCompatConfig() {
  const path = Services.env.get(COMPAT_CONFIG_ENV_VAR);
  if (!path) {
    return null;
  }

  info(`Load backward compatibility configuration from ${path}`);
  const buffer = await IOUtils.read(path);
  return JSON.parse(new TextDecoder().decode(buffer));
}
/* exported getCompatConfig */

/**
 * Register a test which only runs when a server has been provisioned. Without
 * one the task reports a single passing assertion and returns immediately.
 *
 * @param {Function} taskFn
 *        Async test function, called with the configuration returned by
 *        getCompatConfig as its only argument.
 */
function addCompatTask(taskFn) {
  add_task(async function () {
    const config = await getCompatConfig();
    if (!config) {
      ok(
        true,
        `No ${COMPAT_CONFIG_ENV_VAR} in the environment, skipping. ` +
          "Run these tests with `./mach devtools-compat-test`"
      );
      return;
    }

    const { brandName, version, channel } = config.runtime;
    info(
      `Testing against ${brandName} ${version} (${channel}) on ${config.host}`
    );
    await taskFn(config);
  });
}
/* exported addCompatTask */

/**
 * Register the provisioned server as an about:debugging network location and
 * open about:debugging.
 *
 * @param {object} config
 *        The configuration returned by getCompatConfig.
 * @param {object} options
 *        Options forwarded to openAboutDebugging.
 * @returns {object} An object with the properties { tab, document, window }
 *          describing the create about:debugging tab.
 */
async function openAboutDebuggingWithCompatServer(config, options = {}) {
  await pushPref(
    "devtools.aboutdebugging.network-locations",
    JSON.stringify([config.host])
  );

  return openAboutDebugging(options);
}
/* exported openAboutDebuggingWithCompatServer */

/**
 * Run a command on the server Firefox, through the harness control channel.
 *
 * @param {object} config
 *        The configuration returned by getCompatConfig.
 * @param {string} name
 *        Name of the command, see backward_compat_test_server/control.py.
 * @param {object} args
 *        Arguments for the command.
 * @returns {object} The result of the command.
 */
async function runDevToolsServerCommand(config, name, args = {}) {
  info(`Run the '${name}' command on the server`);
  const response = await fetch(`${config.control}/command`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name, args }),
  });

  const body = await response.json();
  if (!response.ok) {
    throw new Error(`Command '${name}' failed: ${body.error}`);
  }
  return body.result;
}
/* exported runDevToolsServerCommand */

/**
 * Open a tab on the server Firefox for one of the fixture pages, and return
 * its window handle.
 */
async function openDevToolsServerTab(config, fixture) {
  const { handle } = await runDevToolsServerCommand(config, "open-tab", {
    url: getCompatFixtureUrl(fixture),
  });
  return handle;
}
/* exported openDevToolsServerTab */

/**
 * Close a tab previously opened with openDevToolsServerTab.
 */
async function closeDevToolsServerTab(config, handle) {
  await runDevToolsServerCommand(config, "close-tab", { handle });
}
/* exported closeDevToolsServerTab */

/**
 * URL of a fixture page, as the server Firefox will load it.
 */
function getCompatFixtureUrl(fixture) {
  return FIXTURE_ROOT + fixture;
}
/* exported getCompatFixtureUrl */

/**
 * Open a fixture page on the server, then open a toolbox on it from the runtime
 * page of the server. This is the setup shared by all the tests inspecting a
 * remote target.
 *
 * @param {object} config
 *        The configuration returned by getCompatConfig.
 * @param {string} fixture
 *        Name of the fixture page to open on the server.
 * @returns {object}
 *        The about:debugging document, tab and window, the toolbox and its tab,
 *        and the window handle of the page opened on the server.
 */
async function openCompatToolbox(config, fixture) {
  const serverHandle = await openDevToolsServerTab(config, fixture);

  const { document, tab, window } =
    await openAboutDebuggingWithCompatServer(config);
  await connectToRuntime(config.host, document);
  await waitForRuntimePage(config.runtime.brandName, document);

  const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
    document,
    tab,
    window,
    getCompatFixtureUrl(fixture)
  );

  return {
    document,
    tab,
    window,
    devtoolsTab,
    devtoolsWindow,
    toolbox: getToolbox(devtoolsWindow),
    serverHandle,
  };
}
/* exported openCompatToolbox */

/**
 * Tear down what openCompatToolbox created.
 */
async function closeCompatToolbox(config, setup) {
  const { document, tab, window, devtoolsTab, serverHandle } = setup;
  await closeAboutDevtoolsToolbox(document, devtoolsTab, window);
  await closeDevToolsServerTab(config, serverHandle);
  await removeTab(tab);
}
/* exported closeCompatToolbox */
