/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const PAGE =
  "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";

// Loads an extension that records menu visibility events in the current tab.
// The returned extension has two helper functions "openContextMenu" and
// "checkIsValid" that are used to verify the behavior of targetElementId.
async function loadExtensionAndTab() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
  gBrowser.selectedTab = tab;

  function contentScript() {
    browser.test.onMessage.addListener(
      (msg, targetElementId, expectedSelector, description) => {
        browser.test.assertEq("checkIsValid", msg, "Expected message");

        let expected = expectedSelector
          ? document.querySelector(expectedSelector)
          : null;
        let elem = browser.menus.getTargetElement(targetElementId);
        browser.test.assertEq(expected, elem, description);
        browser.test.sendMessage("checkIsValidDone");
      }
    );
  }

  async function background() {
    browser.menus.onShown.addListener(async info => {
      browser.test.sendMessage("onShownMenu", info.targetElementId);
    });
    await browser.tabs.executeScript({ file: "contentScript.js" });
    browser.test.sendMessage("ready");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["menus", "http://mochi.test/*"],
    },
    background,
    files: {
      "contentScript.js": contentScript,
    },
  });

  extension.openAndCloseMenu = async selector => {
    await openContextMenu(selector);
    let targetElementId = await extension.awaitMessage("onShownMenu");
    await closeContextMenu();
    return targetElementId;
  };

  extension.checkIsValid = async (
    targetElementId,
    expectedSelector,
    description
  ) => {
    extension.sendMessage(
      "checkIsValid",
      targetElementId,
      expectedSelector,
      description
    );
    await extension.awaitMessage("checkIsValidDone");
  };

  await extension.startup();
  await extension.awaitMessage("ready");
  return { extension, tab };
}

// Tests that info.targetElementId is only available with the right permissions.
add_task(async function required_permission() {
  let { extension, tab } = await loadExtensionAndTab();

  // Load another extension to verify that the permission from the first
  // extension does not enable the "targetElementId" parameter.
  function background() {
    browser.contextMenus.onShown.addListener(info => {
      browser.test.assertEq(
        undefined,
        info.targetElementId,
        "targetElementId requires permission"
      );
      browser.test.sendMessage("onShown");
    });
    browser.contextMenus.onClicked.addListener(async info => {
      browser.test.assertEq(
        undefined,
        info.targetElementId,
        "targetElementId requires permission"
      );
      const code = `
        browser.test.assertEq(undefined, browser.menus, "menus API requires permission in content script");
        browser.test.assertEq(undefined, browser.contextMenus, "contextMenus API not available in content script.");
      `;
      await browser.tabs.executeScript({ code });
      browser.test.sendMessage("onClicked");
    });
    browser.contextMenus.create({ title: "menu for page" }, () => {
      browser.test.sendMessage("ready");
    });
  }
  let extension2 = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus", "http://mochi.test/*"],
    },
    background,
  });
  await extension2.startup();
  await extension2.awaitMessage("ready");

  let menu = await openContextMenu();
  await extension.awaitMessage("onShownMenu");
  let menuItem = menu.getElementsByAttribute("label", "menu for page")[0];
  await closeExtensionContextMenu(menuItem);

  await extension2.awaitMessage("onShown");
  await extension2.awaitMessage("onClicked");
  await extension2.unload();

  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

// Tests that the basic functionality works as expected.
add_task(async function getTargetElement_in_page() {
  let { extension, tab } = await loadExtensionAndTab();

  for (let selector of ["#img1", "#link1", "#password"]) {
    let targetElementId = await extension.openAndCloseMenu(selector);
    ok(
      Number.isInteger(targetElementId),
      `targetElementId (${targetElementId}) should be an integer for ${selector}`
    );

    await extension.checkIsValid(
      targetElementId,
      selector,
      `Expected target to match ${selector}`
    );
  }

  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

add_task(async function getTargetElement_in_frame() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
  gBrowser.selectedTab = tab;

  async function background() {
    let targetElementId;
    browser.menus.onShown.addListener(async (info, tab) => {
      browser.test.assertTrue(
        info.frameUrl.endsWith("context_frame.html"),
        `Expected frame ${info.frameUrl}`
      );
      targetElementId = info.targetElementId;
      let elem = browser.menus.getTargetElement(targetElementId);
      browser.test.assertEq(
        null,
        elem,
        "should not find page element in extension's background"
      );

      await browser.tabs.executeScript(tab.id, {
        code: `{
          let elem = browser.menus.getTargetElement(${targetElementId});
          browser.test.assertEq(null, elem, "should not find element from different frame");
        }`,
      });

      await browser.tabs.executeScript(tab.id, {
        frameId: info.frameId,
        code: `{
          let elem = browser.menus.getTargetElement(${targetElementId});
          browser.test.assertEq(document.body, elem, "should find the target element in the frame");
        }`,
      });
      browser.test.sendMessage("pageAndFrameChecked");
    });

    browser.menus.onClicked.addListener(info => {
      browser.test.assertEq(
        targetElementId,
        info.targetElementId,
        "targetElementId in onClicked must match onShown."
      );
      browser.test.sendMessage("onClickedChecked");
    });

    browser.menus.create(
      { title: "menu for frame", contexts: ["frame"] },
      () => {
        browser.test.sendMessage("ready");
      }
    );
  }
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["menus", "http://mochi.test/*"],
    },
    background,
  });

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

  let menu = await openContextMenuInFrame();
  await extension.awaitMessage("pageAndFrameChecked");
  let menuItem = menu.getElementsByAttribute("label", "menu for frame")[0];
  await closeExtensionContextMenu(menuItem);
  await extension.awaitMessage("onClickedChecked");

  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

// Test that getTargetElement does not return a detached element.
add_task(async function getTargetElement_after_removing_element() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);

  function background() {
    function contentScript(targetElementId) {
      let expectedElem = document.getElementById("edit-me");
      let { nextElementSibling } = expectedElem;

      let elem = browser.menus.getTargetElement(targetElementId);
      browser.test.assertEq(
        expectedElem,
        elem,
        "Expected target element before element removal"
      );

      expectedElem.remove();
      elem = browser.menus.getTargetElement(targetElementId);
      browser.test.assertEq(
        null,
        elem,
        "Expected no target element after element removal."
      );

      nextElementSibling.insertAdjacentElement("beforebegin", expectedElem);
      elem = browser.menus.getTargetElement(targetElementId);
      browser.test.assertEq(
        expectedElem,
        elem,
        "Expected target element after element restoration."
      );
    }
    browser.menus.onClicked.addListener(async (info, tab) => {
      const code = `(${contentScript})(${info.targetElementId})`;
      browser.test.log(code);
      await browser.tabs.executeScript(tab.id, { code });
      browser.test.sendMessage("checkedRemovedElement");
    });
    browser.menus.create({ title: "some menu item" }, () => {
      browser.test.sendMessage("ready");
    });
  }
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["menus", "http://mochi.test/*"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitMessage("ready");
  let menu = await openContextMenu("#edit-me");
  let menuItem = menu.getElementsByAttribute("label", "some menu item")[0];
  await closeExtensionContextMenu(menuItem);
  await extension.awaitMessage("checkedRemovedElement");
  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

// Tests whether targetElementId expires after opening a new menu.
add_task(async function expireTargetElement() {
  let { extension, tab } = await loadExtensionAndTab();

  // Open the menu once to get the first element ID.
  let targetElementId = await extension.openAndCloseMenu("#longtext");

  // Open another menu. The previous ID should expire.
  await extension.openAndCloseMenu("#longtext");
  await extension.checkIsValid(
    targetElementId,
    null,
    `Expected initial target ID to expire after opening another menu`
  );

  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

// Tests whether targetElementId of different tabs are independent.
add_task(async function independentMenusInDifferentTabs() {
  let { extension, tab } = await loadExtensionAndTab();

  let targetElementId = await extension.openAndCloseMenu("#longtext");

  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE + "?");
  gBrowser.selectedTab = tab2;

  let targetElementId2 = await extension.openAndCloseMenu("#editabletext");
  if (targetElementId === targetElementId2) {
    // targetElementId is only guaranteed to be unique within a tab, so odds are
    // that by unlucky coincidence, that the discovered ID accidentally overlaps
    // with the actual ID.
    info(`Got same targetElementId ${targetElementId}, retrying for a new one`);
    targetElementId2 = await extension.openAndCloseMenu("#editabletext");
  }
  Assert.notEqual(targetElementId, targetElementId2, "targetElementId differ");

  await extension.checkIsValid(
    targetElementId2,
    null,
    "targetElementId from different tab should not resolve."
  );
  await extension.checkIsValid(
    targetElementId,
    "#longtext",
    "Expected getTargetElement to work after closing a menu in another tab."
  );

  await extension.unload();
  BrowserTestUtils.removeTab(tab);
  BrowserTestUtils.removeTab(tab2);
});
