/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This test makes sure that arrowing down and up through the view's results
// works correctly with regard to stale results.

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
});

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.suggest.quickactions", false]],
  });

  // Increase the timeout of the remove-stale-rows timer so that it doesn't
  // interfere with the tests.
  let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
  UrlbarView.removeStaleRowsTimeout = 1000;
  registerCleanupFunction(() => {
    UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
  });
});

// This tests the case where queryContext.results.length < the number of rows in
// the view, i.e., the view contains stale rows.
add_task(async function viewContainsStaleRows() {
  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();

  let maxResults = UrlbarPrefs.get("maxRichResults");
  let halfResults = Math.floor(maxResults / 2);

  // Add enough visits to pages with "xx" in the title to fill up half the view.
  for (let i = 0; i < halfResults; i++) {
    await PlacesTestUtils.addVisits({
      uri: "http://mochi.test:8888/" + i,
      title: "xx" + i,
    });
  }

  // Add enough visits to pages with "x" in the title to fill up the entire
  // view.
  for (let i = 0; i < maxResults; i++) {
    await PlacesTestUtils.addVisits({
      uri: "http://example.com/" + i,
      title: "x" + i,
    });
  }

  gURLBar.focus();

  // Search for "x" and wait for the search to finish.  All the "x" results
  // added above should be in the view.  (Actually one fewer will be in the
  // view due to the heuristic result, but that's not important.)
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "x",
    fireInputEvent: true,
  });

  // Below we'll do a search for "xx".  Get the row that will show the last
  // result in that search.
  let row = UrlbarTestUtils.getRowAt(window, halfResults);

  // Add a mutation listener on that row.  Wait for its "stale" attribute to be
  // removed.
  let mutationPromise = new Promise(resolve => {
    let observer = new MutationObserver(mutations => {
      for (let mut of mutations) {
        if (mut.attributeName == "stale" && !row.hasAttribute("stale")) {
          observer.disconnect();
          resolve();
          break;
        }
      }
    });
    observer.observe(row, { attributes: true });
  });

  // Type another "x" so that we search for "xx", but don't wait for the search
  // to finish.  Instead, wait for the row's stale attribute to be removed.
  EventUtils.synthesizeKey("x");
  info("Waiting for 'stale' attribute to be removed... ");
  await mutationPromise;

  // Now arrow down.  The search, which is still ongoing, will now stop and the
  // view won't be updated anymore.
  EventUtils.synthesizeKey("KEY_ArrowDown");

  // Wait for the search to stop.
  info("Waiting for the search to stop... ");
  await gURLBar.lastQueryContextPromise;

  // The query context for the last search ("xx") should contain only
  // halfResults + 1 results (+ 1 for the heuristic).
  Assert.ok(gURLBar.controller._lastQueryContextWrapper);
  let { queryContext } = gURLBar.controller._lastQueryContextWrapper;
  Assert.ok(queryContext);
  Assert.equal(queryContext.results.length, halfResults + 1);

  // But there should be maxResults visible rows in the view.
  let items = Array.from(
    UrlbarTestUtils.getResultsContainer(window).children
  ).filter(r => BrowserTestUtils.is_visible(r));
  Assert.equal(items.length, maxResults);

  // Arrow down through all the results.  After arrowing down from the last "xx"
  // result, the stale "x" results should be selected.  We should *not* enter
  // the one-off search buttons at that point.
  for (let i = 1; i < maxResults; i++) {
    Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
    Assert.equal(result.element.row.result.rowIndex, i);
    EventUtils.synthesizeKey("KEY_ArrowDown");
  }

  // Now the first one-off should be selected.
  Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
  Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);

  // Arrow back up through all the results.
  for (let i = maxResults - 1; i >= 0; i--) {
    EventUtils.synthesizeKey("KEY_ArrowUp");
    Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
  }

  await UrlbarTestUtils.promisePopupClose(window, () =>
    EventUtils.synthesizeKey("KEY_Escape")
  );
});

// This tests the case where, before the search finishes, stale results have
// been removed and replaced with non-stale results.
add_task(async function staleReplacedWithFresh() {
  // For this test, we need one set of results that's added quickly and another
  // set that's added after a delay.  We do an initial search and wait for both
  // sets to be added.  Then we do another search, but this time only wait for
  // the fast results to be added, and then we arrow down to stop the search
  // before the delayed results are added.  The order in which things should
  // happen after the second search goes like this:
  //
  // (1) second search
  // (2) fast results are added
  // (3) remove-stale-rows timer fires and removes stale rows (the rows from
  //     the delayed set of results from the first search)
  // (4) we arrow down to stop the search
  //
  // We use history for the fast results and a slow search engine for the
  // delayed results.
  //
  // NB: If this test ends up failing, it may be because the remove-stale-rows
  // timer fires before the history results are added.  i.e., steps 2 and 3
  // above happen out of order.  If that happens, try increasing
  // UrlbarView.removeStaleRowsTimeout above.

  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();

  // Enable search suggestions, and add an engine that returns suggestions on a
  // delay.
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.suggest.searches", true]],
  });
  let engine = await SearchTestUtils.promiseNewSearchEngine({
    url: getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml",
  });
  let oldDefaultEngine = await Services.search.getDefault();
  await Services.search.moveEngine(engine, 0);
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );

  let maxResults = UrlbarPrefs.get("maxRichResults");

  // Add enough visits to pages with "test" in the title to fill up the entire
  // view.
  for (let i = 0; i < maxResults; i++) {
    await PlacesTestUtils.addVisits({
      uri: "http://example.com/" + i,
      title: "test" + i,
    });
  }

  gURLBar.focus();

  // Search for "tes" and wait for the search to finish.
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "tes",
    fireInputEvent: true,
  });

  // Sanity check the results.  They should be:
  //
  //   tes -- Search with searchSuggestionEngineSlow [heuristic]
  //   tesfoo [search suggestion]
  //   tesbar [search suggestion]
  //   test9 [history]
  //   test8 [history]
  //   test7 [history]
  //   test6 [history]
  //   test5 [history]
  //   test4 [history]
  //   test3 [history]
  let count = UrlbarTestUtils.getResultCount(window);
  Assert.equal(count, maxResults);
  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
  Assert.ok(result.heuristic);
  result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
  Assert.ok(result.searchParams);
  Assert.equal(result.searchParams.suggestion, "tesfoo");
  result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
  Assert.ok(result.searchParams);
  Assert.equal(result.searchParams.suggestion, "tesbar");
  for (let i = 3; i < maxResults; i++) {
    result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
    Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
    Assert.equal(result.title, "test" + (maxResults - i + 2));
  }

  // Below we'll do a search for "test" *but* not wait for the two search
  // suggestion results to be added.  We'll only wait for the history results to
  // be added.  To determine when the history results are added, use a mutation
  // listener on the node containing the rows, and wait until the title of the
  // next-to-last row is "test2".  At that point, the results should be:
  //
  //   test -- Search with searchSuggestionEngineSlow
  //   test9
  //   test8
  //   test7
  //   test6
  //   test5
  //   test4
  //   test3
  //   test2
  //   test1
  let mutationPromise = new Promise(resolve => {
    let observer = new MutationObserver(mutations => {
      let row = UrlbarTestUtils.getRowAt(window, maxResults - 2);
      if (row && row._elements.get("title").textContent == "test2") {
        observer.disconnect();
        resolve();
      }
    });
    observer.observe(UrlbarTestUtils.getResultsContainer(window), {
      subtree: true,
      characterData: true,
      childList: true,
      attributes: true,
    });
  });

  // Now type a "t" so that we search for "test", but only wait for history
  // results to be added, as described above.
  EventUtils.synthesizeKey("t");
  info("Waiting for the 'test2' row... ");
  await mutationPromise;

  // Now arrow down.  The search, which is still ongoing, will now stop and the
  // view won't be updated anymore.
  EventUtils.synthesizeKey("KEY_ArrowDown");

  // Wait for the search to stop.
  info("Waiting for the search to stop... ");
  await gURLBar.lastQueryContextPromise;

  // Sanity check the results.  They should be as described above.
  count = UrlbarTestUtils.getResultCount(window);
  Assert.equal(count, maxResults);
  result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
  Assert.ok(result.heuristic);
  Assert.equal(result.element.row.result.rowIndex, 0);
  for (let i = 1; i < maxResults; i++) {
    result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
    Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
    Assert.equal(result.title, "test" + (maxResults - i));
    Assert.equal(result.element.row.result.rowIndex, i);
  }

  // Arrow down through all the results.  After arrowing down from "test3", we
  // should continue on to "test2".  We should *not* enter the one-off search
  // buttons at that point.
  for (let i = 1; i < maxResults; i++) {
    Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
    EventUtils.synthesizeKey("KEY_ArrowDown");
  }

  // Now the first one-off should be selected.
  Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1);
  Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0);

  // Arrow back up through all the results.
  for (let i = maxResults - 1; i >= 0; i--) {
    EventUtils.synthesizeKey("KEY_ArrowUp");
    Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i);
  }

  await UrlbarTestUtils.promisePopupClose(window, () =>
    EventUtils.synthesizeKey("KEY_Escape")
  );
  await SpecialPowers.popPrefEnv();
  await Services.search.setDefault(
    oldDefaultEngine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
});
