"use strict";

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

loadTestSubscript("head_unified_extensions.js");

async function makeExtension({
  useAddonManager = "temporary",
  manifest_version = 3,
  id,
  permissions,
  host_permissions,
  content_scripts,
  granted,
  default_area,
}) {
  info(
    `Loading extension ` +
      JSON.stringify({ id, permissions, host_permissions, granted })
  );

  let manifest = {
    manifest_version,
    browser_specific_settings: { gecko: { id } },
    permissions,
    host_permissions,
    content_scripts,
    action: {
      default_popup: "popup.html",
      default_area: default_area || "navbar",
    },
  };
  if (manifest_version < 3) {
    manifest.browser_action = manifest.action;
    delete manifest.action;
  }

  let ext = ExtensionTestUtils.loadExtension({
    manifest,

    useAddonManager,

    async background() {
      browser.permissions.onAdded.addListener(({ origins }) => {
        browser.test.sendMessage("granted", origins.join());
      });
      browser.permissions.onRemoved.addListener(({ origins }) => {
        browser.test.sendMessage("revoked", origins.join());
      });

      if (browser.menus) {
        let submenu = browser.menus.create({
          id: "parent",
          title: "submenu",
          contexts: ["action"],
        });
        browser.menus.create({
          id: "child1",
          title: "child1",
          parentId: submenu,
        });
        await new Promise(resolve => {
          browser.menus.create(
            {
              id: "child2",
              title: "child2",
              parentId: submenu,
            },
            resolve
          );
        });
      }

      browser.test.sendMessage("ready");
    },

    files: {
      "popup.html": `<!DOCTYPE html><meta charset=utf-8>Test Popup`,
    },
  });

  if (granted) {
    info("Granting initial permissions.");
    await ExtensionPermissions.add(id, { permissions: [], origins: granted });
  }

  await ext.startup();
  await ext.awaitMessage("ready");
  return ext;
}

async function testOriginControls(
  extension,
  { contextMenuId },
  { items, selected, click, granted, revoked, attention, quarantined }
) {
  info(
    `Testing ${extension.id} on ${gBrowser.currentURI.spec} with contextMenuId=${contextMenuId}.`
  );

  let buttonOrWidget;
  let menu;
  let nextMenuItemClassName;

  switch (contextMenuId) {
    case "toolbar-context-menu":
      let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`;
      buttonOrWidget = document.querySelector(target).parentElement;
      menu = await openChromeContextMenu(contextMenuId, target);
      nextMenuItemClassName = "customize-context-manageExtension";
      break;

    case "unified-extensions-context-menu":
      await openExtensionsPanel();
      buttonOrWidget = getUnifiedExtensionsItem(extension.id);
      menu = await openUnifiedExtensionsContextMenu(extension.id);
      nextMenuItemClassName = "unified-extensions-context-menu-pin-to-toolbar";
      break;

    default:
      throw new Error(`unexpected context menu "${contextMenuId}"`);
  }

  let doc = menu.ownerDocument;
  let visibleOriginItems = menu.querySelectorAll(
    ":is(menuitem, menuseparator):not([hidden])"
  );

  info("Check expected menu items.");
  for (let i = 0; i < items.length; i++) {
    let l10n = doc.l10n.getAttributes(visibleOriginItems[i]);
    Assert.deepEqual(
      l10n,
      items[i],
      `Visible menu item ${i} has correct l10n attrs.`
    );

    let checked = visibleOriginItems[i].getAttribute("checked") === "true";
    is(i === selected, checked, `Expected checked value for item ${i}.`);
  }

  if (items.length) {
    is(
      visibleOriginItems[items.length].nodeName,
      "menuseparator",
      "Found separator."
    );
    is(
      visibleOriginItems[items.length + 1].className,
      nextMenuItemClassName,
      "All items accounted for."
    );
  }

  is(
    buttonOrWidget.hasAttribute("attention"),
    !!attention,
    "Expected attention badge before clicking."
  );

  Assert.deepEqual(
    document.l10n.getAttributes(
      buttonOrWidget.querySelector(".unified-extensions-item-action-button")
    ),
    {
      // eslint-disable-next-line no-nested-ternary
      id: attention
        ? quarantined
          ? "origin-controls-toolbar-button-quarantined"
          : "origin-controls-toolbar-button-permission-needed"
        : "origin-controls-toolbar-button",
      args: {
        extensionTitle: "Generated extension",
      },
    },
    "Correct l10n message."
  );

  let itemToClick;
  if (click) {
    itemToClick = visibleOriginItems[click];
  }

  // Clicking a menu item of the unified extensions context menu should close
  // the unified extensions panel automatically.
  let panelHidden =
    itemToClick && contextMenuId === "unified-extensions-context-menu"
      ? BrowserTestUtils.waitForEvent(document, "popuphidden", true)
      : Promise.resolve();

  await closeChromeContextMenu(contextMenuId, itemToClick);
  await panelHidden;

  // When there is no menu item to close, we should manually close the unified
  // extensions panel because simply closing the context menu will not close
  // it.
  if (!itemToClick && contextMenuId === "unified-extensions-context-menu") {
    await closeExtensionsPanel();
  }

  if (granted) {
    info("Waiting for the permissions.onAdded event.");
    let host = await extension.awaitMessage("granted");
    is(host, granted.join(), "Expected host permission granted.");
  }
  if (revoked) {
    info("Waiting for the permissions.onRemoved event.");
    let host = await extension.awaitMessage("revoked");
    is(host, revoked.join(), "Expected host permission revoked.");
  }
}

// Move the widget to the toolbar or the addons panel (if Unified Extensions
// is enabled) or the overflow panel otherwise.
function moveWidget(ext, pinToToolbar = false) {
  let area = pinToToolbar
    ? CustomizableUI.AREA_NAVBAR
    : CustomizableUI.AREA_ADDONS;
  let widgetId = `${makeWidgetId(ext.id)}-browser-action`;
  CustomizableUI.addWidgetToArea(widgetId, area);
}

const originControlsInContextMenu = async options => {
  // Has no permissions.
  let ext1 = await makeExtension({ id: "ext1@test" });

  // Has activeTab and (ungranted) example.com permissions.
  let ext2 = await makeExtension({
    id: "ext2@test",
    permissions: ["activeTab"],
    host_permissions: ["*://example.com/*"],
    useAddonManager: "permanent",
  });

  // Has ungranted <all_urls>, and granted example.com.
  let ext3 = await makeExtension({
    id: "ext3@test",
    host_permissions: ["<all_urls>"],
    granted: ["*://example.com/*"],
    useAddonManager: "permanent",
  });

  // Has granted <all_urls>.
  let ext4 = await makeExtension({
    id: "ext4@test",
    host_permissions: ["<all_urls>"],
    granted: ["<all_urls>"],
    useAddonManager: "permanent",
  });

  // MV2 extension with an <all_urls> content script and activeTab.
  let ext5 = await makeExtension({
    manifest_version: 2,
    id: "ext5@test",
    permissions: ["activeTab"],
    content_scripts: [
      {
        matches: ["<all_urls>"],
        css: [],
      },
    ],
    useAddonManager: "permanent",
  });

  // Add an extension always visible in the extensions panel.
  let ext6 = await makeExtension({
    id: "ext6@test",
    default_area: "menupanel",
  });

  let extensions = [ext1, ext2, ext3, ext4, ext5, ext6];

  let unifiedButton;
  if (options.contextMenuId === "unified-extensions-context-menu") {
    moveWidget(ext1, false);
    moveWidget(ext2, false);
    moveWidget(ext3, false);
    moveWidget(ext4, false);
    moveWidget(ext5, false);
    unifiedButton = document.querySelector("#unified-extensions-button");
  } else {
    // TestVerify runs this again in the same Firefox instance, so move the
    // widgets back to the toolbar for testing outside the unified extensions
    // panel.
    moveWidget(ext1, true);
    moveWidget(ext2, true);
    moveWidget(ext3, true);
    moveWidget(ext4, true);
    moveWidget(ext5, true);
  }

  const NO_ACCESS = { id: "origin-controls-no-access", args: null };
  const QUARANTINED = { id: "origin-controls-quarantined", args: null };
  const ACCESS_OPTIONS = { id: "origin-controls-options", args: null };
  const ALL_SITES = { id: "origin-controls-option-all-domains", args: null };
  const WHEN_CLICKED = {
    id: "origin-controls-option-when-clicked",
    args: null,
  };

  const UNIFIED_NO_ATTENTION = { id: "unified-extensions-button", args: null };
  const UNIFIED_ATTENTION = {
    id: "unified-extensions-button-permissions-needed",
    args: null,
  };
  const UNIFIED_QUARANTINED = {
    id: "unified-extensions-button-quarantined",
    args: null,
  };

  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await testOriginControls(ext1, options, { items: [NO_ACCESS] });
    await testOriginControls(ext2, options, { items: [NO_ACCESS] });
    await testOriginControls(ext3, options, { items: [NO_ACCESS] });
    await testOriginControls(ext4, options, { items: [NO_ACCESS] });
    await testOriginControls(ext5, options, { items: [] });

    if (unifiedButton) {
      ok(
        !unifiedButton.hasAttribute("attention"),
        "No extension will have attention indicator on about:blank."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_NO_ATTENTION,
        "Unified button has no permissions needed tooltip."
      );
    }
  });

  await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
    const ALWAYS_ON = {
      id: "origin-controls-option-always-on",
      args: { domain: "mochi.test" },
    };

    await testOriginControls(ext1, options, { items: [NO_ACCESS] });

    // Has activeTab.
    await testOriginControls(ext2, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED],
      selected: 1,
      attention: true,
    });

    // Could access mochi.test when clicked.
    await testOriginControls(ext3, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 1,
      attention: true,
    });

    // Has <all_urls> granted.
    await testOriginControls(ext4, options, {
      items: [ACCESS_OPTIONS, ALL_SITES],
      selected: 1,
      attention: false,
    });

    // MV2 extension, has no origin controls, and never flags for attention.
    await testOriginControls(ext5, options, { items: [], attention: false });
    if (unifiedButton) {
      ok(
        unifiedButton.hasAttribute("attention"),
        "Both ext2 and ext3 are WHEN_CLICKED for example.com, so show attention indicator."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_ATTENTION,
        "UEB has permissions needed tooltip."
      );
    }
  });

  info("Testing again with mochi.test now quarantined.");
  await SpecialPowers.pushPrefEnv({
    set: [
      ["extensions.quarantinedDomains.enabled", true],
      ["extensions.quarantinedDomains.list", "mochi.test"],
    ],
  });

  await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
    await testOriginControls(ext1, options, {
      items: [NO_ACCESS],
      attention: false,
    });

    await testOriginControls(ext2, options, {
      items: [QUARANTINED],
      attention: true,
      quarantined: true,
    });
    await testOriginControls(ext3, options, {
      items: [QUARANTINED],
      attention: true,
      quarantined: true,
    });
    await testOriginControls(ext4, options, {
      items: [QUARANTINED],
      attention: true,
      quarantined: true,
    });

    // MV2 normally don't have controls, but we show the quarantined status.
    await testOriginControls(ext5, options, {
      items: [QUARANTINED],
      attention: true,
      quarantined: true,
    });

    if (unifiedButton) {
      ok(unifiedButton.hasAttribute("attention"), "Expected attention UI");
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_QUARANTINED,
        "Expected attention tooltip text for quarantined domains"
      );
    }
  });

  if (unifiedButton) {
    extensions.forEach(extension =>
      moveWidget(extension, /* pinToToolbar */ true)
    );

    await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
      ok(unifiedButton.hasAttribute("attention"), "Expected attention UI");
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_QUARANTINED,
        "Expected attention tooltip text for quarantined domains"
      );

      await openExtensionsPanel();

      const messages = getMessageBars();
      Assert.equal(messages.length, 1, "expected a message");
      const supportLink = messages[0].querySelector("a");
      Assert.equal(
        supportLink.getAttribute("support-page"),
        "quarantined-domains",
        "Expected the correct support page ID"
      );

      await closeExtensionsPanel();
    });

    extensions.forEach(extension =>
      moveWidget(extension, /* pinToToolbar */ false)
    );
  }

  await SpecialPowers.popPrefEnv();

  await BrowserTestUtils.withNewTab("http://example.com/", async () => {
    const ALWAYS_ON = {
      id: "origin-controls-option-always-on",
      args: { domain: "example.com" },
    };

    await testOriginControls(ext1, options, { items: [NO_ACCESS] });

    // Click alraedy selected options, expect no permission changes.
    await testOriginControls(ext2, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 1,
      click: 1,
      attention: true,
    });
    await testOriginControls(ext3, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 2,
      click: 2,
      attention: false,
    });
    await testOriginControls(ext4, options, {
      items: [ACCESS_OPTIONS, ALL_SITES],
      selected: 1,
      click: 1,
      attention: false,
    });

    await testOriginControls(ext5, options, { items: [], attention: false });

    if (unifiedButton) {
      ok(
        unifiedButton.hasAttribute("attention"),
        "ext2 is WHEN_CLICKED for example.com, show attention indicator."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_ATTENTION,
        "UEB attention for only one extension."
      );
    }

    // Click the other option, expect example.com permission granted/revoked.
    await testOriginControls(ext2, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 1,
      click: 2,
      granted: ["*://example.com/*"],
      attention: true,
    });
    if (unifiedButton) {
      ok(
        !unifiedButton.hasAttribute("attention"),
        "Bot ext2 and ext3 are ALWAYS_ON for example.com, so no attention indicator."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_NO_ATTENTION,
        "Unified button has no permissions needed tooltip."
      );
    }

    await testOriginControls(ext3, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 2,
      click: 1,
      revoked: ["*://example.com/*"],
      attention: false,
    });
    if (unifiedButton) {
      ok(
        unifiedButton.hasAttribute("attention"),
        "ext3 is now WHEN_CLICKED for example.com, show attention indicator."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_ATTENTION,
        "UEB attention for only one extension."
      );
    }

    // Other option is now selected.
    await testOriginControls(ext2, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 2,
      attention: false,
    });
    await testOriginControls(ext3, options, {
      items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON],
      selected: 1,
      attention: true,
    });

    if (unifiedButton) {
      ok(
        unifiedButton.hasAttribute("attention"),
        "Still showing the attention indicator."
      );
      Assert.deepEqual(
        document.l10n.getAttributes(unifiedButton),
        UNIFIED_ATTENTION,
        "UEB attention for only one extension."
      );
    }
  });

  await Promise.all(extensions.map(e => e.unload()));
};

add_task(async function originControls_in_browserAction_contextMenu() {
  await originControlsInContextMenu({ contextMenuId: "toolbar-context-menu" });
});

add_task(async function originControls_in_unifiedExtensions_contextMenu() {
  await originControlsInContextMenu({
    contextMenuId: "unified-extensions-context-menu",
  });
});

add_task(async function test_attention_dot_when_pinning_extension() {
  const extension = await makeExtension({ permissions: ["activeTab"] });
  await extension.startup();

  const unifiedButton = document.querySelector("#unified-extensions-button");
  const extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
    extension.id
  );
  const extensionWidget =
    CustomizableUI.getWidget(extensionWidgetID).forWindow(window).node;

  await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => {
    // The extensions should be placed in the navbar by default so we do not
    // expect an attention dot on the Unifed Extensions Button (UEB), only on
    // the extension (widget) itself.
    ok(
      !unifiedButton.hasAttribute("attention"),
      "expected no attention attribute on the UEB"
    );
    ok(
      extensionWidget.hasAttribute("attention"),
      "expected attention attribute on the extension widget"
    );

    // Open the context menu of the extension and unpin the extension.
    let contextMenu = await openChromeContextMenu(
      "toolbar-context-menu",
      `#${CSS.escape(extensionWidgetID)}`
    );
    let pinToToolbar = contextMenu.querySelector(
      ".customize-context-pinToToolbar"
    );
    ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
    // Passing the `pinToToolbar` item to `closeChromeContextMenu()` will
    // activate it before closing the context menu.
    await closeChromeContextMenu(contextMenu.id, pinToToolbar);

    ok(
      unifiedButton.hasAttribute("attention"),
      "expected attention attribute on the UEB"
    );
    // We still expect the attention dot on the extension.
    ok(
      extensionWidget.hasAttribute("attention"),
      "expected attention attribute on the extension widget"
    );

    // Now let's open the unified extensions panel, and pin the same extension
    // to the toolbar, which should hide the attention dot on the UEB again.
    await openExtensionsPanel();
    contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
    pinToToolbar = contextMenu.querySelector(
      ".unified-extensions-context-menu-pin-to-toolbar"
    );
    ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
    const hidden = BrowserTestUtils.waitForEvent(
      gUnifiedExtensions.panel,
      "popuphidden",
      true
    );
    contextMenu.activateItem(pinToToolbar);
    await hidden;

    ok(
      !unifiedButton.hasAttribute("attention"),
      "expected no attention attribute on the UEB"
    );
    // We still expect the attention dot on the extension.
    ok(
      extensionWidget.hasAttribute("attention"),
      "expected attention attribute on the extension widget"
    );
  });

  await extension.unload();
});

async function testWithSubmenu(menu, nextItemClassName) {
  function expectMenuItems() {
    info("Checking expected menu items.");
    let [submenu, sep1, ocMessage, sep2, next] = menu.children;

    is(submenu.tagName, "menu", "First item is a submenu.");
    is(submenu.label, "submenu", "Submenu has the expected label.");
    is(sep1.tagName, "menuseparator", "Second item is a separator.");

    let l10n = menu.ownerDocument.l10n.getAttributes(ocMessage);
    is(ocMessage.tagName, "menuitem", "Third is origin controls message.");
    is(l10n.id, "origin-controls-no-access", "Expected l10n id.");

    is(sep2.tagName, "menuseparator", "Fourth item is a separator.");
    is(next.className, nextItemClassName, "All items accounted for.");
  }

  const [submenu] = menu.children;
  const popup = submenu.querySelector("menupopup");

  // Open and close the submenu repeatedly a few times.
  for (let i = 0; i < 3; i++) {
    expectMenuItems();

    const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
    submenu.openMenu(true);
    await popupShown;

    expectMenuItems();

    const popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
    submenu.openMenu(false);
    await popupHidden;
  }

  menu.hidePopup();
}

add_task(async function test_originControls_with_submenus() {
  let extension = await makeExtension({
    id: "submenus@test",
    permissions: ["menus"],
  });

  await BrowserTestUtils.withNewTab("about:blank", async () => {
    info(`Testing with submenus.`);
    moveWidget(extension, true);
    let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`;

    await testWithSubmenu(
      await openChromeContextMenu("toolbar-context-menu", target),
      "customize-context-manageExtension"
    );

    info(`Testing with submenus inside extensions panel.`);
    moveWidget(extension, false);
    await openExtensionsPanel();

    await testWithSubmenu(
      await openUnifiedExtensionsContextMenu(extension.id),
      "unified-extensions-context-menu-pin-to-toolbar"
    );

    await closeExtensionsPanel();
  });

  await extension.unload();
});
