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

"use strict";

const lazy = {};

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

let sandbox;

add_setup(async function () {
  sandbox = lazy.sinon.createSandbox();
  registerCleanupFunction(() => {
    sandbox.restore();
  });
});

add_task(async function test_muxer() {
  let providersManager = ProvidersManager.getInstanceForSap("urlbar");
  Assert.throws(
    () => providersManager.registerMuxer(),
    /invalid muxer/,
    "Should throw with no arguments"
  );
  Assert.throws(
    () => providersManager.registerMuxer({}),
    /invalid muxer/,
    "Should throw with empty object"
  );
  Assert.throws(
    () =>
      providersManager.registerMuxer({
        name: "",
      }),
    /invalid muxer/,
    "Should throw with empty name"
  );
  Assert.throws(
    () =>
      providersManager.registerMuxer({
        name: "test",
        sort: "no",
      }),
    /invalid muxer/,
    "Should throw with invalid sort"
  );

  let matches = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
      source: UrlbarUtils.RESULT_SOURCE.TABS,
      payload: { url: "http://mozilla.org/tab/" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
      payload: { url: "http://mozilla.org/bookmark/" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/history/" },
    }),
  ];

  let provider = registerBasicTestProvider(matches);
  let context = createContext(undefined, { providers: [provider.name] });
  let controller = UrlbarTestUtils.newMockController();
  /**
   * A test muxer.
   */
  class TestMuxer extends UrlbarMuxer {
    get name() {
      return "TestMuxer";
    }
    sort(queryContext, unsortedResults) {
      queryContext.results = [...unsortedResults].sort((a, b) => {
        if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) {
          return -1;
        }
        if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
          return 1;
        }
        return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1;
      });
    }
  }
  let muxer = new TestMuxer();

  providersManager.registerMuxer(muxer);
  context.muxer = "TestMuxer";

  info("Check results, the order should be: bookmark, history, tab");
  await providersManager.startQuery(context, controller);
  Assert.deepEqual(context.results, [matches[1], matches[2], matches[0]]);

  // Sanity check, should not throw.
  providersManager.unregisterMuxer(muxer);
  providersManager.unregisterMuxer("TestMuxer"); // no-op.
});

add_task(async function test_preselectedHeuristic_singleProvider() {
  let matches = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/a" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      payload: { url: "http://mozilla.org/b" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/c" },
    }),
  ];

  let provider = registerBasicTestProvider(matches);
  let context = createContext(undefined, {
    providers: [provider.name],
  });
  let controller = UrlbarTestUtils.newMockController();

  info("Check results, the order should be: b (heuristic), a, c");
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );
  Assert.deepEqual(context.results, [matches[1], matches[0], matches[2]]);
});

add_task(async function test_preselectedHeuristic_multiProviders() {
  let matches1 = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/a" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/b" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/c" },
    }),
  ];

  let matches2 = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/d" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      payload: { url: "http://mozilla.org/e" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/f" },
    }),
  ];

  let provider1 = registerBasicTestProvider(matches1);
  let provider2 = registerBasicTestProvider(matches2);

  let context = createContext(undefined, {
    providers: [provider1.name, provider2.name],
  });
  let controller = UrlbarTestUtils.newMockController();

  info("Check results, the order should be: e (heuristic), a, b, c, d, f");
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );
  Assert.deepEqual(context.results, [
    matches2[1],
    ...matches1,
    matches2[0],
    matches2[2],
  ]);
});

add_task(async function test_suggestions() {
  Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);

  let matches = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/a" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/b" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: {
        engine: "mozSearch",
        query: "moz",
        suggestion: "mozzarella",
        lowerCaseSuggestion: "mozzarella",
      },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      payload: {
        engine: "mozSearch",
        query: "moz",
        suggestion: "mozilla",
        lowerCaseSuggestion: "mozilla",
      },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      payload: {
        engine: "mozSearch",
        query: "moz",
        providesSearchMode: true,
        keyword: "@moz",
      },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://mozilla.org/c" },
    }),
  ];

  let provider = registerBasicTestProvider(matches);

  let context = createContext(undefined, {
    providers: [provider.name],
  });
  let controller = UrlbarTestUtils.newMockController();

  info("Check results, the order should be: mozzarella, moz, a, b, @moz, c");
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );
  Assert.deepEqual(context.results, [
    matches[2],
    matches[3],
    matches[0],
    matches[1],
    matches[4],
    matches[5],
  ]);

  Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
});

add_task(async function test_deduplicate_for_unitConversion() {
  const searchSuggestion = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.SEARCH,
    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    payload: {
      engine: "Google",
      query: "10cm to m",
      suggestion: "= 0.1 meters",
    },
  });
  const searchProvider = registerBasicTestProvider(
    [searchSuggestion],
    null,
    UrlbarUtils.PROVIDER_TYPE.PROFILE
  );

  const unitConversionSuggestion = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    suggestedIndex: 1,
    payload: {
      dynamicType: "unitConversion",
      output: "0.1 m",
      input: "10cm to m",
    },
  });

  const unitConversion = registerBasicTestProvider(
    [unitConversionSuggestion],
    null,
    UrlbarUtils.PROVIDER_TYPE.PROFILE,
    "UrlbarProviderUnitConversion"
  );

  const context = createContext(undefined, {
    providers: [searchProvider.name, unitConversion.name],
  });
  const controller = UrlbarTestUtils.newMockController();
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );
  Assert.deepEqual(context.results, [unitConversionSuggestion]);
});

// These results are used in the badHeuristicGroups tests below.  The order of
// the results in the array isn't important because they all get added at the
// same time.  It's the resultGroups in each test that is important.
const BAD_HEURISTIC_RESULTS = [
  // heuristic
  new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    heuristic: true,
    payload: { url: "http://mozilla.org/heuristic-0" },
  }),
  // heuristic
  new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    heuristic: true,
    payload: { url: "http://mozilla.org/heuristic-1" },
  }),
  // non-heuristic
  new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    payload: { url: "http://mozilla.org/non-heuristic-0" },
  }),
  // non-heuristic
  new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    payload: { url: "http://mozilla.org/non-heuristic-1" },
  }),
];

const BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC = BAD_HEURISTIC_RESULTS[0];
const BAD_HEURISTIC_RESULTS_GENERAL = [
  BAD_HEURISTIC_RESULTS[2],
  BAD_HEURISTIC_RESULTS[3],
];

add_task(async function test_badHeuristicGroups_multiple_0() {
  await doBadHeuristicGroupsTest(
    [
      // 2 heuristics with child groups
      {
        maxResultCount: 2,
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_1() {
  await doBadHeuristicGroupsTest(
    [
      // infinite heuristics with child groups
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_2() {
  await doBadHeuristicGroupsTest(
    [
      // 2 heuristics
      {
        maxResultCount: 2,
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_3() {
  await doBadHeuristicGroupsTest(
    [
      // infinite heuristics
      {
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_4() {
  await doBadHeuristicGroupsTest(
    [
      // 1 heuristic with child groups
      {
        maxResultCount: 1,
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // 1 heuristic with child groups
      {
        maxResultCount: 1,
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_5() {
  await doBadHeuristicGroupsTest(
    [
      // infinite heuristics with child groups
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // infinite heuristics with child groups
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_6() {
  await doBadHeuristicGroupsTest(
    [
      // 1 heuristic
      {
        maxResultCount: 1,
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // 1 heuristic
      {
        maxResultCount: 1,
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicGroups_multiple_7() {
  await doBadHeuristicGroupsTest(
    [
      // infinite heuristics
      {
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
      // infinite general
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // infinite heuristics
      {
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
    ],
    [BAD_HEURISTIC_RESULTS_FIRST_HEURISTIC, ...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicsGroups_notFirst_0() {
  await doBadHeuristicGroupsTest(
    [
      // infinite general first
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // 1 heuristic with child groups second
      {
        maxResultCount: 1,
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
    ],
    [...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicsGroups_notFirst_1() {
  await doBadHeuristicGroupsTest(
    [
      // infinite general first
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // infinite heuristics with child groups second
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST }],
      },
    ],
    [...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicsGroups_notFirst_2() {
  await doBadHeuristicGroupsTest(
    [
      // infinite general first
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // 1 heuristic second
      {
        maxResultCount: 1,
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
    ],
    [...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicsGroups_notFirst_3() {
  await doBadHeuristicGroupsTest(
    [
      // infinite general first
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // infinite heuristics second
      {
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
    ],
    [...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

add_task(async function test_badHeuristicsGroups_notFirst_4() {
  await doBadHeuristicGroupsTest(
    [
      // 1 general first
      {
        maxResultCount: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      // infinite heuristics second
      {
        group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST,
      },
      // infinite general third
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
    [...BAD_HEURISTIC_RESULTS_GENERAL]
  );
});

/**
 * Sets the resultGroups pref, performs a search, and then checks the results.
 * Regardless of the groups, the muxer should include at most one heuristic in
 * its results and it should always be the first result.
 *
 * @param {Array} resultGroups
 *   The result groups.
 * @param {Array} expectedResults
 *   The expected results.
 */
async function doBadHeuristicGroupsTest(resultGroups, expectedResults) {
  sandbox
    .stub(UrlbarPrefs, "getResultGroups")
    .returns({ children: resultGroups });

  let provider = registerBasicTestProvider(BAD_HEURISTIC_RESULTS);
  let context = createContext("foo", { providers: [provider.name] });
  let controller = UrlbarTestUtils.newMockController();
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );
  Assert.deepEqual(context.results, expectedResults);

  sandbox.restore();
}

// When `maxRichResults` is positive and taken up by suggested-index result(s),
// both the heuristic and suggested-index results should be included because we
// (a) make room for the heuristic and (b) assume all suggested-index results
// should be included even if it means exceeding `maxRichResults`. The specified
// `maxRichResults` span will be exceeded in this case.
add_task(async function roomForHeuristic_suggestedIndex() {
  let results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      payload: { url: "http://example.com/heuristic" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      suggestedIndex: 1,
      payload: { url: "http://example.com/suggestedIndex" },
    }),
  ];

  UrlbarPrefs.set("maxRichResults", 1);

  let provider = registerBasicTestProvider(results);
  let context = createContext(undefined, { providers: [provider.name] });
  await check_results({
    context,
    matches: results,
  });

  UrlbarPrefs.clear("maxRichResults");
});

// When `maxRichResults` is positive but less than the heuristic's result span,
// the heuristic should be included because we make room for it even if it means
// exceeding `maxRichResults`. The specified `maxRichResults` span will be
// exceeded in this case.
add_task(async function roomForHeuristic_largeResultSpan() {
  let results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      resultSpan: 2,
      payload: { url: "http://example.com/heuristic" },
    }),
  ];

  UrlbarPrefs.set("maxRichResults", 1);

  let provider = registerBasicTestProvider(results);
  let context = createContext(undefined, { providers: [provider.name] });
  await check_results({
    context,
    matches: results,
  });

  UrlbarPrefs.clear("maxRichResults");
});

// When `maxRichResults` is zero and there are no suggested-index results, the
// heuristic should not be included.
add_task(async function roomForHeuristic_maxRichResultsZero() {
  let results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      payload: { url: "http://example.com/heuristic" },
    }),
  ];

  UrlbarPrefs.set("maxRichResults", 0);

  let provider = registerBasicTestProvider(results);
  let context = createContext(undefined, { providers: [provider.name] });
  await check_results({
    context,
    matches: [],
  });

  UrlbarPrefs.clear("maxRichResults");
});

// When `maxRichResults` is zero and suggested-index results are present,
// neither the heuristic nor the suggested-index results should be included.
add_task(async function roomForHeuristic_maxRichResultsZero_suggestedIndex() {
  let results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      heuristic: true,
      payload: { url: "http://example.com/heuristic" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      suggestedIndex: 1,
      payload: { url: "http://example.com/suggestedIndex" },
    }),
  ];

  UrlbarPrefs.set("maxRichResults", 0);

  let provider = registerBasicTestProvider(results);
  let context = createContext(undefined, { providers: [provider.name] });
  await check_results({
    context,
    matches: [],
  });

  UrlbarPrefs.clear("maxRichResults");
});

add_task(async function test_orderBy() {
  // The GENERAL groups has an orderBy property, so let's just add to history.
  let results1 = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://example.com/test1", frecency: 10 },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://example.com/test2", frecency: 1000 },
    }),
  ];
  let provider1 = registerBasicTestProvider(results1);
  let results2 = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url: "http://example.com/test3", frecency: 100 },
    }),
  ];
  let provider2 = registerBasicTestProvider(results2);

  let context = createContext(undefined, {
    providers: [provider1.name, provider2.name],
  });
  await check_results({
    context,
    matches: [
      results1[1], // 1000
      results2[0], // 100
      results1[0], // 10
    ],
  });
});

// A result returned by both the semantic history provider and a non-semantic
// provider for the same page should be deduped down to the non-semantic
// result, regardless of which provider's result is added first and regardless
// of scheme/www differences or result type (URL vs switch-to-tab).
async function checkSemanticDedupe({
  semanticFirst,
  semanticUrl = "https://example.com/foo/",
  nonSemanticUrl = "https://example.com/foo/",
  semanticType = UrlbarUtils.RESULT_TYPE.URL,
  nonSemanticType = UrlbarUtils.RESULT_TYPE.URL,
  description = "",
}) {
  let makeMatch = (type, url) =>
    new UrlbarResult({
      type,
      source:
        type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH
          ? UrlbarUtils.RESULT_SOURCE.TABS
          : UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url, title: "Example Page" },
    });
  let semanticMatch = makeMatch(semanticType, semanticUrl);
  let nonSemanticMatch = makeMatch(nonSemanticType, nonSemanticUrl);

  let providersManager = ProvidersManager.getInstanceForSap("urlbar");
  // `unregisterProvider` matches by name, so clear any strays left under the
  // semantic name by a previous run for a clean slate.
  for (let p of [...providersManager.providers]) {
    if (p.name == "UrlbarProviderSemanticHistorySearch") {
      providersManager.unregisterProvider(p);
    }
  }

  // Register the providers in the order under test so the matching result is
  // processed first by the muxer.
  let semanticProvider, nonSemanticProvider;
  let registerSemantic = () =>
    (semanticProvider = registerBasicTestProvider(
      [semanticMatch],
      undefined,
      undefined,
      "UrlbarProviderSemanticHistorySearch"
    ));
  let registerNonSemantic = () =>
    (nonSemanticProvider = registerBasicTestProvider([nonSemanticMatch]));
  if (semanticFirst) {
    registerSemantic();
    registerNonSemantic();
  } else {
    registerNonSemantic();
    registerSemantic();
  }

  let context = createContext(undefined, {
    providers: semanticFirst
      ? [semanticProvider.name, nonSemanticProvider.name]
      : [nonSemanticProvider.name, semanticProvider.name],
  });
  let controller = UrlbarTestUtils.newMockController();
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    controller
  );

  Assert.deepEqual(
    context.results,
    [nonSemanticMatch],
    `Only the non-semantic result should survive (${description}semanticFirst=${semanticFirst})`
  );

  providersManager.unregisterProvider(semanticProvider);
  providersManager.unregisterProvider(nonSemanticProvider);
}

add_task(async function test_dedupe_semantic_seen_first() {
  await checkSemanticDedupe({ semanticFirst: true });
});

add_task(async function test_dedupe_nonSemantic_seen_first() {
  await checkSemanticDedupe({ semanticFirst: false });
});

// The semantic and non-semantic results may have the same stripped URL but
// different schemes (http vs https). The non-semantic result should still win.
add_task(async function test_dedupe_semantic_scheme_mismatch() {
  for (let semanticFirst of [true, false]) {
    await checkSemanticDedupe({
      semanticFirst,
      semanticUrl: "https://example.com/foo/",
      nonSemanticUrl: "http://example.com/foo/",
      description: "scheme mismatch, ",
    });
  }
});

// Same as above but the prefixes differ by www. The non-semantic result wins.
add_task(async function test_dedupe_semantic_www_mismatch() {
  for (let semanticFirst of [true, false]) {
    await checkSemanticDedupe({
      semanticFirst,
      semanticUrl: "https://www.example.com/foo/",
      nonSemanticUrl: "https://example.com/foo/",
      description: "www mismatch, ",
    });
  }
});

// A semantic switch-to-tab result that dupes a non-semantic switch-to-tab
// result for the same open tab should be deduped to the non-semantic one.
add_task(async function test_dedupe_semantic_tab_switch() {
  for (let semanticFirst of [true, false]) {
    await checkSemanticDedupe({
      semanticFirst,
      semanticUrl: "https://example.com/tab/",
      nonSemanticUrl: "https://example.com/tab/",
      semanticType: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
      nonSemanticType: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
      description: "tab switch, ",
    });
  }
});

// A semantic result with no non-semantic counterpart must not be suppressed,
// whether it's a plain URL or a switch-to-tab result.
add_task(async function test_semantic_only_survives() {
  let providersManager = ProvidersManager.getInstanceForSap("urlbar");
  for (let p of [...providersManager.providers]) {
    if (p.name == "UrlbarProviderSemanticHistorySearch") {
      providersManager.unregisterProvider(p);
    }
  }

  let urlMatch = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    payload: { url: "https://semantic-only.example.com/page/", title: "Page" },
  });
  let tabMatch = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
    source: UrlbarUtils.RESULT_SOURCE.TABS,
    payload: { url: "https://semantic-only.example.com/tab/", title: "Tab" },
  });
  let semanticProvider = registerBasicTestProvider(
    [tabMatch, urlMatch],
    undefined,
    undefined,
    "UrlbarProviderSemanticHistorySearch"
  );

  let context = createContext(undefined, {
    providers: [semanticProvider.name],
  });
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    UrlbarTestUtils.newMockController()
  );

  Assert.deepEqual(
    context.results.map(r => r.payload.url).sort(),
    [tabMatch.payload.url, urlMatch.payload.url].sort(),
    "Semantic results with no non-semantic counterpart are kept"
  );

  providersManager.unregisterProvider(semanticProvider);
});

// Two semantic results that are prefix variants of the same page (and have no
// non-semantic counterpart) should still be deduped to a single result by the
// normal prefix-rank logic, since only suppressed semantic dupes are excluded
// from that bookkeeping.
add_task(async function test_dedupe_two_semantic_prefixes() {
  let providersManager = ProvidersManager.getInstanceForSap("urlbar");
  for (let p of [...providersManager.providers]) {
    if (p.name == "UrlbarProviderSemanticHistorySearch") {
      providersManager.unregisterProvider(p);
    }
  }

  let httpsMatch = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    payload: { url: "https://example.com/foo/", title: "Example Page" },
  });
  let httpMatch = new UrlbarResult({
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.HISTORY,
    payload: { url: "http://example.com/foo/", title: "Example Page" },
  });
  let semanticProvider = registerBasicTestProvider(
    [httpsMatch, httpMatch],
    undefined,
    undefined,
    "UrlbarProviderSemanticHistorySearch"
  );

  let context = createContext(undefined, {
    providers: [semanticProvider.name],
  });
  await ProvidersManager.getInstanceForSap("urlbar").startQuery(
    context,
    UrlbarTestUtils.newMockController()
  );

  Assert.deepEqual(
    context.results,
    [httpsMatch],
    "Only the highest-prefix-rank semantic variant should survive"
  );

  providersManager.unregisterProvider(semanticProvider);
});

// When the separateGroup setting is enabled with semantic seearch, semantic results
// share the general area with non-semantic history at a 9:1 ratio: at most ~1 in every
// 10 of these slots is semantic, semantic results are ordered by frecency, and
// they fill the remaining space (without evicting) when there's little other
// history.
//
// The ratio 9:1 ratio enables one sementic result to be visible in the history view
// when there are many semantic results.

add_task(async function test_semantic_history_separate_group_ratio() {
  // The separate semantic group only exists when this pref is enabled.
  UrlbarPrefs.set("suggest.semanticHistory.separateGroup", true);

  let providersManager = ProvidersManager.getInstanceForSap("urlbar");
  // `unregisterProvider` matches by name, and earlier tests register providers
  // under the same semantic name, so clear any strays for a clean slate.
  for (let p of [...providersManager.providers]) {
    if (p.name == "UrlbarProviderSemanticHistorySearch") {
      providersManager.unregisterProvider(p);
    }
  }

  let makeUrlResult = (url, frecency) =>
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.HISTORY,
      payload: { url, title: "Page", frecency },
    });
  let isSemantic = r => r.providerName == "UrlbarProviderSemanticHistorySearch";

  // 12 general results, frecency descending, distinct URLs so nothing dedupes.
  let exactMatches = Array.from({ length: 12 }, (_, i) =>
    makeUrlResult(`https://example.com/exact${i}`, 120 - i * 10)
  );
  let semanticMatches = [
    makeUrlResult("https://example.com/semantic1", 50),
    makeUrlResult("https://example.com/semantic2", 200),
    makeUrlResult("https://example.com/semantic3", 100),
  ];

  let exactProvider = registerBasicTestProvider(exactMatches);
  let semanticProvider = registerBasicTestProvider(
    semanticMatches,
    undefined,
    undefined,
    "UrlbarProviderSemanticHistorySearch"
  );

  // Typical search results with competing search engine resuls. Only room for 3 history related
  // results, so semantic results are supprressed to keep from evicting an exact match result
  let limitedResultsContext = createContext(undefined, {
    providers: [exactProvider.name, semanticProvider.name],
    maxResults: 3,
  });
  await providersManager.startQuery(
    limitedResultsContext,
    UrlbarTestUtils.newMockController()
  );
  Assert.equal(limitedResultsContext.results.length, 3, "Fills the budget");
  Assert.equal(
    limitedResultsContext.results.filter(isSemantic).length,
    0,
    "At no semantic results are shown"
  );
  // History view with with lots of of competing history: the general area holds 10,
  //  so the 9:1 split yields 9 general + 1 semantic.
  let historyViewContext = createContext(undefined, {
    providers: [exactProvider.name, semanticProvider.name],
    maxResults: 10,
  });
  await providersManager.startQuery(
    historyViewContext,
    UrlbarTestUtils.newMockController()
  );
  Assert.equal(historyViewContext.results.length, 10, "Fills the budget");
  Assert.equal(
    historyViewContext.results.filter(isSemantic).length,
    1,
    "At most 1 in 10 results is semantic"
  );
  Assert.deepEqual(
    historyViewContext.results.slice(0, 9),
    exactMatches.slice(0, 9),
    "The 9 highest-frecency general results are kept, ordered by frecency"
  );
  Assert.equal(
    historyViewContext.results[9],
    semanticMatches[1],
    "The shown semantic result is the highest-frecency one (sorted by frecency)"
  );

  // Little competing history: semantic fills the leftover space without
  // evicting the general results.
  let fewExactProvider = registerBasicTestProvider(exactMatches.slice(0, 2));
  let sparseContext = createContext(undefined, {
    providers: [fewExactProvider.name, semanticProvider.name],
    maxResults: 10,
  });
  await providersManager.startQuery(
    sparseContext,
    UrlbarTestUtils.newMockController()
  );
  Assert.equal(
    sparseContext.results.filter(r => !isSemantic(r)).length,
    2,
    "Both general results are kept (not evicted)"
  );
  Assert.equal(
    sparseContext.results.filter(isSemantic).length,
    3,
    "Semantic results fill the leftover space"
  );
  Assert.deepEqual(
    sparseContext.results.filter(isSemantic),
    [semanticMatches[1], semanticMatches[2], semanticMatches[0]],
    "Multiple shown semantic results are ordered by frecency (descending)"
  );

  providersManager.unregisterProvider(exactProvider);
  providersManager.unregisterProvider(fewExactProvider);
  providersManager.unregisterProvider(semanticProvider);
  UrlbarPrefs.clear("suggest.semanticHistory.separateGroup");
});
