/* eslint max-len: ["error", 80] */

requestLongerTimeout(2);

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

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

ChromeUtils.defineESModuleGetters(this, {
  PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});

AddonTestUtils.initMochitest(this);

const server = AddonTestUtils.createHttpServer();

const LOCALE_ADDON_ID = "postponed-langpack@mochi.test";
const HIDDEN_ADDON_ID = "hidden-addon@mochi.test";

let gProvider;
let gCUITestUtils = new CustomizableUITestUtils(window);

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.checkUpdateSecurity", false]],
  });

  // Also include a langpack with a pending postponed install.
  const fakeLocalePostponedInstall = {
    name: "updated langpack",
    version: "2.0",
    state: AddonManager.STATE_POSTPONED,
  };

  gProvider = new MockProvider();
  gProvider.createAddons([
    {
      id: LOCALE_ADDON_ID,
      name: "Postponed Langpack",
      type: "locale",
      version: "1.0",
      // Mock pending upgrade property on the mocked langpack add-on.
      pendingUpgrade: {
        install: fakeLocalePostponedInstall,
      },
    },
    {
      id: HIDDEN_ADDON_ID,
      name: "Hidden Extension (privileged or system)",
      type: "extension",
      version: "1.0",
      // pendingUpgrade is only set when the staged addon is installed
      // in the same location as the existing one.
      pendingUpgrade: null,
      hidden: true,
    },
  ]);

  fakeLocalePostponedInstall.existingAddon = gProvider.addons[0];

  const fakeHiddenAddonPostponedInstall = {
    name: gProvider.addons[1].name,
    version: "2.0",
    state: AddonManager.STATE_POSTPONED,
  };
  fakeHiddenAddonPostponedInstall.existingAddon = gProvider.addons[1];
  fakeHiddenAddonPostponedInstall.addon = new MockAddon(HIDDEN_ADDON_ID);
  fakeHiddenAddonPostponedInstall.addon.version = "2.0";
  fakeHiddenAddonPostponedInstall.addon.hidden = true;

  gProvider.createInstalls([
    fakeLocalePostponedInstall,
    fakeHiddenAddonPostponedInstall,
  ]);

  registerCleanupFunction(() => {
    cleanupPendingNotifications();
  });
});

function createTestExtension({
  id = "test-pending-update@test",
  newManifest = {},
  oldManifest = {},
}) {
  function background() {
    browser.runtime.onUpdateAvailable.addListener(() => {
      browser.test.sendMessage("update-available");
    });

    browser.test.sendMessage("bgpage-ready");
  }

  const serverHost = `http://localhost:${server.identity.primaryPort}`;
  const updatesPath = `/ext-updates-${id}.json`;
  const update_url = `${serverHost}${updatesPath}`;

  const manifest = {
    name: "Test Pending Update",
    ...oldManifest,
    browser_specific_settings: {
      gecko: { id, update_url },
    },
    version: "1",
  };

  let extension = ExtensionTestUtils.loadExtension({
    background,
    manifest: {
      ...oldManifest,
      ...manifest,
      browser_specific_settings: {
        gecko: {
          ...(oldManifest.browser_specific_settings?.gecko ?? {}),
          ...manifest.browser_specific_settings.gecko,
        },
      },
    },
    // Use permanent so the add-on can be updated.
    useAddonManager: "permanent",
  });

  let updateXpi = AddonTestUtils.createTempWebExtensionFile({
    manifest: {
      ...manifest,
      ...newManifest,
      version: "2",
    },
  });

  let xpiFilename = `/update-${id}.xpi`;
  server.registerFile(xpiFilename, updateXpi);
  AddonTestUtils.registerJSON(server, updatesPath, {
    addons: {
      [id]: {
        updates: [
          {
            version: "2",
            update_link: serverHost + xpiFilename,
          },
        ],
      },
    },
  });

  return { extension, updateXpi };
}

async function promiseUpdateAvailable(extension) {
  info("Wait for the extension to receive onUpdateAvailable event");
  await extension.awaitMessage("update-available");
}

function expectUpdatesAvailableBadgeCount({ win, expectedNumber }) {
  const categoriesSidebar = win.document.querySelector("categories-box");
  ok(categoriesSidebar, "Found the categories-box element");
  const availableButton =
    categoriesSidebar.getButtonByName("available-updates");
  is(
    availableButton.badgeCount,
    expectedNumber,
    expectedNumber
      ? `Expect only ${expectedNumber} available updates`
      : "Expect no available updates"
  );
  ok(
    expectedNumber ? !availableButton.hidden : availableButton.hidden,
    `Expect the available updates category to be ${
      expectedNumber ? "visible" : "hidden"
    }`
  );
}

async function expectAddonInstallStatePostponed(id) {
  const [addonInstall] = (await AddonManager.getAllInstalls()).filter(
    install => install.existingAddon && install.existingAddon.id == id
  );
  is(
    addonInstall && addonInstall.state,
    AddonManager.STATE_POSTPONED,
    "AddonInstall is in the postponed state"
  );
}

function expectCardOptionsButtonBadged({ id, win, hasBadge = true }) {
  const card = getAddonCard(win, id);
  const moreOptionsEl = card.querySelector(".more-options-button");
  is(
    moreOptionsEl.classList.contains("more-options-button-badged"),
    hasBadge,
    `The options button should${hasBadge || "n't"} have the update badge`
  );
}

function getCardPostponedBar({ id, win }) {
  const card = getAddonCard(win, id);
  return card.querySelector(".update-postponed-bar");
}

function waitCardAndAddonUpdated({ id, win }) {
  const card = getAddonCard(win, id);
  const updatedExtStarted = AddonTestUtils.promiseWebExtensionStartup(id);
  const updatedCard = BrowserTestUtils.waitForEvent(card, "update");
  return Promise.all([updatedExtStarted, updatedCard]);
}

async function testPostponedBarVisibility({ id, win, hidden = false }) {
  const postponedBar = getCardPostponedBar({ id, win });
  is(
    postponedBar.hidden,
    hidden,
    `${id} update postponed message bar should be ${
      hidden ? "hidden" : "visible"
    }`
  );

  if (!hidden) {
    await expectAddonInstallStatePostponed(id);
  }
}

async function assertPostponedBarVisibleInAllViews({ id, win }) {
  info("Test postponed bar visibility in extension list view");
  await testPostponedBarVisibility({ id, win });

  info("Test postponed bar visibility in available view");
  await switchView(win, "available-updates");
  await testPostponedBarVisibility({ id, win });

  info("Test that available updates count do not include postponed langpacks");
  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 });

  info("Test postponed langpacks are not listed in the available updates view");
  ok(
    !getAddonCard(win, LOCALE_ADDON_ID),
    "Locale addon is expected to not be listed in the updates view"
  );

  info("Test that postponed bar isn't visible on postponed langpacks");
  await switchView(win, "locale");
  await testPostponedBarVisibility({ id: LOCALE_ADDON_ID, win, hidden: true });

  info("Test postponed bar visibility in extension detail view");
  await switchView(win, "extension");
  await switchToDetailView({ win, id });
  await testPostponedBarVisibility({ id, win });
}

async function completePostponedUpdate({ id, win }) {
  expectCardOptionsButtonBadged({ id, win, hasBadge: false });

  await testPostponedBarVisibility({ id, win });

  let addon = await AddonManager.getAddonByID(id);
  is(addon.version, "1", "Addon version is 1");

  const promiseUpdated = waitCardAndAddonUpdated({ id, win });
  const postponedBar = getCardPostponedBar({ id, win });
  postponedBar.querySelector("button").click();
  await promiseUpdated;

  addon = await AddonManager.getAddonByID(id);
  is(addon.version, "2", "Addon version is 2");

  await testPostponedBarVisibility({ id, win, hidden: true });
}

async function verifyAppMenuAddonNotifications({
  expectedNotification,
  clickAddonNotification = false,
}) {
  await gCUITestUtils.openMainMenu();
  is(
    PanelUI.addonNotificationContainer.children.length,
    expectedNotification ? 1 : 0,
    expectedNotification
      ? "Expect messagebar for granting addon update permissions"
      : "Expect no messagebar for granting addon update permissions"
  );
  if (expectedNotification && clickAddonNotification) {
    PanelUI.addonNotificationContainer.children[0].click();
  } else {
    await gCUITestUtils.hideMainMenu();
  }
}

async function simulateBackgroundAddonUpdateCheck(addonId) {
  const addon = await AddonManager.getAddonByID(addonId);
  const promiseDownloadEnded =
    AddonTestUtils.promiseInstallEvent("onDownloadEnded");
  addon.findUpdates(
    {
      onUpdateAvailable(addon, install) {
        install.promptHandler = (...args) => {
          return AddonManager.updatePromptHandler(...args);
        };
        install.install();
      },
    },
    AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
  );
  await promiseDownloadEnded;
}

add_task(async function test_pending_update_with_prompted_permission() {
  const id = "test-pending-update-with-prompted-permission@mochi.test";

  const { extension } = createTestExtension({
    id,
    newManifest: { permissions: ["<all_urls>"] },
  });

  await extension.startup();
  await extension.awaitMessage("bgpage-ready");

  const win = await loadInitialView("extension");

  // Force about:addons to check for updates.
  let promisePermissionHandled = handlePermissionPrompt({
    addonId: extension.id,
    assertIcon: false,
  });
  win.checkForUpdates();
  await promisePermissionHandled;

  await promiseUpdateAvailable(extension);
  await expectAddonInstallStatePostponed(id);

  await completePostponedUpdate({ id, win });

  await closeView(win);
  await extension.unload();
});

add_task(async function test_pending_manual_install_over_existing() {
  const id = "test-pending-manual-install-over-existing@mochi.test";

  const { extension, updateXpi } = createTestExtension({
    id,
  });

  await extension.startup();
  await extension.awaitMessage("bgpage-ready");

  let win = await loadInitialView("extension");

  info("Manually install new xpi over the existing extension");
  const promiseInstalled = AddonTestUtils.promiseInstallFile(updateXpi);
  await promiseUpdateAvailable(extension);

  await assertPostponedBarVisibleInAllViews({ id, win });

  info("Test postponed bar visibility after reopening about:addons");
  await closeView(win);
  win = await loadInitialView("extension");
  await assertPostponedBarVisibleInAllViews({ id, win });

  await completePostponedUpdate({ id, win });
  await promiseInstalled;

  await closeView(win);
  await extension.unload();
});

add_task(async function test_pending_update_no_prompted_permission() {
  const id = "test-pending-update-no-prompted-permission@mochi.test";

  const { extension } = createTestExtension({ id });

  await extension.startup();
  await extension.awaitMessage("bgpage-ready");

  let win = await loadInitialView("extension");

  info("Force about:addons to check for updates");
  win.checkForUpdates();
  await promiseUpdateAvailable(extension);

  await assertPostponedBarVisibleInAllViews({ id, win });

  info("Test postponed bar visibility after reopening about:addons");
  await closeView(win);
  win = await loadInitialView("extension");
  await assertPostponedBarVisibleInAllViews({ id, win });

  await completePostponedUpdate({ id, win });

  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 0 });

  info("Reopen about:addons again and verify postponed bar hidden");
  await closeView(win);
  win = await loadInitialView("extension");
  await testPostponedBarVisibility({ id, win, hidden: true });

  await closeView(win);
  await extension.unload();
});

add_task(async function test_hidden_addon_pending_update() {
  const win = await loadInitialView("extension");

  await expectAddonInstallStatePostponed(HIDDEN_ADDON_ID);
  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 0 });

  await closeView(win);
});

add_task(async function test_pending_update_ignored_manual_install() {
  const id = "test-update-ignored-manual-install@mochi.test";

  const { extension, updateXpi } = createTestExtension({ id });

  await extension.startup();
  await extension.awaitMessage("bgpage-ready");

  const win = await loadInitialView("extension");

  info("Force about:addons to check for updates");
  win.checkForUpdates();
  await promiseUpdateAvailable(extension);

  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 });

  info("Manually install new xpi over the existing extension");
  const promiseInstalled = AddonTestUtils.promiseInstallFile(updateXpi);
  await promiseUpdateAvailable(extension);

  // At this point, internally there may be two AddonInstall instances for the
  // same extension, but we should count only one update.
  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 });

  await completePostponedUpdate({ id, win });
  await promiseInstalled;

  expectUpdatesAvailableBadgeCount({ win, expectedNumber: 0 });

  await closeView(win);
  await extension.unload();

  // We do currently not clean up stale AddonInstall instances when another
  // one is created and completed (bug 2007749), so we have to manually cancel
  // the remaining install. Not doing so would cause a test-only check in
  // registerCleanupFunction at toolkit/mozapps/extensions/test/browser/head.js
  // to fail with "Should not have seen an install of <url to XPI> in state 7".
  const unwantedInstalls = (await AddonManager.getAllInstalls()).filter(
    install => install.addon?.id === id || install.existingAddon?.id === id
  );
  is(unwantedInstalls.length, 1, "Found unwanted remaining install");
  is(unwantedInstalls[0].state, AddonManager.STATE_POSTPONED, "Got state");
  unwantedInstalls[0].cancel();
});

add_task(async function test_pending_update_with_prompted_data_permission() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.dataCollectionPermissions.enabled", true]],
  });

  const assertSectionHeaders = (popupContentEl, expectedHeaders = []) => {
    for (const { id, isVisible, fluentId } of expectedHeaders) {
      const titleEl = popupContentEl.querySelector(`#${id}`);
      ok(titleEl, `Expected element for ${id}`);
      Assert.equal(
        BrowserTestUtils.isVisible(titleEl),
        isVisible,
        `Expected ${id} to${isVisible ? "" : " not"} be visible`
      );
      if (isVisible) {
        Assert.equal(
          titleEl.textContent,
          PERMISSION_L10N.formatValueSync(fluentId),
          `Expected formatted string for ${id}`
        );
      }
    }
  };

  const TEST_CASES = [
    {
      title: "With data collection",
      data_collection_permissions: {
        required: ["locationInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string"
        );
        Assert.equal(
          popupContentEl.introEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-list-intro-with-data-collection"
          ),
          "Expected list intro string"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: false,
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.ok(
          popupContentEl.permsListDataCollectionEl.querySelector(
            "li.webext-data-collection-perm-granted"
          ),
          "Expected data collection item"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "location",
            }
          ),
          "Expected formatted data collection permission string"
        );
        Assert.ok(
          popupContentEl.hasAttribute("learnmoreurl"),
          "Expected a learn more link"
        );
      },
    },
    {
      title: "With data collection and required permission",
      permissions: ["bookmarks"],
      old_data_collection_permissions: {
        required: ["locationInfo"],
      },
      data_collection_permissions: {
        required: ["locationInfo", "healthInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string with perms"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: true,
            fluentId: "webext-perms-header-update-required-perms",
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListEl.childElementCount,
          1,
          "Expected a required permission"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.equal(
          popupContentEl.permsListEl.textContent,
          PERMISSION_L10N.formatValueSync("webext-perms-description-bookmarks"),
          "Expected formatted permission string"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "health information",
            }
          ),
          "Expected formatted data collection permission string"
        );
        Assert.ok(
          popupContentEl.hasAttribute("learnmoreurl"),
          "Expected a learn more link"
        );
      },
    },
    {
      title: "With data collection changing from required to optional",
      permissions: ["bookmarks"],
      old_data_collection_permissions: {
        required: ["bookmarksInfo"],
      },
      data_collection_permissions: {
        required: ["locationInfo", "healthInfo"],
        optional: ["bookmarksInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string with perms"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: true,
            fluentId: "webext-perms-header-update-required-perms",
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListEl.childElementCount,
          1,
          "Expected a required permission"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.equal(
          popupContentEl.permsListEl.textContent,
          PERMISSION_L10N.formatValueSync("webext-perms-description-bookmarks"),
          "Expected formatted bookmarks permission string"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "location, health information",
            }
          ),
          "Expected formatted data collection permission string"
        );
        Assert.ok(
          popupContentEl.hasAttribute("learnmoreurl"),
          "Expected a learn more link"
        );
      },
    },
    {
      title: "Has previous consent set to false",
      data_collection_permissions: {
        has_previous_consent: false,
        required: ["locationInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string"
        );
        Assert.equal(
          popupContentEl.introEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-list-intro-with-data-collection"
          ),
          "Expected list intro string"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: false,
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "location",
            }
          ),
          "Expected formatted data collection permission string"
        );
      },
    },
    {
      title:
        "Has previous consent with data collection permissions defined before",
      old_data_collection_permissions: {
        // addons-linter requires this property to be required and non-empty.
        required: ["none"],
      },
      data_collection_permissions: {
        has_previous_consent: true,
        required: ["locationInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string"
        );
        Assert.equal(
          popupContentEl.introEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-list-intro-with-data-collection"
          ),
          "Expected list intro string"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: false,
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "location",
            }
          ),
          "Expected formatted data collection permission string"
        );
      },
    },
    {
      title: "Has previous consent with other permissions",
      permissions: ["bookmarks"],
      data_collection_permissions: {
        has_previous_consent: true,
        required: ["locationInfo"],
      },
      verifyDialog(popupContentEl, { extensionId }) {
        Assert.equal(
          popupContentEl.querySelector(".popup-notification-description")
            .textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-text-with-data-collection",
            { extension: extensionId }
          ),
          "Expected header string"
        );
        Assert.equal(
          popupContentEl.introEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-update-list-intro-with-data-collection"
          ),
          "Expected list intro string"
        );
        assertSectionHeaders(popupContentEl, [
          {
            id: "addon-webext-perm-title-required",
            isVisible: true,
            fluentId: "webext-perms-header-update-required-perms",
          },
          {
            id: "addon-webext-perm-title-data-collection",
            isVisible: true,
            fluentId: "webext-perms-header-update-data-collection-perms",
          },
          {
            id: "addon-webext-perm-title-optional",
            isVisible: false,
          },
        ]);
        Assert.equal(
          popupContentEl.permsListEl.childElementCount,
          1,
          "Expected a required permission"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.childElementCount,
          1,
          "Expected a data collection permission"
        );
        Assert.equal(
          popupContentEl.permsListEl.textContent,
          PERMISSION_L10N.formatValueSync("webext-perms-description-bookmarks"),
          "Expected formatted bookmarks permission string"
        );
        Assert.equal(
          popupContentEl.permsListDataCollectionEl.textContent,
          PERMISSION_L10N.formatValueSync(
            "webext-perms-description-data-some-update",
            {
              permissions: "location",
            }
          ),
          "Expected formatted data collection permission string"
        );
      },
    },
  ];

  for (const {
    title,
    permissions,
    data_collection_permissions,
    old_data_collection_permissions,
    verifyDialog,
  } of TEST_CASES) {
    info(title);

    const id = `@${title.toLowerCase().replaceAll(/[^\w]+/g, "-")}`;
    const testExtensionProps = {
      id,
      oldManifest: {
        browser_specific_settings: {
          gecko: {
            ...(old_data_collection_permissions
              ? { data_collection_permissions: old_data_collection_permissions }
              : {}),
          },
        },
      },
      newManifest: {
        name: id,
        permissions,
        browser_specific_settings: {
          gecko: {
            id,
            data_collection_permissions,
          },
        },
      },
    };

    const { extension } = createTestExtension(testExtensionProps);
    await extension.startup();
    await extension.awaitMessage("bgpage-ready");
    const win = await loadInitialView("extension");

    let dialogPromise = promisePopupNotificationShown(
      "addon-webext-permissions"
    );
    // This `promptPromise` is retrieving data from the prompt internals, while
    // the `dialogPromise` will return the actual dialog element.
    let promptPromise = promisePermissionPrompt(id);
    win.checkForUpdates();
    let [popupContentEl, infoProps] = await Promise.all([
      dialogPromise,
      promptPromise,
    ]);

    verifyDialog(popupContentEl, { extensionId: id });

    // Confirm the update, and proceed.
    let waitForManagementUpdate = new Promise(resolve => {
      const { Management } = ChromeUtils.importESModule(
        "resource://gre/modules/Extension.sys.mjs"
      );
      Management.once("update", resolve);
    });
    infoProps.resolve();
    await promiseUpdateAvailable(extension);
    await completePostponedUpdate({ id, win });
    // Ensure that the bootstrap scope update method has been executed
    // successfully and emitted the update Management event.
    info("Wait for the Management update to be emitted");
    await waitForManagementUpdate;

    await extension.unload();
    await closeView(win);

    info("Test again on simulated background update");
    await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
      const { extension: extension2 } = createTestExtension(testExtensionProps);
      await extension2.startup();
      await extension2.awaitMessage("bgpage-ready");

      dialogPromise = promisePopupNotificationShown("addon-webext-permissions");
      promptPromise = BrowserUtils.promiseObserved(
        "webextension-update-permission-prompt",
        subject => subject.wrappedJSObject?.addon?.id == id
      ).then(({ subject }) => subject.wrappedJSObject);

      await simulateBackgroundAddonUpdateCheck(id);

      await verifyAppMenuAddonNotifications({
        expectedNotification: true,
        clickAddonNotification: true,
      });

      [popupContentEl, infoProps] = await Promise.all([
        dialogPromise,
        promptPromise,
      ]);
      verifyDialog(popupContentEl, { extensionId: id });

      // Confirm the update, and proceed.
      waitForManagementUpdate = new Promise(resolve => {
        const { Management } = ChromeUtils.importESModule(
          "resource://gre/modules/Extension.sys.mjs"
        );
        Management.once("update", resolve);
      });
      infoProps.resolve();
      await promiseUpdateAvailable(extension2);
      const win2 = gBrowser.selectedBrowser.contentWindow;
      await completePostponedUpdate({ id, win: win2 });
      // Ensure that the bootstrap scope update method has been executed
      // successfully and emitted the update Management event.
      info("Wait for the Management update to be emitted");
      await waitForManagementUpdate;

      await extension2.unload();
    });
  }

  await SpecialPowers.popPrefEnv();
});

add_task(async function test_pending_update_with_no_prompted_permission() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.dataCollectionPermissions.enabled", true]],
  });

  const TEST_CASES = [
    {
      title: "No data collection",
      data_collection_permissions: {},
    },
    {
      title: "Explicit no data collection",
      data_collection_permissions: {
        required: ["none"],
      },
    },
    {
      title: "Optional data collection",
      data_collection_permissions: {
        optional: ["technicalAndInteraction"],
      },
    },
    {
      title: "Has previous consent",
      data_collection_permissions: {
        has_previous_consent: true,
        required: ["locationInfo"],
      },
    },
  ];

  for (const { title, data_collection_permissions } of TEST_CASES) {
    info(title);

    const id = `@${title.toLowerCase().replaceAll(/[^\w]+/g, "-")}`;
    const testExtensionProps = {
      id,
      newManifest: {
        name: id,
        browser_specific_settings: {
          gecko: {
            id,
            data_collection_permissions,
          },
        },
      },
    };
    const { extension } = createTestExtension(testExtensionProps);

    await extension.startup();
    await extension.awaitMessage("bgpage-ready");
    let win = await loadInitialView("extension");

    win.checkForUpdates();
    await promiseUpdateAvailable(extension);
    await completePostponedUpdate({ id, win });

    await closeView(win);
    await extension.unload();

    info("Test again on simulated background update");
    const { extension: extension2 } = createTestExtension(testExtensionProps);
    await extension2.startup();
    await extension2.awaitMessage("bgpage-ready");
    await simulateBackgroundAddonUpdateCheck(id);
    await verifyAppMenuAddonNotifications({
      expectedNotification: false,
    });
    let win2 = await loadInitialView("extension");
    await promiseUpdateAvailable(extension2);
    await completePostponedUpdate({ id, win: win2 });
    await closeView(win2);
    await extension2.unload();
  }

  await SpecialPowers.popPrefEnv();
});

add_task(
  async function test_pending_update_does_not_grant_technicalAndInteraction() {
    await SpecialPowers.pushPrefEnv({
      set: [["extensions.dataCollectionPermissions.enabled", true]],
    });

    const id = "@test-id";
    const { extension } = createTestExtension({
      id,
      oldManifest: {
        browser_specific_settings: {
          gecko: {
            id,
            data_collection_permissions: {},
          },
        },
      },
      newManifest: {
        permissions: ["bookmarks"],
        browser_specific_settings: {
          gecko: {
            id,
            data_collection_permissions: {
              optional: ["technicalAndInteraction"],
            },
          },
        },
      },
    });

    await extension.startup();
    await extension.awaitMessage("bgpage-ready");
    const win = await loadInitialView("extension");

    const dialogPromise = promisePopupNotificationShown(
      "addon-webext-permissions"
    );
    win.checkForUpdates();
    const popupContentEl = await dialogPromise;

    // Confirm the update, and proceed.
    const waitForManagementUpdate = new Promise(resolve => {
      const { Management } = ChromeUtils.importESModule(
        "resource://gre/modules/Extension.sys.mjs"
      );
      Management.once("update", resolve);
    });
    popupContentEl.button.click();
    await promiseUpdateAvailable(extension);
    await completePostponedUpdate({ id, win });
    // Ensure that the bootstrap scope update method has been executed
    // successfully and emitted the update Management event.
    info("Wait for the Management update to be emitted");
    await waitForManagementUpdate;

    // This test verifies that we don't accidentally grant the
    // "technicalAndInteraction" data collection permission on update because
    // that's controlled by the `showTechnicalAndInteractionCheckbox` value in
    // `ExtensionUI.showPermissionsPrompt()`.
    const perms = await ExtensionPermissions.get(id);
    Assert.deepEqual(
      perms,
      {
        permissions: [],
        origins: [],
        data_collection: [],
      },
      "Expected no stored permission"
    );

    await closeView(win);
    await extension.unload();

    await SpecialPowers.popPrefEnv();
  }
);
