// Tests that the suggestion popup appears at the right times in response to
// focus and user events (mouse, keyboard, drop).

const searchPopup = document.getElementById("PopupSearchAutoComplete");
const kValues = ["long text", "long text 2", "long text 3"];

async function endCustomizing(aWindow = window) {
  if (aWindow.document.documentElement.getAttribute("customizing") != "true") {
    return true;
  }
  let eventPromise = BrowserTestUtils.waitForEvent(
    aWindow.gNavToolbox,
    "aftercustomization"
  );
  aWindow.gCustomizeMode.exit();
  return eventPromise;
}

async function startCustomizing(aWindow = window) {
  if (aWindow.document.documentElement.getAttribute("customizing") == "true") {
    return true;
  }
  let eventPromise = BrowserTestUtils.waitForEvent(
    aWindow.gNavToolbox,
    "customizationready"
  );
  aWindow.gCustomizeMode.enter();
  return eventPromise;
}

let searchbar;
let textbox;
let searchIcon;
let goButton;
let engine;

add_setup(async function () {
  searchbar = await gCUITestUtils.addSearchBar();
  textbox = searchbar.textbox;
  searchIcon = searchbar.querySelector(".searchbar-search-button");
  goButton = searchbar.querySelector(".search-go-button");

  engine = await SearchTestUtils.promiseNewSearchEngine({
    url: getRootDirectory(gTestPath) + "testEngine.xml",
    setAsDefault: true,
  });

  await clearSearchbarHistory();

  let addOps = kValues.map(value => {
    return { op: "add", fieldname: "searchbar-history", value };
  });
  info("adding search history values: " + kValues);
  await FormHistory.update(addOps);

  registerCleanupFunction(async () => {
    await clearSearchbarHistory();
    gCUITestUtils.removeSearchBar();
  });
});

// Adds a task that shouldn't show the search suggestions popup.
function add_no_popup_task(task) {
  add_task(async function () {
    let sawPopup = false;
    function listener() {
      sawPopup = true;
    }

    info("Entering test " + task.name);
    searchPopup.addEventListener("popupshowing", listener);
    await task();
    searchPopup.removeEventListener("popupshowing", listener);
    ok(!sawPopup, "Shouldn't have seen the suggestions popup");
    info("Leaving test " + task.name);
  });
}

// Simulates the full set of events for a context click
function context_click(target) {
  for (let event of ["mousedown", "contextmenu"]) {
    EventUtils.synthesizeMouseAtCenter(target, { type: event, button: 2 });
  }
}

// Right clicking the icon should not open the popup.
add_no_popup_task(async function open_icon_context() {
  gURLBar.focus();
  let toolbarPopup = document.getElementById("toolbar-context-menu");

  let promise = promiseEvent(toolbarPopup, "popupshown");
  context_click(searchIcon);
  await promise;

  promise = promiseEvent(toolbarPopup, "popuphidden");
  toolbarPopup.hidePopup();
  await promise;
});

// With no text in the search box left clicking the icon should open the popup.
// Clicking the icon again should hide the popup and not show it again.
add_task(async function open_empty() {
  gURLBar.focus();

  let promise = promiseEvent(searchPopup, "popupshown");
  info("Clicking icon");
  is(
    searchIcon.getAttribute("aria-expanded"),
    "false",
    "The search icon is not expanded by default"
  );
  EventUtils.synthesizeMouseAtCenter(searchIcon, {});
  await promise;
  is(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should only show the settings"
  );
  is(
    searchIcon.getAttribute("aria-expanded"),
    "true",
    "The search icon is now expanded"
  );
  is(textbox.mController.searchString, "", "Should be an empty search string");

  let image = searchPopup.querySelector(".searchbar-engine-image");
  Assert.equal(
    image.src,
    await engine.getIconURL(16),
    "Should have the correct icon"
  );

  // By giving the textbox some text any next attempt to open the search popup
  // from the click handler will try to search for this text.
  textbox.value = "foo";

  promise = promiseEvent(searchPopup, "popuphidden");

  info("Hiding popup");
  await EventUtils.promiseNativeMouseEventAndWaitForEvent({
    type: "click",
    target: searchIcon,
    atCenter: true,
    eventTypeToWait: "mouseup",
  });
  await promise;

  is(
    textbox.mController.searchString,
    "",
    "Should not have started to search for the new text"
  );
  is(
    searchIcon.getAttribute("aria-expanded"),
    "false",
    "The search icon should not be expanded"
  );

  // Cancel the search if it started.
  if (textbox.mController.searchString != "") {
    textbox.mController.stopSearch();
  }

  textbox.value = "";
});

// With no text in the search box left clicking it should not open the popup.
add_no_popup_task(function click_doesnt_open_popup() {
  gURLBar.focus();

  EventUtils.synthesizeMouseAtCenter(textbox, {});
  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 0, "Should have selected all of the text");
});

// Left clicking in a non-empty search box when unfocused should focus it and open the popup.
add_task(async function click_opens_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  searchPopup.hidePopup();
  await promise;

  textbox.value = "";
});

add_task(async function open_empty_hiddenOneOffs() {
  // Disable all the engines but the current one and check the oneoffs.
  let defaultEngine = await Services.search.getDefault();
  let engines = (await Services.search.getVisibleEngines()).filter(
    e => e.name != defaultEngine.name
  );

  engines.forEach(e => {
    e.hideOneOffButton = true;
  });

  textbox.value = "foo";
  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;

  Assert.ok(
    searchPopup.searchOneOffsContainer.hasAttribute("hidden"),
    "The one-offs buttons container should have the hidden attribute."
  );
  Assert.ok(
    BrowserTestUtils.isHidden(searchPopup.searchOneOffsContainer),
    "The one-off buttons container should be hidden."
  );

  promise = promiseEvent(searchPopup, "popuphidden");

  info("Hiding popup");
  await EventUtils.promiseNativeMouseEventAndWaitForEvent({
    type: "click",
    target: searchIcon,
    atCenter: true,
    eventTypeToWait: "mouseup",
  });
  await promise;

  engines.forEach(e => {
    e.hideOneOffButton = false;
  });
  textbox.value = "";
});

// Right clicking in a non-empty search box when unfocused should open the edit context menu.
add_no_popup_task(async function right_click_doesnt_open_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  // Can't wait for an event on the actual menu since it is created
  // lazily the first time it is displayed.
  let promise = new Promise(resolve => {
    let listener = event => {
      if (searchbar._menupopup && event.target == searchbar._menupopup) {
        resolve(searchbar._menupopup);
      }
    };
    window.addEventListener("popupshown", listener);
  });
  context_click(textbox);
  let contextPopup = await promise;

  // Assert that the context menu click inside the popup does nothing. If it
  // opens something, assert_no_popup_task will make us fail. On macOS this
  // doesn't work because of native context menus.
  if (!navigator.platform.includes("Mac")) {
    context_click(contextPopup);
  }

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(contextPopup, "popuphidden");
  contextPopup.hidePopup();
  await promise;

  textbox.value = "";
});

// Moving focus away from the search box should close the popup
add_task(async function focus_change_closes_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  let promise2 = promiseEvent(searchbar.textbox, "blur");
  EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
  await promise;
  await promise2;

  textbox.value = "";
});

// Moving focus away from the search box should close the small popup
add_task(async function focus_change_closes_small_popup() {
  gURLBar.focus();

  let promise = promiseEvent(searchPopup, "popupshown");
  // For some reason sending the mouse event immediately doesn't open the popup.
  SimpleTest.executeSoon(() => {
    EventUtils.synthesizeMouseAtCenter(searchIcon, {});
  });
  await promise;
  is(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the small popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );

  promise = promiseEvent(searchPopup, "popuphidden");
  let promise2 = promiseEvent(searchbar.textbox, "blur");
  EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
  await promise;
  await promise2;
});

// Pressing escape should close the popup.
add_task(async function escape_closes_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("KEY_Escape");
  await promise;

  textbox.value = "";
});

// Pressing contextmenu should close the popup.
add_task(async function contextmenu_closes_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  let contextPopup = searchbar._menupopup;
  let contextMenuShownPromise = promiseEvent(contextPopup, "popupshown");
  let searchPopupHiddenPromise = promiseEvent(searchPopup, "popuphidden");
  context_click(textbox);
  await contextMenuShownPromise;
  await searchPopupHiddenPromise;

  let contextMenuHiddenPromise = promiseEvent(contextPopup, "popuphidden");
  contextPopup.hidePopup();
  await contextMenuHiddenPromise;

  textbox.value = "";
});

// Tabbing to the search box should open the popup if it contains text.
add_task(async function tab_opens_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeKey("KEY_Tab");
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  searchPopup.hidePopup();
  await promise;

  textbox.value = "";
});

// Tabbing to the search box should not open the popup if it doesn't contain text.
add_no_popup_task(function tab_doesnt_open_popup() {
  gURLBar.focus();
  textbox.value = "foo";

  EventUtils.synthesizeKey("KEY_Tab");

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  textbox.value = "";
});

// Switching back to the window when the search box has focus from mouse should not open the popup.
add_task(async function refocus_window_doesnt_open_popup_mouse() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(searchbar, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  let newWin = OpenBrowserWindow();
  await new Promise(resolve => waitForFocus(resolve, newWin));
  await promise;

  function listener() {
    ok(false, "Should not have shown the popup.");
  }
  searchPopup.addEventListener("popupshowing", listener);

  promise = promiseEvent(searchbar.textbox, "focus");
  newWin.close();
  await promise;

  // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
  await new Promise(resolve => executeSoon(resolve));
  await new Promise(resolve => executeSoon(resolve));
  await new Promise(resolve => executeSoon(resolve));

  searchPopup.removeEventListener("popupshowing", listener);
  textbox.value = "";
});

// Switching back to the window when the search box has focus from keyboard should not open the popup.
add_task(async function refocus_window_doesnt_open_popup_keyboard() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeKey("KEY_Tab");
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  let newWin = OpenBrowserWindow();
  await new Promise(resolve => waitForFocus(resolve, newWin));
  await promise;

  function listener() {
    ok(false, "Should not have shown the popup.");
  }
  searchPopup.addEventListener("popupshowing", listener);

  promise = promiseEvent(searchbar.textbox, "focus");
  newWin.close();
  await promise;

  // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
  await new Promise(resolve => executeSoon(resolve));
  await new Promise(resolve => executeSoon(resolve));
  await new Promise(resolve => executeSoon(resolve));

  searchPopup.removeEventListener("popupshowing", listener);
  textbox.value = "";
});

// Clicking the search go button shouldn't open the popup
add_no_popup_task(async function search_go_doesnt_open_popup() {
  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);

  gURLBar.focus();
  textbox.value = "foo";
  searchbar.updateGoButtonVisibility();

  let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
  EventUtils.synthesizeMouseAtCenter(goButton, {});
  await promise;

  textbox.value = "";
  gBrowser.removeCurrentTab();
});

// Clicks outside the search popup should close the popup but not consume the click.
add_task(async function dont_consume_clicks() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  is(textbox.selectionStart, 0, "Should have selected all of the text");
  is(textbox.selectionEnd, 3, "Should have selected all of the text");

  promise = promiseEvent(searchPopup, "popuphidden");
  await EventUtils.promiseNativeMouseEventAndWaitForEvent({
    type: "click",
    target: gURLBar.inputField,
    atCenter: true,
    eventTypeToWait: "mouseup",
  });
  await promise;

  is(
    Services.focus.focusedElement,
    gURLBar.inputField,
    "Should have focused the URL bar"
  );

  textbox.value = "";
});

// Dropping text to the searchbar should open the popup
add_task(async function drop_opens_popup() {
  CustomizableUI.addWidgetToArea("home-button", "nav-bar");
  // The previous task leaves focus in the URL bar. However, in that case drags
  // can be interpreted as being selection drags by the drag manager, which
  // breaks the drag synthesis from EventUtils.js below. To avoid this, focus
  // the browser content instead.
  let focusEventPromise = BrowserTestUtils.waitForEvent(
    gBrowser.selectedBrowser,
    "focus"
  );
  gBrowser.selectedBrowser.focus();
  await focusEventPromise;

  let promise = promiseEvent(searchPopup, "popupshown");
  // Use a source for the drop that is outside of the search bar area, to avoid
  // it receiving a mousedown and causing the popup to sometimes open.
  let homeButton = document.getElementById("home-button");
  EventUtils.synthesizeDrop(
    homeButton,
    textbox,
    [[{ type: "text/plain", data: "foo" }]],
    "move",
    window
  );
  await promise;

  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );
  is(
    Services.focus.focusedElement,
    textbox,
    "Should have focused the search bar"
  );
  promise = promiseEvent(searchPopup, "popuphidden");
  searchPopup.hidePopup();
  await promise;

  textbox.value = "";
  CustomizableUI.removeWidgetFromArea("home-button");
});

// Moving the caret using the cursor keys should not close the popup.
add_task(async function dont_rollup_oncaretmove() {
  gURLBar.focus();
  textbox.value = "long text";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeMouseAtCenter(textbox, {});
  await promise;

  // Deselect the text
  EventUtils.synthesizeKey("KEY_ArrowRight");
  is(
    textbox.selectionStart,
    9,
    "Should have moved the caret (selectionStart after deselect right)"
  );
  is(
    textbox.selectionEnd,
    9,
    "Should have moved the caret (selectionEnd after deselect right)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  EventUtils.synthesizeKey("KEY_ArrowLeft");
  is(
    textbox.selectionStart,
    8,
    "Should have moved the caret (selectionStart after left)"
  );
  is(
    textbox.selectionEnd,
    8,
    "Should have moved the caret (selectionEnd after left)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  EventUtils.synthesizeKey("KEY_ArrowRight");
  is(
    textbox.selectionStart,
    9,
    "Should have moved the caret (selectionStart after right)"
  );
  is(
    textbox.selectionEnd,
    9,
    "Should have moved the caret (selectionEnd after right)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  // Ensure caret movement works while a suggestion is selected.
  is(textbox.popup.selectedIndex, -1, "No selected item in list");
  EventUtils.synthesizeKey("KEY_ArrowDown");
  is(textbox.popup.selectedIndex, 0, "Selected item in list");
  is(
    textbox.selectionStart,
    9,
    "Should have moved the caret to the end (selectionStart after selection)"
  );
  is(
    textbox.selectionEnd,
    9,
    "Should have moved the caret to the end (selectionEnd after selection)"
  );

  EventUtils.synthesizeKey("KEY_ArrowLeft");
  is(
    textbox.selectionStart,
    8,
    "Should have moved the caret again (selectionStart after left)"
  );
  is(
    textbox.selectionEnd,
    8,
    "Should have moved the caret again (selectionEnd after left)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  EventUtils.synthesizeKey("KEY_ArrowLeft");
  is(
    textbox.selectionStart,
    7,
    "Should have moved the caret (selectionStart after left)"
  );
  is(
    textbox.selectionEnd,
    7,
    "Should have moved the caret (selectionEnd after left)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  EventUtils.synthesizeKey("KEY_ArrowRight");
  is(
    textbox.selectionStart,
    8,
    "Should have moved the caret (selectionStart after right)"
  );
  is(
    textbox.selectionEnd,
    8,
    "Should have moved the caret (selectionEnd after right)"
  );
  is(searchPopup.state, "open", "Popup should still be open");

  if (!navigator.platform.includes("Mac")) {
    EventUtils.synthesizeKey("KEY_Home");
    is(
      textbox.selectionStart,
      0,
      "Should have moved the caret (selectionStart after home)"
    );
    is(
      textbox.selectionEnd,
      0,
      "Should have moved the caret (selectionEnd after home)"
    );
    is(searchPopup.state, "open", "Popup should still be open");
  }

  // Close the popup again
  promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("KEY_Escape");
  await promise;

  textbox.value = "";
});

// Entering customization mode shouldn't open the popup.
add_task(async function dont_open_in_customization() {
  gURLBar.focus();
  textbox.value = "foo";

  let promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeKey("KEY_Tab");
  await promise;
  isnot(
    searchPopup.getAttribute("showonlysettings"),
    "true",
    "Should show the full popup"
  );

  info("Entering customization mode");
  let sawPopup = false;
  function listener() {
    sawPopup = true;
  }
  searchPopup.addEventListener("popupshowing", listener);
  await gCUITestUtils.openMainMenu();
  promise = promiseEvent(searchPopup, "popuphidden");
  await startCustomizing();
  await promise;

  searchPopup.removeEventListener("popupshowing", listener);
  ok(!sawPopup, "Shouldn't have seen the suggestions popup");

  await endCustomizing();
  textbox.value = "";
});

add_task(async function cleanup() {
  info("removing search history values: " + kValues);
  let removeOps = kValues.map(value => {
    return { op: "remove", fieldname: "searchbar-history", value };
  });
  FormHistory.update(removeOps);
});
