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

"use strict";

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

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

const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF();
const MLBF_LOAD_ATTEMPTS = [];
ExtensionBlocklistMLBF._fetchMLBF = async record => {
  MLBF_LOAD_ATTEMPTS.push(record);
  return {
    generationTime: 0,
    cascadeFilter: {
      has(blockKey) {
        if (blockKey === "@onlyblockedbymlbf:1") {
          return true;
        }
        throw new Error("bloom filter should not be used in this test");
      },
    },
  };
};

async function checkBlockState(addonId, version, expectBlocked) {
  let addon = {
    id: addonId,
    version,
    // Note: signedDate is missing, so the MLBF does not apply
    // and we will effectively only test stashing.
  };
  let state = await Blocklist.getAddonBlocklistState(addon);
  if (expectBlocked) {
    Assert.equal(state, Ci.nsIBlocklistService.STATE_BLOCKED);
  } else {
    Assert.equal(state, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
  }
}

add_task(async function setup() {
  await promiseStartupManager();
});

// Tests that add-ons can be blocked / unblocked via the stash.
add_task(async function basic_stash() {
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash_time: 0,
        stash: {
          blocked: ["@blocked:1"],
          unblocked: ["@notblocked:2"],
        },
      },
    ],
  });
  await checkBlockState("@blocked", "1", true);
  await checkBlockState("@notblocked", "2", false);
  // Not in stash (but unsigned, so shouldn't reach MLBF):
  await checkBlockState("@blocked", "2", false);

  Assert.equal(
    await Blocklist.getAddonBlocklistState({
      id: "@onlyblockedbymlbf",
      version: "1",
      signedDate: new Date(0), // = the MLBF's generationTime.
      signedState: AddonManager.SIGNEDSTATE_SIGNED,
    }),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "falls through to MLBF if entry is not found in stash"
  );

  Assert.deepEqual(MLBF_LOAD_ATTEMPTS, [null], "MLBF attachment not found");
});

// To complement the privileged_xpi_not_blocked in test_blocklist_mlbf.js,
// verify that privileged add-ons can still be blocked through stashes.
add_task(async function privileged_addon_blocked_by_stash() {
  const system_addon = {
    id: "@blocked",
    version: "1",
    signedDate: new Date(0), // = the MLBF's generationTime.
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
  };
  Assert.equal(
    await Blocklist.getAddonBlocklistState(system_addon),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "Privileged add-ons can still be blocked by a stash"
  );

  system_addon.signedState = AddonManager.SIGNEDSTATE_SYSTEM;
  Assert.equal(
    await Blocklist.getAddonBlocklistState(system_addon),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "Privileged system add-ons can still be blocked by a stash"
  );

  // For comparison, when an add-on is only blocked by a MLBF, the block
  // decision is ignored.
  system_addon.id = "@onlyblockedbymlbf";
  Assert.equal(
    await Blocklist.getAddonBlocklistState(system_addon),
    Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
    "Privileged add-ons cannot be blocked via a MLBF"
  );
  // (note that we haven't checked that SIGNEDSTATE_PRIVILEGED is not blocked
  // via the MLBF, but that is already covered by test_blocklist_mlbf.js ).
});

// To complement langpack_not_blocked_on_Nightly in test_blocklist_mlbf.js,
// verify that langpacks can still be blocked through stashes.
add_task(async function langpack_blocked_by_stash() {
  const langpack_addon = {
    id: "@blocked",
    type: "locale",
    version: "1",
    signedDate: new Date(0), // = the MLBF's generationTime.
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };
  Assert.equal(
    await Blocklist.getAddonBlocklistState(langpack_addon),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "Langpack add-ons can still be blocked by a stash"
  );

  // For comparison, when an add-on is only blocked by a MLBF, the block
  // decision is ignored on Nightly (but blocked on non-Nightly).
  langpack_addon.id = "@onlyblockedbymlbf";
  if (AppConstants.NIGHTLY_BUILD) {
    Assert.equal(
      await Blocklist.getAddonBlocklistState(langpack_addon),
      Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
      "Langpack add-ons cannot be blocked via a MLBF on Nightly"
    );
  } else {
    Assert.equal(
      await Blocklist.getAddonBlocklistState(langpack_addon),
      Ci.nsIBlocklistService.STATE_BLOCKED,
      "Langpack add-ons can be blocked via a MLBF on non-Nightly"
    );
  }
});

// Tests that invalid stash entries are ignored.
add_task(async function invalid_stashes() {
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {},
      { stash: null },
      { stash: 1 },
      { stash: {} },
      { stash: { blocked: ["@broken:1", "@okid:1"] } },
      { stash: { unblocked: ["@broken:2"] } },
      // The only correct entry:
      { stash: { blocked: ["@okid:2"], unblocked: ["@okid:1"] } },
      { stash: { blocked: ["@broken:1", "@okid:1"] } },
      { stash: { unblocked: ["@broken:2", "@okid:2"] } },
    ],
  });
  // The valid stash entry should be applied:
  await checkBlockState("@okid", "1", false);
  await checkBlockState("@okid", "2", true);
  // Entries from invalid stashes should be ignored:
  await checkBlockState("@broken", "1", false);
  await checkBlockState("@broken", "2", false);
});

// Blocklist stashes should be processed in the reverse chronological order.
add_task(async function stash_time_order() {
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      // "@a:1" and "@a:2" are blocked at time 1, but unblocked later.
      { stash_time: 2, stash: { blocked: [], unblocked: ["@a:1"] } },
      { stash_time: 1, stash: { blocked: ["@a:1", "@a:2"], unblocked: [] } },
      { stash_time: 3, stash: { blocked: [], unblocked: ["@a:2"] } },

      // "@b:1" and "@b:2" are unblocked at time 4, but blocked later.
      { stash_time: 5, stash: { blocked: ["@b:1"], unblocked: [] } },
      { stash_time: 4, stash: { blocked: [], unblocked: ["@b:1", "@b:2"] } },
      { stash_time: 6, stash: { blocked: ["@b:2"], unblocked: [] } },
    ],
  });
  await checkBlockState("@a", "1", false);
  await checkBlockState("@a", "2", false);

  await checkBlockState("@b", "1", true);
  await checkBlockState("@b", "2", true);
});

// Attachments with unsupported attachment_type should be ignored.
add_task(async function mlbf_bloomfilter_full_ignored() {
  MLBF_LOAD_ATTEMPTS.length = 0;

  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [{ attachment_type: "bloomfilter-full", attachment: {} }],
  });

  // Only bloomfilter-base records should be used.
  // Since there are no such records, we shouldn't find anything.
  Assert.deepEqual(MLBF_LOAD_ATTEMPTS, [null], "no matching MLBFs found");
});

// Tests that the most recent MLBF is downloaded.
add_task(async function mlbf_generation_time_recent() {
  MLBF_LOAD_ATTEMPTS.length = 0;
  const records = [
    { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 2 },
    { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 3 },
    { attachment_type: "bloomfilter-base", attachment: {}, generation_time: 1 },
  ];
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: records });
  Assert.equal(
    MLBF_LOAD_ATTEMPTS[0].generation_time,
    3,
    "expected to load most recent MLBF"
  );
});
