/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js",
  this
);
/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell, promiseLibrary, promiseLibraryClosed
 */

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

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

  gBrowser.selectedTab = tab1;

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus"],
    },

    background: function () {
      browser.test.assertEq(
        browser.contextMenus.ContextType.TAB,
        "tab",
        "ContextType is available"
      );
      browser.contextMenus.create({
        id: "clickme-image",
        title: "Click me!",
        contexts: ["image"],
      });
      browser.contextMenus.create(
        {
          id: "clickme-page",
          title: "Click me!",
          contexts: ["page"],
        },
        () => {
          browser.test.sendMessage("ready");
        }
      );
    },
  });

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

  let contentAreaContextMenu = await openContextMenu("#img1");
  let item = contentAreaContextMenu.getElementsByAttribute(
    "label",
    "Click me!"
  );
  is(item.length, 1, "contextMenu item for image was found");
  await closeContextMenu();

  contentAreaContextMenu = await openContextMenu("body");
  item = contentAreaContextMenu.getElementsByAttribute("label", "Click me!");
  is(item.length, 1, "contextMenu item for page was found");
  await closeContextMenu();

  await extension.unload();

  BrowserTestUtils.removeTab(tab1);
});

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

  gBrowser.selectedTab = tab1;

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus"],
    },

    background: async function () {
      browser.test.onMessage.addListener(msg => {
        if (msg == "removeall") {
          browser.contextMenus.removeAll();
          browser.test.sendMessage("removed");
        }
      });

      // A generic onclick callback function.
      function genericOnClick(info, tab) {
        browser.test.sendMessage("onclick", { info, tab });
      }

      browser.contextMenus.onClicked.addListener((info, tab) => {
        browser.test.sendMessage("browser.contextMenus.onClicked", {
          info,
          tab,
        });
      });

      browser.contextMenus.create({
        contexts: ["all"],
        type: "separator",
      });

      let contexts = [
        "page",
        "link",
        "selection",
        "image",
        "editable",
        "password",
      ];
      for (let i = 0; i < contexts.length; i++) {
        let context = contexts[i];
        let title = context;
        browser.contextMenus.create({
          title: title,
          contexts: [context],
          id: "ext-" + context,
          onclick: genericOnClick,
        });
        if (context == "selection") {
          browser.contextMenus.update("ext-selection", {
            title: "selection is: '%s'",
            onclick: genericOnClick,
          });
        }
      }

      let parent = browser.contextMenus.create({
        title: "parent",
      });
      browser.contextMenus.create({
        title: "child1",
        parentId: parent,
        onclick: genericOnClick,
      });
      let child2 = browser.contextMenus.create({
        title: "child2",
        parentId: parent,
        onclick: genericOnClick,
      });

      let parentToDel = browser.contextMenus.create({
        title: "parentToDel",
      });
      browser.contextMenus.create({
        title: "child1",
        parentId: parentToDel,
        onclick: genericOnClick,
      });
      browser.contextMenus.create({
        title: "child2",
        parentId: parentToDel,
        onclick: genericOnClick,
      });
      browser.contextMenus.remove(parentToDel);

      browser.contextMenus.create({
        title: "Without onclick property",
        id: "ext-without-onclick",
      });

      await browser.test.assertRejects(
        browser.contextMenus.update(parent, { parentId: child2 }),
        /cannot be an ancestor/,
        "Should not be able to reparent an item as descendent of itself"
      );

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

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

  let expectedClickInfo = {
    menuItemId: "ext-image",
    mediaType: "image",
    srcUrl:
      "http://mochi.test:8888/browser/browser/components/extensions/test/browser/ctxmenu-image.png",
    pageUrl: PAGE,
    editable: false,
  };

  function checkClickInfo(result) {
    for (let i of Object.keys(expectedClickInfo)) {
      is(
        result.info[i],
        expectedClickInfo[i],
        "click info " +
          i +
          " expected to be: " +
          expectedClickInfo[i] +
          " but was: " +
          result.info[i]
      );
    }
    is(
      expectedClickInfo.pageSrc,
      result.tab.url,
      "click info page source is the right tab"
    );
  }

  let extensionMenuRoot = await openExtensionContextMenu();

  // Check some menu items
  let items = extensionMenuRoot.getElementsByAttribute("label", "image");
  is(items.length, 1, "contextMenu item for image was found (context=image)");
  let image = items[0];

  items = extensionMenuRoot.getElementsByAttribute("label", "selection-edited");
  is(
    items.length,
    0,
    "contextMenu item for selection was not found (context=image)"
  );

  items = extensionMenuRoot.getElementsByAttribute("label", "parentToDel");
  is(
    items.length,
    0,
    "contextMenu item for removed parent was not found (context=image)"
  );

  items = extensionMenuRoot.getElementsByAttribute("label", "parent");
  is(items.length, 1, "contextMenu item for parent was found (context=image)");

  is(
    items[0].menupopup.children.length,
    2,
    "child items for parent were found (context=image)"
  );

  // Click on ext-image item and check the click results
  await closeExtensionContextMenu(image);

  let result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  // Test "link" context and OnClick data property.
  extensionMenuRoot = await openExtensionContextMenu("[href=some-link]");

  // Click on ext-link and check the click results
  items = extensionMenuRoot.getElementsByAttribute("label", "link");
  is(items.length, 1, "contextMenu item for parent was found (context=link)");
  let link = items[0];

  expectedClickInfo = {
    menuItemId: "ext-link",
    linkUrl:
      "http://mochi.test:8888/browser/browser/components/extensions/test/browser/some-link",
    linkText: "Some link",
    pageUrl: PAGE,
    editable: false,
  };

  await closeExtensionContextMenu(link);

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  // Test "editable" context and OnClick data property.
  extensionMenuRoot = await openExtensionContextMenu("#edit-me");

  // Check some menu items.
  items = extensionMenuRoot.getElementsByAttribute("label", "editable");
  is(
    items.length,
    1,
    "contextMenu item for text input element was found (context=editable)"
  );
  let editable = items[0];

  // Click on ext-editable item and check the click results.
  await closeExtensionContextMenu(editable);

  expectedClickInfo = {
    menuItemId: "ext-editable",
    pageUrl: PAGE,
    editable: true,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  extensionMenuRoot = await openExtensionContextMenu("#readonly-text");

  // Check some menu items.
  items = extensionMenuRoot.getElementsByAttribute("label", "editable");
  is(
    items.length,
    0,
    "contextMenu item for text input element was not found (context=editable fails for readonly items)"
  );

  // Hide the popup "manually" because there's nothing to click.
  await closeContextMenu();

  // Test "editable" context on type=tel and type=number items, and OnClick data property.
  extensionMenuRoot = await openExtensionContextMenu("#call-me-maybe");

  // Check some menu items.
  items = extensionMenuRoot.getElementsByAttribute("label", "editable");
  is(
    items.length,
    1,
    "contextMenu item for text input element was found (context=editable)"
  );
  editable = items[0];

  // Click on ext-editable item and check the click results.
  await closeExtensionContextMenu(editable);

  expectedClickInfo = {
    menuItemId: "ext-editable",
    pageUrl: PAGE,
    editable: true,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  extensionMenuRoot = await openExtensionContextMenu("#number-input");

  // Check some menu items.
  items = extensionMenuRoot.getElementsByAttribute("label", "editable");
  is(
    items.length,
    1,
    "contextMenu item for text input element was found (context=editable)"
  );
  editable = items[0];

  // Click on ext-editable item and check the click results.
  await closeExtensionContextMenu(editable);

  expectedClickInfo = {
    menuItemId: "ext-editable",
    pageUrl: PAGE,
    editable: true,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  extensionMenuRoot = await openExtensionContextMenu("#password");
  items = extensionMenuRoot.getElementsByAttribute("label", "password");
  is(
    items.length,
    1,
    "contextMenu item for password input element was found (context=password)"
  );
  let password = items[0];
  await closeExtensionContextMenu(password);
  expectedClickInfo = {
    menuItemId: "ext-password",
    pageUrl: PAGE,
    editable: true,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  extensionMenuRoot = await openExtensionContextMenu("#noneditablepassword");
  items = extensionMenuRoot.getElementsByAttribute("label", "password");
  is(
    items.length,
    1,
    "contextMenu item for password input element was found (context=password)"
  );
  password = items[0];
  await closeExtensionContextMenu(password);
  expectedClickInfo.editable = false;
  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  // Select some text
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
    let doc = content.document;
    let range = doc.createRange();
    let selection = content.getSelection();
    selection.removeAllRanges();
    let textNode = doc.getElementById("img1").previousSibling;
    range.setStart(textNode, 0);
    range.setEnd(textNode, 100);
    selection.addRange(range);
  });

  // Bring up context menu again
  extensionMenuRoot = await openExtensionContextMenu();

  // Check some menu items
  items = extensionMenuRoot.getElementsByAttribute(
    "label",
    "Without onclick property"
  );
  is(items.length, 1, "contextMenu item was found (context=page)");

  await closeExtensionContextMenu(items[0]);

  expectedClickInfo = {
    menuItemId: "ext-without-onclick",
    pageUrl: PAGE,
  };

  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  // Bring up context menu again
  extensionMenuRoot = await openExtensionContextMenu();

  // Check some menu items
  items = extensionMenuRoot.getElementsByAttribute(
    "label",
    "selection is: 'just some text 12345678901234567890123456789012\u2026'"
  );
  is(
    items.length,
    1,
    "contextMenu item for selection was found (context=selection)"
  );
  let selectionItem = items[0];

  items = extensionMenuRoot.getElementsByAttribute("label", "selection");
  is(
    items.length,
    0,
    "contextMenu item label update worked (context=selection)"
  );

  await closeExtensionContextMenu(selectionItem);

  expectedClickInfo = {
    menuItemId: "ext-selection",
    pageUrl: PAGE,
    selectionText:
      " just some text 1234567890123456789012345678901234567890123456789012345678901234567890123456789012",
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);

  // Select a lot of text
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
    let doc = content.document;
    let range = doc.createRange();
    let selection = content.getSelection();
    selection.removeAllRanges();
    let textNode = doc.getElementById("longtext").firstChild;
    range.setStart(textNode, 0);
    range.setEnd(textNode, textNode.length);
    selection.addRange(range);
  });

  // Bring up context menu again
  extensionMenuRoot = await openExtensionContextMenu("#longtext");

  // Check some menu items
  items = extensionMenuRoot.getElementsByAttribute(
    "label",
    "selection is: 'Sed ut perspiciatis unde omnis iste natus error\u2026'"
  );
  is(
    items.length,
    1,
    `contextMenu item for longtext selection was found (context=selection)`
  );
  await closeExtensionContextMenu(items[0]);

  expectedClickInfo = {
    menuItemId: "ext-selection",
    pageUrl: PAGE,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);
  ok(
    result.info.selectionText.endsWith("quo voluptas nulla pariatur?"),
    "long text selection worked"
  );

  // Select a lot of text, excercise the editable element code path in
  // the Browser:GetSelection handler.
  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
    let doc = content.document;
    let node = doc.getElementById("editabletext");
    // content.js handleContentContextMenu fails intermittently without focus.
    node.focus();
    node.selectionStart = 0;
    node.selectionEnd = 844;
  });

  // Bring up context menu again
  extensionMenuRoot = await openExtensionContextMenu("#editabletext");

  // Check some menu items
  items = extensionMenuRoot.getElementsByAttribute("label", "editable");
  is(
    items.length,
    1,
    "contextMenu item for text input element was found (context=editable)"
  );
  await closeExtensionContextMenu(items[0]);

  expectedClickInfo = {
    menuItemId: "ext-editable",
    editable: true,
    pageUrl: PAGE,
  };

  result = await extension.awaitMessage("onclick");
  checkClickInfo(result);
  result = await extension.awaitMessage("browser.contextMenus.onClicked");
  checkClickInfo(result);
  ok(
    result.info.selectionText.endsWith(
      "perferendis doloribus asperiores repellat."
    ),
    "long text selection worked"
  );

  extension.sendMessage("removeall");
  await extension.awaitMessage("removed");

  let contentAreaContextMenu = await openContextMenu("#img1");
  items = contentAreaContextMenu.getElementsByAttribute(
    "ext-type",
    "top-level-menu"
  );
  is(items.length, 0, "top level item was not found (after removeAll()");
  await closeContextMenu();

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

add_task(async function testRemoveAllWithTwoExtensions() {
  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
  const manifest = { permissions: ["contextMenus"] };

  const first = ExtensionTestUtils.loadExtension({
    manifest,
    background() {
      browser.contextMenus.create({ title: "alpha", contexts: ["all"] });

      browser.contextMenus.onClicked.addListener(() => {
        browser.contextMenus.removeAll();
      });
      browser.test.onMessage.addListener(msg => {
        if (msg == "ping") {
          browser.test.sendMessage("pong-alpha");
          return;
        }
        browser.contextMenus.create({ title: "gamma", contexts: ["all"] });
      });
    },
  });

  const second = ExtensionTestUtils.loadExtension({
    manifest,
    background() {
      browser.contextMenus.create({ title: "beta", contexts: ["all"] });

      browser.contextMenus.onClicked.addListener(() => {
        browser.contextMenus.removeAll();
      });

      browser.test.onMessage.addListener(() => {
        browser.test.sendMessage("pong-beta");
      });
    },
  });

  await first.startup();
  await second.startup();

  async function confirmMenuItems(...items) {
    // Round-trip to extension to make sure that the context menu state has been
    // updated by the async contextMenus.create / contextMenus.removeAll calls.
    first.sendMessage("ping");
    second.sendMessage("ping");
    await first.awaitMessage("pong-alpha");
    await second.awaitMessage("pong-beta");

    const menu = await openContextMenu();
    for (const id of ["alpha", "beta", "gamma"]) {
      const expected = items.includes(id);
      const found = menu.getElementsByAttribute("label", id);
      is(
        !!found.length,
        expected,
        `menu item ${id} ${expected ? "" : "not "}found`
      );
    }
    // Return the first menu item, we need to click it.
    return menu.getElementsByAttribute("label", items[0])[0];
  }

  // Confirm alpha, beta exist; click alpha to remove it.
  const alpha = await confirmMenuItems("alpha", "beta");
  await closeExtensionContextMenu(alpha);

  // Confirm only beta exists.
  await confirmMenuItems("beta");
  await closeContextMenu();

  // Create gamma, confirm, click.
  first.sendMessage("create");
  const beta = await confirmMenuItems("beta", "gamma");
  await closeExtensionContextMenu(beta);

  // Confirm only gamma is left.
  await confirmMenuItems("gamma");
  await closeContextMenu();

  await first.unload();
  await second.unload();
  BrowserTestUtils.removeTab(tab);
});

function bookmarkContextMenuExtension() {
  return ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus", "bookmarks", "activeTab"],
    },
    async background() {
      const url = "https://example.com/";
      const title = "Example";
      let newBookmark = await browser.bookmarks.create({
        url,
        title,
        parentId: "toolbar_____",
      });
      browser.contextMenus.onClicked.addListener(async (info, tab) => {
        browser.test.assertEq(
          undefined,
          tab,
          "click event in bookmarks menu is not associated with any tab"
        );
        browser.test.assertEq(
          newBookmark.id,
          info.bookmarkId,
          "Bookmark ID matches"
        );

        await browser.test.assertRejects(
          browser.tabs.executeScript({ code: "'some code';" }),
          /Missing host permission for the tab/,
          "Content script should not run, activeTab should not be granted to bookmark menu events"
        );

        let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
        browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
        browser.test.assertEq(url, bookmark.url, "Bookmark url matches");
        browser.test.assertFalse(
          info.hasOwnProperty("pageUrl"),
          "Context menu does not expose pageUrl"
        );
        await browser.bookmarks.remove(info.bookmarkId);
        browser.test.sendMessage("test-finish");
      });
      browser.contextMenus.create(
        {
          title: "Get bookmark",
          contexts: ["bookmark"],
        },
        () => {
          browser.test.sendMessage("bookmark-created", newBookmark.id);
        }
      );
    },
  });
}

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

  await toggleBookmarksToolbar(true);

  const extension = bookmarkContextMenuExtension();

  await extension.startup();
  await extension.awaitMessage("bookmark-created");
  let menu = await openChromeContextMenu(
    "placesContext",
    "#PersonalToolbar .bookmark-item:last-child"
  );

  let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
  closeChromeContextMenu("placesContext", menuItem);

  await extension.awaitMessage("test-finish");
  await extension.unload();
  await toggleBookmarksToolbar(false);

  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_bookmark_sidebar_contextmenu() {
  await withSidebarTree("bookmarks", async tree => {
    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);

    let extension = bookmarkContextMenuExtension();
    await extension.startup();
    let bookmarkGuid = await extension.awaitMessage("bookmark-created");

    let sidebar = window.SidebarController.browser;
    let menu = sidebar.contentDocument.getElementById("placesContext");
    tree.selectItems([bookmarkGuid]);
    let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
    synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
    await shown;

    let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
    closeChromeContextMenu("placesContext", menuItem, sidebar.contentWindow);
    await extension.awaitMessage("test-finish");
    await extension.unload();

    BrowserTestUtils.removeTab(tab);
  });
});

function bookmarkFolderContextMenuExtension() {
  return ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus", "bookmarks"],
    },
    async background() {
      const title = "Example";
      let newBookmark = await browser.bookmarks.create({
        title,
        parentId: "toolbar_____",
      });
      await new Promise(resolve =>
        browser.contextMenus.create(
          {
            title: "Get bookmark",
            contexts: ["bookmark"],
          },
          resolve
        )
      );
      browser.contextMenus.onClicked.addListener(async info => {
        browser.test.assertEq(
          newBookmark.id,
          info.bookmarkId,
          "Bookmark ID matches"
        );

        let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
        browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
        browser.test.assertFalse(
          info.hasOwnProperty("pageUrl"),
          "Context menu does not expose pageUrl"
        );
        await browser.bookmarks.remove(info.bookmarkId);
        browser.test.sendMessage("test-finish");
      });
      browser.test.sendMessage("bookmark-created", newBookmark.id);
    },
  });
}

add_task(async function test_organizer_contextmenu() {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
  let library = await promiseLibrary("BookmarksToolbar");

  let menu = library.document.getElementById("placesContext");
  let mainTree = library.document.getElementById("placeContent");
  let leftTree = library.document.getElementById("placesList");

  let tests = [
    [mainTree, bookmarkContextMenuExtension],
    [mainTree, bookmarkFolderContextMenuExtension],
    [leftTree, bookmarkFolderContextMenuExtension],
  ];

  for (let [tree, makeExtension] of tests) {
    let extension = makeExtension();
    await extension.startup();
    let bookmarkGuid = await extension.awaitMessage("bookmark-created");

    tree.selectItems([bookmarkGuid]);
    let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
    synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
    await shown;

    let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
    closeChromeContextMenu("placesContext", menuItem, library);
    await extension.awaitMessage("test-finish");
    await extension.unload();
  }

  await promiseLibraryClosed(library);
  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_bookmark_context_requires_permission() {
  await toggleBookmarksToolbar(true);

  const extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["contextMenus"],
    },
    background() {
      browser.contextMenus.create(
        {
          title: "Get bookmark",
          contexts: ["bookmark"],
        },
        () => {
          browser.test.sendMessage("bookmark-created");
        }
      );
    },
  });
  await extension.startup();
  await extension.awaitMessage("bookmark-created");
  let menu = await openChromeContextMenu(
    "placesContext",
    "#PersonalToolbar .bookmark-item:last-child"
  );

  Assert.equal(
    menu.getElementsByAttribute("label", "Get bookmark").length,
    0,
    "bookmark context menu not created with `bookmarks` permission."
  );

  closeChromeContextMenu("placesContext");

  await extension.unload();
  await toggleBookmarksToolbar(false);
});
