/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint max-len: ["error", 80] */

"use strict";

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

const appVersion = Services.appinfo.version;
const SUPPORT_URL = Services.urlFormatter.formatURL(
  Services.prefs.getStringPref("app.support.baseURL")
);

add_setup(async function () {
  gProvider = new MockProvider();
});

async function checkAddonCard(doc, id, expected, followLink = false) {
  let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
  let messageBar = card.querySelector(".addon-card-message");

  if (!expected) {
    ok(messageBar.hidden, `messagebar is hidden (addon-card ${id})`);
  } else {
    const { linkUrl, linkIsSumo, text, type } = expected;

    await BrowserTestUtils.waitForMutationCondition(
      messageBar,
      { attributes: true },
      () => !messageBar.hidden && messageBar.getAttribute("type") === type
    );
    ok(!messageBar.hidden, `messagebar is visible (addon-card ${id})`);

    is(messageBar.getAttribute("type"), type, "message has the right type");
    Assert.deepEqual(
      document.l10n.getAttributes(messageBar),
      { id: text.id, args: text.args },
      "message l10n data is set correctly"
    );

    const link = messageBar.querySelector(
      linkIsSumo ? `a[slot=support-link]` : `button[slot=actions]`
    );

    if (linkUrl) {
      ok(link, "Link element found");
      ok(BrowserTestUtils.isVisible(link), "Link is visible");
      is(
        link.getAttribute("data-l10n-id"),
        linkIsSumo ? "moz-support-link-text" : text.linkId,
        "link l10n id is correct"
      );
      if (followLink) {
        const newTab = BrowserTestUtils.waitForNewTab(gBrowser, linkUrl);
        link.click();
        BrowserTestUtils.removeTab(await newTab);
      } else {
        // Links to the blocklist details are button elements with the url
        // set on the url attribute.
        const actualLinkUrl = link.href ?? link.getAttribute("url");
        is(actualLinkUrl, linkUrl, "link should have the expected url");
      }
    } else {
      ok(!link, "Expect no slotted link element");
      is(messageBar.childElementCount, 0, "Expect no child element");
    }
  }

  return card;
}

async function checkMessageState(id, addonType, expected) {
  let win = await loadInitialView(addonType);
  let doc = win.document;

  // Check the list view.
  ok(doc.querySelector("addon-list"), "this is a list view");
  let card = await checkAddonCard(doc, id, expected, true);

  // Load the detail view.
  let loaded = waitForViewLoad(win);
  card.querySelector('[action="expand"]').click();
  await loaded;

  // Check the detail view.
  ok(!doc.querySelector("addon-list"), "this isn't a list view");
  await checkAddonCard(doc, id, expected, true);

  await closeView(win);
}

add_task(async function testNoMessageExtension() {
  let id = "no-message@mochi.test";
  let extension = ExtensionTestUtils.loadExtension({
    manifest: { browser_specific_settings: { gecko: { id } } },
    useAddonManager: "temporary",
  });
  await extension.startup();

  await checkMessageState(id, "extension", null);

  await extension.unload();
});

add_task(async function testNoMessageLangpack() {
  let id = "no-message@mochi.test";
  gProvider.createAddons([
    {
      appDisabled: true,
      id,
      name: "Signed Langpack",
      signedState: AddonManager.SIGNEDSTATE_SIGNED,
      type: "locale",
    },
  ]);

  await checkMessageState(id, "locale", null);
});

add_task(async function testHardBlocked() {
  for (const addonType of ["extension", "theme"]) {
    const id = `blocked-${addonType}@mochi.test`;
    const linkUrl = "https://example.com/addon-blocked";
    gProvider.createAddons([
      {
        appDisabled: true,
        blocklistState: STATE_BLOCKED,
        blocklistURL: linkUrl,
        id,
        name: `blocked ${addonType}`,
        type: addonType,
      },
    ]);

    let typeSuffix = addonType === "extension" ? "extension" : "other";
    await checkMessageState(id, addonType, {
      linkUrl,
      text: {
        id: `details-notification-hard-blocked-${typeSuffix}`,
        linkId: "details-notification-blocked-link2",
      },
      type: "error",
    });
  }
});

add_task(async function testSoftBlocked() {
  async function testSoftBlockedAddon({ mockAddon, expectedFluentId }) {
    const [testAddon] = gProvider.createAddons([
      {
        appDisabled: false,
        blocklistState: STATE_SOFTBLOCKED,
        ...mockAddon,
      },
    ]);
    await checkMessageState(mockAddon.id, mockAddon.type ?? "extension", {
      linkUrl: mockAddon.blocklistURL,
      text: {
        id: expectedFluentId,
        args: null,
        linkId: "details-notification-softblocked-link2",
      },
      type: "warning",
    });
    await testAddon.uninstall();
  }

  // Verify soft-block message on a softdisabled extension and theme.
  await testSoftBlockedAddon({
    expectedFluentId: "details-notification-soft-blocked-extension-disabled2",
    mockAddon: {
      id: "softblocked-extension@mochi.test",
      name: "Soft-Blocked Extension",
      type: "extension",
      blocklistURL: "https://example.com/addon-blocked",
      softDisabled: true,
    },
  });
  await testSoftBlockedAddon({
    expectedFluentId: "details-notification-soft-blocked-other-disabled2",
    mockAddon: {
      id: "softblocked-theme@mochi.test",
      name: "Soft-Blocked Theme",
      type: "theme",
      blocklistURL: "https://example.com/addon-blocked",
      softDisabled: true,
    },
  });

  // Verify soft-block message on a re-enabled extension and theme.
  await testSoftBlockedAddon({
    expectedFluentId: "details-notification-soft-blocked-extension-enabled2",
    mockAddon: {
      id: "softblocked-extension@mochi.test",
      name: "Soft-Blocked Extension",
      type: "extension",
      blocklistURL: "https://example.com/addon-blocked",
      userDisabled: false,
    },
  });
  await testSoftBlockedAddon({
    expectedFluentId: "details-notification-soft-blocked-other-enabled2",
    mockAddon: {
      id: "softblocked-theme@mochi.test",
      name: "Soft-Blocked Theme",
      type: "theme",
      blocklistURL: "https://example.com/addon-blocked",
      userDisabled: false,
    },
  });
});

add_task(async function testUnsignedDisabled() {
  // This pref being disabled will cause the `specialpowers` addon to be
  // uninstalled, which can cause a number of test failures due to features no
  // longer working correctly.
  // In order to avoid those issues, this code manually disables the pref, and
  // ensures that `SpecialPowers` is fully re-enabled at the end of the test.
  const sigPref = "xpinstall.signatures.required";
  Services.prefs.setBoolPref(sigPref, true);

  const id = "unsigned@mochi.test";
  const name = "Unsigned";
  gProvider.createAddons([
    {
      appDisabled: true,
      id,
      name,
      signedState: AddonManager.SIGNEDSTATE_MISSING,
    },
  ]);
  await checkMessageState(id, "extension", {
    linkUrl: SUPPORT_URL + "unsigned-addons",
    linkIsSumo: true,
    text: {
      id: "details-notification-unsigned-and-disabled2",
      args: { name },
    },
    type: "error",
  });

  // Ensure that `SpecialPowers` is fully re-initialized at the end of this
  // test. This requires removing the existing binding so that it's
  // re-registered, re-enabling unsigned extensions, and then waiting for the
  // actor to be registered and ready.
  delete window.SpecialPowers;
  Services.prefs.setBoolPref(sigPref, false);
  await TestUtils.waitForCondition(() => {
    try {
      return !!windowGlobalChild.getActor("SpecialPowers");
    } catch (e) {
      return false;
    }
  }, "wait for SpecialPowers to be reloaded");
  ok(window.SpecialPowers, "SpecialPowers should be re-defined");
});

add_task(async function testUnsignedLangpackDisabled() {
  const id = "unsigned-langpack@mochi.test";
  const name = "Unsigned";
  gProvider.createAddons([
    {
      appDisabled: true,
      id,
      name,
      signedState: AddonManager.SIGNEDSTATE_MISSING,
      type: "locale",
    },
  ]);
  await checkMessageState(id, "locale", {
    linkUrl: SUPPORT_URL + "unsigned-addons",
    linkIsSumo: true,
    text: {
      id: "details-notification-unsigned-and-disabled2",
      args: { name },
    },
    type: "error",
  });
});

add_task(async function testIncompatible() {
  const id = "incompatible@mochi.test";
  const name = "Incompatible";
  gProvider.createAddons([
    {
      appDisabled: true,
      id,
      isActive: false,
      isCompatible: false,
      name,
    },
  ]);
  await checkMessageState(id, "extension", {
    text: {
      id: "details-notification-incompatible2",
      args: { name, version: appVersion },
    },
    type: "error",
  });
});

add_task(async function testUnsignedEnabled() {
  const id = "unsigned-allowed@mochi.test";
  const name = "Unsigned";
  gProvider.createAddons([
    {
      id,
      name,
      signedState: AddonManager.SIGNEDSTATE_MISSING,
    },
  ]);
  await checkMessageState(id, "extension", {
    linkUrl: SUPPORT_URL + "unsigned-addons",
    linkIsSumo: true,
    text: { id: "details-notification-unsigned2", args: { name } },
    type: "warning",
  });
});

add_task(async function testUnsignedLangpackEnabled() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.langpacks.signatures.required", false]],
  });

  const id = "unsigned-allowed-langpack@mochi.test";
  const name = "Unsigned Langpack";
  gProvider.createAddons([
    {
      id,
      name,
      signedState: AddonManager.SIGNEDSTATE_MISSING,
      type: "locale",
    },
  ]);
  await checkMessageState(id, "locale", {
    linkUrl: SUPPORT_URL + "unsigned-addons",
    linkIsSumo: true,
    text: { id: "details-notification-unsigned2", args: { name } },
    type: "warning",
  });

  await SpecialPowers.popPrefEnv();
});

add_task(async function testPluginInstalling() {
  const id = "plugin-installing@mochi.test";
  const name = "Plugin Installing";
  gProvider.createAddons([
    {
      id,
      isActive: true,
      isGMPlugin: true,
      isInstalled: false,
      name,
      type: "plugin",
    },
  ]);
  await checkMessageState(id, "plugin", {
    text: { id: "details-notification-gmp-pending2", args: { name } },
    type: "warning",
  });
});

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

  let needsCleanupBlocklist = true;
  const cleanupBlocklist = async () => {
    if (!needsCleanupBlocklist) {
      return;
    }
    await AddonTestUtils.loadBlocklistRawData({
      extensionsMLBF: [],
    });
    needsCleanupBlocklist = false;
  };
  registerCleanupFunction(cleanupBlocklist);

  // This test:
  // - does not use the MockProvider to also verify that
  //   the XPIProvider is emitting calls to the onPropertyChanged
  //   addon listeners on blocklistState changes.
  // - uses a signed xpi to ensure the addon card messagebar for unsigned
  //   addons is not shown.
  const id = "amosigned-xpi@tests.mozilla.org";
  const version = "2.2";
  const XPI_URL = `${TESTROOT}../xpinstall/amosigned.xpi`;
  let install = await AddonManager.getInstallForURL(XPI_URL);
  await install.install();
  const addon = await AddonManager.getAddonByID(id);

  await checkMessageState(id, "extension", null);

  // Open the about:addons list view and keep it open to verify
  // it is being refreshed when the addon blocklistState is
  // expected to change
  let win = await loadInitialView("extension");
  let doc = win.document;
  ok(doc.querySelector("addon-list"), "this is a list view");

  // Sanity checks:
  // - addon blocklistState initially set to STATE_NOT_BLOCKED.
  // - addon card message bar should be hidden.
  Assert.equal(
    addon.blocklistState,
    STATE_NOT_BLOCKED,
    "Expect test extension to NOT be initially blocked"
  );
  await checkAddonCard(doc, id, null);

  // We intentionally turn off this a11y check, because the following click
  // is purposefully targeting a non-interactive element to clear the focused
  // state with a mouse which can be done by assistive technology and keyboard
  // by pressing `Esc`, this rule check shall be ignored by a11y_checks suite.
  AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
  // Click outside the list to clear any focus (needed to ensure the test will
  // not get stuck forever waiting for the "move" event to be dispatched on
  // the addon-list custom element).
  EventUtils.synthesizeMouseAtCenter(
    doc.querySelector(".header-name"),
    {},
    win
  );
  AccessibilityUtils.resetEnv();

  let moved = BrowserTestUtils.waitForEvent(
    doc.querySelector("addon-list"),
    "move"
  );
  await addon.disable();
  await moved;

  info("Verify blocklistState changing from unblocked to hard-blocked");

  const blockKey = `${id}:${version}`;
  const waitForBlocklistStateChanged = () =>
    AddonTestUtils.promiseAddonEvent(
      "onPropertyChanged",
      (addon, changedProps) =>
        addon.id === id && changedProps.includes("blocklistState")
    );

  let promiseBlocklistStateChanged = waitForBlocklistStateChanged();
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash: {
          blocked: [blockKey],
          softblocked: [],
          unblocked: [],
        },
      },
    ],
  });
  await promiseBlocklistStateChanged;

  const baseDetailsURL =
    "https://addons.mozilla.org/en-US/firefox/blocked-addon";
  const linkUrl = `${baseDetailsURL}/${id}/${version}/`;

  await checkAddonCard(doc, id, {
    linkUrl,
    text: {
      id: `details-notification-hard-blocked-extension`,
      linkId: "details-notification-blocked-link2",
    },
    type: "error",
  });

  info("Verify blocklistState changing from hard-blocked to soft-blocked");

  promiseBlocklistStateChanged = waitForBlocklistStateChanged();
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash: {
          blocked: [],
          softblocked: [blockKey],
          unblocked: [],
        },
      },
    ],
  });
  await promiseBlocklistStateChanged;

  await checkAddonCard(doc, id, {
    linkUrl,
    text: {
      id: `details-notification-soft-blocked-extension-disabled2`,
      linkId: "details-notification-softblocked-link2",
    },
    type: "warning",
  });

  info("Enable soft-blocked addon");
  moved = BrowserTestUtils.waitForEvent(
    doc.querySelector("addon-list"),
    "move"
  );
  await addon.enable();
  await moved;

  await checkAddonCard(doc, id, {
    linkUrl,
    text: {
      id: `details-notification-soft-blocked-extension-enabled2`,
      linkId: "details-notification-softblocked-link2",
    },
    type: "warning",
  });

  info("Verify blocklistState changing from soft-blocked to not-blocked");

  promiseBlocklistStateChanged = waitForBlocklistStateChanged();
  await AddonTestUtils.loadBlocklistRawData({
    extensionsMLBF: [
      {
        stash: {
          blocked: [],
          softblocked: [],
          unblocked: [blockKey],
        },
      },
    ],
  });
  await promiseBlocklistStateChanged;

  await checkAddonCard(doc, id, null);

  await closeView(win);
  await addon.uninstall();

  await cleanupBlocklist();
});
