// Tests that keyboard navigation in the search panel works as designed.

const searchbar = document.getElementById("searchbar");
const textbox = searchbar._textbox;
const searchPopup = document.getElementById("PopupSearchAutoComplete");

const kValues = ["foo1", "foo2", "foo3"];
const kUserValue = "foo";

// Get an array of the one-off buttons.
function getOneOffs() {
  let oneOffs = [];
  let oneOff = document.getAnonymousElementByAttribute(searchPopup, "anonid",
                                                       "search-panel-one-offs");
  for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
    if (oneOff.classList.contains("dummy"))
      break;
    oneOffs.push(oneOff);
  }

  return oneOffs;
}

function getOpenSearchItems() {
  let os = [];

  let addEngineList =
    document.getAnonymousElementByAttribute(searchPopup, "anonid",
                                            "add-engines");
  for (let item = addEngineList.firstChild; item; item = item.nextSibling)
    os.push(item);

  return os;
}

add_task(function* init() {
  yield promiseNewEngine("testEngine.xml");

  // First cleanup the form history in case other tests left things there.
  yield new Promise((resolve, reject) => {
    info("cleanup the search history");
    searchbar.FormHistory.update({op: "remove", fieldname: "searchbar-history"},
                                 {handleCompletion: resolve,
                                  handleError: reject});
  });

  yield new Promise((resolve, reject) => {
    info("adding search history values: " + kValues);
    let ops = kValues.map(value => { return {op: "add",
                                             fieldname: "searchbar-history",
                                             value: value}
                                   });
    searchbar.FormHistory.update(ops, {
      handleCompletion: function() {
        registerCleanupFunction(() => {
          info("removing search history values: " + kValues);
          let ops =
            kValues.map(value => { return {op: "remove",
                                           fieldname: "searchbar-history",
                                           value: value}
                                 });
          searchbar.FormHistory.update(ops);
        });
        resolve();
      },
      handleError: reject
    });
  });

  textbox.value = kUserValue;
  registerCleanupFunction(() => { textbox.value = ""; });
});


add_task(function* test_arrows() {
  let promise = promiseEvent(searchPopup, "popupshown");
  info("Opening search panel");
  searchbar.focus();
  yield promise;
  is(textbox.mController.searchString, kUserValue, "The search string should be 'foo'");

  // Check the initial state of the panel before sending keyboard events.
  is(searchPopup.view.rowCount, kValues.length, "There should be 3 suggestions");
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");

  // The tests will be less meaningful if the first, second, last, and
  // before-last one-off buttons aren't different. We should always have more
  // than 4 default engines, but it's safer to check this assumption.
  let oneOffs = getOneOffs();
  ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed")

  ok(!textbox.selectedButton, "no one-off button should be selected");

  // The down arrow should first go through the suggestions.
  for (let i = 0; i < kValues.length; ++i) {
    EventUtils.synthesizeKey("VK_DOWN", {});
    is(searchPopup.selectedIndex, i,
       "the suggestion at index " + i + " should be selected");
    is(textbox.value, kValues[i],
       "the textfield value should be " + kValues[i]);
  }

  // Pressing down again should remove suggestion selection and change the text
  // field value back to what the user typed, and select the first one-off.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue,
     "the textfield value should be back to initial value");

  // now cycle through the one-off items, the first one is already selected.
  for (let i = 0; i < oneOffs.length; ++i) {
    is(textbox.selectedButton, oneOffs[i],
       "the one-off button #" + (i + 1) + " should be selected");
    EventUtils.synthesizeKey("VK_DOWN", {});
  }

  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");
  EventUtils.synthesizeKey("VK_DOWN", {});

  // We should now be back to the initial situation.
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  ok(!textbox.selectedButton, "no one-off button should be selected");

  info("now test the up arrow key");
  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

  // cycle through the one-off items, the first one is already selected.
  for (let i = oneOffs.length; i; --i) {
    EventUtils.synthesizeKey("VK_UP", {});
    is(textbox.selectedButton, oneOffs[i - 1],
       "the one-off button #" + i + " should be selected");
  }

  // Another press on up should clear the one-off selection and select the
  // last suggestion.
  EventUtils.synthesizeKey("VK_UP", {});
  ok(!textbox.selectedButton, "no one-off button should be selected");

  for (let i = kValues.length - 1; i >= 0; --i) {
    is(searchPopup.selectedIndex, i,
       "the suggestion at index " + i + " should be selected");
    is(textbox.value, kValues[i],
       "the textfield value should be " + kValues[i]);
    EventUtils.synthesizeKey("VK_UP", {});
  }

  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue,
     "the textfield value should be back to initial value");
});

add_task(function* test_typing_clears_button_selection() {
  is(Services.focus.focusedElement, textbox.inputField,
     "the search bar should be focused"); // from the previous test.
  ok(!textbox.selectedButton, "no button should be selected");

  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

  // Type a character.
  EventUtils.synthesizeKey("a", {});
  ok(!textbox.selectedButton, "the settings item should be de-selected");

  // Remove the character.
  EventUtils.synthesizeKey("VK_BACK_SPACE", {});
});

add_task(function* test_tab() {
  is(Services.focus.focusedElement, textbox.inputField,
     "the search bar should be focused"); // from the previous test.

  let oneOffs = getOneOffs();
  ok(!textbox.selectedButton, "no one-off button should be selected");

  // Pressing tab should select the first one-off without selecting suggestions.
  // now cycle through the one-off items, the first one is already selected.
  for (let i = 0; i < oneOffs.length; ++i) {
    EventUtils.synthesizeKey("VK_TAB", {});
    is(textbox.selectedButton, oneOffs[i],
       "the one-off button #" + (i + 1) + " should be selected");
  }
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue, "the textfield value should be unmodified");

  // One more <tab> selects the settings button.
  EventUtils.synthesizeKey("VK_TAB", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

  // Pressing tab again should close the panel...
  let promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("VK_TAB", {});
  yield promise;

  // ... and move the focus out of the searchbox.
  isnot(Services.focus.focusedElement, textbox.inputField,
        "the search bar no longer be focused");
});

add_task(function* test_shift_tab() {
  // First reopen the panel.
  let promise = promiseEvent(searchPopup, "popupshown");
  info("Opening search panel");
  searchbar.focus();
  yield promise;

  let oneOffs = getOneOffs();
  ok(!textbox.selectedButton, "no one-off button should be selected");

  // Press up once to select the last button.
  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

  // Press up again to select the last one-off button.
  EventUtils.synthesizeKey("VK_UP", {});

  // Pressing shift+tab should cycle through the one-off items.
  for (let i = oneOffs.length - 1; i >= 0; --i) {
    is(textbox.selectedButton, oneOffs[i],
       "the one-off button #" + (i + 1) + " should be selected");
    if (i)
      EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
  }
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue, "the textfield value should be unmodified");

  // Pressing shift+tab again should close the panel...
  promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
  yield promise;

  // ... and move the focus out of the searchbox.
  isnot(Services.focus.focusedElement, textbox.inputField,
        "the search bar no longer be focused");
});

add_task(function* test_alt_down() {
  // First refocus the panel.
  let promise = promiseEvent(searchPopup, "popupshown");
  info("Opening search panel");
  searchbar.focus();
  yield promise;

  // close the panel using the escape key.
  promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("VK_ESCAPE", {});
  yield promise;

  // check that alt+down opens the panel...
  promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
  yield promise;

  // ... and does nothing else.
  ok(!textbox.selectedButton, "no one-off button should be selected");
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue, "the textfield value should be unmodified");

  // Pressing alt+down should select the first one-off without selecting suggestions
  // and cycle through the one-off items.
  let oneOffs = getOneOffs();
  for (let i = 0; i < oneOffs.length; ++i) {
    EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
    is(textbox.selectedButton, oneOffs[i],
       "the one-off button #" + (i + 1) + " should be selected");
    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  }

  // One more alt+down keypress and nothing should be selected.
  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
  ok(!textbox.selectedButton, "no one-off button should be selected");

  // another one and the first one-off should be selected.
  EventUtils.synthesizeKey("VK_DOWN", {altKey: true});
  is(textbox.selectedButton, oneOffs[0],
     "the first one-off button should be selected");
});

add_task(function* test_alt_up() {
  // close the panel using the escape key.
  let promise = promiseEvent(searchPopup, "popuphidden");
  EventUtils.synthesizeKey("VK_ESCAPE", {});
  yield promise;

  // check that alt+up opens the panel...
  promise = promiseEvent(searchPopup, "popupshown");
  EventUtils.synthesizeKey("VK_UP", {altKey: true});
  yield promise;

  // ... and does nothing else.
  ok(!textbox.selectedButton, "no one-off button should be selected");
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue, "the textfield value should be unmodified");

  // Pressing alt+up should select the last one-off without selecting suggestions
  // and cycle up through the one-off items.
  let oneOffs = getOneOffs();
  for (let i = oneOffs.length - 1; i >= 0; --i) {
    EventUtils.synthesizeKey("VK_UP", {altKey: true});
    is(textbox.selectedButton, oneOffs[i],
       "the one-off button #" + (i + 1) + " should be selected");
    is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  }

  // One more alt+down keypress and nothing should be selected.
  EventUtils.synthesizeKey("VK_UP", {altKey: true});
  ok(!textbox.selectedButton, "no one-off button should be selected");

  // another one and the last one-off should be selected.
  EventUtils.synthesizeKey("VK_UP", {altKey: true});
  is(textbox.selectedButton, oneOffs[oneOffs.length - 1],
     "the last one-off button should be selected");

  // Cleanup for the next test.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");
  EventUtils.synthesizeKey("VK_DOWN", {});
  ok(!textbox.selectedButton, "no one-off should be selected anymore");
});

add_task(function* test_tab_and_arrows() {
  // Check the initial state is as expected.
  ok(!textbox.selectedButton, "no one-off button should be selected");
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue, "the textfield value should be unmodified");

  // After pressing down, the first sugggestion should be selected.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(searchPopup.selectedIndex, 0, "first suggestion should be selected");
  is(textbox.value, kValues[0], "the textfield value should have changed");
  ok(!textbox.selectedButton, "no one-off button should be selected");

  // After pressing tab, the first one-off should be selected,
  // and the first suggestion still selected.
  let oneOffs = getOneOffs();
  EventUtils.synthesizeKey("VK_TAB", {});
  is(textbox.selectedButton, oneOffs[0],
     "the first one-off button should be selected");
  is(searchPopup.selectedIndex, 0, "first suggestion should still be selected");

  // After pressing down, the second suggestion should be selected,
  // and the first one-off still selected.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(textbox.selectedButton, oneOffs[0],
     "the first one-off button should still be selected");
  is(searchPopup.selectedIndex, 1, "second suggestion should be selected");

  // After pressing up, the first suggestion should be selected again,
  // and the first one-off still selected.
  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton, oneOffs[0],
     "the first one-off button should still be selected");
  is(searchPopup.selectedIndex, 0, "second suggestion should be selected again");

  // After pressing up again, we should have no suggestion selected anymore,
  // the textfield value back to the user-typed value, and still the first one-off
  // selected.
  EventUtils.synthesizeKey("VK_UP", {});
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  is(textbox.value, kUserValue,
     "the textfield value should be back to user typed value");
  is(textbox.selectedButton, oneOffs[0],
     "the first one-off button should still be selected");

  // Now pressing down should select the second one-off.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(textbox.selectedButton, oneOffs[1],
     "the second one-off button should be selected");
  is(searchPopup.selectedIndex, -1, "there should still be no selected suggestion");

  // Finally close the panel.
  let promise = promiseEvent(searchPopup, "popuphidden");
  searchPopup.hidePopup();
  yield promise;
});

add_task(function* test_open_search() {
  let rootDir = getRootDirectory(gTestPath);
  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, rootDir + "opensearch.html");

  let promise = promiseEvent(searchPopup, "popupshown");
  info("Opening search panel");
  searchbar.focus();
  yield promise;

  let engines = getOpenSearchItems();
  is(engines.length, 2, "the opensearch.html page exposes 2 engines")

  // Check that there's initially no selection.
  is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
  ok(!textbox.selectedButton, "no button should be selected");

  // Pressing up once selects the setting button...
  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

  // ...and then pressing up selects open search engines.
  for (let i = engines.length; i; --i) {
    EventUtils.synthesizeKey("VK_UP", {});
    let selectedButton = textbox.selectedButton;
    is(selectedButton, engines[i - 1],
       "the engine #" + i + " should be selected");
    ok(selectedButton.classList.contains("addengine-item"),
       "the button is themed as an engine item");
  }

  // Pressing up again should select the last one-off button.
  EventUtils.synthesizeKey("VK_UP", {});
  is(textbox.selectedButton, getOneOffs().pop(),
     "the last one-off button should be selected");

  info("now check that the down key navigates open search items as expected");
  for (let i = 0; i < engines.length; ++i) {
    EventUtils.synthesizeKey("VK_DOWN", {});
    is(textbox.selectedButton, engines[i],
       "the engine #" + (i + 1) + " should be selected");
  }

  // Pressing down on the last engine item selects the settings button.
  EventUtils.synthesizeKey("VK_DOWN", {});
  is(textbox.selectedButton.getAttribute("anonid"), "search-settings",
     "the settings item should be selected");

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

  gBrowser.removeCurrentTab();
});
