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

"use strict";

// Tests that changing preferences in the options panel updates the prefs
// and toggles appropriate things in the toolbox.

var doc = null,
  toolbox = null,
  panelWin = null,
  modifiedPrefs = [];
const L10N = new LocalizationHelper(
  "devtools/client/locales/toolbox.properties"
);
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
const {
  BOOLEAN_CONFIGURATION_PREFS,
} = require("resource://devtools/client/framework/toolbox.js");

add_task(async function () {
  const URL =
    "data:text/html;charset=utf8,test for dynamically registering " +
    "and unregistering tools";
  registerNewTool();
  const tab = await addTab(URL);
  toolbox = await gDevTools.showToolboxForTab(tab);

  doc = toolbox.doc;
  await registerNewPerToolboxTool();
  await testSelectTool();
  await testOptionsShortcut();
  await testOptions();
  await testToggleTools();

  // Test that registered WebExtensions becomes entries in the
  // options panel and toggling their checkbox toggle the related
  // preference.
  await registerNewWebExtensions();
  await testToggleWebExtensions();

  await cleanup();
});

function registerNewTool() {
  const toolDefinition = {
    id: "testTool",
    isToolSupported: () => true,
    visibilityswitch: "devtools.test-tool.enabled",
    url: "about:blank",
    label: "someLabel",
  };

  ok(gDevTools, "gDevTools exists");
  ok(
    !gDevTools.getToolDefinitionMap().has("testTool"),
    "The tool is not registered"
  );

  gDevTools.registerTool(toolDefinition);
  ok(
    gDevTools.getToolDefinitionMap().has("testTool"),
    "The tool is registered"
  );
}

// Register a fake WebExtension to check that it is
// listed in the toolbox options.
function registerNewWebExtensions() {
  // Register some fake extensions and init the related preferences
  // (similarly to ext-devtools.js).
  for (let i = 0; i < 2; i++) {
    const extPref = `devtools.webextensions.fakeExtId${i}.enabled`;
    Services.prefs.setBoolPref(extPref, true);

    toolbox.registerWebExtension(`fakeUUID${i}`, {
      name: `Fake WebExtension ${i}`,
      pref: extPref,
    });
  }
}

function registerNewPerToolboxTool() {
  const toolDefinition = {
    id: "test-pertoolbox-tool",
    isToolSupported: () => true,
    visibilityswitch: "devtools.test-pertoolbox-tool.enabled",
    url: "about:blank",
    label: "perToolboxSomeLabel",
  };

  ok(gDevTools, "gDevTools exists");
  ok(
    !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"),
    "The per-toolbox tool is not registered globally"
  );

  ok(toolbox, "toolbox exists");
  ok(
    !toolbox.hasAdditionalTool("test-pertoolbox-tool"),
    "The per-toolbox tool is not yet registered to the toolbox"
  );

  toolbox.addAdditionalTool(toolDefinition);

  ok(
    !gDevTools.getToolDefinitionMap().has("test-pertoolbox-tool"),
    "The per-toolbox tool is not registered globally"
  );
  ok(
    toolbox.hasAdditionalTool("test-pertoolbox-tool"),
    "The per-toolbox tool has been registered to the toolbox"
  );
}

async function testSelectTool() {
  info("Checking to make sure that the options panel can be selected.");

  const onceSelected = toolbox.once("options-selected");
  toolbox.selectTool("options");
  await onceSelected;
  ok(true, "Toolbox selected via selectTool method");
}

async function testOptionsShortcut() {
  info("Selecting another tool, then reselecting options panel with keyboard.");

  await toolbox.selectTool("webconsole");
  is(toolbox.currentToolId, "webconsole", "webconsole is selected");
  synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
  is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key");
  synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
  is(toolbox.currentToolId, "webconsole", "webconsole is reselected");
  synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
  is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key");
}

async function testOptions() {
  const tool = toolbox.getPanel("options");
  panelWin = tool.panelWin;
  const prefNodes = tool.panelDoc.querySelectorAll(
    "input[type=checkbox][data-pref]"
  );
  ok(
    [...prefNodes].some(prefNode => prefNode.hasAttribute("data-force-reload")),
    "There's at least one checkbox with the data-force-reload attribute"
  );

  // Store modified pref names so that they can be cleared on error.
  for (const node of tool.panelDoc.querySelectorAll("[data-pref]")) {
    const pref = node.getAttribute("data-pref");
    modifiedPrefs.push(pref);
  }

  for (const node of prefNodes) {
    const prefValue = GetPref(node.getAttribute("data-pref"));

    // Test clicking the checkbox for each options pref
    await testMouseClick(node, prefValue);

    // Do again with opposite values to reset prefs
    await testMouseClick(node, !prefValue);
  }

  const prefSelects = tool.panelDoc.querySelectorAll("select[data-pref]");
  for (const node of prefSelects) {
    await testSelect(node);
  }
}

async function testSelect(select) {
  const pref = select.getAttribute("data-pref");
  const options = Array.from(select.options);
  info("Checking select for: " + pref);

  is(
    `${select.options[select.selectedIndex].value}`,
    `${GetPref(pref)}`,
    "select starts out selected"
  );

  for (const option of options) {
    if (options.indexOf(option) === select.selectedIndex) {
      continue;
    }

    const observer = new PrefObserver("devtools.");

    let changeSeen = false;
    const changeSeenPromise = new Promise(resolve => {
      observer.once(pref, () => {
        changeSeen = true;
        is(
          `${GetPref(pref)}`,
          `${option.value}`,
          "Preference been switched for " + pref
        );
        resolve();
      });
    });

    select.selectedIndex = options.indexOf(option);
    const changeEvent = new Event("change");
    select.dispatchEvent(changeEvent);

    await changeSeenPromise;

    ok(changeSeen, "Correct pref was changed");
    observer.destroy();
  }
}

async function testMouseClick(node, prefValue) {
  const observer = new PrefObserver("devtools.");

  const pref = node.getAttribute("data-pref");
  let changeSeen = false;
  const changeSeenPromise = new Promise(resolve => {
    observer.once(pref, () => {
      changeSeen = true;
      is(GetPref(pref), !prefValue, "New value is correct for " + pref);
      resolve();
    });
  });

  // if changing the setting reloads the page, waits for the toolbox to be reloaded
  const waitForDevToolsReload = node.hasAttribute("data-force-reload")
    ? await watchForDevToolsReload(gBrowser.selectedBrowser)
    : null;

  const onNewConfigurationApplied = Object.keys(
    BOOLEAN_CONFIGURATION_PREFS
  ).includes(pref)
    ? toolbox.once("new-configuration-applied")
    : null;

  node.scrollIntoView();

  // We use executeSoon here to ensure that the element is in view and
  // clickable.
  executeSoon(function () {
    info("Click event synthesized for pref " + pref);
    EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
  });

  await changeSeenPromise;

  ok(changeSeen, "Correct pref was changed");

  if (onNewConfigurationApplied) {
    await onNewConfigurationApplied;
    ok(true, `Configuration was changed when updating pref "${pref}"`);
  }

  if (waitForDevToolsReload) {
    await waitForDevToolsReload();
    ok(true, `The page was reloaded when toggling ${node.outerHTML}`);
  }

  observer.destroy();
}

async function testToggleWebExtensions() {
  const disabledExtensions = new Set();
  const toggleableWebExtensions = toolbox.listWebExtensions();

  function toggleWebExtension(node) {
    node.scrollIntoView();
    EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);
  }

  function assertExpectedDisabledExtensions() {
    for (const ext of toggleableWebExtensions) {
      if (disabledExtensions.has(ext)) {
        ok(
          !toolbox.isWebExtensionEnabled(ext.uuid),
          `The WebExtension "${ext.name}" should be disabled`
        );
      } else {
        ok(
          toolbox.isWebExtensionEnabled(ext.uuid),
          `The WebExtension "${ext.name}" should  be enabled`
        );
      }
    }
  }

  function assertAllExtensionsDisabled() {
    const enabledUUIDs = toggleableWebExtensions
      .filter(ext => toolbox.isWebExtensionEnabled(ext.uuid))
      .map(ext => ext.uuid);

    Assert.deepEqual(
      enabledUUIDs,
      [],
      "All the registered WebExtensions should be disabled"
    );
  }

  function assertAllExtensionsEnabled() {
    const disabledUUIDs = toolbox
      .listWebExtensions()
      .filter(ext => !toolbox.isWebExtensionEnabled(ext.uuid))
      .map(ext => ext.uuid);

    Assert.deepEqual(
      disabledUUIDs,
      [],
      "All the registered WebExtensions should be enabled"
    );
  }

  function getWebExtensionNodes() {
    const toolNodes = panelWin.document.querySelectorAll(
      "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
        "#additional-tools-box input[type=checkbox]:not([data-unsupported])"
    );

    return [...toolNodes].filter(node => {
      return toggleableWebExtensions.some(
        ({ uuid }) => node.getAttribute("id") === `webext-${uuid}`
      );
    });
  }

  let webExtensionNodes = getWebExtensionNodes();

  is(
    webExtensionNodes.length,
    toggleableWebExtensions.length,
    "There should be a toggle checkbox for every WebExtension registered"
  );

  for (const ext of toggleableWebExtensions) {
    ok(
      toolbox.isWebExtensionEnabled(ext.uuid),
      `The WebExtension "${ext.name}" is initially enabled`
    );
  }

  // Store modified pref names so that they can be cleared on error.
  for (const ext of toggleableWebExtensions) {
    modifiedPrefs.push(ext.pref);
  }

  // Turn each registered WebExtension to disabled.
  for (const node of webExtensionNodes) {
    toggleWebExtension(node);

    const toggledExt = toggleableWebExtensions.find(ext => {
      return node.id == `webext-${ext.uuid}`;
    });
    ok(toggledExt, "Found a WebExtension for the checkbox element");
    disabledExtensions.add(toggledExt);

    assertExpectedDisabledExtensions();
  }

  assertAllExtensionsDisabled();

  // Turn each registered WebExtension to enabled.
  for (const node of webExtensionNodes) {
    toggleWebExtension(node);

    const toggledExt = toggleableWebExtensions.find(ext => {
      return node.id == `webext-${ext.uuid}`;
    });
    ok(toggledExt, "Found a WebExtension for the checkbox element");
    disabledExtensions.delete(toggledExt);

    assertExpectedDisabledExtensions();
  }

  assertAllExtensionsEnabled();

  // Unregister the WebExtensions one by one, and check that only the expected
  // ones have been unregistered, and the remaining onea are still listed.
  for (const ext of toggleableWebExtensions) {
    ok(
      !!toolbox.listWebExtensions().length,
      "There should still be extensions registered"
    );
    toolbox.unregisterWebExtension(ext.uuid);

    const registeredUUIDs = toolbox.listWebExtensions().map(item => item.uuid);
    ok(
      !registeredUUIDs.includes(ext.uuid),
      `the WebExtension "${ext.name}" should have been unregistered`
    );

    webExtensionNodes = getWebExtensionNodes();

    const checkboxEl = webExtensionNodes.find(
      el => el.id === `webext-${ext.uuid}`
    );
    is(
      checkboxEl,
      undefined,
      "The unregistered WebExtension checkbox should have been removed"
    );

    is(
      registeredUUIDs.length,
      webExtensionNodes.length,
      "There should be the expected number of WebExtensions checkboxes"
    );
  }

  is(
    toolbox.listWebExtensions().length,
    0,
    "All WebExtensions have been unregistered"
  );

  webExtensionNodes = getWebExtensionNodes();

  is(
    webExtensionNodes.length,
    0,
    "There should not be any checkbox for the unregistered WebExtensions"
  );
}

function getToolNode(id) {
  return panelWin.document.getElementById(id);
}

async function testToggleTools() {
  const toolNodes = panelWin.document.querySelectorAll(
    "#default-tools-box input[type=checkbox]:not([data-unsupported])," +
      "#additional-tools-box input[type=checkbox]:not([data-unsupported])"
  );
  const toolNodeIds = [...toolNodes].map(node => node.id);
  const enabledToolIds = [...toolNodes]
    .filter(node => node.checked)
    .map(node => node.id);

  const toggleableTools = gDevTools
    .getDefaultTools()
    .filter(tool => {
      return tool.visibilityswitch;
    })
    .concat(gDevTools.getAdditionalTools())
    .concat(toolbox.getAdditionalTools());

  for (const node of toolNodes) {
    const id = node.getAttribute("id");
    ok(
      toggleableTools.some(tool => tool.id === id),
      "There should be a toggle checkbox for: " + id
    );
  }

  // Store modified pref names so that they can be cleared on error.
  for (const tool of toggleableTools) {
    const pref = tool.visibilityswitch;
    modifiedPrefs.push(pref);
  }

  // Toggle each tool
  for (const id of toolNodeIds) {
    await toggleTool(getToolNode(id));
  }

  // Toggle again to reset tool enablement state
  for (const id of toolNodeIds) {
    await toggleTool(getToolNode(id));
  }

  // Test that a tool can still be added when no tabs are present:
  // Disable all tools
  for (const id of enabledToolIds) {
    await toggleTool(getToolNode(id));
  }
  // Re-enable the tools which are enabled by default
  for (const id of enabledToolIds) {
    await toggleTool(getToolNode(id));
  }

  // Toggle first, middle, and last tools to ensure that toolbox tabs are
  // inserted in order
  const firstToolId = toolNodeIds[0];
  const middleToolId = toolNodeIds[(toolNodeIds.length / 2) | 0];
  const lastToolId = toolNodeIds[toolNodeIds.length - 1];

  await toggleTool(getToolNode(firstToolId));
  await toggleTool(getToolNode(firstToolId));
  await toggleTool(getToolNode(middleToolId));
  await toggleTool(getToolNode(middleToolId));
  await toggleTool(getToolNode(lastToolId));
  await toggleTool(getToolNode(lastToolId));
}

/**
 * Toggle tool node checkbox. Note: because toggling the checkbox will result in
 * re-rendering of the tool list, we must re-query the checkboxes every time.
 */
async function toggleTool(node) {
  const toolId = node.getAttribute("id");

  const registeredPromise = new Promise(resolve => {
    if (node.checked) {
      gDevTools.once(
        "tool-unregistered",
        checkUnregistered.bind(null, toolId, resolve)
      );
    } else {
      gDevTools.once(
        "tool-registered",
        checkRegistered.bind(null, toolId, resolve)
      );
    }
  });
  node.scrollIntoView();
  EventUtils.synthesizeMouseAtCenter(node, {}, panelWin);

  await registeredPromise;
}

function checkUnregistered(toolId, resolve, data) {
  if (data == toolId) {
    ok(true, "Correct tool removed");
    // checking tab on the toolbox
    ok(
      !doc.getElementById("toolbox-tab-" + toolId),
      "Tab removed for " + toolId
    );
  } else {
    ok(false, "Something went wrong, " + toolId + " was not unregistered");
  }
  resolve();
}

async function checkRegistered(toolId, resolve, data) {
  if (data == toolId) {
    ok(true, "Correct tool added back");
    // checking tab on the toolbox
    const button = await lookupButtonForToolId(toolId);
    ok(button, "Tab added back for " + toolId);
  } else {
    ok(false, "Something went wrong, " + toolId + " was not registered");
  }
  resolve();
}

function GetPref(name) {
  const type = Services.prefs.getPrefType(name);
  switch (type) {
    case Services.prefs.PREF_STRING:
      return Services.prefs.getCharPref(name);
    case Services.prefs.PREF_INT:
      return Services.prefs.getIntPref(name);
    case Services.prefs.PREF_BOOL:
      return Services.prefs.getBoolPref(name);
    default:
      throw new Error("Unknown type");
  }
}

/**
 * Find the button from specified toolId.
 * Generally, button which access to the tool panel is in toolbox or
 * tools menu(in the Chevron menu).
 */
async function lookupButtonForToolId(toolId) {
  let button = doc.getElementById("toolbox-tab-" + toolId);
  if (!button) {
    // search from the tools menu.
    await openChevronMenu(toolbox);
    button = doc.querySelector("#tools-chevron-menupopup-" + toolId);

    await closeChevronMenu(toolbox);
  }
  return button;
}

async function cleanup() {
  gDevTools.unregisterTool("testTool");
  await toolbox.destroy();
  gBrowser.removeCurrentTab();
  for (const pref of modifiedPrefs) {
    Services.prefs.clearUserPref(pref);
  }
  toolbox = doc = panelWin = modifiedPrefs = null;
}
