/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { JsonSchema } = ChromeUtils.importESModule(
  "resource://gre/modules/JsonSchema.sys.mjs"
);

const { TelemetryArchive } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryArchive.sys.mjs"
);

const { TelemetryStorage } = ChromeUtils.importESModule(
  "resource://gre/modules/TelemetryStorage.sys.mjs"
);

const ORIG_AVAILABLE_LOCALES = Services.locale.availableLocales;
const ORIG_REQUESTED_LOCALES = Services.locale.requestedLocales;

const PREF_ION_ID = "toolkit.telemetry.pioneerId";
const PREF_ION_NEW_STUDIES_AVAILABLE =
  "toolkit.telemetry.pioneer-new-studies-available";
const PREF_ION_COMPLETED_STUDIES =
  "toolkit.telemetry.pioneer-completed-studies";

const PREF_TEST_CACHED_CONTENT = "toolkit.pioneer.testCachedContent";
const PREF_TEST_CACHED_ADDONS = "toolkit.pioneer.testCachedAddons";
const PREF_TEST_ADDONS = "toolkit.pioneer.testAddons";

const CACHED_CONTENT = [
  {
    title: "<b>test title</b><p>test title line 2</p>",
    summary: "<p>test summary</p>test summary line 2",
    details:
      "<ol><li>test details</li><li>test details line 2</li><li>test details line</li></ol>",
    data: "<b>test data</b>",
    joinIonConsent: "<p>test join consent</p><p>join consent line 2</p>",
    leaveIonConsent:
      "<p>test leave consent</p><p>test leave consent line 2</p>",
  },
];

const CACHED_ADDONS = [
  {
    addon_id: "ion-v2-example@mozilla.org",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Demo Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Ion Developers",
      url: "https://addons.mozilla.org/en-US/firefox/user/6510522/",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: false,
    studyEnded: true,
    joinStudyConsent: "<b>test123</b>",
    leaveStudyConsent: `<a href="test345">test345</a>`,
  },
  {
    addon_id: "ion-v2-default-example@mozilla.org",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Demo Default Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Ion Developers",
      url: "https://addons.mozilla.org/en-US/firefox/user/6510522/",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: true,
    studyEnded: false,
    joinStudyConsent: "test456",
    leaveStudyConsent: "test789",
  },
  {
    addon_id: "study@partner",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Example Partner Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Study Partners",
      url: "http://localhost",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: false,
    studyEnded: false,
    joinStudyConsent: "test012",
    leaveStudyConsent: "test345",
  },
  {
    addon_id: "second-study@partner",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Example Second Partner Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Second Study Partners",
      url: "https://localhost",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: false,
    studyEnded: false,
    joinStudyConsent: "test678",
    leaveStudyConsent: "test901",
  },
];

const CACHED_ADDONS_BAD_DEFAULT = [
  {
    addon_id: "ion-v2-bad-default-example@mozilla.org",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Demo Default Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Ion Developers",
      url: "https://addons.mozilla.org/en-US/firefox/user/6510522/",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: true,
    studyEnded: false,
    joinStudyConsent: "test456",
    leaveStudyConsent: "test789",
  },
  {
    addon_id: "study@partner",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Example Partner Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Study Partners",
      url: "http://localhost",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: false,
    studyEnded: false,
    joinStudyConsent: "test012",
    leaveStudyConsent: "test345",
  },
  {
    addon_id: "second-study@partner",
    icons: {
      32: "https://localhost/user-media/addon_icons/2644/2644632-32.png?modified=4a64e2bc",
      64: "https://localhost/user-media/addon_icons/2644/2644632-64.png?modified=4a64e2bc",
      128: "https://localhost/user-media/addon_icons/2644/2644632-128.png?modified=4a64e2bc",
    },
    name: "Example Second Partner Study",
    version: "1.0",
    sourceURI: {
      spec: "https://localhost",
    },
    description: "Study purpose: Testing Ion.",
    privacyPolicy: {
      spec: "http://localhost",
    },
    studyType: "extension",
    authors: {
      name: "Second Study Partners",
      url: "https://localhost",
    },
    dataCollectionDetails: ["test123", "test345"],
    moreInfo: {
      spec: "http://localhost",
    },
    isDefault: false,
    studyEnded: false,
    joinStudyConsent: "test678",
    leaveStudyConsent: "test901",
  },
];

const TEST_ADDONS = [
  { id: "ion-v2-example@ion.mozilla.org" },
  { id: "ion-v2-default-example@mozilla.org" },
  { id: "study@partner" },
  { id: "second-study@parnter" },
];

const setupLocale = async locale => {
  Services.locale.availableLocales = [locale];
  Services.locale.requestedLocales = [locale];
};

const clearLocale = async () => {
  Services.locale.availableLocales = ORIG_AVAILABLE_LOCALES;
  Services.locale.requestedLocales = ORIG_REQUESTED_LOCALES;
};

add_task(async function testMockSchema() {
  for (const [schemaName, values] of [
    ["IonContentSchema", CACHED_CONTENT],
    ["IonStudyAddonsSchema", CACHED_ADDONS],
  ]) {
    const response = await fetch(
      `resource://testing-common/${schemaName}.json`
    );

    const schema = await response.json();
    if (!schema) {
      throw new Error(`Failed to load ${schemaName}`);
    }

    const validator = new JsonSchema.Validator(schema, { shortCircuit: false });

    for (const entry of values) {
      const result = validator.validate(entry);
      if (!result.valid) {
        throw new Error(JSON.stringify(result.errors));
      }
    }
  }
});

add_task(async function testBadDefaultAddon() {
  const cachedContent = JSON.stringify(CACHED_CONTENT);
  const cachedAddons = JSON.stringify(CACHED_ADDONS_BAD_DEFAULT);

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_ADDONS, cachedAddons],
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [
      [PREF_ION_ID, ""],
      [PREF_ION_COMPLETED_STUDIES, "[]"],
    ],
  });

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(beforePref === null, "before enrollment, Ion pref is null.");
      const enrollmentButton =
        content.document.getElementById("enrollment-button");
      enrollmentButton.click();

      const dialog = content.document.getElementById("join-ion-consent-dialog");
      ok(dialog.open, "after clicking enrollment, consent dialog is open.");

      // When a modal dialog is cancelled, the inertness for other elements
      // is reverted. However, in order to have the new state (non-inert)
      // effective, Firefox needs to do a frame flush. This flush is taken
      // place when it's really needed.
      // getBoundingClientRect forces a frame flush here to ensure the
      // following click is going to work properly.
      enrollmentButton.getBoundingClientRect();
      enrollmentButton.click();
      ok(dialog.open, "after retrying enrollment, consent dialog is open.");

      const acceptDialogButton = content.document.getElementById(
        "join-ion-accept-dialog-button"
      );
      // Wait for the enrollment button to change its label to "leave", meaning
      // that the policy was accepted.
      let promiseDialogAccepted = BrowserTestUtils.waitForAttribute(
        "data-l10n-id",
        enrollmentButton
      );
      acceptDialogButton.click();

      const ionEnrolled = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(ionEnrolled, "after enrollment, Ion pref is set.");

      await promiseDialogAccepted;
      ok(
        document.l10n.getAttributes(enrollmentButton).id ==
          "ion-unenrollment-button",
        "After Ion enrollment, join button is now leave button"
      );

      const availableStudies =
        content.document.getElementById("available-studies");
      ok(
        document.l10n.getAttributes(availableStudies).id ==
          "ion-no-current-studies",
        "No studies are available if default add-on install fails."
      );
    }
  );
});

add_task(async function testAboutPage() {
  const cachedContent = JSON.stringify(CACHED_CONTENT);
  const cachedAddons = JSON.stringify(CACHED_ADDONS);

  // Clear any previously generated archived ping before moving on
  // with this test.
  await TelemetryStorage.runCleanPingArchiveTask();

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_ADDONS, cachedAddons],
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [
      [PREF_ION_ID, ""],
      [PREF_ION_COMPLETED_STUDIES, "[]"],
    ],
  });

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(beforePref === null, "before enrollment, Ion pref is null.");

      const beforeToolbarButton = document.getElementById("ion-button");
      ok(
        beforeToolbarButton.hidden,
        "before enrollment, Ion toolbar button is hidden."
      );

      const enrollmentButton =
        content.document.getElementById("enrollment-button");

      for (const section of ["details", "data"]) {
        ok(
          content.document.getElementById(section).open === true,
          "before enrollment, dialog sections are open."
        );
      }

      enrollmentButton.click();

      const dialog = content.document.getElementById("join-ion-consent-dialog");
      ok(dialog.open, "after clicking enrollment, consent dialog is open.");

      const cancelDialogButton = content.document.getElementById(
        "join-ion-cancel-dialog-button"
      );
      cancelDialogButton.click();

      ok(
        !dialog.open,
        "after cancelling enrollment, consent dialog is closed."
      );

      const canceledEnrollment = Services.prefs.getStringPref(
        PREF_ION_ID,
        null
      );

      ok(
        !canceledEnrollment,
        "after cancelling enrollment, Ion is not enrolled."
      );

      // When a modal dialog is cancelled, the inertness for other elements
      // is reverted. However, in order to have the new state (non-inert)
      // effective, Firefox needs to do a frame flush. This flush is taken
      // place when it's really needed.
      // getBoundingClientRect forces a frame flush here to ensure the
      // following click is going to work properly.
      enrollmentButton.getBoundingClientRect();
      enrollmentButton.click();
      ok(dialog.open, "after retrying enrollment, consent dialog is open.");

      const acceptDialogButton = content.document.getElementById(
        "join-ion-accept-dialog-button"
      );
      // Wait for the enrollment button to change its label to "leave", meaning
      // that the policy was accepted.
      let promiseDialogAccepted = BrowserTestUtils.waitForAttribute(
        "data-l10n-id",
        enrollmentButton
      );
      acceptDialogButton.click();

      const ionEnrolled = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(ionEnrolled, "after enrollment, Ion pref is set.");

      await promiseDialogAccepted;
      ok(
        document.l10n.getAttributes(enrollmentButton).id ==
          "ion-unenrollment-button",
        "After Ion enrollment, join button is now leave button"
      );

      const enrolledToolbarButton = document.getElementById("ion-button");
      ok(
        !enrolledToolbarButton.hidden,
        "after enrollment, Ion toolbar button is not hidden."
      );

      for (const section of ["details", "data"]) {
        ok(
          content.document.getElementById(section).open === false,
          "after enrollment, dialog sections are closed."
        );
      }

      for (const cachedAddon of CACHED_ADDONS) {
        const addonId = cachedAddon.addon_id;
        const joinButton = content.document.getElementById(
          `${addonId}-join-button`
        );

        if (cachedAddon.isDefault) {
          ok(!joinButton, "There is no join button for default study.");
          continue;
        }

        const completedStudies = Services.prefs.getStringPref(
          PREF_ION_COMPLETED_STUDIES,
          "{}"
        );

        const studies = JSON.parse(completedStudies);

        if (cachedAddon.studyEnded || Object.keys(studies).includes(addonId)) {
          ok(
            joinButton.disabled,
            "Join button is disabled, study has already ended."
          );
          continue;
        }

        ok(
          !joinButton.disabled,
          "Before study enrollment, join button is enabled."
        );

        const studyCancelButton = content.document.getElementById(
          "join-study-cancel-dialog-button"
        );

        const joinDialogOpen = new Promise(resolve => {
          content.document
            .getElementById("join-study-consent-dialog")
            .addEventListener("open", () => {
              // Run resolve() on the next tick.
              setTimeout(() => resolve(), 0);
            });
        });

        // When a modal dialog is cancelled, the inertness for other elements
        // is reverted. However, in order to have the new state (non-inert)
        // effective, Firefox needs to do a frame flush. This flush is taken
        // place when it's really needed.
        // getBoundingClientRect forces a frame flush here to ensure the
        // following click is going to work properly.
        //
        // Note: this initial call is required because we're cycling through
        // addons. So while in the first iteration this would work, it could
        // fail on the second or third.
        joinButton.getBoundingClientRect();
        joinButton.click();

        await joinDialogOpen;

        ok(
          content.document.getElementById("join-study-consent").innerHTML ==
            `${cachedAddon.joinStudyConsent}`,
          "Join consent text matches remote settings data."
        );

        studyCancelButton.click();

        ok(
          !joinButton.disabled,
          "After canceling study enrollment, join button is enabled."
        );

        // When a modal dialog is cancelled, the inertness for other elements
        // is reverted. However, in order to have the new state (non-inert)
        // effective, Firefox needs to do a frame flush. This flush is taken
        // place when it's really needed.
        // getBoundingClientRect forces a frame flush here to ensure the
        // following click is going to work properly.
        joinButton.getBoundingClientRect();
        joinButton.click();

        const studyAcceptButton = content.document.getElementById(
          "join-study-accept-dialog-button"
        );

        // Wait for the "Join Button" to change to a "leave button".
        let promiseJoinTurnsToLeave = BrowserTestUtils.waitForAttribute(
          "data-l10n-id",
          joinButton
        );
        studyAcceptButton.click();
        await promiseJoinTurnsToLeave;

        ok(
          document.l10n.getAttributes(joinButton).id == "ion-leave-study",
          "After study enrollment, join button is now leave button"
        );

        ok(
          !joinButton.disabled,
          "After study enrollment, leave button is enabled."
        );

        const leaveStudyCancelButton = content.document.getElementById(
          "leave-study-cancel-dialog-button"
        );

        const leaveDialogOpen = new Promise(resolve => {
          content.document
            .getElementById("leave-study-consent-dialog")
            .addEventListener("open", () => {
              // Run resolve() on the next tick.
              setTimeout(() => resolve(), 0);
            });
        });

        // When a modal dialog is cancelled, the inertness for other elements
        // is reverted. However, in order to have the new state (non-inert)
        // effective, Firefox needs to do a frame flush. This flush is taken
        // place when it's really needed.
        // getBoundingClientRect forces a frame flush here to ensure the
        // following click is going to work properly.
        joinButton.getBoundingClientRect();
        joinButton.click();

        await leaveDialogOpen;

        ok(
          content.document.getElementById("leave-study-consent").innerHTML ==
            `${cachedAddon.leaveStudyConsent}`,
          "Leave consent text matches remote settings data."
        );

        leaveStudyCancelButton.click();

        ok(
          !joinButton.disabled,
          "After canceling study leave, leave/join button is enabled."
        );

        // When a modal dialog is cancelled, the inertness for other elements
        // is reverted. However, in order to have the new state (non-inert)
        // effective, Firefox needs to do a frame flush. This flush is taken
        // place when it's really needed.
        // getBoundingClientRect forces a frame flush here to ensure the
        // following click is going to work properly.
        joinButton.getBoundingClientRect();
        joinButton.click();

        const acceptStudyCancelButton = content.document.getElementById(
          "leave-study-accept-dialog-button"
        );

        let promiseJoinButtonDisabled = BrowserTestUtils.waitForAttribute(
          "disabled",
          joinButton
        );
        acceptStudyCancelButton.click();
        await promiseJoinButtonDisabled;

        ok(
          joinButton.disabled,
          "After leaving study, join button is disabled."
        );

        ok(
          Services.prefs.getStringPref(PREF_TEST_ADDONS, null) == "[]",
          "Correct add-on was uninstalled"
        );
      }

      enrollmentButton.click();

      const cancelUnenrollmentDialogButton = content.document.getElementById(
        "leave-ion-cancel-dialog-button"
      );
      cancelUnenrollmentDialogButton.click();

      const ionStillEnrolled = Services.prefs.getStringPref(PREF_ION_ID, null);

      ok(
        ionStillEnrolled,
        "after canceling unenrollment, Ion pref is still set."
      );

      enrollmentButton.click();

      const acceptUnenrollmentDialogButton = content.document.getElementById(
        "leave-ion-accept-dialog-button"
      );

      acceptUnenrollmentDialogButton.click();

      // Wait for deletion ping, uninstalls, and UI updates...
      const ionUnenrolled = await new Promise((resolve, reject) => {
        Services.prefs.addObserver(
          PREF_ION_ID,
          function observer(subject, topic, data) {
            try {
              const prefValue = Services.prefs.getStringPref(PREF_ION_ID, null);
              Services.prefs.removeObserver(PREF_ION_ID, observer);
              resolve(prefValue);
            } catch (ex) {
              Services.prefs.removeObserver(PREF_ION_ID, observer);
              reject(ex);
            }
          }
        );
      });

      ok(!ionUnenrolled, "after accepting unenrollment, Ion pref is null.");

      const unenrolledToolbarButton = document.getElementById("ion-button");
      ok(
        unenrolledToolbarButton.hidden,
        "after unenrollment, Ion toolbar button is hidden."
      );

      await TelemetryStorage.testClearPendingPings();
      let pings = await TelemetryArchive.promiseArchivedPingList();

      let pingDetails = [];
      for (const ping of pings) {
        ok(
          ping.type == "pioneer-study",
          "ping is of expected type pioneer-study"
        );
        const details = await TelemetryArchive.promiseArchivedPingById(ping.id);
        pingDetails.push(details.payload.studyName);
      }

      Services.prefs.setStringPref(PREF_TEST_ADDONS, "[]");

      for (const cachedAddon of CACHED_ADDONS) {
        const addonId = cachedAddon.addon_id;

        ok(
          pingDetails.includes(addonId),
          "each test add-on has sent a deletion ping"
        );

        const joinButton = content.document.getElementById(
          `${addonId}-join-button`
        );

        if (cachedAddon.isDefault) {
          ok(!joinButton, "There is no join button for default study.");
        } else {
          ok(
            joinButton.disabled,
            "After unenrollment, join button is disabled."
          );
        }

        for (const section of ["details", "data"]) {
          ok(
            content.document.getElementById(section).open === true,
            "after unenrollment, dialog sections are open."
          );
        }
      }
    }
  );
});

add_task(async function testEnrollmentPings() {
  const CACHED_TEST_ADDON = CACHED_ADDONS[2];
  const cachedContent = JSON.stringify(CACHED_CONTENT);
  const cachedAddons = JSON.stringify([CACHED_TEST_ADDON]);

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_ADDONS, cachedAddons],
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [
      [PREF_ION_ID, ""],
      [PREF_ION_COMPLETED_STUDIES, "[]"],
    ],
  });

  // Clear any pending pings.
  await TelemetryStorage.testClearPendingPings();

  // Check how many archived pings we already have, so that we can count new pings.
  let beginPingCount = (await TelemetryArchive.promiseArchivedPingList())
    .length;

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(beforePref === null, "before enrollment, Ion pref is null.");

      // Enroll in ion.
      const enrollmentButton =
        content.document.getElementById("enrollment-button");

      let promiseDialogAccepted = BrowserTestUtils.waitForAttribute(
        "data-l10n-id",
        enrollmentButton
      );

      // When a modal dialog is cancelled, the inertness for other elements
      // is reverted. However, in order to have the new state (non-inert)
      // effective, Firefox needs to do a frame flush. This flush is taken
      // place when it's really needed.
      // getBoundingClientRect forces a frame flush here to ensure the
      // following click is going to work properly.
      enrollmentButton.getBoundingClientRect();
      enrollmentButton.click();

      const acceptDialogButton = content.document.getElementById(
        "join-ion-accept-dialog-button"
      );
      acceptDialogButton.click();

      const ionId = Services.prefs.getStringPref(PREF_ION_ID, null);
      ok(ionId, "after enrollment, Ion pref is set.");

      await promiseDialogAccepted;

      // Enroll in the CACHED_TEST_ADDON study.
      const joinButton = content.document.getElementById(
        `${CACHED_TEST_ADDON.addon_id}-join-button`
      );

      const joinDialogOpen = new Promise(resolve => {
        content.document
          .getElementById("join-study-consent-dialog")
          .addEventListener("open", () => {
            resolve();
          });
      });

      // When a modal dialog is cancelled, the inertness for other elements
      // is reverted. However, in order to have the new state (non-inert)
      // effective, Firefox needs to do a frame flush. This flush is taken
      // place when it's really needed.
      // getBoundingClientRect forces a frame flush here to ensure the
      // following click is going to work properly.
      joinButton.getBoundingClientRect();
      joinButton.click();

      await joinDialogOpen;

      // Accept consent for the study.
      const studyAcceptButton = content.document.getElementById(
        "join-study-accept-dialog-button"
      );

      studyAcceptButton.click();

      // Verify that the proper pings were generated.
      let pings;
      await TestUtils.waitForCondition(async function () {
        pings = await TelemetryArchive.promiseArchivedPingList();
        return pings.length - beginPingCount >= 2;
      }, "Wait until we have at least 2 pings in the telemetry archive");

      let pingDetails = [];
      for (const ping of pings) {
        ok(
          ping.type == "pioneer-study",
          "ping is of expected type pioneer-study"
        );
        const details = await TelemetryArchive.promiseArchivedPingById(ping.id);
        pingDetails.push({
          schemaName: details.payload.schemaName,
          schemaNamespace: details.payload.schemaNamespace,
          studyName: details.payload.studyName,
          pioneerId: details.payload.pioneerId,
        });
      }

      // We expect 1 ping with just the ion id (ion consent) and another
      // with both the ion id and the study id (study consent).
      ok(
        pingDetails.find(
          p =>
            p.schemaName == "pioneer-enrollment" &&
            p.schemaNamespace == "pioneer-meta" &&
            p.pioneerId == ionId &&
            p.studyName == "pioneer-meta"
        ),
        "We expect the Ion program consent to be present"
      );

      ok(
        pingDetails.find(
          p =>
            p.schemaName == "pioneer-enrollment" &&
            p.schemaNamespace == CACHED_TEST_ADDON.addon_id &&
            p.pioneerId == ionId &&
            p.studyName == CACHED_TEST_ADDON.addon_id
        ),
        "We expect the study consent to be present"
      );
    }
  );
});

add_task(async function testIonBadge() {
  await SpecialPowers.pushPrefEnv({
    set: [[PREF_ION_NEW_STUDIES_AVAILABLE, true]],
    clear: [
      [PREF_ION_NEW_STUDIES_AVAILABLE, false],
      [PREF_ION_ID, ""],
    ],
  });

  let ionTab = await BrowserTestUtils.openNewForegroundTab({
    url: "about:ion",
    gBrowser,
  });

  const enrollmentButton = content.document.getElementById("enrollment-button");
  enrollmentButton.click();

  let blankTab = await BrowserTestUtils.openNewForegroundTab({
    url: "about:home",
    gBrowser,
  });

  Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, true);

  const toolbarButton = document.getElementById("ion-button");
  const toolbarBadge = toolbarButton.querySelector(".toolbarbutton-badge");

  ok(
    toolbarBadge.classList.contains("feature-callout"),
    "When pref is true, Ion toolbar button is called out in the current window."
  );

  toolbarButton.click();

  await ionTab;

  ok(
    !toolbarBadge.classList.contains("feature-callout"),
    "When about:ion toolbar button is pressed, call-out is removed."
  );

  Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, true);

  const newWin = await BrowserTestUtils.openNewBrowserWindow();
  const newToolbarBadge = toolbarButton.querySelector(".toolbarbutton-badge");

  ok(
    newToolbarBadge.classList.contains("feature-callout"),
    "When pref is true, Ion toolbar button is called out in a new window."
  );

  await BrowserTestUtils.closeWindow(newWin);
  await BrowserTestUtils.removeTab(ionTab);
  await BrowserTestUtils.removeTab(blankTab);
});

add_task(async function testContentReplacement() {
  const cachedContent = JSON.stringify(CACHED_CONTENT);

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [[PREF_ION_ID, ""]],
  });

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      // Check that text was updated from Remote Settings.
      console.log("debug:", content.document.getElementById("title").innerHTML);
      ok(
        content.document.getElementById("title").innerHTML ==
          "<b>test title</b><p>test title line 2</p>",
        "Title was replaced correctly."
      );
      ok(
        content.document.getElementById("summary").innerHTML ==
          "<p>test summary</p>test summary line 2",
        "Summary was replaced correctly."
      );
      ok(
        content.document.getElementById("details").innerHTML ==
          "<ol><li>test details</li><li>test details line 2</li><li>test details line</li></ol>",
        "Details was replaced correctly."
      );
      ok(
        content.document.getElementById("data").innerHTML == "<b>test data</b>",
        "Data was replaced correctly."
      );
      ok(
        content.document.getElementById("join-ion-consent").innerHTML ==
          "<p>test join consent</p><p>join consent line 2</p>",
        "Join consent was replaced correctly."
      );
      ok(
        content.document.getElementById("leave-ion-consent").innerHTML ==
          "<p>test leave consent</p><p>test leave consent line 2</p>",
        "Leave consent was replaced correctly."
      );
    }
  );
});

add_task(async function testBadContentReplacement() {
  const cachedContent = JSON.stringify([
    {
      joinIonConsent: "<script>alert('failed')</script>",
      leaveIonConsent: `<a href="blob:blah">blob</a>`,
    },
  ]);

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [[PREF_ION_ID, ""]],
  });

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      // Check that text was updated from Remote Settings.
      ok(
        content.document.getElementById("join-ion-consent").innerHTML == "",
        "Script tags are skipped."
      );
      ok(
        content.document.getElementById("leave-ion-consent").innerHTML ==
          "<a>blob</a>",
        "Bad HREFs are stripped."
      );
    }
  );
});

add_task(async function testLocaleGating() {
  const cachedContent = JSON.stringify(CACHED_CONTENT);
  const cachedAddons = JSON.stringify(CACHED_ADDONS);

  await SpecialPowers.pushPrefEnv({
    set: [
      [PREF_TEST_CACHED_ADDONS, cachedAddons],
      [PREF_TEST_CACHED_CONTENT, cachedContent],
      [PREF_TEST_ADDONS, "[]"],
    ],
    clear: [
      [PREF_ION_ID, ""],
      [PREF_ION_COMPLETED_STUDIES, "[]"],
    ],
  });

  await setupLocale("de");

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      const localeNotificationBar = content.document.getElementById(
        "locale-notification"
      );

      is(
        Services.locale.requestedLocales[0],
        "de",
        "The requestedLocales has been set to German ('de')."
      );

      is(
        getComputedStyle(localeNotificationBar).display,
        "block",
        "Because the page locale is German, the notification bar is not hidden."
      );
    }
  );

  await clearLocale();

  await BrowserTestUtils.withNewTab(
    {
      url: "about:ion",
      gBrowser,
    },
    async function taskFn(browser) {
      const localeNotificationBar = content.document.getElementById(
        "locale-notification"
      );

      is(
        Services.locale.requestedLocales[0],
        "en-US",
        "The requestedLocales has been set to English ('en-US')."
      );

      is(
        getComputedStyle(localeNotificationBar).display,
        "none",
        "Because the page locale is en-US, the notification bar is hidden."
      );
    }
  );
});
