/* -*- 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.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 ContentTask.spawn(gBrowser.selectedBrowser, { }, async function(arg) {
    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 ContentTask.spawn(gBrowser.selectedBrowser, { }, function* (arg) {
    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 ContentTask.spawn(gBrowser.selectedBrowser, { }, function(arg) {
    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.SidebarUI.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);
});
