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

"use strict";

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

Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);
Services.prefs.setBoolPref("extensions.blocklist.softblock.enabled", true);

createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");

const { STATE_NOT_BLOCKED, STATE_SOFTBLOCKED, STATE_BLOCKED } =
  Ci.nsIBlocklistService;

function getTestExtensionDefinition(
  addonId,
  addonVersion,
  addonType = "extension",
  { hidden = false } = {}
) {
  let manifestData = {};
  let files = {};
  switch (addonType) {
    case "extension":
      manifestData = { hidden };
      break;
    case "theme":
      manifestData = { theme: {} };
      break;
    case "dictionary":
      manifestData = { dictionaries: { "en-US": "en-US.dic" } };
      files = { "en-US.dic": "", "en-US.aff": "" };
      break;
    case "locale":
      manifestData = {
        langpack_id: "und",
        languages: {},
      };
      break;
    default:
      throw new Error(
        `Unexpected addonType for ${addonId}:${addonVersion}: ${addonType}`
      );
  }
  return {
    useAddonManager: "permanent",
    manifest: {
      ...manifestData,
      version: addonVersion,
      browser_specific_settings: { gecko: { id: addonId } },
    },
    files,
  };
}

async function installTestExtension(addonId) {
  const extension = ExtensionTestUtils.loadExtension(
    getTestExtensionDefinition(addonId, "1.0", "extension")
  );
  await extension.startup();
  const addon = await AddonManager.getAddonByID(addonId);
  Assert.equal(
    addon.type,
    "extension",
    `${addonId} should have the expected addon type`
  );
  return extension;
}

async function installTestAddon(addonId, addonType, { hidden = false } = {}) {
  const xpi = AddonTestUtils.createTempWebExtensionFile(
    getTestExtensionDefinition(addonId, "1.0", addonType, { hidden })
  );
  await AddonTestUtils.promiseInstallFile(xpi);
  const addon = await AddonManager.getAddonByID(addonId);
  Assert.equal(
    addon.type,
    addonType,
    `${addonId} should have the expected addon type`
  );
  Assert.equal(
    addon.hidden,
    hidden,
    `${addonId} is expected to ${hidden ? "" : "NOT"} be hidden`
  );
  return addon;
}

function assertXPIDatabaseBlocklistAttentionAddonIds(expectedAddonIdsArray) {
  const { XPIDatabase } = AddonTestUtils.getXPIExports();
  Assert.deepEqual(
    expectedAddonIdsArray,
    Array.from(XPIDatabase.blocklistAttentionAddonIdsSet),
    "Got the expected addon ids in the XPIDatabase.blocklistAttentionAddonIdsSet"
  );
}

function getBlockKey({ id, version }) {
  if (!id || !version) {
    // Throw an error if the resulting block key would not be a valid one.
    throw new Error(
      "getBlockKey requires id and version to be defined and non-empty"
    );
  }
  return `${id}:${version}`;
}

function createStashRecord({
  blocked = [],
  softblocked = [],
  unblocked = [],
  stash_time = 0,
} = {}) {
  return {
    stash_time,
    stash: {
      blocked: blocked.map(getBlockKey),
      softblocked: softblocked.map(getBlockKey),
      unblocked: unblocked.map(getBlockKey),
    },
  };
}

async function assertBlocklistAttentionInfo(expected) {
  let baInfo = await AddonManager.getBlocklistAttentionInfo();
  Assert.deepEqual(
    baInfo.addons?.map(it => it.id),
    expected.addons,
    "blocklistAttentionInfo.addons"
  );
  Assert.equal(
    baInfo.shouldShow,
    expected.shouldShow,
    "blocklistAttentionInfo.shouldShow"
  );
  Assert.equal(
    baInfo.hasSoftBlocked,
    expected.hasSoftBlocked,
    "blocklistAttentionInfo.hasSoftBlocked"
  );
  Assert.equal(
    baInfo.hasHardBlocked,
    expected.hasHardBlocked,
    "blocklistAttentionInfo.hasHardBlocked"
  );
  Assert.equal(
    baInfo.extensionsCount,
    expected.addons.length,
    "blocklistAttentionInfo.extensionsCount"
  );
  return baInfo;
}

async function testBlocklistAttentionScenario({
  blocklistStashRecords,
  expectBeforeDismiss,
  expectAfterDismiss,
}) {
  if (blocklistStashRecords) {
    info("Loading test blocklist data");
    await AddonTestUtils.loadBlocklistRawData({
      extensionsMLBF: blocklistStashRecords,
    });
  }

  info("Verify expectations before dismissing the blocklist attention info");

  for (const [addonId, blocklistState] of Object.entries(
    expectBeforeDismiss.blocklistStates
  )) {
    // Sanity check the expected addon blocklist states.
    const addon = await AddonManager.getAddonByID(addonId);
    Assert.equal(
      addon?.blocklistState,
      blocklistState,
      `Got expected blocklistState for ${addonId}`
    );
  }
  for (const [addonId, blocklistAttentionDismissed] of Object.entries(
    expectBeforeDismiss.dismissedStates
  )) {
    const addon = await AddonManager.getAddonByID(addonId);
    Assert.equal(
      addon?.blocklistAttentionDismissed,
      blocklistAttentionDismissed,
      `Got expected blocklistAttentionDismissed for ${addonId}`
    );
  }
  Assert.equal(
    AddonManager.shouldShowBlocklistAttention(),
    expectBeforeDismiss.shouldShowBlocklistAttention,
    "AddonManager.shouldShowBlocklistAttention()"
  );

  const baInfo = await assertBlocklistAttentionInfo(
    expectBeforeDismiss.blocklistAttentionInfo
  );

  if (!expectAfterDismiss) {
    return;
  }

  let managerListener = {
    onBlocklistAttentionUpdated: sinon.spy(),
  };
  AddonManager.addManagerListener(managerListener);
  baInfo.dismiss();
  AddonManager.removeManagerListener(managerListener);

  info("Verify expected managerListener call count");
  Assert.equal(
    managerListener.onBlocklistAttentionUpdated.callCount,
    expectAfterDismiss.managerListenerCallCount ?? 0,
    "Got the expected number of call to onBlocklistAttentionUpdated manager listeners"
  );

  info("Verify expectations before dismissing the blocklist attention info");
  for (const [addonId, blocklistAttentionDismissed] of Object.entries(
    expectAfterDismiss.dismissedStates
  )) {
    const addon = await AddonManager.getAddonByID(addonId);
    Assert.equal(
      addon?.blocklistAttentionDismissed,
      blocklistAttentionDismissed,
      `Got expected blocklistAttentionDismissed for ${addonId}`
    );
  }
  Assert.equal(
    AddonManager.shouldShowBlocklistAttention(),
    expectAfterDismiss.shouldShowBlocklistAttention,
    "AddonManager.shouldShowBlocklistAttention()"
  );

  {
    const expected = expectAfterDismiss.blocklistAttentionInfo;
    Assert.equal(
      baInfo.shouldShow,
      expected.shouldShow,
      "blocklistAttentionInfo.shouldShow"
    );
  }
}

add_setup(async function setup() {
  await promiseStartupManager();
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
});

add_task(async function test_blocklist_attention_basic() {
  const ext1 = await installTestExtension("@ext1");
  const ext2 = await installTestExtension("@ext2");
  const ext3 = await installTestExtension("@ext3");

  const addon1 = await AddonManager.getAddonByID(ext1.id);
  const addon2 = await AddonManager.getAddonByID(ext2.id);
  const addon3 = await AddonManager.getAddonByID(ext3.id);

  // Sanity checks.
  for (const addon of [addon1, addon2, addon3]) {
    Assert.ok(addon, "Expect addon to be found");
    Assert.equal(
      addon.blocklistState,
      STATE_NOT_BLOCKED,
      "Expect addon to not be blocked"
    );
    Assert.ok(addon.isActive, "Expect addon to be enabled");
    Assert.ok(
      !addon.blocklistAttentionDismissed,
      "Expect addon.blocklistAttentionDismissed to be false"
    );
  }
  Assert.ok(
    !AddonManager.shouldShowBlocklistAttention(),
    "Expect shouldShowBlocklistAttention to be initially returning false"
  );

  info("Test blocklist attention info on a single hard-blocked addon");

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [createStashRecord({ blocked: [addon1] })],
    expectBeforeDismiss: {
      blocklistStates: {
        [addon1.id]: STATE_BLOCKED,
        [addon2.id]: STATE_NOT_BLOCKED,
        [addon3.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: false,
        hasHardBlocked: true,
        addons: [addon1.id],
      },
      dismissedStates: {
        [addon1.id]: false,
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 1,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      dismissedStates: {
        [addon1.id]: true,
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  info("Test blocklist attention info on new single soft-blocked addon");

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [
      createStashRecord({ blocked: [addon1] }),
      createStashRecord({ softblocked: [addon2] }),
    ],
    expectBeforeDismiss: {
      blocklistStates: {
        [addon1.id]: STATE_BLOCKED,
        [addon2.id]: STATE_SOFTBLOCKED,
        [addon3.id]: STATE_NOT_BLOCKED,
      },
      // blocklistAttentionInfo should reflect that the attention
      // on addon1 hard-block was already dismissed before.
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: false,
        addons: [addon2.id],
      },
      dismissedStates: {
        [addon1.id]: true, // Dismissed as part of the previous scenario.
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 1,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      dismissedStates: {
        [addon1.id]: true,
        [addon2.id]: true,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  info("Test blocklist attention info after all blocks are removed");

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [createStashRecord()],
    expectBeforeDismiss: {
      blocklistStates: {
        [addon1.id]: STATE_NOT_BLOCKED,
        [addon2.id]: STATE_NOT_BLOCKED,
        [addon3.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: false,
        hasSoftBlocked: false,
        hasHardBlocked: false,
        addons: [],
      },
      // The flag should have been cleared when the blocklistState for the test addons changed
      // to STATE_NOT_BLOCKED as a side-effect of the blocklist stash record used by this call.
      dismissedStates: {
        [addon1.id]: false,
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 0,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      // None of the 3 test addons should have the blocklistAttentionDismissed flag set
      // to true after blocklistAttentionInfo.dismiss method was called.
      dismissedStates: {
        [addon1.id]: false,
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  info(
    "Test blocklist attention info after new hard and soft blocks have been received"
  );

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [
      createStashRecord({ blocked: [addon2], softblocked: [addon3] }),
    ],
    expectBeforeDismiss: {
      blocklistStates: {
        [addon1.id]: STATE_NOT_BLOCKED,
        [addon2.id]: STATE_BLOCKED,
        [addon3.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: true,
        addons: [addon2.id, addon3.id],
      },
      dismissedStates: {
        [addon1.id]: false,
        [addon2.id]: false,
        [addon3.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 2,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      dismissedStates: {
        [addon1.id]: false,
        [addon2.id]: true,
        [addon3.id]: true,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  await ext3.unload();
  await ext2.unload();
  await ext1.unload();
});

add_task(async function test_attentions_reinitalized_on_restart() {
  const ext1 = await installTestExtension("@ext1");
  const ext2 = await installTestExtension("@ext2");
  const ext3 = await installTestExtension("@ext3");

  // Test blocklist attention without dismissing it.
  await testBlocklistAttentionScenario({
    blocklistStashRecords: [
      createStashRecord({ blocked: [ext1], softblocked: [ext2] }),
    ],
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_BLOCKED,
        [ext2.id]: STATE_SOFTBLOCKED,
        [ext3.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: true,
        addons: [ext1.id, ext2.id],
      },
      dismissedStates: {
        [ext1.id]: false,
        [ext2.id]: false,
        [ext3.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  assertXPIDatabaseBlocklistAttentionAddonIds([ext1.id, ext2.id]);

  info("Simulate AOM shutdown");
  await promiseShutdownManager();
  AddonTestUtils.getXPIExports().XPIDatabase.clearBlocklistAttentionAddonIdsSet();
  assertXPIDatabaseBlocklistAttentionAddonIds([]);

  const promiseBlocklistAttentionUpdated = AddonTestUtils.promiseManagerEvent(
    "onBlocklistAttentionUpdated",
    () => {
      const { XPIDatabase } = AddonTestUtils.getXPIExports();
      // The BlocklistAttentionUpdated may have been fired more than once,
      // but we want to be sure that it is fired at least once when the
      // XPIDatabase blocklistAttention add-on ids has been populated
      // with all the expected addon ids.
      return XPIDatabase.blocklistAttentionAddonIdsSet.size == 2;
    }
  );
  info("Simulate new AOM startup");
  await promiseStartupManager();
  info("Wait for onBlocklistAttentionUpdated Manager listener to be called");
  await promiseBlocklistAttentionUpdated;
  assertXPIDatabaseBlocklistAttentionAddonIds([ext1.id, ext2.id]);

  // Test blocklist attention again after AOM restart
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_BLOCKED,
        [ext2.id]: STATE_SOFTBLOCKED,
        [ext3.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: true,
        addons: [ext1.id, ext2.id],
      },
      dismissedStates: {
        [ext1.id]: false,
        [ext2.id]: false,
        [ext3.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  await ext3.unload();
  await ext2.unload();
  await ext1.unload();
});

add_task(async function test_dismissed_flag_cleared_on_addon_updates() {
  const ext1 = await installTestExtension("@ext1");
  const ext2 = await installTestExtension("@ext2");

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [createStashRecord({ blocked: [ext1, ext2] })],
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_BLOCKED,
        [ext2.id]: STATE_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: false,
        hasHardBlocked: true,
        addons: [ext1.id, ext2.id],
      },
      dismissedStates: {
        [ext1.id]: false,
        [ext2.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 2,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      dismissedStates: {
        [ext1.id]: true,
        [ext2.id]: true,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  await ext1.upgrade(getTestExtensionDefinition(ext1.id, "2.0"));
  Assert.equal(
    ext1.version,
    "2.0",
    "Expect test extension version changed to 2.0"
  );

  const addon1v2 = await AddonManager.getAddonByID(ext1.id);
  Assert.equal(
    addon1v2.version,
    "2.0",
    "Expect new AddonWrapper instance version to be 2.0"
  );
  // The blocklistAttentionDismissed field is not propagated from the old to the new AddonInternal
  // instances when the addon is being updated, that ensures the flag is implicitly cleared
  // when the addon is updated.
  Assert.equal(
    addon1v2.blocklistAttentionDismissed,
    false,
    "Expect new AddonWrapper instance blocklistAttentionDismissed to be false"
  );

  // The XPIDatabase blocklistAttentionAddonIdsSet is expected to be empty as a side-effect
  // of the user dismissing the blocklist attention.
  assertXPIDatabaseBlocklistAttentionAddonIds([]);

  await promiseRestartManager();

  // After restarting the AddonManager we expect the XPIDatabase blocklistAttentionAddonIdsSet
  // to still be empty because the only blocked addons have already been dismissed by the user.
  assertXPIDatabaseBlocklistAttentionAddonIds([]);

  const addon1v2AfterRestart = await AddonManager.getAddonByID(ext1.id);
  Assert.equal(
    addon1v2AfterRestart.blocklistAttentionDismissed,
    false,
    "Expect addon1 blocklistAttentionDismissed to be false"
  );

  // addon2 was not updated and so we expect the blocklistAttentionDismissed flag to have been
  // loaded from the addonDB and still be set to true.
  const addon2 = await AddonManager.getAddonByID(ext2.id);
  Assert.equal(
    addon2.blocklistAttentionDismissed,
    true,
    "Expect addon2 blocklistAttentionDismissed to be true"
  );

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  await ext2.unload();
  await ext1.unload();
});

add_task(async function test_blocklist_attention_on_reenabled_softblock() {
  async function assertExtensionBlocklistStatePropagated(addonId, msgSuffix) {
    const addon = await AddonManager.getAddonByID(addonId);
    Assert.equal(
      addon.blocklistState,
      WebExtensionPolicy.getByID(addonId).extension.blocklistState,
      `Expect extension.blocklistState to match addon.blocklistState ${msgSuffix}`
    );
    Assert.equal(
      WebExtensionPolicy.getByID(addonId).extension.isSoftBlocked,
      addon.blocklistState === STATE_SOFTBLOCKED,
      `Expect extension.isSoftBlocked to be false ${msgSuffix}`
    );
  }

  const ADDON_ID = "@ext1";
  const ext1 = await installTestExtension(ADDON_ID);
  await assertExtensionBlocklistStatePropagated(
    ADDON_ID,
    "(not blocked extension)"
  );

  await promiseRestartManager();
  await ext1.awaitStartup();
  await assertExtensionBlocklistStatePropagated(
    ADDON_ID,
    "(not blocked extension - after startup)"
  );

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [createStashRecord({ softblocked: [ext1] })],
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: false,
        addons: [ext1.id],
      },
      dismissedStates: {
        [ext1.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  let addon1 = await AddonManager.getAddonByID(ext1.id);
  Assert.equal(
    addon1.softDisabled,
    true,
    "Expect test addon to be softDisabled"
  );
  const promiseBlocklistAttentionUpdated = AddonTestUtils.promiseManagerEvent(
    "onBlocklistAttentionUpdated"
  );
  await addon1.enable();
  Assert.equal(
    addon1.softDisabled,
    false,
    "Expect test addon to NOT be softDisabled after being explicitly enabled"
  );

  info(
    "Wait for onBlocklistAttentionUpdated Manager listeners to have been called"
  );
  await promiseBlocklistAttentionUpdated;

  await assertExtensionBlocklistStatePropagated(
    ADDON_ID,
    "(softblocked extension re-enabled)"
  );

  info("Verify again after AOM restart");
  await promiseRestartManager();
  await ext1.awaitStartup();
  await assertExtensionBlocklistStatePropagated(
    ADDON_ID,
    "(softblocked extension re-enabled - after startup)"
  );

  info(
    "Verify blocklistAttentionInfo again after softblocked addon has been re-enabled"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: false,
        hasSoftBlocked: false,
        hasHardBlocked: false,
        addons: [],
      },
      dismissedStates: {
        [ext1.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  info(
    "Verify onPropertyChanged call and extension.blocklistState update on softblock removal"
  );

  addon1 = await AddonManager.getAddonByID(ext1.id);
  const promisePropertyChanged = AddonTestUtils.promiseAddonEvent(
    "onPropertyChanged",
    (addon, properties) =>
      addon.id === addon1.id && properties.includes("blocklistState")
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  Assert.equal(
    addon1.blocklistState,
    STATE_NOT_BLOCKED,
    "Expect addon.blocklistState to be STATE_NOT_BLOCKED (softblock lifted)"
  );

  info("Wait for onPropertyChanged Addon listeners to have been called");
  await promisePropertyChanged;
  await assertExtensionBlocklistStatePropagated(ADDON_ID, "(softblock lifted)");

  info("Verify again after AOM restart");
  await promiseRestartManager();
  await ext1.awaitStartup();
  addon1 = await AddonManager.getAddonByID(ext1.id);
  Assert.equal(
    addon1.blocklistState,
    STATE_NOT_BLOCKED,
    "Expect addon.blocklistState to be STATE_NOT_BLOCKED (softblock lifted)"
  );
  await assertExtensionBlocklistStatePropagated(
    ADDON_ID,
    "(softblocked lifted - after startup)"
  );

  await ext1.unload();
});

add_task(async function test_attention_after_uninstalls() {
  const ext1 = await installTestExtension("@ext1");
  const ext2 = await installTestExtension("@ext2");

  let managerListener = {
    onBlocklistAttentionUpdated: sinon.spy(),
  };
  AddonManager.addManagerListener(managerListener);

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [
      createStashRecord({ blocked: [ext1], softblocked: [ext2] }),
    ],
    expectBeforeDismiss: {
      blocklistStates: {
        [ext1.id]: STATE_BLOCKED,
        [ext2.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: true,
        addons: [ext1.id, ext2.id],
      },
      dismissedStates: {
        [ext1.id]: false,
        [ext2.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  Assert.equal(
    managerListener.onBlocklistAttentionUpdated.callCount,
    2,
    "Expect onBlocklistAttentionUpdated to have been called once for each of the blocked extensions"
  );

  const addon1 = await AddonManager.getAddonByID(ext1.id);
  managerListener.onBlocklistAttentionUpdated.resetHistory();
  await addon1.uninstall();

  Assert.equal(
    managerListener.onBlocklistAttentionUpdated.callCount,
    1,
    "Expect onBlocklistAttentionUpdated to have been called once after ext1 uninstalled"
  );

  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [ext2.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: false,
        addons: [ext2.id],
      },
      dismissedStates: {
        [ext2.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  const addon2 = await AddonManager.getAddonByID(ext2.id);
  managerListener.onBlocklistAttentionUpdated.resetHistory();
  await addon2.uninstall();

  Assert.equal(
    managerListener.onBlocklistAttentionUpdated.callCount,
    1,
    "Expect onBlocklistAttentionUpdated to have been called again after ext2 uninstalled"
  );

  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {},
      blocklistAttentionInfo: {
        shouldShow: false,
        hasSoftBlocked: false,
        hasHardBlocked: false,
        addons: [],
      },
      dismissedStates: {},
      shouldShowBlocklistAttention: false,
    },
  });

  AddonManager.removeManagerListener(managerListener);
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
});

add_task(async function test_hidden_addons() {
  const extensionAddon = await installTestAddon("@extension", "extension");
  const hiddenAddon1 = await installTestAddon(
    "@hidden-extension1",
    "extension",
    {
      hidden: true,
    }
  );
  const hiddenAddon2 = await installTestAddon(
    "@hidden-extension2",
    "extension",
    {
      hidden: true,
    }
  );

  await testBlocklistAttentionScenario({
    blocklistStashRecords: [
      createStashRecord({
        blocked: [extensionAddon, hiddenAddon1],
        softblocked: [hiddenAddon2],
      }),
    ],
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_BLOCKED,
        [hiddenAddon1.id]: STATE_BLOCKED,
        [hiddenAddon2.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: false,
        hasHardBlocked: true,
        addons: [extensionAddon.id],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
        [hiddenAddon1.id]: false,
        [hiddenAddon2.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
    expectAfterDismiss: {
      managerListenerCallCount: 1,
      blocklistAttentionInfo: {
        shouldShow: false,
      },
      dismissedStates: {
        [extensionAddon.id]: true,
        [hiddenAddon1.id]: false,
        [hiddenAddon2.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  await hiddenAddon2.uninstall();
  await hiddenAddon1.uninstall();
  await extensionAddon.uninstall();
});

// Android builds do not support the other XPIProvider addon types.
add_task(
  { skip_if: () => IS_ANDROID_BUILD },
  async function test_other_addon_types() {
    const extensionAddon = await installTestAddon("@extension", "extension");
    const themeAddon = await installTestAddon("@theme", "theme");
    const localeAddon = await installTestAddon("@langpack", "locale");
    const dictionaryAddon = await installTestAddon("@dictionary", "dictionary");

    await testBlocklistAttentionScenario({
      blocklistStashRecords: [
        createStashRecord({
          blocked: [extensionAddon, localeAddon, dictionaryAddon],
          softblocked: [themeAddon],
        }),
      ],
      expectBeforeDismiss: {
        blocklistStates: {
          [extensionAddon.id]: STATE_BLOCKED,
          [themeAddon.id]: STATE_SOFTBLOCKED,
          [localeAddon.id]: STATE_BLOCKED,
          [dictionaryAddon.id]: STATE_BLOCKED,
        },
        blocklistAttentionInfo: {
          shouldShow: true,
          hasSoftBlocked: false,
          hasHardBlocked: true,
          addons: [extensionAddon.id],
        },
        dismissedStates: {
          [extensionAddon.id]: false,
          [themeAddon.id]: false,
          [localeAddon.id]: false,
          [dictionaryAddon.id]: false,
        },
        shouldShowBlocklistAttention: true,
      },
      expectAfterDismiss: {
        managerListenerCallCount: 1,
        blocklistAttentionInfo: {
          shouldShow: false,
        },
        dismissedStates: {
          [extensionAddon.id]: true,
          [themeAddon.id]: false,
          [localeAddon.id]: false,
          [dictionaryAddon.id]: false,
        },
        shouldShowBlocklistAttention: false,
      },
    });

    await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
    await dictionaryAddon.uninstall();
    await localeAddon.uninstall();
    await themeAddon.uninstall();
    await extensionAddon.uninstall();
  }
);

add_task(async function test_blocklist_attention_on_blocklist_updates() {
  let managerListener = {
    onBlocklistAttentionUpdated: sinon.spy(),
  };
  function assertManagerListenerCallCount(expectedCallCount, msg) {
    Assert.equal(
      managerListener.onBlocklistAttentionUpdated.callCount,
      expectedCallCount,
      msg
    );
    managerListener.onBlocklistAttentionUpdated.resetHistory();
  }
  AddonManager.addManagerListener(managerListener);

  const extensionAddon = await installTestAddon("@extension", "extension");
  assertManagerListenerCallCount(
    0,
    "Expect no onBlocklistAttentionUpdated calls on a non-blocked install"
  );

  info(
    "Simulate blocklistState change from STATE_NOT_BLOCKED to STATE_BLOCKED"
  );
  let stashRecords = [
    createStashRecord({ blocked: [extensionAddon], stash_time: 0 }),
  ];
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: stashRecords });
  assertManagerListenerCallCount(
    1,
    "Expect a onBlocklistAttentionUpdated calls on a new hard-block"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: false,
        hasHardBlocked: true,
        addons: [extensionAddon.id],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  info(
    "Simulate blocklistState change from STATE_BLOCKED to STATE_SOFTBLOCKED"
  );
  stashRecords.push(
    createStashRecord({ softblocked: [extensionAddon], stash_time: 1 })
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: stashRecords });
  // The disabled state of the addon didn't change and so we don't call the manager listener
  // because it doesn't change the results we would get by AddonManager.shouldShowBlocklistAttention()
  // (e.g. the attention dot wouldn't need to be refreshed).
  assertManagerListenerCallCount(
    0,
    "Expect no onBlocklistAttentionUpdated calls on hard-block to soft-block"
  );
  // On the contrary we still expect AddonManager.getBlocklistAttentionInfo to reflect the blocklistState
  // change.
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: false,
        addons: [extensionAddon.id],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  info(
    "Simulate blocklistState change from STATE_SOFTBLOCKED to STATE_BLOCKED"
  );
  stashRecords.push(
    createStashRecord({ blocked: [extensionAddon], stash_time: 2 })
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: stashRecords });
  // Same as hard to soft block transistion, we expect soft to hard block to not be
  // triggering the onBlocklistAttentionUpdated if the addon was disabled also while
  // soft blocked.
  assertManagerListenerCallCount(
    0,
    "Expect no onBlocklistAttentionUpdated calls on soft-block to hard-block"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: false,
        hasHardBlocked: true,
        addons: [extensionAddon.id],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  info(
    "Simulate blocklistState change from STATE_BLOCKED to STATE_NOT_BLOCKED"
  );
  stashRecords.push(
    createStashRecord({ unblocked: [extensionAddon], stash_time: 3 })
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: stashRecords });
  assertManagerListenerCallCount(
    1,
    "Expect a onBlocklistAttentionUpdated calls on hard-block to not-blocked"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: false,
        hasSoftBlocked: false,
        hasHardBlocked: false,
        addons: [],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  info(
    "Simulate blocklistState change from STATE_NOT_BLOCKED to STATE_SOFTBLOCKED"
  );
  stashRecords.push(
    createStashRecord({ softblocked: [extensionAddon], stash_time: 4 })
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: stashRecords });
  assertManagerListenerCallCount(
    1,
    "Expect a onBlocklistAttentionUpdated calls on a new soft-block"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_SOFTBLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: true,
        hasSoftBlocked: true,
        hasHardBlocked: false,
        addons: [extensionAddon.id],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: true,
    },
  });

  info(
    "Simulate blocklistState change from STATE_SOFTBLOCKED to STATE_NOT_BLOCKED"
  );
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
  assertManagerListenerCallCount(
    1,
    "Expect a onBlocklistAttentionUpdated calls on soft-block to not-blocked"
  );
  await testBlocklistAttentionScenario({
    expectBeforeDismiss: {
      blocklistStates: {
        [extensionAddon.id]: STATE_NOT_BLOCKED,
      },
      blocklistAttentionInfo: {
        shouldShow: false,
        hasSoftBlocked: false,
        hasHardBlocked: false,
        addons: [],
      },
      dismissedStates: {
        [extensionAddon.id]: false,
      },
      shouldShowBlocklistAttention: false,
    },
  });

  AddonManager.removeManagerListener(managerListener);
  await extensionAddon.uninstall();
});

add_task(async function test_blocklist_attention_on_blocked_install() {
  const SIGNED_ADDON_XPI_FILE = do_get_file("amosigned.xpi");
  const SIGNED_ADDON_ID = "amosigned-xpi@tests.mozilla.org";
  const SIGNED_ADDON_VERSION = "2.2";
  const SIGNED_ADDON_KEY = `${SIGNED_ADDON_ID}:${SIGNED_ADDON_VERSION}`;

  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash: {
          blocked: [],
          softblocked: [SIGNED_ADDON_KEY],
          unblocked: [],
        },
        stash_time: Date.now(),
      },
    ],
  });

  let managerListener = {
    onBlocklistAttentionUpdated: sinon.spy(),
  };

  AddonManager.addManagerListener(managerListener);

  const install = await promiseInstallFile(SIGNED_ADDON_XPI_FILE);
  Assert.equal(
    install.error,
    AddonManager.ERROR_SOFT_BLOCKED,
    "Install should have an error"
  );
  Assert.equal(
    managerListener.onBlocklistAttentionUpdated.callCount,
    0,
    "Expect no onBlocklistAttentionUpdated calls on blocked installs"
  );
  assertXPIDatabaseBlocklistAttentionAddonIds([]);
  await assertBlocklistAttentionInfo({
    addons: [],
    shouldShow: false,
    hasSoftBlocked: false,
    hasHardBlocked: false,
  });
  Assert.ok(
    !AddonManager.shouldShowBlocklistAttention(),
    "Expect shouldShowBlocklistAttention to be false"
  );

  AddonManager.removeManagerListener(managerListener);
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] });
});
