/* eslint-disable mozilla/no-arbitrary-setTimeout */
ChromeUtils.import("resource://gre/modules/Timer.jsm", this);

const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s.";
// Using 'javascript' schema to bypass E10SUtils.canLoadURIInRemoteType, because
// it does not allow 'data:' URI to be loaded in the parent process.
const E10S_PARENT_TEST_PAGE_URI =
  getRootDirectory(gTestPath) + "file_empty.html";
const TEST_PAGE_URI_WITHIFRAME =
  "https://example.com/browser/toolkit/content/tests/browser/file_findinframe.html";

/**
 * Makes sure that the findbar hotkeys (' and /) event listeners
 * are added to the system event group and do not get blocked
 * by calling stopPropagation on a keypress event on a page.
 */
add_task(async function test_hotkey_event_propagation() {
  info("Ensure hotkeys are not affected by stopPropagation.");

  // Opening new tab
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );
  let browser = gBrowser.getBrowserForTab(tab);
  let findbar = await gBrowser.getFindBar();

  // Pressing these keys open the findbar.
  const HOTKEYS = ["/", "'"];

  // Checking if findbar appears when any hotkey is pressed.
  for (let key of HOTKEYS) {
    is(findbar.hidden, true, "Findbar is hidden now.");
    gBrowser.selectedTab = tab;
    await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
    await BrowserTestUtils.sendChar(key, browser);
    is(findbar.hidden, false, "Findbar should not be hidden.");
    await closeFindbarAndWait(findbar);
  }

  // Stop propagation for all keyboard events.
  await SpecialPowers.spawn(browser, [], () => {
    const stopPropagation = e => {
      e.stopImmediatePropagation();
    };
    let window = content.document.defaultView;
    window.addEventListener("keydown", stopPropagation);
    window.addEventListener("keypress", stopPropagation);
    window.addEventListener("keyup", stopPropagation);
  });

  // Checking if findbar still appears when any hotkey is pressed.
  for (let key of HOTKEYS) {
    is(findbar.hidden, true, "Findbar is hidden now.");
    gBrowser.selectedTab = tab;
    await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
    await BrowserTestUtils.sendChar(key, browser);
    is(findbar.hidden, false, "Findbar should not be hidden.");
    await closeFindbarAndWait(findbar);
  }

  gBrowser.removeTab(tab);
});

add_task(async function test_not_found() {
  info("Check correct 'Phrase not found' on new tab");

  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );

  // Search for the first word.
  await promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false);
  let findbar = gBrowser.getCachedFindBar();
  is(
    findbar._findStatusDesc.textContent,
    findbar._notFoundStr,
    "Findbar status text should be 'Phrase not found'"
  );

  gBrowser.removeTab(tab);
});

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

  // Search for a string that WILL be found, with 'Highlight All' on
  await promiseFindFinished("S", true);
  ok(
    !gBrowser.getCachedFindBar()._findStatusDesc.textContent,
    "Findbar status should be empty"
  );

  gBrowser.removeTab(tab);
});

// Setting first findbar to case-sensitive mode should not affect
// new tab find bar.
add_task(async function test_tabwise_case_sensitive() {
  let tab1 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );
  let findbar1 = await gBrowser.getFindBar();

  let tab2 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );
  let findbar2 = await gBrowser.getFindBar();

  // Toggle case sensitivity for first findbar
  findbar1.getElement("find-case-sensitive").click();

  gBrowser.selectedTab = tab1;

  // Not found for first tab.
  await promiseFindFinished("S", true);
  is(
    findbar1._findStatusDesc.textContent,
    findbar1._notFoundStr,
    "Findbar status text should be 'Phrase not found'"
  );

  gBrowser.selectedTab = tab2;

  // But it didn't affect the second findbar.
  await promiseFindFinished("S", true);
  ok(!findbar2._findStatusDesc.textContent, "Findbar status should be empty");

  gBrowser.removeTab(tab1);
  gBrowser.removeTab(tab2);
});

/**
 * Navigating from a web page (for example mozilla.org) to an internal page
 * (like about:addons) might trigger a change of browser's remoteness.
 * 'Remoteness change' means that rendering page content moves from child
 * process into the parent process or the other way around.
 * This test ensures that findbar properly handles such a change.
 */
add_task(async function test_reinitialization_at_remoteness_change() {
  // This test only makes sence in e10s evironment.
  if (!gMultiProcessBrowser) {
    info("Skipping this test because of non-e10s environment.");
    return;
  }

  info("Ensure findbar re-initialization at remoteness change.");

  // Load a remote page and trigger findbar construction.
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );
  let browser = gBrowser.getBrowserForTab(tab);
  let findbar = await gBrowser.getFindBar();

  // Findbar should operate normally.
  await promiseFindFinished("z", false);
  is(
    findbar._findStatusDesc.textContent,
    findbar._notFoundStr,
    "Findbar status text should be 'Phrase not found'"
  );

  await promiseFindFinished("s", false);
  ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty");

  // Moving browser into the parent process and reloading sample data.
  ok(browser.isRemoteBrowser, "Browser should be remote now.");
  await promiseRemotenessChange(tab, false);
  let docLoaded = BrowserTestUtils.browserLoaded(
    browser,
    false,
    E10S_PARENT_TEST_PAGE_URI
  );
  BrowserTestUtils.loadURI(browser, E10S_PARENT_TEST_PAGE_URI);
  await docLoaded;
  ok(!browser.isRemoteBrowser, "Browser should not be remote any more.");
  browser.contentDocument.body.append("The letter s.");
  browser.contentDocument.body.clientHeight; // Force flush.

  // Findbar should keep operating normally after remoteness change.
  await promiseFindFinished("z", false);
  is(
    findbar._findStatusDesc.textContent,
    findbar._notFoundStr,
    "Findbar status text should be 'Phrase not found'"
  );

  await promiseFindFinished("s", false);
  ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty");

  BrowserTestUtils.removeTab(tab);
});

/**
 * Ensure that the initial typed characters aren't lost immediately after
 * opening the find bar.
 */
add_task(async function e10sLostKeys() {
  // This test only makes sence in e10s evironment.
  if (!gMultiProcessBrowser) {
    info("Skipping this test because of non-e10s environment.");
    return;
  }

  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI
  );

  ok(!gFindBarInitialized, "findbar isn't initialized yet");

  await gFindBarPromise;
  let findBar = gFindBar;
  let initialValue = findBar._findField.value;

  await EventUtils.synthesizeAndWaitKey(
    "F",
    { accelKey: true },
    window,
    null,
    () => {
      // We can't afford to wait for the promise to resolve, by then the
      // find bar is visible and focused, so sending characters to the
      // content browser wouldn't work.
      isnot(
        document.activeElement,
        findBar._findField,
        "findbar is not yet focused"
      );
      EventUtils.synthesizeKey("a");
      EventUtils.synthesizeKey("b");
      EventUtils.synthesizeKey("c");
      is(
        findBar._findField.value,
        initialValue,
        "still has initial find query"
      );
    }
  );

  await BrowserTestUtils.waitForCondition(
    () => findBar._findField.value.length == 3
  );
  is(document.activeElement, findBar._findField, "findbar is now focused");
  is(findBar._findField.value, "abc", "abc fully entered as find query");

  BrowserTestUtils.removeTab(tab);
});

/**
 * This test makes sure that keyboard operations still occur
 * after the findbar is opened and closed.
 */
add_task(async function test_open_and_close_keys() {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "data:text/html,<body style='height: 5000px;'>Hello There</body>"
  );

  await gFindBarPromise;
  let findBar = gFindBar;

  is(findBar.hidden, true, "Findbar is hidden now.");
  let openedPromise = BrowserTestUtils.waitForEvent(findBar, "findbaropen");
  await EventUtils.synthesizeKey("f", { accelKey: true });
  await openedPromise;

  is(findBar.hidden, false, "Findbar should not be hidden.");

  let closedPromise = BrowserTestUtils.waitForEvent(findBar, "findbarclose");
  await EventUtils.synthesizeKey("KEY_Escape");
  await closedPromise;

  let scrollPromise = BrowserTestUtils.waitForContentEvent(
    tab.linkedBrowser,
    "scroll"
  );
  await EventUtils.synthesizeKey("KEY_ArrowDown");
  await scrollPromise;

  let scrollPosition = await SpecialPowers.spawn(
    tab.linkedBrowser,
    [],
    async function() {
      return content.document.body.scrollTop;
    }
  );

  ok(scrollPosition > 0, "Scrolled ok to " + scrollPosition);

  BrowserTestUtils.removeTab(tab);
});

// This test loads an editable area within an iframe and then
// performs a search. Focusing the editable area should still
// allow keyboard events to be received.
add_task(async function test_hotkey_insubframe() {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_PAGE_URI_WITHIFRAME
  );

  await gFindBarPromise;
  let findBar = gFindBar;

  // Focus the editable area within the frame.
  let browser = gBrowser.selectedBrowser;
  let frameBC = browser.browsingContext.children[0];
  await SpecialPowers.spawn(frameBC, [], async () => {
    content.document.body.focus();
    content.document.defaultView.focus();
  });

  // Start a find and wait for the findbar to open.
  let findBarOpenPromise = BrowserTestUtils.waitForEvent(
    gBrowser,
    "findbaropen"
  );
  EventUtils.synthesizeKey("f", { accelKey: true });
  await findBarOpenPromise;

  // Opening the findbar would have focused the find textbox.
  // Focus the editable area again.
  let cursorPos = await SpecialPowers.spawn(frameBC, [], async () => {
    content.document.body.focus();
    content.document.defaultView.focus();
    return content.getSelection().anchorOffset;
  });
  is(cursorPos, 0, "initial cursor position");

  // Try moving the caret.
  await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, frameBC);

  cursorPos = await SpecialPowers.spawn(frameBC, [], async () => {
    return content.getSelection().anchorOffset;
  });
  is(cursorPos, 1, "cursor moved");

  await closeFindbarAndWait(findBar);
  gBrowser.removeTab(tab);
});

/**
 * Reloading a page should use the same match case / whole word
 * state for the search.
 */
add_task(async function test_preservestate_on_reload() {
  for (let stateChange of ["case-sensitive", "entire-word"]) {
    let tab = await BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      "data:text/html,<p>There is a cat named Theo in the kitchen with another cat named Catherine. The two of them are thirsty."
    );

    // Start a find and wait for the findbar to open.
    let findBarOpenPromise = BrowserTestUtils.waitForEvent(
      gBrowser,
      "findbaropen"
    );
    EventUtils.synthesizeKey("f", { accelKey: true });
    await findBarOpenPromise;

    let isEntireWord = stateChange == "entire-word";

    let findbar = await gBrowser.getFindBar();

    // Find some text.
    let promiseMatches = promiseGetMatchCount(findbar);
    await promiseFindFinished("The", true);

    let matches = await promiseMatches;
    is(matches.current, 1, "Correct match position " + stateChange);
    is(matches.total, 7, "Correct number of matches " + stateChange);

    // Turn on the case sensitive or entire word option.
    findbar.getElement("find-" + stateChange).click();

    promiseMatches = promiseGetMatchCount(findbar);
    gFindBar.onFindAgainCommand();
    matches = await promiseMatches;
    is(
      matches.current,
      2,
      "Correct match position after state change matches " + stateChange
    );
    is(
      matches.total,
      isEntireWord ? 2 : 3,
      "Correct number after state change matches " + stateChange
    );

    // Reload the page.
    let loadedPromise = BrowserTestUtils.browserLoaded(
      gBrowser.selectedBrowser,
      true
    );
    gBrowser.reload();
    await loadedPromise;

    // Perform a find again. The state should be preserved.
    promiseMatches = promiseGetMatchCount(findbar);
    gFindBar.onFindAgainCommand();
    matches = await promiseMatches;
    is(
      matches.current,
      1,
      "Correct match position after reload and find again " + stateChange
    );
    is(
      matches.total,
      isEntireWord ? 2 : 3,
      "Correct number of matches after reload and find again " + stateChange
    );

    // Turn off the case sensitive or entire word option and find again.
    findbar.getElement("find-" + stateChange).click();

    promiseMatches = promiseGetMatchCount(findbar);
    gFindBar.onFindAgainCommand();
    matches = await promiseMatches;

    is(
      matches.current,
      isEntireWord ? 4 : 2,
      "Correct match position after reload and find again reset " + stateChange
    );
    is(
      matches.total,
      7,
      "Correct number of matches after reload and find again reset " +
        stateChange
    );

    findbar.clear();
    await closeFindbarAndWait(findbar);

    gBrowser.removeTab(tab);
  }
});

async function promiseFindFinished(searchText, highlightOn) {
  let findbar = await gBrowser.getFindBar();
  findbar.startFind(findbar.FIND_NORMAL);
  let highlightElement = findbar.getElement("highlight");
  if (highlightElement.checked != highlightOn) {
    highlightElement.click();
  }
  return new Promise(resolve => {
    executeSoon(() => {
      findbar._findField.value = searchText;

      let resultListener;
      // When highlighting is on the finder sends a second "FOUND" message after
      // the search wraps. This causes timing problems with e10s. waitMore
      // forces foundOrTimeout wait for the second "FOUND" message before
      // resolving the promise.
      let waitMore = highlightOn;
      let findTimeout = setTimeout(() => foundOrTimedout(null), 2000);
      let foundOrTimedout = function(aData) {
        if (aData !== null && waitMore) {
          waitMore = false;
          return;
        }
        if (aData === null) {
          info("Result listener not called, timeout reached.");
        }
        clearTimeout(findTimeout);
        findbar.browser.finder.removeResultListener(resultListener);
        resolve();
      };

      resultListener = {
        onFindResult: foundOrTimedout,
        onCurrentSelection() {},
        onMatchesCountResult() {},
        onHighlightFinished() {},
      };
      findbar.browser.finder.addResultListener(resultListener);
      findbar._find();
    });
  });
}

function promiseGetMatchCount(findbar) {
  return new Promise(resolve => {
    let resultListener = {
      onFindResult() {},
      onCurrentSelection() {},
      onHighlightFinished() {},
      onMatchesCountResult(response) {
        if (response.total > 0) {
          findbar.browser.finder.removeResultListener(resultListener);
          resolve(response);
        }
      },
    };
    findbar.browser.finder.addResultListener(resultListener);
  });
}

function promiseRemotenessChange(tab, shouldBeRemote) {
  return new Promise(resolve => {
    let browser = gBrowser.getBrowserForTab(tab);
    tab.addEventListener(
      "TabRemotenessChange",
      function() {
        resolve();
      },
      { once: true }
    );
    let remoteType = shouldBeRemote
      ? E10SUtils.DEFAULT_REMOTE_TYPE
      : E10SUtils.NOT_REMOTE;
    gBrowser.updateBrowserRemoteness(browser, { remoteType });
  });
}
