/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests that search engine suggestions are returned by
 * UrlbarProviderSearchSuggestions.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  sinon: "resource://testing-common/Sinon.sys.mjs",
});

const SUGGEST_PREF = "browser.urlbar.suggest.searches";
const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private";
const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
const TAB_TO_SEARCH_PREF = "browser.urlbar.suggest.engines";
const TRENDING_PREF = "browser.urlbar.trending.featureGate";
const QUICKACTIONS_PREF = "browser.urlbar.suggest.quickactions";
const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
const SHOW_SEARCH_SUGGESTIONS_FIRST_PREF =
  "browser.urlbar.showSearchSuggestionsFirst";
const SEARCH_STRING = "hello";

const MAX_RESULTS = Services.prefs.getIntPref(MAX_RICH_RESULTS_PREF, 10);

var suggestionsFn;
var previousSuggestionsFn;
let port;
let sandbox;

/**
 * Set the current suggestion funciton.
 *
 * @param {Function} fn
 *   A function that that a search string and returns an array of strings that
 *   will be used as search suggestions.
 *   Note: `fn` should return > 0 suggestions in most cases. Otherwise, you may
 *         encounter unexpected behaviour with UrlbarProviderSuggestion's
 *         _lastLowResultsSearchSuggestion safeguard.
 */
function setSuggestionsFn(fn) {
  previousSuggestionsFn = suggestionsFn;
  suggestionsFn = fn;
}

async function cleanup() {
  Services.prefs.clearUserPref("browser.urlbar.autoFill");
  Services.prefs.clearUserPref(SUGGEST_PREF);
  Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesUtils.history.clear();
  sandbox.restore();
}

async function cleanUpSuggestions() {
  await cleanup();
  if (previousSuggestionsFn) {
    suggestionsFn = previousSuggestionsFn;
    previousSuggestionsFn = null;
  }
}

function makeFormHistoryResults(context, count) {
  let results = [];
  for (let i = 0; i < count; i++) {
    results.push(
      makeFormHistoryResult(context, {
        suggestion: `${SEARCH_STRING} world Form History ${i}`,
        engineName: SUGGESTIONS_ENGINE_NAME,
      })
    );
  }
  return results;
}

function makeRemoteSuggestionResults(
  context,
  { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
) {
  // The suggestions function in `setup` returns:
  // [searchString, searchString + "foo", searchString + "bar"]
  // But when the heuristic is a search result, the muxer discards suggestion
  // results that match the search string, and therefore we expect only two
  // remote suggestion results, the "foo" and "bar" ones.
  return [
    makeSearchResult(context, {
      query,
      engineName: SUGGESTIONS_ENGINE_NAME,
      suggestion: suggestionPrefix + " foo",
    }),
    makeSearchResult(context, {
      query,
      engineName: SUGGESTIONS_ENGINE_NAME,
      suggestion: suggestionPrefix + " bar",
    }),
  ];
}

function setResultGroups(groups) {
  sandbox.restore();
  sandbox.stub(UrlbarPrefs, "resultGroups").get(() => {
    return {
      children: [
        // heuristic
        {
          maxResultCount: 1,
          children: [
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
            { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
          ],
        },
        // extensions using the omnibox API
        {
          group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
        },
        ...groups,
      ],
    };
  });
}

add_task(async function setup() {
  sandbox = lazy.sinon.createSandbox();

  let engine = await addTestSuggestionsEngine(searchStr => {
    return suggestionsFn(searchStr);
  });
  port = engine.getSubmission("").uri.port;

  setSuggestionsFn(searchStr => {
    let suffixes = ["foo", "bar"];
    return [searchStr].concat(suffixes.map(s => searchStr + " " + s));
  });

  // Install the test engine.
  let oldDefaultEngine = await Services.search.getDefault();
  registerCleanupFunction(async () => {
    Services.search.setDefault(
      oldDefaultEngine,
      Ci.nsISearchService.CHANGE_REASON_UNKNOWN
    );
    Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
    Services.prefs.clearUserPref(TRENDING_PREF);
    Services.prefs.clearUserPref(QUICKACTIONS_PREF);
    Services.prefs.clearUserPref(TAB_TO_SEARCH_PREF);
    sandbox.restore();
  });
  Services.search.setDefault(engine, Ci.nsISearchService.CHANGE_REASON_UNKNOWN);
  Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
  Services.prefs.setBoolPref(TRENDING_PREF, false);
  Services.prefs.setBoolPref(QUICKACTIONS_PREF, false);
  // Tab-to-search engines can introduce unexpected results, espescially because
  // they depend on real en-US engines.
  Services.prefs.setBoolPref(TAB_TO_SEARCH_PREF, false);

  // Add MAX_RESULTS form history.
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  let entries = makeFormHistoryResults(context, MAX_RESULTS).map(r => ({
    value: r.payload.suggestion,
    source: SUGGESTIONS_ENGINE_NAME,
  }));
  await UrlbarTestUtils.formHistory.add(entries);
});

add_task(async function disabled_urlbarSuggestions() {
  Services.prefs.setBoolPref(SUGGEST_PREF, false);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });
  await cleanUpSuggestions();
});

add_task(async function disabled_allSuggestions() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });
  await cleanUpSuggestions();
});

add_task(async function disabled_privateWindow() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
  let context = createContext(SEARCH_STRING, { isPrivate: true });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });
  await cleanUpSuggestions();
});

add_task(async function disabled_urlbarSuggestions_withRestrictionToken() {
  Services.prefs.setBoolPref(SUGGEST_PREF, false);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  let context = createContext(
    `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
    { isPrivate: false }
  );
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        query: SEARCH_STRING,
        alias: UrlbarTokenizer.RESTRICT.SEARCH,
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, {
        query: SEARCH_STRING,
      }),
    ],
  });
  await cleanUpSuggestions();
});

add_task(
  async function disabled_urlbarSuggestions_withRestrictionToken_private() {
    Services.prefs.setBoolPref(SUGGEST_PREF, false);
    Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
    Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
    let context = createContext(
      `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
      { isPrivate: true }
    );
    await check_results({
      context,
      matches: [
        makeSearchResult(context, {
          query: SEARCH_STRING,
          alias: UrlbarTokenizer.RESTRICT.SEARCH,
          engineName: SUGGESTIONS_ENGINE_NAME,
          heuristic: true,
        }),
      ],
    });
    await cleanUpSuggestions();
  }
);

add_task(
  async function disabled_urlbarSuggestions_withRestrictionToken_private_enabled() {
    Services.prefs.setBoolPref(SUGGEST_PREF, false);
    Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
    Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
    let context = createContext(
      `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
      { isPrivate: true }
    );
    await check_results({
      context,
      matches: [
        makeSearchResult(context, {
          query: SEARCH_STRING,
          alias: UrlbarTokenizer.RESTRICT.SEARCH,
          engineName: SUGGESTIONS_ENGINE_NAME,
          heuristic: true,
        }),
        ...makeFormHistoryResults(context, MAX_RESULTS - 3),
        ...makeRemoteSuggestionResults(context, {
          query: SEARCH_STRING,
        }),
      ],
    });
    await cleanUpSuggestions();
  }
);

add_task(async function enabled_by_pref_privateWindow() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
  let context = createContext(SEARCH_STRING, { isPrivate: true });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context),
    ],
  });
  await cleanUpSuggestions();

  Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF);
});

add_task(async function singleWordQuery() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  let context = createContext(SEARCH_STRING, { isPrivate: false });

  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function multiWordQuery() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
  const query = `${SEARCH_STRING} world`;
  let context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: query,
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function suffixMatch() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);

  setSuggestionsFn(searchStr => {
    let prefixes = ["baz", "quux"];
    return prefixes.map(p => p + " " + searchStr);
  });

  let context = createContext(SEARCH_STRING, { isPrivate: false });

  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "baz " + SEARCH_STRING,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "quux " + SEARCH_STRING,
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function remoteSuggestionsDupeSearchString() {
  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);

  // Return remote suggestions with the trimmed search string, the uppercased
  // search string, and the search string with a trailing space, plus the usual
  // "foo" and "bar" suggestions.
  setSuggestionsFn(searchStr => {
    let suffixes = ["foo", "bar"];
    return [searchStr.trim(), searchStr.toUpperCase(), searchStr + " "].concat(
      suffixes.map(s => searchStr + " " + s)
    );
  });

  // Do a search with a trailing space.  All the variations of the search string
  // with regard to spaces and case should be discarded from the remote
  // suggestions, leaving only the usual "foo" and "bar" suggestions.
  let query = SEARCH_STRING + " ";
  let context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        query,
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  await cleanUpSuggestions();
  Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
});

add_task(async function queryIsNotASubstring() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);

  setSuggestionsFn(searchStr => {
    return ["aaa", "bbb"];
  });

  let context = createContext(SEARCH_STRING, { isPrivate: false });

  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "aaa",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "bbb",
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function restrictToken() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);

  // Add a visit and a bookmark.  Actually, make the bookmark visited too so
  // that it's guaranteed, with its higher frecency, to appear above the search
  // suggestions.
  await PlacesTestUtils.addVisits([
    {
      uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`),
      title: `${SEARCH_STRING} visit`,
    },
    {
      uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
      title: `${SEARCH_STRING} bookmark`,
    },
  ]);

  await PlacesTestUtils.addBookmarkWithDetails({
    uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
    title: `${SEARCH_STRING} bookmark`,
  });

  let context = createContext(SEARCH_STRING, { isPrivate: false });

  // Do an unrestricted search to make sure everything appears in it, including
  // the visit and bookmark.
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 5),
      ...makeRemoteSuggestionResults(context),
      makeBookmarkResult(context, {
        uri: `http://example.com/${SEARCH_STRING}-bookmark`,
        title: `${SEARCH_STRING} bookmark`,
      }),
      makeVisitResult(context, {
        uri: `http://example.com/${SEARCH_STRING}-visit`,
        title: `${SEARCH_STRING} visit`,
      }),
    ],
  });

  // Now do a restricted search to make sure only suggestions appear.
  context = createContext(
    `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
    {
      isPrivate: false,
    }
  );
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        alias: UrlbarTokenizer.RESTRICT.SEARCH,
        query: SEARCH_STRING,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: SEARCH_STRING,
        query: SEARCH_STRING,
      }),
    ],
  });

  // Typing the search restriction char shows the Search Engine entry and local
  // results.
  context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        query: "",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 1),
    ],
  });

  // Also if followed by multiple spaces.
  context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}  `, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        alias: UrlbarTokenizer.RESTRICT.SEARCH,
        query: "",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 1),
    ],
  });

  // If followed by any char we should fetch suggestions.
  // Note this uses "h" to match form history.
  context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}h`, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        query: "h",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "h",
        query: "h",
      }),
    ],
  });

  // Also if followed by a space and single char.
  context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} h`, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        alias: UrlbarTokenizer.RESTRICT.SEARCH,
        query: "h",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "h",
        query: "h",
      }),
    ],
  });

  // Leading search-mode restriction tokens are removed.
  context = createContext(
    `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${SEARCH_STRING}`,
    { isPrivate: false }
  );
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        heuristic: true,
        query: SEARCH_STRING,
        alias: UrlbarTokenizer.RESTRICT.BOOKMARK,
      }),
      makeBookmarkResult(context, {
        uri: `http://example.com/${SEARCH_STRING}-bookmark`,
        title: `${SEARCH_STRING} bookmark`,
      }),
    ],
  });

  // Non-search-mode restriction tokens remain in the query and heuristic search
  // result.
  let token;
  for (let t of Object.values(UrlbarTokenizer.RESTRICT)) {
    if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(t)) {
      token = t;
      break;
    }
  }
  Assert.ok(
    token,
    "Non-search-mode restrict token exists -- if not, you can probably remove me!"
  );
  context = createContext(token, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function mixup_frecency() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);

  // At most, we should have 22 results in this subtest. We set this to 30 to
  // make we're not cutting off any results and we are actually getting 22.
  Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 30);

  // Add a visit and a bookmark.  Actually, make the bookmark visited too so
  // that it's guaranteed, with its higher frecency, to appear above the search
  // suggestions.
  await PlacesTestUtils.addVisits([
    {
      uri: Services.io.newURI("http://example.com/lo0"),
      title: `${SEARCH_STRING} low frecency 0`,
    },
    {
      uri: Services.io.newURI("http://example.com/lo1"),
      title: `${SEARCH_STRING} low frecency 1`,
    },
    {
      uri: Services.io.newURI("http://example.com/lo2"),
      title: `${SEARCH_STRING} low frecency 2`,
    },
    {
      uri: Services.io.newURI("http://example.com/lo3"),
      title: `${SEARCH_STRING} low frecency 3`,
    },
    {
      uri: Services.io.newURI("http://example.com/lo4"),
      title: `${SEARCH_STRING} low frecency 4`,
    },
  ]);

  for (let i = 0; i < 5; i++) {
    await PlacesTestUtils.addVisits([
      {
        uri: Services.io.newURI("http://example.com/hi0"),
        title: `${SEARCH_STRING} high frecency 0`,
        transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
      },
      {
        uri: Services.io.newURI("http://example.com/hi1"),
        title: `${SEARCH_STRING} high frecency 1`,
        transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
      },
      {
        uri: Services.io.newURI("http://example.com/hi2"),
        title: `${SEARCH_STRING} high frecency 2`,
        transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
      },
      {
        uri: Services.io.newURI("http://example.com/hi3"),
        title: `${SEARCH_STRING} high frecency 3`,
        transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
      },
    ]);
  }

  for (let i = 0; i < 4; i++) {
    let href = `http://example.com/hi${i}`;
    await PlacesTestUtils.addBookmarkWithDetails({
      uri: href,
      title: `${SEARCH_STRING} high frecency ${i}`,
    });
  }

  // Do an unrestricted search to make sure everything appears in it, including
  // the visit and bookmark.
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS),
      ...makeRemoteSuggestionResults(context),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi3",
        title: `${SEARCH_STRING} high frecency 3`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi2",
        title: `${SEARCH_STRING} high frecency 2`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi1",
        title: `${SEARCH_STRING} high frecency 1`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi0",
        title: `${SEARCH_STRING} high frecency 0`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo4",
        title: `${SEARCH_STRING} low frecency 4`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo3",
        title: `${SEARCH_STRING} low frecency 3`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo2",
        title: `${SEARCH_STRING} low frecency 2`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo1",
        title: `${SEARCH_STRING} low frecency 1`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo0",
        title: `${SEARCH_STRING} low frecency 0`,
      }),
    ],
  });

  // Change the mixup.
  setResultGroups([
    // 1 suggestion
    {
      maxResultCount: 1,
      children: [
        { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
        { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
      ],
    },
    // 5 general
    {
      maxResultCount: 5,
      group: UrlbarUtils.RESULT_GROUP.GENERAL,
    },
    // 1 suggestion
    {
      maxResultCount: 1,
      children: [
        { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
        { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
      ],
    },
    // remaining general
    { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    // remaining suggestions
    { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
    { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
  ]);

  // Do an unrestricted search to make sure everything appears in it, including
  // the visits and bookmarks.
  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, 1),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi3",
        title: `${SEARCH_STRING} high frecency 3`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi2",
        title: `${SEARCH_STRING} high frecency 2`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi1",
        title: `${SEARCH_STRING} high frecency 1`,
      }),
      makeBookmarkResult(context, {
        uri: "http://example.com/hi0",
        title: `${SEARCH_STRING} high frecency 0`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo4",
        title: `${SEARCH_STRING} low frecency 4`,
      }),
      ...makeFormHistoryResults(context, 2).slice(1),
      makeVisitResult(context, {
        uri: "http://example.com/lo3",
        title: `${SEARCH_STRING} low frecency 3`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo2",
        title: `${SEARCH_STRING} low frecency 2`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo1",
        title: `${SEARCH_STRING} low frecency 1`,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/lo0",
        title: `${SEARCH_STRING} low frecency 0`,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS).slice(2),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF);
  await cleanUpSuggestions();
});

add_task(async function prohibit_suggestions() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(
    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
    false
  );

  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  Services.prefs.setBoolPref(
    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
    true
  );
  registerCleanupFunction(() => {
    Services.prefs.setBoolPref(
      `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
      false
    );
  });
  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `http://${SEARCH_STRING}/`,
        fallbackTitle: `http://${SEARCH_STRING}/`,
        iconUri: "",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 2),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: false,
      }),
    ],
  });

  // When using multiple words, we should still get suggestions:
  let query = `${SEARCH_STRING} world`;
  context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, { suggestionPrefix: query }),
    ],
  });

  // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single
  // word instead:
  Services.prefs.setBoolPref(
    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
    false
  );
  Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
  });

  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `http://${SEARCH_STRING}/`,
        fallbackTitle: `http://${SEARCH_STRING}/`,
        iconUri: "",
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 2),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: false,
      }),
    ],
  });

  context = createContext("somethingelse", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://somethingelse/",
        fallbackTitle: "http://somethingelse/",
        iconUri: "",
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: false,
      }),
    ],
  });

  // When using multiple words, we should still get suggestions:
  query = `${SEARCH_STRING} world`;
  context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context, { suggestionPrefix: query }),
    ],
  });

  Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");

  context = createContext("http://1.2.3.4/", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://1.2.3.4/",
        fallbackTitle: "http://1.2.3.4/",
        iconUri: "page-icon:http://1.2.3.4/",
        heuristic: true,
      }),
    ],
  });

  context = createContext("[2001::1]:30", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://[2001::1]:30/",
        fallbackTitle: "http://[2001::1]:30/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("user:pass@test", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://user:pass@test/",
        fallbackTitle: "http://user:pass@test/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("data:text/plain,Content", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "data:text/plain,Content",
        fallbackTitle: "data:text/plain,Content",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("a", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function uri_like_queries() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);

  // We should not fetch any suggestions for an actual URL.
  let query = "mozilla.org";
  let context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        fallbackTitle: `http://${query}/`,
        uri: `http://${query}/`,
        iconUri: "",
        heuristic: true,
      }),
      makeSearchResult(context, { query, engineName: SUGGESTIONS_ENGINE_NAME }),
    ],
  });

  // We should also not fetch suggestions for a partially-typed URL.
  query = "mozilla.o";
  context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  // Now trying queries that could be confused for URLs. They should return
  // results.
  const uriLikeQueries = [
    "mozilla.org is a great website",
    "I like mozilla.org",
    "a/b testing",
    "he/him",
    "Google vs.",
    "5.8 cm",
  ];
  for (query of uriLikeQueries) {
    context = createContext(query, { isPrivate: false });
    await check_results({
      context,
      matches: [
        makeSearchResult(context, {
          engineName: SUGGESTIONS_ENGINE_NAME,
          heuristic: true,
        }),
        ...makeRemoteSuggestionResults(context, {
          suggestionPrefix: query,
        }),
      ],
    });
  }

  await cleanUpSuggestions();
});

add_task(async function avoid_remote_url_suggestions_1() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  setSuggestionsFn(searchStr => {
    let suffixes = [".com", "/test", ":1]", "@test", ". com"];
    return suffixes.map(s => searchStr + s);
  });

  const query = "test";

  await UrlbarTestUtils.formHistory.add([`${query}.com`]);

  let context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: `${query}.com`,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: `${query}. com`,
      }),
    ],
  });

  await cleanUpSuggestions();
  await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
});

add_task(async function avoid_remote_url_suggestions_2() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref("browser.urlbar.autoFill", false);

  setSuggestionsFn(searchStr => {
    let suffixes = ["ed", "eds"];
    return suffixes.map(s => searchStr + s);
  });

  let context = createContext("htt", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "htted",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "htteds",
      }),
    ],
  });

  context = createContext("ftp", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "ftped",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "ftpeds",
      }),
    ],
  });

  context = createContext("http", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httped",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httpeds",
      }),
    ],
  });

  context = createContext("http:", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("https", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httpsed",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httpseds",
      }),
    ],
  });

  context = createContext("https:", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("httpd", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httpded",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "httpdeds",
      }),
    ],
  });

  // Check FTP disabled
  context = createContext("ftp:", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("ftp:/", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("ftp://", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("ftp://test", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "ftp://test/",
        fallbackTitle: "ftp://test/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("http:/", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("https:/", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("http://", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("https://", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("http://www", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://www/",
        fallbackTitle: "http://www/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("https://www", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "https://www/",
        fallbackTitle: "https://www/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("http://test", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://test/",
        fallbackTitle: "http://test/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("https://test", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "https://test/",
        fallbackTitle: "https://test/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("http://www.test", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://www.test/",
        fallbackTitle: "http://www.test/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("http://www.test.com", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://www.test.com/",
        fallbackTitle: "http://www.test.com/",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("file", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "fileed",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "fileeds",
      }),
    ],
  });

  context = createContext("file:", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("file:///Users", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "file:///Users",
        fallbackTitle: "file:///Users",
        iconUri: "",
        heuristic: true,
      }),
    ],
  });

  context = createContext("moz-test://", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("moz+test://", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  context = createContext("about", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "abouted",
      }),
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: "abouteds",
      }),
    ],
  });

  await cleanUpSuggestions();
});

add_task(async function restrict_remote_suggestions_after_no_results() {
  // We don't fetch remote suggestions if a query with a length over
  // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to
  // avoid constructing a 100+ character string.
  Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4);
  setSuggestionsFn(searchStr => {
    return [];
  });

  const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1);
  let context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 1),
    ],
  });

  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 1),
      // Because the previous search returned no suggestions, we will not fetch
      // remote suggestions for this query that is just a longer version of the
      // previous query.
    ],
  });

  // Do one more search before resetting maxCharsForSearchSuggestions to reset
  // the search suggestion provider's _lastLowResultsSearchSuggestion property.
  // Otherwise it will be stuck at SEARCH_STRING, which interferes with
  // subsequent tests.
  context = createContext("not the search string", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
    ],
  });

  Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");

  await cleanUpSuggestions();
});

add_task(async function formHistory() {
  Services.prefs.setBoolPref(SUGGEST_PREF, true);
  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);

  // `maxHistoricalSearchSuggestions` is no longer treated as a max count but as
  // a boolean: If it's zero, then the user has opted out of form history so we
  // shouldn't include any at all; if it's non-zero, then we include form
  // history according to the limits specified in the muxer's result groups.

  // zero => no form history
  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  // non-zero => allow form history
  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  // non-zero => allow form history
  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
  context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      ...makeRemoteSuggestionResults(context),
    ],
  });

  Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);

  // Do a search for exactly the suggestion of the first form history result.
  // The heuristic's query should be the suggestion; the first form history
  // result should not be included since it dupes the heuristic; the other form
  // history results should not be included since they don't match; and both
  // remote suggestions should be included.
  let firstSuggestion = makeFormHistoryResults(context, 1)[0].payload
    .suggestion;
  context = createContext(firstSuggestion, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: firstSuggestion,
      }),
    ],
  });

  // Do the same search but in uppercase with a trailing space.  We should get
  // the same results, i.e., the form history result dupes the trimmed search
  // string so it shouldn't be included.
  let query = firstSuggestion.toUpperCase() + " ";
  context = createContext(query, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        query,
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: firstSuggestion.toUpperCase(),
      }),
    ],
  });

  // Add a form history entry that dupes the first remote suggestion and do a
  // search that triggers both.  The form history should be included but the
  // remote suggestion should not since it dupes the form history.
  let suggestionPrefix = "dupe";
  let dupeSuggestion = makeRemoteSuggestionResults(context, {
    suggestionPrefix,
  })[0].payload.suggestion;
  Assert.ok(dupeSuggestion, "Sanity check: dupeSuggestion is defined");
  await UrlbarTestUtils.formHistory.add([dupeSuggestion]);

  context = createContext(suggestionPrefix, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: dupeSuggestion,
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, { suggestionPrefix }).slice(1),
    ],
  });

  await UrlbarTestUtils.formHistory.remove([dupeSuggestion]);

  // Add these form history strings to use below.
  let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
  await UrlbarTestUtils.formHistory.add(formHistoryStrings);

  // Search for "foo".  "foo" and "FOO " shouldn't be included since they dupe
  // the heuristic.  Both "foobar" and "fooquux" should be included even though
  // the max form history count is only two and there are four matching form
  // history results (including the discarded "foo" and "FOO ").
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
    ],
  });

  // Add a visit that matches "foo" and will autofill so that the heuristic is
  // not a search result.  Now the "foo" and "foobar" form history should be
  // included.  The "foo" remote suggestion should not be included since it
  // dupes the "foo" form history.
  await PlacesTestUtils.addVisits("http://foo.example.com/");
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.HISTORY,
        uri: "http://foo.example.com/",
        title: "test visit for http://foo.example.com/",
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foo",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
    ],
  });
  await PlacesUtils.history.clear();

  // Add SERPs for "foobar", "fooBAR ", and "food", and search for "foo".  The
  // "foo" form history should be excluded since it dupes the heuristic; the
  // "foobar" and "fooquux" form history should be included; the "food" SERP
  // should be included since it doesn't dupe either form history result; and
  // the "foobar" and "fooBAR " SERPs depend on the result groups, see below.
  let engine = await Services.search.getDefault();
  let serpURLs = ["foobar", "fooBAR ", "food"].map(
    term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
  );
  await PlacesTestUtils.addVisits(serpURLs);

  // First set showSearchSuggestionsFirst = false so that general results appear
  // before suggestions, which means that the muxer visits the "foobar" and
  // "fooBAR " SERPs before visiting the "foobar" form history, and so it
  // doesn't see that these two SERPs dupe the form history.  They are therefore
  // included.
  Services.prefs.setBoolPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF, false);
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=food`,
        title: `test visit for http://localhost:${port}/search?q=food`,
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=fooBAR+`,
        title: `test visit for http://localhost:${port}/search?q=fooBAR+`,
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=foobar`,
        title: `test visit for http://localhost:${port}/search?q=foobar`,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
    ],
  });

  // Now clear showSearchSuggestionsFirst so that suggestions appear before
  // general results.  Now the muxer will see that the "foobar" and "fooBAR "
  // SERPs dupe the "foobar" form history, so it will exclude them.
  Services.prefs.clearUserPref(SHOW_SEARCH_SUGGESTIONS_FIRST_PREF);
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=food`,
        title: `test visit for http://localhost:${port}/search?q=food`,
      }),
    ],
  });

  await UrlbarTestUtils.formHistory.remove(formHistoryStrings);

  await cleanUpSuggestions();
  await PlacesUtils.history.clear();
});

// When the heuristic is hidden, search results that match the heuristic should
// be included and not deduped.
add_task(async function hideHeuristic() {
  UrlbarPrefs.set("experimental.hideHeuristic", true);
  UrlbarPrefs.set("browser.search.suggest.enabled", true);
  UrlbarPrefs.set("suggest.searches", true);
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      ...makeFormHistoryResults(context, MAX_RESULTS - 3),
      makeSearchResult(context, {
        query: SEARCH_STRING,
        engineName: SUGGESTIONS_ENGINE_NAME,
        suggestion: SEARCH_STRING,
      }),
      ...makeRemoteSuggestionResults(context),
    ],
  });
  await cleanUpSuggestions();
  UrlbarPrefs.clear("experimental.hideHeuristic");
});

// When the heuristic is hidden, form history results that match the heuristic
// should be included and not deduped.
add_task(async function hideHeuristic_formHistory() {
  UrlbarPrefs.set("experimental.hideHeuristic", true);
  UrlbarPrefs.set("browser.search.suggest.enabled", true);
  UrlbarPrefs.set("suggest.searches", true);

  // Search for exactly the suggestion of the first form history result.
  // Expected results:
  //
  // * First form history should be included even though it dupes the heuristic
  // * Other form history should not be included because they don't match the
  //   search string
  // * The first remote suggestion that just echoes the search string should not
  //   be included because it dupes the first form history
  // * The remaining remote suggestions should be included because they don't
  //   dupe anything
  let context = createContext(SEARCH_STRING, { isPrivate: false });
  let firstFormHistory = makeFormHistoryResults(context, 1)[0];
  context = createContext(firstFormHistory.payload.suggestion, {
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      firstFormHistory,
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: firstFormHistory.payload.suggestion,
      }),
    ],
  });

  // Add these form history strings to use below.
  let formHistoryStrings = ["foo", "FOO ", "foobar", "fooquux"];
  await UrlbarTestUtils.formHistory.add(formHistoryStrings);

  // Search for "foo". Expected results:
  //
  // * "foo" form history should be included even though it dupes the heuristic
  // * "FOO " form history should not be included because it dupes the "foo"
  //   form history
  // * "foobar" and "fooqux" form history should be included because they don't
  //   dupe anything
  // * "foo" remote suggestion should not be included because it dupes the "foo"
  //   form history
  // * "foo foo" and "foo bar" remote suggestions should be included because
  //   they don't dupe anything
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foo",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
    ],
  });

  // Add SERPs for "foo" and "food", and search for "foo". Expected results:
  //
  // * "foo" form history should be included even though it dupes the heuristic
  // * "foobar" and "fooqux" form history should be included because they don't
  //   dupe anything
  // * "foo" SERP depends on `showSearchSuggestionsFirst`, see below
  // * "food" SERP should be include because it doesn't dupe anything
  // * "foo" remote suggestion should not be included because it dupes the "foo"
  //   form history
  // * "foo foo" and "foo bar" remote suggestions should be included because
  //   they don't dupe anything
  let engine = await Services.search.getDefault();
  let serpURLs = ["foo", "food"].map(
    term => UrlbarUtils.getSearchQueryUrl(engine, term)[0]
  );
  await PlacesTestUtils.addVisits(serpURLs);

  // With `showSearchSuggestionsFirst = false` so that general results appear
  // before suggestions, the muxer visits the "foo" (and "food") SERPs before
  // visiting the "foo" form history, and so it doesn't see that the "foo" SERP
  // dupes the form history. The SERP is therefore included.
  UrlbarPrefs.set("showSearchSuggestionsFirst", false);
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=food`,
        title: `test visit for http://localhost:${port}/search?q=food`,
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=foo`,
        title: `test visit for http://localhost:${port}/search?q=foo`,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foo",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
    ],
  });

  // Now clear `showSearchSuggestionsFirst` so that suggestions appear before
  // general results. Now the muxer will see that the "foo" SERP dupes the "foo"
  // form history, so it will exclude it.
  UrlbarPrefs.clear("showSearchSuggestionsFirst");
  context = createContext("foo", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        heuristic: true,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foo",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "foobar",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      makeFormHistoryResult(context, {
        suggestion: "fooquux",
        engineName: SUGGESTIONS_ENGINE_NAME,
      }),
      ...makeRemoteSuggestionResults(context, {
        suggestionPrefix: "foo",
      }),
      makeVisitResult(context, {
        uri: `http://localhost:${port}/search?q=food`,
        title: `test visit for http://localhost:${port}/search?q=food`,
      }),
    ],
  });

  await UrlbarTestUtils.formHistory.remove(formHistoryStrings);

  await cleanUpSuggestions();
  UrlbarPrefs.clear("experimental.hideHeuristic");
});
