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

"use strict";

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);

const { ExtensionProcessCrashObserver, Management } =
  ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");

AddonTestUtils.initMochitest(this);

add_task(async function test_ExtensionProcessCrashObserver() {
  await SpecialPowers.pushPrefEnv({
    // This test triggers a crash and so it will be restarting all builtin
    // extensions persistent background pages as a side effect (and that would
    // be make this test to hit failures due to the builtin background pages
    // still in the process of being restarted being detected as shutdown leaks).
    set: [["extensions.background.disableRestartPersistentAfterCrash", true]],
  });
  let mv2Extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      manifest_version: 2,
    },
    background() {
      browser.test.sendMessage("background_running");
    },
  });

  await mv2Extension.startup();
  await mv2Extension.awaitMessage("background_running");

  let {
    currentProcessChildID,
    lastCrashedProcessChildID,
    processSpawningDisabled,
    lastCrashTimestamps,
  } = ExtensionProcessCrashObserver;

  Assert.notEqual(
    currentProcessChildID,
    undefined,
    "Expect ExtensionProcessCrashObserver.currentProcessChildID to be set"
  );

  Assert.equal(
    ChromeUtils.getAllDOMProcesses().find(
      pp => pp.childID == currentProcessChildID
    )?.remoteType,
    "extension",
    "Expect a child process with remoteType extension to be found for the process childID set"
  );

  Assert.notEqual(
    lastCrashedProcessChildID,
    currentProcessChildID,
    "Expect lastCrashedProcessChildID to not be set to the same value that currentProcessChildID is set"
  );

  Assert.equal(
    processSpawningDisabled,
    false,
    "Expect process spawning to be enabled"
  );

  Assert.deepEqual(lastCrashTimestamps, [], "Expect no crash timestamps");

  let mv3Extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "temporary",
    manifest: {
      manifest_version: 3,
    },
    background() {
      browser.test.sendMessage("background_running");
    },
  });

  const waitForExtensionBrowserInserted = () =>
    new Promise(resolve => {
      const listener = (_eventName, browser) => {
        if (!browser.getAttribute("webextension-view-type") === "background") {
          return;
        }
        Management.off("extension-browser-inserted", listener);
        resolve(browser);
      };
      Management.on("extension-browser-inserted", listener);
    });

  const waitForExtensionProcessCrashNotified = () =>
    new Promise(resolve => {
      Management.once("extension-process-crash", (_evt, data) => resolve(data));
    });

  const promiseBackgroundBrowser = waitForExtensionBrowserInserted();

  const promiseExtensionProcessCrashNotified =
    waitForExtensionProcessCrashNotified();

  await mv3Extension.startup();
  await mv3Extension.awaitMessage("background_running");
  const bgPageBrowser = await promiseBackgroundBrowser;

  Assert.greater(
    Glean.extensions.processEvent.created_fg.testGetValue(),
    0,
    "Expect glean processEvent.created_fg to be set."
  );
  Assert.equal(
    undefined,
    Glean.extensions.processEvent.created_bg.testGetValue(),
    "Creating in the background is not expected on desktop."
  );

  info("Force extension process crash");
  // Clear any existing telemetry data, so that we can be sure we can
  // assert the glean process_event metric labels values to be strictly
  // equal to 1 after the extension process crashed.
  Services.fog.testResetFOG();
  // NOTE: shouldShowTabCrashPage option needs to be set to false
  // to make sure crashFrame method resolves without waiting for a
  // tab crash page (which is not going to be shown for a background
  // page browser element).
  await BrowserTestUtils.crashFrame(
    bgPageBrowser,
    /* shouldShowTabCrashPage */ false
  );

  info("Verify ExtensionProcessCrashObserver after extension process crash");
  Assert.ok(Number.isInteger(currentProcessChildID), "correct type");
  Assert.equal(
    ExtensionProcessCrashObserver.lastCrashedProcessChildID,
    currentProcessChildID,
    "Expect ExtensionProcessCrashObserver.lastCrashedProcessChildID to be set to the expected childID"
  );

  Assert.equal(
    ExtensionProcessCrashObserver.processSpawningDisabled,
    false,
    "Expect process spawning to still be enabled"
  );
  Assert.equal(
    ExtensionProcessCrashObserver.lastCrashTimestamps.length,
    1,
    "Expect a crash timestamp"
  );

  info("Expect the same childID to have been notified as a Management event");
  Assert.deepEqual(
    await promiseExtensionProcessCrashNotified,
    {
      childID: currentProcessChildID,
      processSpawningDisabled: false,
      // This boolean flag is expected to be always true on Desktop builds.
      appInForeground: true,
    },
    "Got the expected childID notified as part of the extension-process-crash Management event"
  );

  Assert.greater(
    Glean.extensions.processEvent.crashed_fg.testGetValue(),
    0,
    "Expect glean processEvent.crashed_fg to be set"
  );
  Assert.equal(
    undefined,
    Glean.extensions.processEvent.crashed_bg.testGetValue(),
    "Crashing in the background is not expected on desktop."
  );

  info("Wait for mv3 extension shutdown");
  await mv3Extension.unload();
  info("Wait for mv2 extension shutdown");
  await mv2Extension.unload();

  // Reset this array to prevent TV failures.
  ExtensionProcessCrashObserver.lastCrashTimestamps = [];

  await SpecialPowers.popPrefEnv();
});
