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

if (SpecialPowers.useRemoteSubframes) {
  requestLongerTimeout(2);
}

const DEFAULT_PROCESS_COUNT = Services.prefs
  .getDefaultBranch(null)
  .getIntPref("dom.ipc.processCount");

/**
 * A test that checks whether any preference getter from the given list
 * of stats was called more often than the max parameter.
 *
 * @param {Array}  stats - an array of [prefName, accessCount] tuples
 * @param {Number} max - the maximum number of times any of the prefs should
 *                 have been called.
 * @param {Object} knownProblematicPrefs (optional) - an object that defines
 *                 prefs that should be exempt from checking the
 *                 maximum access. It looks like the following:
 *
 *                 pref_name: {
 *                   min: [Number] the minimum amount of times this should have
 *                                 been called (to avoid keeping around dead items)
 *                   max: [Number] the maximum amount of times this should have
 *                                 been called (to avoid this creeping up further)
 *                 }
 */
function checkPrefGetters(stats, max, knownProblematicPrefs = {}) {
  let getterStats = Object.entries(stats).sort(
    ([, val1], [, val2]) => val2 - val1
  );

  // Clone the list to be able to delete entries to check if we
  // forgot any later on.
  knownProblematicPrefs = Object.assign({}, knownProblematicPrefs);

  for (let [pref, count] of getterStats) {
    let prefLimits = knownProblematicPrefs[pref];
    if (!prefLimits) {
      Assert.lessOrEqual(
        count,
        max,
        `${pref} should not be accessed more than ${max} times.`
      );
    } else {
      // Still record how much this pref was accessed even if we don't do any real assertions.
      if (!prefLimits.min && !prefLimits.max) {
        info(
          `${pref} should not be accessed more than ${max} times and was accessed ${count} times.`
        );
      }

      if (prefLimits.min) {
        Assert.lessOrEqual(
          prefLimits.min,
          count,
          `${pref} should be accessed at least ${prefLimits.min} times.`
        );
      }
      if (prefLimits.max) {
        Assert.lessOrEqual(
          count,
          prefLimits.max,
          `${pref} should be accessed at most ${prefLimits.max} times.`
        );
      }
      delete knownProblematicPrefs[pref];
    }
  }

  // This pref will be accessed by mozJSComponentLoader when loading modules,
  // which fails TV runs since they run the test multiple times without restarting.
  // We just ignore this pref, since it's for testing only anyway.
  if (knownProblematicPrefs["browser.startup.record"]) {
    delete knownProblematicPrefs["browser.startup.record"];
  }

  let unusedPrefs = Object.keys(knownProblematicPrefs);
  is(
    unusedPrefs.length,
    0,
    `Should have accessed all known problematic prefs. Remaining: ${unusedPrefs}`
  );
}

/**
 * A helper function to read preference access data
 * using the Services.prefs.readStats() function.
 */
function getPreferenceStats() {
  let stats = {};
  Services.prefs.readStats((key, value) => (stats[key] = value));
  return stats;
}

add_task(async function debug_only() {
  ok(AppConstants.DEBUG, "You need to run this test on a debug build.");
});

// Just checks how many prefs were accessed during startup.
add_task(async function startup() {
  let max = 40;

  let knownProblematicPrefs = {
    "browser.startup.record": {
      // This pref is accessed in Nighly and debug builds only.
      min: 200,
      max: 400,
    },
    "network.loadinfo.skip_type_assertion": {
      // This is accessed in debug only.
    },
    "chrome.override_package.global": {
      min: 0,
      max: 50,
    },
  };

  let startupRecorder =
    Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
  await startupRecorder.done;

  ok(startupRecorder.data.prefStats, "startupRecorder has prefStats");

  checkPrefGetters(startupRecorder.data.prefStats, max, knownProblematicPrefs);
});

// This opens 10 tabs and checks pref getters.
add_task(async function open_10_tabs() {
  // This is somewhat arbitrary. When we had a default of 4 content processes
  // the value was 15. We need to scale it as we increase the number of
  // content processes so we approximate with 4 * process_count.
  const max = 4 * DEFAULT_PROCESS_COUNT;

  let knownProblematicPrefs = {
    "browser.startup.record": {
      max: 20,
    },
    "browser.tabs.remote.logSwitchTiming": {
      max: 35,
    },
    "network.loadinfo.skip_type_assertion": {
      // This is accessed in debug only.
    },
  };

  Services.prefs.resetStats();

  let tabs = [];
  while (tabs.length < 10) {
    tabs.push(
      await BrowserTestUtils.openNewForegroundTab(
        gBrowser,
        // eslint-disable-next-line @microsoft/sdl/no-insecure-url
        "http://example.com",
        true,
        true
      )
    );
  }

  for (let tab of tabs) {
    await BrowserTestUtils.removeTab(tab);
  }

  checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
});

// This navigates to 50 sites and checks pref getters.
add_task(async function navigate_around() {
  await SpecialPowers.pushPrefEnv({
    set: [
      // Disable bfcache so that we can measure more accurately the number of
      // pref accesses in the child processes.
      // If bfcache is enabled on Fission
      // dom.ipc.keepProcessesAlive.webIsolated.perOrigin and
      // security.sandbox.content.force-namespace are accessed only a couple of
      // times.
      ["browser.sessionhistory.max_total_viewers", 0],
    ],
  });

  let max = 40;

  let knownProblematicPrefs = {
    "network.loadinfo.skip_type_assertion": {
      // This is accessed in debug only.
    },
  };

  if (Services.prefs.getBoolPref("browser.translations.enable")) {
    // The translations pref logs the translation decision on each DOMContentLoaded,
    // and only shows the log by the preferences set in the console.createInstance.
    // See Bug 1835693. This means that it is invoked on each page load.
    knownProblematicPrefs["browser.translations.logLevel"] = {
      min: 50,
      max: 150,
    };
  }

  if (SpecialPowers.useRemoteSubframes) {
    // We access this when considering starting a new content process.
    // Because there is no complete list of content process types,
    // caching this is not trivial. Opening 50 different content
    // processes and throwing them away immediately is a bit artificial;
    // we're more likely to keep some around so this shouldn't be quite
    // this bad in practice. Fixing this is
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1600266
    knownProblematicPrefs["dom.ipc.processCount.webIsolated"] = {
      min: 50,
      max: 51,
    };
    // This pref is only accessed in automation to speed up tests.
    knownProblematicPrefs["dom.ipc.keepProcessesAlive.webIsolated.perOrigin"] =
      {
        min: 100,
        max: 102,
      };
    if (AppConstants.platform == "linux") {
      // The following sandbox pref is covered by
      // https://bugzilla.mozilla.org/show_bug.cgi?id=1600189
      knownProblematicPrefs["security.sandbox.content.force-namespace"] = {
        min: 45,
        max: 55,
      };
    } else if (AppConstants.platform == "win") {
      // The following 2 graphics prefs are covered by
      // https://bugzilla.mozilla.org/show_bug.cgi?id=1639497
      knownProblematicPrefs["gfx.canvas.azure.backends"] = {
        min: 90,
        max: 110,
      };
      knownProblematicPrefs["gfx.content.azure.backends"] = {
        min: 90,
        max: 110,
      };
      // The following 2 sandbox prefs are covered by
      // https://bugzilla.mozilla.org/show_bug.cgi?id=1639494
      knownProblematicPrefs["security.sandbox.content.read_path_whitelist"] = {
        min: 47,
        max: 55,
      };
      knownProblematicPrefs["security.sandbox.logging.enabled"] = {
        min: 47,
        max: 55,
      };
    }
  }

  Services.prefs.resetStats();

  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example.com",
    true,
    true
  );

  let urls = [
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example.com/",
    "https://example.com/",
    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    "http://example.org/",
    "https://example.org/",
  ];

  for (let i = 0; i < 50; i++) {
    let url = urls[i % urls.length];
    info(`Navigating to ${url}...`);
    BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
    await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
    info(`Loaded ${url}.`);
  }

  await BrowserTestUtils.removeTab(tab);

  checkPrefGetters(getPreferenceStats(), max, knownProblematicPrefs);
});
