/* global browser */
"use strict";

function frameScript() {
  let docShell = content.docShell;
  let controller = docShell
    .QueryInterface(Ci.nsIInterfaceRequestor)
    .getInterface(Ci.nsISelectionDisplay)
    .QueryInterface(Ci.nsISelectionController);
  let selection = controller.getSelection(controller.SELECTION_FIND);
  if (!selection.rangeCount) {
    return {
      text: "",
    };
  }

  let range = selection.getRangeAt(0);
  const { FindContent } = ChromeUtils.importESModule(
    "resource://gre/modules/FindContent.sys.mjs"
  );
  let highlighter = new FindContent(docShell).highlighter;
  let r1 = content.parent.frameElement.getBoundingClientRect();
  let f1 = highlighter._getFrameElementOffsets(content.parent);
  let r2 = content.frameElement.getBoundingClientRect();
  let f2 = highlighter._getFrameElementOffsets(content);
  let r3 = range.getBoundingClientRect();
  let rect = {
    top: r1.top + r2.top + r3.top + f1.y + f2.y,
    left: r1.left + r2.left + r3.left + f1.x + f2.x,
  };
  return {
    text: selection.toString(),
    rect,
  };
}

add_task(async function testFind() {
  async function background() {
    function awaitLoad(tabId, url) {
      return new Promise(resolve => {
        browser.tabs.onUpdated.addListener(function listener(
          tabId_,
          changed,
          tab
        ) {
          if (
            tabId == tabId_ &&
            changed.status == "complete" &&
            tab.url == url
          ) {
            browser.tabs.onUpdated.removeListener(listener);
            resolve();
          }
        });
      });
    }

    let url =
      "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
    let tab = await browser.tabs.update({ url });
    await awaitLoad(tab.id, url);

    let data = await browser.find.find("banana", { includeRangeData: true });
    let rangeData = data.rangeData;

    browser.test.log("Test that `data.count` is the expected value.");
    browser.test.assertEq(
      6,
      data.count,
      "The value returned from `data.count`"
    );

    browser.test.log("Test that `rangeData` has the proper number of values.");
    browser.test.assertEq(
      6,
      rangeData.length,
      "The number of values held in `rangeData`"
    );

    browser.test.log(
      "Test that the text found in the top window and nested frames corresponds to the proper position."
    );
    let terms = ["Bánana", "bAnana", "baNana", "banAna", "banaNa", "bananA"];
    for (let i = 0; i < terms.length; i++) {
      browser.test.assertEq(
        terms[i],
        rangeData[i].text,
        `The text at range position ${i}:`
      );
    }

    browser.test.log("Test that case sensitive match works properly.");
    data = await browser.find.find("baNana", {
      caseSensitive: true,
      includeRangeData: true,
    });
    browser.test.assertEq(1, data.count, "The number of matches found:");
    browser.test.assertEq("baNana", data.rangeData[0].text, "The text found:");

    browser.test.log("Test that diacritic sensitive match works properly.");
    data = await browser.find.find("bánana", {
      matchDiacritics: true,
      includeRangeData: true,
    });
    browser.test.assertEq(1, data.count, "The number of matches found:");
    browser.test.assertEq("Bánana", data.rangeData[0].text, "The text found:");

    browser.test.log("Test that case insensitive match works properly.");
    data = await browser.find.find("banana", { caseSensitive: false });
    browser.test.assertEq(6, data.count, "The number of matches found:");

    browser.test.log("Test that entire word match works properly.");
    data = await browser.find.find("banana", { entireWord: true });
    browser.test.assertEq(
      4,
      data.count,
      'The number of matches found, should skip 2 matches, "banaNaland" and "bananAland":'
    );

    let expectedRangeData = [
      {
        framePos: 0,
        text: "example",
        startTextNodePos: 16,
        startOffset: 11,
        endTextNodePos: 16,
        endOffset: 18,
      },
      {
        framePos: 0,
        text: "example",
        startTextNodePos: 16,
        startOffset: 25,
        endTextNodePos: 16,
        endOffset: 32,
      },
      {
        framePos: 0,
        text: "example",
        startTextNodePos: 19,
        startOffset: 6,
        endTextNodePos: 19,
        endOffset: 13,
      },
      {
        framePos: 0,
        text: "example",
        startTextNodePos: 21,
        startOffset: 3,
        endTextNodePos: 21,
        endOffset: 10,
      },
      {
        framePos: 1,
        text: "example",
        startTextNodePos: 0,
        startOffset: 0,
        endTextNodePos: 0,
        endOffset: 7,
      },
      {
        framePos: 2,
        text: "example",
        startTextNodePos: 0,
        startOffset: 0,
        endTextNodePos: 0,
        endOffset: 7,
      },
    ];

    browser.test.log(
      "Test that word found in the same node, different nodes and different frames returns the correct rangeData results."
    );
    data = await browser.find.find("example", { includeRangeData: true });
    for (let i = 0; i < data.rangeData.length; i++) {
      for (let name in data.rangeData[i]) {
        browser.test.assertEq(
          expectedRangeData[i][name],
          data.rangeData[i][name],
          `rangeData[${i}].${name}:`
        );
      }
    }

    browser.test.log(
      "Test that `rangeData` is not returned if `includeRangeData` is false."
    );
    data = await browser.find.find("banana", {
      caseSensitive: false,
      includeRangeData: false,
    });
    browser.test.assertEq(
      false,
      !!data.rangeData,
      "The boolean cast value of `rangeData`:"
    );

    browser.test.log(
      "Test that `rectData` is not returned if `includeRectData` is false."
    );
    data = await browser.find.find("banana", {
      caseSensitive: false,
      includeRectData: false,
    });
    browser.test.assertEq(
      false,
      !!data.rectData,
      "The boolean cast value of `rectData`:"
    );

    browser.test.log(
      "Test that text spanning multiple inline elements is found."
    );
    data = await browser.find.find("fruitcake");
    browser.test.assertEq(1, data.count, "The number of matches found:");

    browser.test.log(
      "Test that text spanning multiple block elements is not found."
    );
    data = await browser.find.find("angelfood");
    browser.test.assertEq(0, data.count, "The number of matches found:");

    browser.test.log(
      "Test that `highlightResults` returns proper status code."
    );
    await browser.find.find("banana");

    await browser.test.assertRejects(
      browser.find.highlightResults({ rangeIndex: 6 }),
      /index supplied was out of range/,
      "rejected Promise should pass the expected error"
    );

    data = await browser.find.find("xyz");
    await browser.test.assertRejects(
      browser.find.highlightResults({ rangeIndex: 0 }),
      /no search results to highlight/,
      "rejected Promise should pass the expected error"
    );

    // Test highlightResults without any arguments, especially `rangeIndex`.
    data = await browser.find.find("example");
    browser.test.assertEq(6, data.count, "The number of matches found:");
    await browser.find.highlightResults();

    await browser.find.removeHighlighting();

    data = await browser.find.find("banana", { includeRectData: true });
    await browser.find.highlightResults({ rangeIndex: 5 });

    browser.test.sendMessage("test:find:WebExtensionFinished", data.rectData);
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["find", "tabs"],
    },
    background,
  });

  await extension.startup();
  let rectData = await extension.awaitMessage("test:find:WebExtensionFinished");
  let { top, left } = rectData[5].rectsAndTexts.rectList[0];
  await extension.unload();

  let subFrameBrowsingContext =
    gBrowser.selectedBrowser.browsingContext.children[0].children[1];
  let result = await SpecialPowers.spawn(
    subFrameBrowsingContext,
    [],
    frameScript
  );

  info("Test that text was highlighted properly.");
  is(
    result.text,
    "bananA",
    `The text that was highlighted: - Expected: bananA, Actual: ${result.text}`
  );

  info(
    "Test that rectangle data returned from the search matches the highlighted result."
  );
  is(
    result.rect.top,
    top,
    `rect.top: - Expected: ${result.rect.top}, Actual: ${top}`
  );
  is(
    result.rect.left,
    left,
    `rect.left: - Expected: ${result.rect.left}, Actual: ${left}`
  );
});

add_task(async function testRemoveHighlighting() {
  async function background() {
    function awaitLoad(tabId, url) {
      return new Promise(resolve => {
        browser.tabs.onUpdated.addListener(function listener(
          tabId_,
          changed,
          tab
        ) {
          if (
            tabId == tabId_ &&
            changed.status == "complete" &&
            tab.url == url
          ) {
            browser.tabs.onUpdated.removeListener(listener);
            resolve();
          }
        });
      });
    }

    let url =
      "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
    let tab = await browser.tabs.update({ url });
    await awaitLoad(tab.id, url);

    let data = await browser.find.find("banana", { includeRangeData: true });

    browser.test.log("Test that `data.count` is the expected value.");
    browser.test.assertEq(
      6,
      data.count,
      "The value returned from `data.count`"
    );

    await browser.find.highlightResults({ rangeIndex: 5 });

    browser.find.removeHighlighting();

    browser.test.sendMessage("test:find:WebExtensionFinished");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["find", "tabs"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitMessage("test:find:WebExtensionFinished");
  await extension.unload();

  let subFrameBrowsingContext =
    gBrowser.selectedBrowser.browsingContext.children[0].children[1];
  let result = await SpecialPowers.spawn(
    subFrameBrowsingContext,
    [],
    frameScript
  );

  info("Test that highlight was cleared properly.");
  is(
    result.text,
    "",
    `The text that was highlighted: - Expected: '', Actual: ${result.text}`
  );
});

add_task(async function testAboutFind() {
  async function background() {
    await browser.test.assertRejects(
      browser.find.find("banana"),
      /Unable to search:/,
      "Should not be able to search about tabs"
    );

    browser.test.sendMessage("done");
  }

  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["find", "tabs"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitMessage("done");
  await extension.unload();
  BrowserTestUtils.removeTab(tab);
});

add_task(async function testIncognitoFind() {
  async function background() {
    await browser.test.assertRejects(
      browser.find.find("banana"),
      /Unable to search:/,
      "Should not be able to search private window"
    );
    await browser.test.assertRejects(
      browser.find.highlightResults(),
      /Unable to search:/,
      "Should not be able to highlight in private window"
    );
    await browser.test.assertRejects(
      browser.find.removeHighlighting(),
      /Invalid tab ID:/,
      "Should not be able to remove highlight in private window"
    );

    browser.test.sendMessage("done");
  }

  let privateWin = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  BrowserTestUtils.loadURIString(
    privateWin.gBrowser.selectedBrowser,
    "http://example.com"
  );
  await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["find", "tabs"],
    },
    background,
  });

  await extension.startup();
  await extension.awaitMessage("done");
  await extension.unload();
  await BrowserTestUtils.closeWindow(privateWin);
});

add_task(async function testIncognitoFindAllowed() {
  // We're only testing we can make the calls in a private window,
  // testFind above tests full functionality.
  async function background() {
    await browser.find.find("banana");
    await browser.find.highlightResults({ rangeIndex: 0 });
    await browser.find.removeHighlighting();

    browser.test.sendMessage("done");
  }

  let url =
    "http://example.com/browser/browser/components/extensions/test/browser/file_find_frames.html";
  let privateWin = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  BrowserTestUtils.loadURIString(privateWin.gBrowser.selectedBrowser, url);
  await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["find", "tabs"],
    },
    background,
    incognitoOverride: "spanning",
  });

  await extension.startup();
  await extension.awaitMessage("done");
  await extension.unload();
  await BrowserTestUtils.closeWindow(privateWin);
});
