/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

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

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "1",
  "43"
);

let { promiseRestartManager, promiseShutdownManager, promiseStartupManager } =
  AddonTestUtils;

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

const server = AddonTestUtils.createHttpServer({
  hosts: ["example.com"],
});
function registerSlowStyleSheet() {
  // We can delay DOMContentLoaded of a background page by loading a slow
  // stylesheet and using `<script defer>`. For more detail about this
  // trick, see test_ext_background_iframe.js.
  let allowStylesheetToLoad;
  let stylesheetBlockerPromise = new Promise(resolve => {
    allowStylesheetToLoad = resolve;
  });
  let resolveFirstLoad;
  let firstLoadPromise = new Promise(resolve => {
    resolveFirstLoad = resolve;
  });
  let requestCount = 0;
  server.registerPathHandler("/slow.css", (request, response) => {
    response.setHeader("Cache-Control", "no-cache", false);
    response.setHeader("Content-Type", "text/css", false);
    response.processAsync();
    ++requestCount;
    resolveFirstLoad();
    stylesheetBlockerPromise.then(() => {
      response.write("body { color: rgb(1, 2, 3); }");
      response.finish();
    });
  });
  const getRequestCount = () => requestCount;
  return { allowStylesheetToLoad, getRequestCount, firstLoadPromise };
}

// We can only crash the extension process when they are running out-of-process.
// Otherwise we would be killing the test runner itself...
const CAN_CRASH_EXTENSIONS = WebExtensionPolicy.useRemoteWebExtensions;

add_setup(function () {
  // Set a high threshold because this test crashes a few times on purpose and
  // we don't want to disable process spawning.
  Services.prefs.setIntPref("extensions.webextensions.crash.threshold", 100);

  // Need a profile to init Glean.
  do_get_profile();
  Services.fog.initializeFOG();

  Assert.equal(
    ExtensionProcessCrashObserver.appInForeground,
    AppConstants.platform !== "android",
    "Expect appInForeground to be initially true on desktop and false on android builds"
  );

  // For Android build we mock the app moving in the foreground for the first time
  // (which, in a real Fenix instance, happens when the application receives the first
  // call to the onPause lifecycle callback and the geckoview-initial-foreground
  // topic is being notified to Gecko as a side-effect of that).
  //
  // We have to mock the app moving in the foreground before any of the test extension
  // startup, so that both Desktop and Mobile builds are in the same initial foreground
  // state for the rest of the test file.
  if (AppConstants.platform === "android") {
    info("Mock geckoview-initial-foreground observer service topic");
    ExtensionProcessCrashObserver.observe(null, "geckoview-initial-foreground");
    Assert.equal(
      ExtensionProcessCrashObserver.appInForeground,
      true,
      "Expect appInForeground to be true after geckoview-initial-foreground topic"
    );
  }
});

add_setup(
  // Crash dumps are only generated when MOZ_CRASHREPORTER is set.
  // Crashes are only generated if tests can crash the extension process.
  { skip_if: () => !AppConstants.MOZ_CRASHREPORTER || !CAN_CRASH_EXTENSIONS },
  setup_crash_reporter_override_and_cleaner
);

// Verifies that a delayed background page is not loaded when an extension is
// shut down during startup.
add_task(async function test_unload_extension_before_background_page_startup() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      browser.test.sendMessage("background_startup_observed");
    },
  });

  // Delayed startup are only enabled for browser (re)starts, so we need to
  // install the extension first, and then unload it.
  // ^ Note: an alternative is to use APP_STARTUP, see elsewhere in this file.

  await extension.startup();
  await extension.awaitMessage("background_startup_observed");

  // Now the actual test: Unloading an extension before the startup has
  // finished should interrupt the start-up and abort pending delayed loads.
  info("Starting extension whose startup will be interrupted");
  await promiseRestartManager({ earlyStartup: false });
  await extension.awaitStartup();

  let extensionBrowserInsertions = 0;
  let onExtensionBrowserInserted = () => ++extensionBrowserInsertions;
  Management.on("extension-browser-inserted", onExtensionBrowserInserted);

  info("Unloading extension before the delayed background page starts loading");
  await extension.addon.disable();

  // Re-enable the add-on to let enough time pass to load a whole background
  // page. If at the end of this the original background page hasn't loaded,
  // we can consider the test successful.
  await extension.addon.enable();

  // Trigger the notification that would load a background page.
  info("Forcing pending delayed background page to load");
  AddonTestUtils.notifyLateStartup();

  // This is the expected message from the re-enabled add-on.
  await extension.awaitMessage("background_startup_observed");
  await extension.unload();

  await promiseShutdownManager();

  Management.off("extension-browser-inserted", onExtensionBrowserInserted);
  Assert.equal(
    extensionBrowserInsertions,
    1,
    "Extension browser should have been inserted only once"
  );
});

// Verifies that the "build" method of BackgroundPage in ext-backgroundPage.js
// does not deadlock when startup is interrupted by extension shutdown.
add_task(async function test_unload_extension_during_background_page_startup() {
  await promiseStartupManager();

  let extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    background() {
      browser.test.sendMessage("background_starting");
    },
  });

  // Delayed startup are only enabled for browser (re)starts, so we need to
  // install the extension first, and then reload it.
  // ^ Note: an alternative is to use APP_STARTUP, see elsewhere in this file.
  await extension.startup();
  await extension.awaitMessage("background_starting");

  await promiseRestartManager({ lateStartup: false });
  await extension.awaitStartup();

  let bgStartupPromise = new Promise(resolve => {
    function onBackgroundPageDone(eventName) {
      extension.extension.off(
        "background-script-started",
        onBackgroundPageDone
      );
      extension.extension.off(
        "background-script-aborted",
        onBackgroundPageDone
      );

      if (eventName === "background-script-aborted") {
        info("Background script startup was interrupted");
        resolve("bg_aborted");
      } else {
        info("Background script startup finished normally");
        resolve("bg_fully_loaded");
      }
    }
    extension.extension.on("background-script-started", onBackgroundPageDone);
    extension.extension.on("background-script-aborted", onBackgroundPageDone);
  });

  let bgStartingPromise = new Promise(resolve => {
    let backgroundLoadCount = 0;
    let backgroundPageUrl = extension.extension.baseURI.resolve(
      "_generated_background_page.html"
    );

    // Prevent the background page from actually loading.
    Management.once("extension-browser-inserted", (eventName, browser) => {
      // Intercept background page load.
      let browserFixupAndLoadURIString = browser.fixupAndLoadURIString;
      browser.fixupAndLoadURIString = function () {
        Assert.equal(++backgroundLoadCount, 1, "loadURI should be called once");
        Assert.equal(
          arguments[0],
          backgroundPageUrl,
          "Expected background page"
        );
        // Reset to "about:blank" to not load the actual background page.
        arguments[0] = "about:blank";
        browserFixupAndLoadURIString.apply(this, arguments);

        // And force the extension process to crash.
        if (CAN_CRASH_EXTENSIONS) {
          crashFrame(browser);
        } else {
          // If extensions are not running in out-of-process mode, then the
          // non-remote process should not be killed (or the test runner dies).
          // Remove <browser> instead, to simulate the immediate disconnection
          // of the message manager (that would happen if the process crashed).
          browser.remove();
        }
        resolve();
      };
    });
  });

  // Force background page to initialize.
  AddonTestUtils.notifyLateStartup();
  await bgStartingPromise;

  await extension.unload();
  await promiseShutdownManager();

  // This part is the regression test for bug 1501375. It verifies that the
  // background building completes eventually.
  // If it does not, then the next line will cause a timeout.
  info("Waiting for background builder to finish");
  let bgLoadState = await bgStartupPromise;
  Assert.equal(bgLoadState, "bg_aborted", "Startup should be interrupted");
});

// Verifies correct state when background crashes while starting.
// The difference with test_unload_extension_during_background_page_startup is
// that in this version, the background has progressed far enough for the
// extension context to have been initialized, but before the background is
// considered loaded.
// withContext: Whether to trigger initialization of ProxyContextParent.
async function do_test_crash_while_starting_background({
  // Whether to trigger initialization of ProxyContextParent during startup.
  withContext = false,
  // Whether to use an event page instead of a persistent background page.
  isEventPage = false,
}) {
  let extension = ExtensionTestUtils.loadExtension({
    // Delay startup, so that we can get an extension reference before the
    // background page starts.
    startupReason: "APP_STARTUP",
    // APP_STARTUP is not enough, delayedStartup is needed (bug 1756225).
    delayedStartup: true,
    manifest: {
      background: {
        page: "background.html",
        persistent: !isEventPage,
      },
    },
    files: {
      "background.html": `<!DOCTYPE html>
         <meta charset="utf-8">
         <script src="background-immediate.js"></script>

         <!-- Delays DOMContentLoaded - see registerSlowStyleSheet -->
         <link rel="stylesheet" href="http://example.com/slow.css">
         <script src="background-deferred.js" defer></script>
      `,
      "background-immediate.js": String.raw`
        dump("background-immediate.js is executing as expected.\n");
        if (${!!withContext}) {
          // Accessing the browser API triggers context creation.
          browser.test.sendMessage("background_started_to_load");
        }
      `,
      "background-deferred.js": () => {
        dump("background-deferred.js is UNEXPECTEDLY executing.\n");
        browser.test.fail("Background startup should have been interrupted");
      },
    },
  });
  let slowStyleSheet = registerSlowStyleSheet();
  await ExtensionTestCommon.resetStartupPromises();
  await extension.startup();
  function assertBackgroundState(expected, message) {
    Assert.equal(extension.extension.backgroundState, expected, message);
  }

  assertBackgroundState("stopped", "Background should not have started yet");

  let bgBrowserPromise = new Promise(resolve => {
    Management.once("extension-browser-inserted", (eventName, browser) => {
      assertBackgroundState("starting", "State when bg <browser> is inserted");
      resolve(browser);
    });
  });

  info("Triggering background creation...");
  await ExtensionTestCommon.notifyEarlyStartup();
  await ExtensionTestCommon.notifyLateStartup();

  let bgBrowser = await bgBrowserPromise;

  if (withContext) {
    info("Waiting for background-immediate.js to notify us...");
    await extension.awaitMessage("background_started_to_load");
    Assert.ok(
      extension.extension.backgroundContext,
      "Context exists when an extension API was called"
    );
    // Probably resolved by now, but wait explicitly in case it hasn't, so we
    // know that the stylesheet has started to load.
    await slowStyleSheet.firstLoadPromise;
  } else {
    // Wait for the stylesheet request to infer that the background content has
    // started to be loaded.
    await slowStyleSheet.firstLoadPromise;
    Assert.ok(
      !extension.extension.backgroundContext,
      "Context should not be set while loading"
    );
  }

  // Still starting because registerSlowStyleSheet postponed startup completion.
  assertBackgroundState("starting", "Background should still be loading");

  await crashExtensionBackground(extension, bgBrowser);

  assertBackgroundState("stopped", "Background state after crash");

  // Now that the background is gone, the server can respond without the
  // possibility of triggering the execution of background-deferred.js
  slowStyleSheet.allowStylesheetToLoad();
  await extension.unload();

  // Can't be 0 because the background has started to load.
  // Can't be 2 because we are loading the background only once.
  Assert.equal(
    slowStyleSheet.getRequestCount(),
    1,
    "Expected exactly one request for slow.css from background page"
  );
}

add_task(
  {
    // TODO: consider adding explicit coverage for auto-restart behavior
    // when a crash is hit while there is not background context yet.
    pref_set: [
      ["extensions.background.disableRestartPersistentAfterCrash", true],
    ],
  },
  async function test_crash_while_starting_background_without_context() {
    await do_test_crash_while_starting_background({ withContext: false });
  }
);

add_task(
  {
    // Expected auto-restart behavior is tested in the test task named
    // test_persistent_restarted_after_crash.
    pref_set: [
      ["extensions.background.disableRestartPersistentAfterCrash", true],
    ],
  },
  async function test_crash_while_starting_background_with_context() {
    await do_test_crash_while_starting_background({ withContext: true });
  }
);

add_task(async function test_crash_while_starting_event_page_without_context() {
  await do_test_crash_while_starting_background({
    withContext: false,
    isEventPage: true,
  });
});

add_task(async function test_crash_while_starting_event_page_with_context() {
  await do_test_crash_while_starting_background({
    withContext: true,
    isEventPage: true,
  });
});

async function do_test_crash_while_running_background({ isEventPage = false }) {
  // wakeupBackground() only wakes up after the early startup notification.
  // Trigger explicitly to be independent of other tests.
  await ExtensionTestCommon.resetStartupPromises();
  await AddonTestUtils.notifyEarlyStartup();

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      background: { persistent: !isEventPage },
    },
    background() {
      window.onload = () => {
        browser.test.sendMessage("background_has_fully_loaded");
      };
    },
  });
  await extension.startup();
  function assertBackgroundState(expected, message) {
    Assert.equal(extension.extension.backgroundState, expected, message);
  }

  await extension.awaitMessage("background_has_fully_loaded");
  assertBackgroundState("running", "Background should have started");

  await crashExtensionBackground(extension);
  assertBackgroundState("stopped", "Background state after crash");

  await extension.wakeupBackground();
  await extension.awaitMessage("background_has_fully_loaded");
  assertBackgroundState("running", "Background resumed after crash recovery");
  await extension.terminateBackground();
  assertBackgroundState("stopped", "Background can sleep after crash recovery");

  await extension.unload();
}

add_task(
  {
    // Disable auto-restart persistent background pages after a crash, this test
    // case is checking that the backgroundState is set to stopped when an
    // extension process crash is it but if the background page is restarted
    // automatically then the background state will be already set to "starting".
    pref_set: [
      ["extensions.background.disableRestartPersistentAfterCrash", true],
    ],
  },
  async function test_crash_after_background_startup() {
    await do_test_crash_while_running_background({ isEventPage: false });
  }
);

add_task(async function test_crash_after_event_page_startup() {
  await do_test_crash_while_running_background({ isEventPage: true });
});

add_task(async function test_crash_and_wakeup_via_persistent_listeners() {
  // Ensure that the background can start in response to a primed listener.
  await ExtensionTestCommon.resetStartupPromises();
  await AddonTestUtils.notifyEarlyStartup();

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      background: { persistent: false },
      optional_permissions: ["tabs"],
    },
    background() {
      const domreadyPromise = new Promise(resolve => {
        window.addEventListener("load", resolve, { once: true });
      });
      browser.permissions.onAdded.addListener(() => {
        browser.test.log("permissions.onAdded has fired");
        // Wait for DOMContentLoaded to have fired before notifying the test.
        // This guarantees that backgroundState is "running" instead of
        // potentially "starting".
        domreadyPromise.then(() => {
          browser.test.sendMessage("event_fired");
        });
      });
    },
  });
  await extension.startup();
  function assertBackgroundState(expected, message) {
    Assert.equal(extension.extension.backgroundState, expected, message);
  }
  function triggerEventInEventPage() {
    // Trigger an event, with the expectation that the event page will wake up.
    // As long as we are the only one to trigger the extension API event in this
    // test, the exact event is not significant. Trigger permissions.onAdded:
    Management.emit("change-permissions", {
      extensionId: extension.id,
      added: {
        origins: [],
        permissions: ["tabs"],
      },
    });
  }

  assertBackgroundState("running", "Background after extension.startup()");

  // Sanity check: triggerEventInEventPage does actually trigger event_fired.
  triggerEventInEventPage();
  await extension.awaitMessage("event_fired");

  // Restart a few times to verify that the behavior is consistent over time.
  const TEST_RESTART_ATTEMPTS = 5;
  for (let i = 1; i <= TEST_RESTART_ATTEMPTS; ++i) {
    info(`Testing that a crashed background wakes via event, attempt ${i}/5`);

    await crashExtensionBackground(extension);

    assertBackgroundState("stopped", "Background state after crash");

    triggerEventInEventPage();
    await extension.awaitMessage("event_fired");

    assertBackgroundState("running", "Persistent event can wake up event page");
  }

  await extension.unload();
});

add_task(
  {
    skip_if: () => !CAN_CRASH_EXTENSIONS,
    pref_set: [
      ["extensions.webextensions.crash.threshold", 3],
      // Set a long timeframe to make sure the few crashes we produce in this
      // test will all be counted within the same timeframe.
      ["extensions.webextensions.crash.timeframe", 60 * 1000],
    ],
  },
  async function test_process_spawning_disabled_because_of_too_many_crashes() {
    // Force-enable process spawning because that will reset the internals of
    // the crash observer.
    ExtensionProcessCrashObserver.enableProcessSpawning();
    Services.fog.testResetFOG();

    function assertCrashThresholdTelemetry({ expectToBeSet }) {
      // Desktop builds are only expected to record crashed_over_threshold_fg,
      // on Android builds xpcshell tests are detected as being in foreground
      // unless we explicitly mock the app being moved in the background as
      // the test tasks test_background_restarted_after_crash already does
      // (and crashed_over_threshold_bg is covered in that test task).

      equal(
        undefined,
        Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(),
        `Initial value of crashed_over_threshold_bg.`
      );

      if (expectToBeSet) {
        Assert.greater(
          Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(),
          0,
          "Expect crashed_over_threshold_fg count to be set."
        );
        return;
      }

      equal(
        undefined,
        Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(),
        `Initial value of crashed_over_threshold_fg.`
      );
    }

    assertCrashThresholdTelemetry({ expectToBeSet: false });

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

    // Ensure that the background can start in response to a primed listener.
    await ExtensionTestCommon.resetStartupPromises();
    await AddonTestUtils.notifyEarlyStartup();

    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        background: { persistent: false },
        optional_permissions: ["tabs"],
      },
      background() {
        const domreadyPromise = new Promise(resolve => {
          window.addEventListener("load", resolve, { once: true });
        });
        browser.permissions.onAdded.addListener(() => {
          browser.test.log("permissions.onAdded has fired");
          // Wait for DOMContentLoaded to have fired before notifying the test.
          // This guarantees that backgroundState is "running" instead of
          // potentially "starting".
          domreadyPromise.then(() => {
            browser.test.sendMessage("event_fired");
          });
        });
      },
    });
    await extension.startup();
    function assertBackgroundState(expected, message) {
      Assert.equal(extension.extension.backgroundState, expected, message);
    }
    function triggerEventInEventPage() {
      // Trigger an event, with the expectation that the event page will wake up.
      // As long as we are the only one to trigger the extension API event in this
      // test, the exact event is not significant. Trigger permissions.onAdded:
      Management.emit("change-permissions", {
        extensionId: extension.id,
        added: {
          origins: [],
          permissions: ["tabs"],
        },
      });
    }

    assertBackgroundState("running", "Background after extension.startup()");

    // Sanity check: triggerEventInEventPage does actually trigger event_fired.
    triggerEventInEventPage();
    await extension.awaitMessage("event_fired");

    // Crash/restart a few times to force the crash observer to disable process
    // spawning on the crash _after_ the loop. Note that the value below should
    // match the "threshold" pref set above.
    const TEST_RESTART_ATTEMPTS = 3;
    for (let i = 1; i <= TEST_RESTART_ATTEMPTS; ++i) {
      info(
        `Crash/restart extension background, attempt ${i}/${TEST_RESTART_ATTEMPTS}`
      );

      await crashExtensionBackground(extension);
      assertBackgroundState("stopped", "Background state after crash");

      triggerEventInEventPage();
      await extension.awaitMessage("event_fired");
      assertBackgroundState(
        "running",
        "Persistent event can wake up event page"
      );
    }

    assertCrashThresholdTelemetry({ expectToBeSet: false });

    info("Crash one more time");
    await crashExtensionBackground(extension);

    assertCrashThresholdTelemetry({ expectToBeSet: true });

    Assert.ok(
      ExtensionProcessCrashObserver.processSpawningDisabled,
      "Expect process spawning to be disabled"
    );

    info("Trigger an event, which shouldn't wake up the event page");
    triggerEventInEventPage();
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, 100));
    assertBackgroundState("stopped", "Background should not have started yet");

    info("Enable process spawning");
    ExtensionProcessCrashObserver.enableProcessSpawning();
    Assert.equal(
      ExtensionProcessCrashObserver.processSpawningDisabled,
      false,
      "Expect process spawning to be enabled"
    );
    assertBackgroundState("stopped", "Background should still be suspended");

    info("Trigger an event, which should wake up the event page");
    triggerEventInEventPage();
    await extension.awaitMessage("event_fired");
    assertBackgroundState("running", "Persistent event can wake up event page");

    info("Crash again");
    await crashExtensionBackground(extension);
    assertBackgroundState("stopped", "Background state after crash");
    Assert.equal(
      ExtensionProcessCrashObserver.processSpawningDisabled,
      false,
      "Expect process spawning to be enabled"
    );

    info("Trigger an event, which should wake up the event page again");
    triggerEventInEventPage();
    await extension.awaitMessage("event_fired");
    assertBackgroundState("running", "Persistent event can wake up event page");

    await extension.unload();
  }
);

add_task(
  {
    skip_if: () => !CAN_CRASH_EXTENSIONS,
    pref_set: [
      ["extensions.webextensions.crash.threshold", 2],
      // Set a long timeframe to make sure the few crashes we produce in this
      // test will all be counted within the same timeframe.
      ["extensions.webextensions.crash.timeframe", 60 * 1000],
    ],
  },
  async function test_background_restarted_after_crash() {
    // Force-enable process spawning because that will reset the internals of
    // the crash observer.
    ExtensionProcessCrashObserver.enableProcessSpawning();
    Assert.equal(
      ExtensionProcessCrashObserver.processSpawningDisabled,
      false,
      "Expect process spawning to be enabled"
    );

    function assertCrashThresholdTelemetry({ fg, bg }) {
      if (fg) {
        Assert.greater(
          Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(),
          0,
          "Expect crashed_over_threshold_fg count to be set."
        );
      } else {
        equal(
          undefined,
          Glean.extensions.processEvent.crashed_over_threshold_fg.testGetValue(),
          `Initial value of crashed_over_threshold_fg.`
        );
      }
      if (bg) {
        Assert.greater(
          Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(),
          0,
          "Expect crashed_over_threshold_bg count to be set."
        );
      } else {
        equal(
          undefined,
          Glean.extensions.processEvent.crashed_over_threshold_bg.testGetValue(),
          `Initial value of crashed_over_threshold_bg.`
        );
      }
    }

    // Setup test environment to match a fully started browser instance
    await ExtensionTestCommon.resetStartupPromises();
    await AddonTestUtils.notifyEarlyStartup();
    Services.fog.testResetFOG();

    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        background: { persistent: true },
      },
      background() {
        window.addEventListener(
          "load",
          () => {
            browser.test.sendMessage("persistentbg_started");
          },
          { once: true }
        );
      },
    });

    await extension.startup();
    await extension.awaitMessage("persistentbg_started");

    function assertBackgroundState(expected, message) {
      Assert.equal(extension.extension.backgroundState, expected, message);
    }

    async function assertStillStoppedAfterTimeout(timeout = 100) {
      // Confirm that the state is still stopped and the background page
      // was not actually in the process of being restarted.
      // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
      await new Promise(resolve => setTimeout(resolve, timeout));
      assertBackgroundState("stopped", "Background should still be stopped");
    }

    async function mockCrashOnAndroidAppInBackground() {
      info("Mock application-background observer service topic");
      ExtensionProcessCrashObserver.observe(null, "application-background");

      Assert.equal(
        ExtensionProcessCrashObserver.appInForeground,
        false,
        "Got expected value set on ExtensionProcessCrashObserver.appInForeground"
      );
      await crashExtensionBackground(extension);
      assertBackgroundState(
        "stopped",
        "Persistent Background state after crash while in the background"
      );

      await assertStillStoppedAfterTimeout();

      info("Mock application-foreground observer service topic");
      ExtensionProcessCrashObserver.observe(null, "application-foreground");

      Assert.equal(
        ExtensionProcessCrashObserver.appInForeground,
        true,
        "Ensure the application is detected as being in foreground"
      );
    }

    assertBackgroundState("running", "Background after extension.startup()");

    // Restart a few times to verify that the behavior is consistent over time.
    info(
      "Testing that a crashed persistent background is restarted after a crash"
    );

    Assert.equal(
      ExtensionProcessCrashObserver.appInForeground,
      true,
      "Ensure the application is detected as being in foreground"
    );
    await crashExtensionBackground(extension);

    await extension.awaitMessage("persistentbg_started");
    assertBackgroundState("running", "Persistent Background state after crash");

    // Mock application moved into the background and background page
    // auto-restart to be deferred to the application being moved
    // back in the foreground.
    if (ExtensionProcessCrashObserver._isAndroid) {
      await mockCrashOnAndroidAppInBackground();
    } else {
      await crashExtensionBackground(extension);
    }

    info("Wait for the persistent background context to be started");
    await extension.awaitMessage("persistentbg_started");
    assertBackgroundState("running", "Persistent Background state after crash");

    info("Mock another crash to be exceeding enforced crash threshold");

    assertCrashThresholdTelemetry({ fg: false, bg: false });

    // Mock application moved into the background and background page
    // auto-restart to be deferred to the application being moved
    // back in the foreground.
    if (ExtensionProcessCrashObserver._isAndroid) {
      await mockCrashOnAndroidAppInBackground();
      assertCrashThresholdTelemetry({ fg: false, bg: true });
    } else {
      await crashExtensionBackground(extension);
      assertCrashThresholdTelemetry({ fg: true, bg: false });
    }

    Assert.ok(
      ExtensionProcessCrashObserver.processSpawningDisabled,
      "Expect process spawning to be disabled"
    );

    assertBackgroundState(
      "stopped",
      "Persistent Background state after crash exceeding threshold"
    );

    await assertStillStoppedAfterTimeout();

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

    info("Wait for Persistent Background Context to be started");
    await extension.awaitMessage("persistentbg_started");
    assertBackgroundState("running", "Persistent Background to be running");

    // Crash again to confirm the threshold has been reset.
    await crashExtensionBackground(extension);
    info("Wait for Persistent Background Context to be started");
    await extension.awaitMessage("persistentbg_started");
    assertBackgroundState("running", "Persistent Background to be running");

    // Crash again to cover explicitly exceeding the crash threshold
    // while the application is in foreground.
    if (ExtensionProcessCrashObserver._isAndroid) {
      Assert.equal(
        ExtensionProcessCrashObserver.appInForeground,
        true,
        "Ensure the application is detected as being in foreground"
      );

      await crashExtensionBackground(extension);

      info("Wait for Persistent Background Context to be started");
      await extension.awaitMessage("persistentbg_started");
      assertBackgroundState("running", "Persistent Background to be running");

      // Crash one more time to exceed the threshold.
      await crashExtensionBackground(extension);

      assertCrashThresholdTelemetry({ fg: true, bg: true });

      Assert.ok(
        ExtensionProcessCrashObserver.processSpawningDisabled,
        "Expect process spawning to be disabled"
      );
      await assertStillStoppedAfterTimeout();
    }

    await extension.unload();
  }
);
