/* 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/. */

"use strict";

const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback";

const origin = "example.com";

async function cleanup() {
  let suggestPrefs = ["history", "bookmark", "openpage"];
  for (let type of suggestPrefs) {
    Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
  }
  await cleanupPlaces();
}

testEngine_setup();

registerCleanupFunction(async () => {
  Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
});
Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);

// "example.com/" should match http://example.com/.  i.e., the search string
// should be treated as if it didn't have the trailing slash.
add_task(async function trailingSlash() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
    },
  ]);

  let context = createContext(`${origin}/`, { isPrivate: false });
  await check_results({
    context,
    autofilled: `${origin}/`,
    completed: `http://${origin}/`,
    matches: [
      makeVisitResult(context, {
        uri: `http://${origin}/`,
        title: `test visit for http://${origin}/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "example.com/" should match http://www.example.com/.  i.e., the search string
// should be treated as if it didn't have the trailing slash.
add_task(async function trailingSlashWWW() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://www.example.com/",
    },
  ]);
  let context = createContext(`${origin}/`, { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: "http://www.example.com/",
    matches: [
      makeVisitResult(context, {
        uri: `http://www.${origin}/`,
        title: `test visit for http://www.${origin}/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "ex" should match http://example.com:8888/, and the port should be completed.
add_task(async function port() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com:8888/",
    },
  ]);
  let context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com:8888/",
    completed: "http://example.com:8888/",
    matches: [
      makeVisitResult(context, {
        uri: `http://${origin}:8888/`,
        title: `test visit for http://${origin}:8888/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "example.com:8" should match http://example.com:8888/, and the port should
// be completed.
add_task(async function portPartial() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com:8888/",
    },
  ]);
  let context = createContext(`${origin}:8`, { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com:8888/",
    completed: "http://example.com:8888/",
    matches: [
      makeVisitResult(context, {
        uri: `http://${origin}:8888/`,
        title: `test visit for http://${origin}:8888/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "EXaM" should match http://example.com/ and the case of the search string
// should be preserved in the autofilled value.
add_task(async function preserveCase() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
    },
  ]);
  let context = createContext("EXaM", { isPrivate: false });
  await check_results({
    context,
    autofilled: "EXaMple.com/",
    completed: "http://example.com/",
    matches: [
      makeVisitResult(context, {
        uri: `http://${origin}/`,
        title: `test visit for http://${origin}/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "EXaM" should match http://example.com:8888/, the port should be completed,
// and the case of the search string should be preserved in the autofilled
// value.
add_task(async function preserveCasePort() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com:8888/",
    },
  ]);
  let context = createContext("EXaM", { isPrivate: false });
  await check_results({
    context,
    autofilled: "EXaMple.com:8888/",
    completed: "http://example.com:8888/",
    matches: [
      makeVisitResult(context, {
        uri: `http://${origin}:8888/`,
        title: `test visit for http://${origin}:8888/`,
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// "example.com:89" should *not* match http://example.com:8888/.
add_task(async function portNoMatch1() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com:8888/",
    },
  ]);
  let context = createContext(`${origin}:89`, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `http://${origin}:89/`,
        fallbackTitle: `http://${origin}:89/`,
        iconUri: "",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });
  await cleanup();
});

// "example.com:9" should *not* match http://example.com:8888/.
add_task(async function portNoMatch2() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com:8888/",
    },
  ]);
  let context = createContext(`${origin}:9`, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `http://${origin}:9/`,
        fallbackTitle: `http://${origin}:9/`,
        iconUri: "",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });
  await cleanup();
});

// "example/" should *not* match http://example.com/.
add_task(async function trailingSlash_2() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://example.com/",
    },
  ]);
  let context = createContext("example/", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: "http://example/",
        fallbackTitle: "http://example/",
        iconUri: "page-icon:http://example/",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });
  await cleanup();
});

// multi.dotted.domain, search up to dot.
add_task(async function multidotted() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://www.example.co.jp:8888/",
    },
  ]);
  let context = createContext("www.example.co.", { isPrivate: false });
  await check_results({
    context,
    autofilled: "www.example.co.jp:8888/",
    completed: "http://www.example.co.jp:8888/",
    matches: [
      makeVisitResult(context, {
        uri: "http://www.example.co.jp:8888/",
        title: "test visit for http://www.example.co.jp:8888/",
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

add_task(async function test_ip() {
  // IP addresses have complicated rules around whether they show
  // HeuristicFallback's backup search result. Flip this pref to disable that
  // backup search and simplify ths subtest.
  Services.prefs.setBoolPref("keyword.enabled", false);
  for (let str of [
    "192.168.1.1/",
    "255.255.255.255:8080/",
    "[2001:db8::1428:57ab]/",
    "[::c0a8:5909]/",
    "[::1]/",
  ]) {
    info("testing " + str);
    await PlacesTestUtils.addVisits("http://" + str);
    for (let i = 1; i < str.length; ++i) {
      let context = createContext(str.substring(0, i), { isPrivate: false });
      await check_results({
        context,
        autofilled: str,
        completed: "http://" + str,
        matches: [
          makeVisitResult(context, {
            uri: "http://" + str,
            title: `test visit for http://${str}`,
            heuristic: true,
          }),
        ],
      });
    }
    await cleanup();
  }
  Services.prefs.clearUserPref("keyword.enabled");
});

// host starting with large number.
add_task(async function large_number_host() {
  await PlacesTestUtils.addVisits([
    {
      uri: "http://12345example.it:8888/",
    },
  ]);
  let context = createContext("1234", { isPrivate: false });
  await check_results({
    context,
    autofilled: "12345example.it:8888/",
    completed: "http://12345example.it:8888/",
    matches: [
      makeVisitResult(context, {
        uri: "http://12345example.it:8888/",
        title: "test visit for http://12345example.it:8888/",
        heuristic: true,
      }),
    ],
  });
  await cleanup();
});

// When determining which origins should be autofilled, all the origins sharing
// a host should be added together to get their combined frecency -- i.e.,
// prefixes should be collapsed.  And then from that list, the origin with the
// highest frecency should be chosen.
add_task(async function groupByHost() {
  // Add some visits to the same host, example.com.  Add one http and two https
  // so that https has a higher frecency and is therefore the origin that should
  // be autofilled.  Also add another origin that has a higher frecency than
  // both so that alone, neither http nor https would be autofilled, but added
  // together they should be.
  await PlacesTestUtils.addVisits([
    { uri: "http://example.com/" },

    { uri: "https://example.com/" },
    { uri: "https://example.com/" },

    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
  ]);

  let httpFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    { url: "http://example.com/" }
  );
  let httpsFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    { url: "https://example.com/" }
  );
  let otherFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    { url: "https://mozilla.org/" }
  );
  Assert.less(httpFrec, httpsFrec, "Sanity check");
  Assert.less(httpsFrec, otherFrec, "Sanity check");

  // Make sure the frecencies of the three origins are as expected in relation
  // to the threshold.
  let threshold = await getOriginAutofillThreshold();
  Assert.less(httpFrec, threshold, "http origin should be < threshold");
  Assert.less(httpsFrec, threshold, "https origin should be < threshold");
  Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");

  Assert.ok(
    threshold <= httpFrec + httpsFrec,
    "http and https origin added together should cross threshold"
  );

  // The https origin should be autofilled.
  let context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: "https://example.com/",
    matches: [
      makeVisitResult(context, {
        uri: "https://example.com/",
        title: "test visit for https://example.com/",
        heuristic: true,
      }),
    ],
  });

  await cleanup();
});

// This is the same as the previous (groupByHost), but it changes the standard
// deviation multiplier by setting the corresponding pref.  This makes sure that
// the pref is respected.
add_task(async function groupByHostNonDefaultStddevMultiplier() {
  let stddevMultiplier = 1.5;
  Services.prefs.setCharPref(
    "browser.urlbar.autoFill.stddevMultiplier",
    Number(stddevMultiplier).toFixed(1)
  );

  await PlacesTestUtils.addVisits([
    { uri: "http://example.com/" },
    { uri: "http://example.com/" },

    { uri: "https://example.com/" },
    { uri: "https://example.com/" },
    { uri: "https://example.com/" },

    { uri: "https://foo.com/" },
    { uri: "https://foo.com/" },
    { uri: "https://foo.com/" },

    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
    { uri: "https://mozilla.org/" },
  ]);

  let httpFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    {
      url: "http://example.com/",
    }
  );
  let httpsFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    {
      url: "https://example.com/",
    }
  );
  let otherFrec = await PlacesTestUtils.getDatabaseValue(
    "moz_places",
    "frecency",
    {
      url: "https://mozilla.org/",
    }
  );
  Assert.less(httpFrec, httpsFrec, "Sanity check");
  Assert.less(httpsFrec, otherFrec, "Sanity check");

  // Make sure the frecencies of the three origins are as expected in relation
  // to the threshold.
  let threshold = await getOriginAutofillThreshold();
  Assert.less(httpFrec, threshold, "http origin should be < threshold");
  Assert.less(httpsFrec, threshold, "https origin should be < threshold");
  Assert.ok(threshold <= otherFrec, "Other origin should cross threshold");

  Assert.ok(
    threshold <= httpFrec + httpsFrec,
    "http and https origin added together should cross threshold"
  );

  // The https origin should be autofilled.
  let context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: "https://example.com/",
    matches: [
      makeVisitResult(context, {
        uri: "https://example.com/",
        title: "test visit for https://example.com/",
        heuristic: true,
      }),
    ],
  });

  Services.prefs.clearUserPref("browser.urlbar.autoFill.stddevMultiplier");

  await cleanup();
});

// This is similar to suggestHistoryFalse_bookmark_0 in test_autofill_tasks.js,
// but it adds unbookmarked visits for multiple URLs with the same origin.
add_task(async function suggestHistoryFalse_bookmark_multiple() {
  // Force only bookmarked pages to be suggested and therefore only bookmarked
  // pages to be completed.
  Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);

  let search = "ex";
  let baseURL = "http://example.com/";
  let bookmarkedURL = baseURL + "bookmarked";

  // Add visits for three different URLs all sharing the same origin, and then
  // bookmark the second one.  After that, the origin should be autofilled.  The
  // reason for adding unbookmarked visits before and after adding the
  // bookmarked visit is to make sure our aggregate SQL query for determining
  // whether an origin is bookmarked is correct.

  await PlacesTestUtils.addVisits([
    {
      uri: baseURL + "other1",
    },
  ]);
  let context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
        heuristic: true,
      }),
    ],
  });

  await PlacesTestUtils.addVisits([
    {
      uri: bookmarkedURL,
    },
  ]);
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
        heuristic: true,
      }),
    ],
  });

  await PlacesTestUtils.addVisits([
    {
      uri: baseURL + "other2",
    },
  ]);
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
        heuristic: true,
      }),
    ],
  });

  // Now bookmark the second URL.  It should be suggested and completed.
  await PlacesTestUtils.addBookmarkWithDetails({
    uri: bookmarkedURL,
  });
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: baseURL,
    matches: [
      makeVisitResult(context, {
        uri: baseURL,
        fallbackTitle: UrlbarTestUtils.trimURL(baseURL),
        heuristic: true,
      }),
      makeBookmarkResult(context, {
        uri: bookmarkedURL,
        title: "A bookmark",
      }),
    ],
  });

  await cleanup();
});

// This is similar to suggestHistoryFalse_bookmark_prefix_0 in
// autofill_test_autofill_originsAndQueries.js, but it adds unbookmarked visits
// for multiple URLs with the same origin.
add_task(async function suggestHistoryFalse_bookmark_prefix_multiple() {
  // Force only bookmarked pages to be suggested and therefore only bookmarked
  // pages to be completed.
  Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);

  let search = "http://ex";
  let baseURL = "http://example.com/";
  let bookmarkedURL = baseURL + "bookmarked";

  // Add visits for three different URLs all sharing the same origin, and then
  // bookmark the second one.  After that, the origin should be autofilled.  The
  // reason for adding unbookmarked visits before and after adding the
  // bookmarked visit is to make sure our aggregate SQL query for determining
  // whether an origin is bookmarked is correct.

  await PlacesTestUtils.addVisits([
    {
      uri: baseURL + "other1",
    },
  ]);
  let context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `${search}/`,
        fallbackTitle: `${search}/`,
        iconUri: "",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });

  await PlacesTestUtils.addVisits([
    {
      uri: bookmarkedURL,
    },
  ]);
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `${search}/`,
        fallbackTitle: `${search}/`,
        iconUri: "",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });

  await PlacesTestUtils.addVisits([
    {
      uri: baseURL + "other2",
    },
  ]);
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeVisitResult(context, {
        source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
        uri: `${search}/`,
        fallbackTitle: `${search}/`,
        iconUri: "",
        heuristic: true,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
      }),
    ],
  });

  // Now bookmark the second URL.  It should be suggested and completed.
  await PlacesTestUtils.addBookmarkWithDetails({
    uri: bookmarkedURL,
  });
  context = createContext(search, { isPrivate: false });
  await check_results({
    context,
    autofilled: "http://example.com/",
    completed: baseURL,
    matches: [
      makeVisitResult(context, {
        uri: baseURL,
        fallbackTitle: UrlbarTestUtils.trimURL(baseURL),
        heuristic: true,
      }),
      makeBookmarkResult(context, {
        uri: bookmarkedURL,
        title: "A bookmark",
      }),
    ],
  });

  await cleanup();
});

// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
// not be included in the results since it dupes the autofill result.
add_task(async function searchParams() {
  await PlacesTestUtils.addVisits([
    "http://example.com/",
    "http://example.com/?",
    "http://example.com/?foo",
  ]);

  // First, do a search with autofill disabled to make sure the visits were
  // properly added. `example.com/?foo` has the highest frecency because it was
  // added last; `example.com/?` has the next highest. `example.com/` dupes
  // `example.com/?`, so it should not appear.
  UrlbarPrefs.set("autoFill", false);
  let context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/?foo",
        title: "test visit for http://example.com/?foo",
      }),
      makeVisitResult(context, {
        uri: "http://example.com/?",
        title: "test visit for http://example.com/?",
      }),
    ],
  });

  // Now do a search with autofill enabled. This time `example.com/` will be
  // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
  // appear.
  UrlbarPrefs.clear("autoFill");
  context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: "http://example.com/",
    matches: [
      makeVisitResult(context, {
        uri: "http://example.com/",
        title: "test visit for http://example.com/",
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: "http://example.com/?foo",
        title: "test visit for http://example.com/?foo",
      }),
    ],
  });

  await cleanup();
});

// When the autofilled URL is `example.com/`, a visit for `example.com/?` should
// not be included in the results since it dupes the autofill result. (Same as
// the previous task but with https URLs instead of http. There shouldn't be any
// substantive difference.)
add_task(async function searchParams_https() {
  await PlacesTestUtils.addVisits([
    "https://example.com/",
    "https://example.com/?",
    "https://example.com/?foo",
  ]);

  // First, do a search with autofill disabled to make sure the visits were
  // properly added. `example.com/?foo` has the highest frecency because it was
  // added last; `example.com/?` has the next highest. `example.com/` dupes
  // `example.com/?`, so it should not appear.
  UrlbarPrefs.set("autoFill", false);
  let context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    matches: [
      makeSearchResult(context, {
        engineName: SUGGESTIONS_ENGINE_NAME,
        providerName: HEURISTIC_FALLBACK_PROVIDERNAME,
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: "https://example.com/?foo",
        title: "test visit for https://example.com/?foo",
      }),
      makeVisitResult(context, {
        uri: "https://example.com/?",
        title: "test visit for https://example.com/?",
      }),
    ],
  });

  // Now do a search with autofill enabled. This time `example.com/` will be
  // autofilled, and since `example.com/?` dupes it, `example.com/?` should not
  // appear.
  UrlbarPrefs.clear("autoFill");
  context = createContext("ex", { isPrivate: false });
  await check_results({
    context,
    autofilled: "example.com/",
    completed: "https://example.com/",
    matches: [
      makeVisitResult(context, {
        uri: "https://example.com/",
        title: "test visit for https://example.com/",
        heuristic: true,
      }),
      makeVisitResult(context, {
        uri: "https://example.com/?foo",
        title: "test visit for https://example.com/?foo",
      }),
    ],
  });

  await cleanup();
});

// Checks an origin that looks like a prefix: a scheme with no dots + a port.
add_task(async function originLooksLikePrefix() {
  let hostAndPort = "localhost:8888";
  let address = `http://${hostAndPort}/`;
  await PlacesTestUtils.addVisits([{ uri: address }]);

  // addTestSuggestionsEngine adds a search engine
  // with localhost as a server, so we have to disable the
  // TTS result or else it will show up as a second result
  // when searching l to localhost
  UrlbarPrefs.set("suggest.engines", false);

  for (let search of ["lo", "localhost", "localhost:", "localhost:8888"]) {
    let context = createContext(search, { isPrivate: false });
    await check_results({
      context,
      autofilled: hostAndPort + "/",
      completed: address,
      matches: [
        makeVisitResult(context, {
          uri: address,
          title: `test visit for http://${hostAndPort}/`,
          heuristic: true,
        }),
      ],
    });
  }
  await cleanup();
});

// Checks an origin whose prefix is "about:".
add_task(async function about() {
  const testData = [
    {
      uri: "about:config",
      input: "conf",
      results: [
        context =>
          makeSearchResult(context, {
            engineName: "Suggestions",
            heuristic: true,
          }),
        context =>
          makeBookmarkResult(context, {
            uri: "about:config",
            title: "A bookmark",
          }),
      ],
    },
    {
      uri: "about:blank",
      input: "about:blan",
      results: [
        context =>
          makeVisitResult(context, {
            uri: "about:blan",
            fallbackTitle: "about:blan",
            source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
            heuristic: true,
          }),
        context =>
          makeBookmarkResult(context, {
            uri: "about:blank",
            title: "A bookmark",
          }),
      ],
    },
  ];

  for (const { uri, input, results } of testData) {
    await PlacesTestUtils.addBookmarkWithDetails({ uri });

    const context = createContext(input, { isPrivate: false });
    await check_results({
      context,
      matches: results.map(f => f(context)),
    });
    await cleanup();
  }
});

// Checks an origin whose prefix is "place:".
add_task(async function place() {
  const testData = [
    {
      uri: "place:transition=7&sort=4",
      input: "tran",
    },
    {
      uri: "place:transition=7&sort=4",
      input: "place:tran",
    },
  ];

  for (const { uri, input } of testData) {
    await PlacesTestUtils.addBookmarkWithDetails({ uri });

    const context = createContext(input, { isPrivate: false });
    await check_results({
      context,
      matches: [
        makeSearchResult(context, {
          engineName: "Suggestions",
          heuristic: true,
        }),
      ],
    });
    await cleanup();
  }
});

add_task(async function nullTitle() {
  await doTitleTest({
    visits: [
      {
        uri: "http://example.com/",
        // Set title of visits data to an empty string causes
        // the title to be null in the database.
        title: "",
        frecency: 100,
      },
      {
        uri: "https://www.example.com/",
        title: "high frecency",
        frecency: 50,
      },
      {
        uri: "http://www.example.com/",
        title: "low frecency",
        frecency: 1,
      },
    ],
    input: "example.com",
    expected: {
      autofilled: "example.com/",
      completed: "http://example.com/",
      matches: context => [
        makeVisitResult(context, {
          uri: "http://example.com/",
          title: "high frecency",
          heuristic: true,
        }),
        makeVisitResult(context, {
          uri: "https://www.example.com/",
          title: "high frecency",
        }),
      ],
    },
  });
});

add_task(async function domainTitle() {
  await doTitleTest({
    visits: [
      {
        uri: "http://example.com/",
        title: "example.com",
        frecency: 100,
      },
      {
        uri: "https://www.example.com/",
        title: "",
        frecency: 50,
      },
      {
        uri: "http://www.example.com/",
        title: "lowest frecency but has title",
        frecency: 1,
      },
    ],
    input: "example.com",
    expected: {
      autofilled: "example.com/",
      completed: "http://example.com/",
      matches: context => [
        makeVisitResult(context, {
          uri: "http://example.com/",
          title: "lowest frecency but has title",
          heuristic: true,
        }),
        makeVisitResult(context, {
          uri: "https://www.example.com/",
          title: "www.example.com",
        }),
      ],
    },
  });
});

add_task(async function exactMatchedTitle() {
  await doTitleTest({
    visits: [
      {
        uri: "http://example.com/",
        title: "exact match",
        frecency: 50,
      },
      {
        uri: "https://www.example.com/",
        title: "high frecency uri",
        frecency: 100,
      },
    ],
    input: "http://example.com/",
    expected: {
      autofilled: "http://example.com/",
      completed: "http://example.com/",
      matches: context => [
        makeVisitResult(context, {
          uri: "http://example.com/",
          title: "exact match",
          heuristic: true,
        }),
        makeVisitResult(context, {
          uri: "https://www.example.com/",
          title: "high frecency uri",
        }),
      ],
    },
  });
});

async function doTitleTest({ visits, input, expected }) {
  await PlacesTestUtils.addVisits(visits);
  for (const { uri, frecency } of visits) {
    // Prepare data.
    await PlacesUtils.withConnectionWrapper("test::doTitleTest", async db => {
      await db.execute(
        `UPDATE moz_places SET frecency = :frecency, recalc_frecency=0 WHERE url = :url`,
        {
          frecency,
          url: uri,
        }
      );
      await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
    });
  }

  const context = createContext(input, { isPrivate: false });
  await check_results({
    context,
    autofilled: expected.autofilled,
    completed: expected.completed,
    matches: expected.matches(context),
  });

  await cleanup();
}

/* Tests sorting order when only unvisited bookmarks are available (e.g. in
  permanent private browsing mode), then the only information we have is the
  number of bookmarks per origin, and we're going to use that. */
add_task(async function just_multiple_unvisited_bookmarks() {
  // These are sorted to avoid confusion with natural sorting, so the one with
  // the highest score is added in the middle.
  let filledUrl = "https://www.tld2.com/";
  let urls = [
    {
      url: "https://tld1.com/",
      count: 1,
    },
    {
      url: "https://tld2.com/",
      count: 2,
    },
    {
      url: filledUrl,
      count: 2,
    },
    {
      url: "https://tld3.com/",
      count: 3,
    },
  ];

  await PlacesUtils.history.clear();
  for (let { url, count } of urls) {
    while (count--) {
      await PlacesTestUtils.addBookmarkWithDetails({
        uri: url,
      });
    }
  }
  await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();

  let context = createContext("tld", { isPrivate: false });
  await check_results({
    context,
    autofilled: "tld2.com/",
    completed: filledUrl,
    matches: [
      makeVisitResult(context, {
        uri: filledUrl,
        title: "A bookmark",
        heuristic: true,
      }),
      makeBookmarkResult(context, {
        uri: "https://tld3.com/",
        title: "A bookmark",
      }),
      makeBookmarkResult(context, {
        uri: "https://tld2.com/",
        title: "A bookmark",
      }),
      makeBookmarkResult(context, {
        uri: "https://tld1.com/",
        title: "A bookmark",
      }),
    ],
  });

  await cleanup();
});
