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

/* This test records which services, JS components, frame scripts, process
 * scripts, and JS modules are loaded when creating a new content process.
 *
 * If you made changes that cause this test to fail, it's likely because you
 * are loading more JS code during content process startup.
 *
 * If your code isn't strictly required to show a page, consider loading it
 * lazily. If you can't, consider delaying its load until after we have started
 * handling user events.
 */

"use strict";

/* Set this to true only for debugging purpose; it makes the output noisy. */
const kDumpAllStacks = false;

const whitelist = {
  modules: new Set([
    "chrome://mochikit/content/ShutdownLeaksCollector.jsm",
    "resource://specialpowers/specialpowers.js",
    "resource://specialpowers/specialpowersAPI.js",

    "resource://gre/modules/ContentProcessSingleton.jsm",

    // General utilities
    "resource://gre/modules/AppConstants.jsm",
    "resource://gre/modules/AsyncShutdown.jsm",
    "resource://gre/modules/DeferredTask.jsm",
    "resource://gre/modules/PromiseUtils.jsm",
    "resource://gre/modules/Services.jsm", // bug 1464542
    "resource://gre/modules/Timer.jsm",
    "resource://gre/modules/XPCOMUtils.jsm",

    // Logging related
    "resource://gre/modules/Log.jsm",

    // Session store
    "resource:///modules/sessionstore/ContentSessionStore.jsm",
    "resource://gre/modules/sessionstore/SessionHistory.jsm",

    // Browser front-end
    "resource:///actors/AboutReaderChild.jsm",
    "resource:///actors/BrowserTabChild.jsm",
    "resource:///modules/ContentMetaHandler.jsm",
    "resource:///actors/LinkHandlerChild.jsm",
    "resource:///actors/SearchTelemetryChild.jsm",
    "resource://gre/modules/ActorChild.jsm",
    "resource://gre/modules/ActorManagerChild.jsm",
    "resource://gre/modules/E10SUtils.jsm",
    "resource://gre/modules/Readerable.jsm",
    "resource://gre/modules/WebProgressChild.jsm",

    // Telemetry
    "resource://gre/modules/TelemetryController.jsm", // bug 1470339
    "resource://gre/modules/TelemetryUtils.jsm", // bug 1470339

    // Extensions
    "resource://gre/modules/ExtensionProcessScript.jsm",
    "resource://gre/modules/ExtensionUtils.jsm",
    "resource://gre/modules/MessageChannel.jsm",
  ]),
  frameScripts: new Set([
    // Test related
    "resource://specialpowers/MozillaLogger.js",
    "resource://specialpowers/specialpowersFrameScript.js",
    "chrome://mochikit/content/shutdown-leaks-collector.js",
    "chrome://mochikit/content/tests/SimpleTest/AsyncUtilsContent.js",
    "chrome://mochikit/content/tests/BrowserTestUtils/content-utils.js",

    // Browser front-end
    "chrome://global/content/browser-content.js",

    // Forms
    "chrome://formautofill/content/FormAutofillFrameScript.js",

    // Extensions
    "resource://gre/modules/addons/Content.js",
  ]),
  processScripts: new Set([
    "chrome://global/content/process-content.js",
    "resource:///modules/ContentObservers.js",
    "data:,ChromeUtils.import('resource://gre/modules/ExtensionProcessScript.jsm')",
    "resource://devtools/client/jsonview/converter-observer.js",
    "resource://gre/modules/WebRequestContent.js",
  ]),
};

// Items on this list are allowed to be loaded but not
// required, as opposed to items in the main whitelist,
// which are all required.
const intermittently_loaded_whitelist = {
  modules: new Set([
    "resource://gre/modules/nsAsyncShutdown.jsm",
    "resource://gre/modules/sessionstore/Utils.jsm",

    // Webcompat about:config front-end. This is presently nightly-only and
    // part of a system add-on which may not load early enough for the test.
    "resource://webcompat/AboutCompat.jsm",
  ]),
  frameScripts: new Set([]),
  processScripts: new Set([
    // Webcompat about:config front-end. This is presently nightly-only and
    // part of a system add-on which may not load early enough for the test.
    "resource://webcompat/aboutPageProcessScript.js",
  ]),
};

const blacklist = {
  services: new Set([
    "@mozilla.org/base/telemetry-startup;1",
    "@mozilla.org/embedcomp/default-tooltiptextprovider;1",
    "@mozilla.org/push/Service;1",
  ]),
};

add_task(async function() {
  SimpleTest.requestCompleteLog();

  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
                                                         forceNewProcess: true});

  let mm = gBrowser.selectedBrowser.messageManager;
  let promise = BrowserTestUtils.waitForMessage(mm, "Test:LoadedScripts");

  // Load a custom frame script to avoid using ContentTask which loads Task.jsm
  mm.loadFrameScript("data:text/javascript,(" + function() {
    /* eslint-env mozilla/frame-script */
    const Cm = Components.manager;
    Cm.QueryInterface(Ci.nsIServiceManager);
    const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
    let collectStacks = AppConstants.NIGHTLY_BUILD || AppConstants.DEBUG;
    let components = {};
    for (let component of Cu.loadedComponents) {
      /* Keep only the file name for components, as the path is an absolute file
         URL rather than a resource:// URL like for modules. */
      components[component.replace(/.*\//, "")] =
        collectStacks ? Cu.getComponentLoadStack(component) : "";
    }
    let modules = {};
    for (let module of Cu.loadedModules) {
      modules[module] = collectStacks ? Cu.getModuleImportStack(module) : "";
    }
    let services = {};
    for (let contractID of Object.keys(Cc)) {
      try {
        if (Cm.isServiceInstantiatedByContractID(contractID, Ci.nsISupports)) {
          services[contractID] = "";
        }
      } catch (e) {}
    }
    sendAsyncMessage("Test:LoadedScripts", {components, modules, services});
  } + ")()", false);

  let loadedInfo = await promise;

  // Gather loaded frame scripts.
  loadedInfo.frameScripts = {};
  for (let [uri] of Services.mm.getDelayedFrameScripts()) {
    loadedInfo.frameScripts[uri] = "";
  }

  // Gather loaded process scripts.
  loadedInfo.processScripts = {};
  for (let [uri] of Services.ppmm.getDelayedProcessScripts()) {
    loadedInfo.processScripts[uri] = "";
  }

  let loadedList = {};

  for (let scriptType in whitelist) {
    loadedList[scriptType] = Object.keys(loadedInfo[scriptType]).filter(c => {
      if (!whitelist[scriptType].has(c))
        return true;
      whitelist[scriptType].delete(c);
      return false;
    });

    loadedList[scriptType] = loadedList[scriptType].filter(c => {
      return !intermittently_loaded_whitelist[scriptType].has(c);
    });

    is(loadedList[scriptType].length, 0,
       `should have no unexpected ${scriptType} loaded on content process startup`);

    for (let script of loadedList[scriptType]) {
      ok(false, `Unexpected ${scriptType} loaded during content process startup: ${script}`);
      info(`Stack that loaded ${script}:\n`);
      info(loadedInfo[scriptType][script]);
    }

    is(whitelist[scriptType].size, 0,
       `all ${scriptType} whitelist entries should have been used`);

    for (let script of whitelist[scriptType]) {
      ok(false, `${scriptType} is whitelisted for content process startup but wasn't used: ${script}`);
    }

    if (kDumpAllStacks) {
      info(`Stacks for all loaded ${scriptType}:`);
      for (let file in loadedInfo[scriptType]) {
        if (loadedInfo[scriptType][file]) {
          info(`${file}\n------------------------------------\n` + loadedInfo[scriptType][file] + "\n");
        }
      }
    }
  }

  for (let scriptType in blacklist) {
    for (let script of blacklist[scriptType]) {
      let loaded = script in loadedInfo[scriptType];
      if (loaded) {
        ok(false, `Unexpected ${scriptType} loaded during content process startup: ${script}`);
        if (loadedInfo[scriptType][script]) {
          info(`Stack that loaded ${script}:\n`);
          info(loadedInfo[scriptType][script]);
        }
      }
    }
  }

  BrowserTestUtils.removeTab(tab);
});
