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

// Tests the muxer's result groups composition logic: child groups,
// `availableSpan`, `maxResultCount`, flex, etc. The purpose of this test is to
// check the composition logic, not every possible result type or group.

"use strict";

const lazy = {};

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

// The possible limit-related properties in result groups.
const LIMIT_KEYS = ["availableSpan", "maxResultCount"];

// Most of this test adds tasks using `add_resultGroupsLimit_tasks`. It works
// like this. Instead of defining `maxResultCount` or `availableSpan` in their
// result groups, tasks define a `limit` property. The value of this property is
// a number just like any of the values for the limit-related properties. At
// runtime, `add_resultGroupsLimit_tasks` adds multiple tasks, one for each key
// in `LIMIT_KEYS`. In each of these tasks, the `limit` property is replaced
// with the actual limit key. This allows us to run checks against each of the
// limit keys using essentially the same task.

const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";

// For simplicity, most of the flex tests below assume that this is 10, so
// you'll need to update them if you change this.
const MAX_RESULTS = 10;

let sandbox;

add_task(async function setup() {
  // Set a specific maxRichResults for sanity's sake.
  Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, MAX_RESULTS);

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

add_resultGroupsLimit_tasks({
  testName: "empty root",
  resultGroups: {},
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [],
});

add_resultGroupsLimit_tasks({
  testName: "root with empty children",
  resultGroups: {
    children: [],
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [],
});

add_resultGroupsLimit_tasks({
  testName: "root no match",
  resultGroups: {
    group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [],
});

add_resultGroupsLimit_tasks({
  testName: "children no match",
  resultGroups: {
    children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [],
});

add_resultGroupsLimit_tasks({
  // The actual max result count on the root is always context.maxResults and
  // limit is ignored, so we expect the result in this case.
  testName: "root limit: 0",
  resultGroups: {
    limit: 0,
    group: UrlbarUtils.RESULT_GROUP.GENERAL,
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  // The actual max result count on the root is always context.maxResults and
  // limit is ignored, so we expect the result in this case.
  testName: "root limit: 0 with children",
  resultGroups: {
    limit: 0,
    children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "child limit: 0",
  resultGroups: {
    children: [
      {
        limit: 0,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [],
});

add_resultGroupsLimit_tasks({
  testName: "root group",
  resultGroups: {
    group: UrlbarUtils.RESULT_GROUP.GENERAL,
  },
  providerResults: [...makeHistoryResults(1)],
  expectedResultIndexes: [...makeIndexRange(0, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "root group multiple",
  resultGroups: {
    group: UrlbarUtils.RESULT_GROUP.GENERAL,
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "child group multiple",
  resultGroups: {
    children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0, 1],
});

add_resultGroupsLimit_tasks({
  testName: "simple limit",
  resultGroups: {
    children: [
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "limit siblings",
  resultGroups: {
    children: [
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0, 1],
});

add_resultGroupsLimit_tasks({
  testName: "limit nested",
  resultGroups: {
    children: [
      {
        limit: 1,
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "limit nested siblings",
  resultGroups: {
    children: [
      {
        limit: 1,
        children: [
          { group: UrlbarUtils.RESULT_GROUP.GENERAL },
          { group: UrlbarUtils.RESULT_GROUP.GENERAL },
        ],
      },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "limit nested uncle",
  resultGroups: {
    children: [
      {
        limit: 1,
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0, 1],
});

add_resultGroupsLimit_tasks({
  testName: "limit nested override bad",
  resultGroups: {
    children: [
      {
        limit: 1,
        children: [
          {
            limit: 99,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "limit nested override good",
  resultGroups: {
    children: [
      {
        limit: 99,
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [...makeHistoryResults(2)],
  expectedResultIndexes: [0],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups",
  resultGroups: {
    children: [
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
      { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups limit 1",
  resultGroups: {
    children: [
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups limit 2",
  resultGroups: {
    children: [
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups limit 3",
  resultGroups: {
    children: [
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [
    ...makeIndexRange(2, 1),
    ...makeIndexRange(0, 2),
    ...makeIndexRange(3, 1),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups limit 4",
  resultGroups: {
    children: [
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
      {
        limit: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested 1",
  resultGroups: {
    children: [
      {
        children: [
          { group: UrlbarUtils.RESULT_GROUP.GENERAL },
          { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested 2",
  resultGroups: {
    children: [
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 1",
  resultGroups: {
    children: [
      {
        limit: 1,
        children: [
          { group: UrlbarUtils.RESULT_GROUP.GENERAL },
          { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 2",
  resultGroups: {
    children: [
      {
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 1), ...makeIndexRange(0, 2)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 3",
  resultGroups: {
    children: [
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
      {
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 4",
  resultGroups: {
    children: [
      {
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION }],
      },
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [
    ...makeIndexRange(2, 1),
    ...makeIndexRange(0, 2),
    ...makeIndexRange(3, 1),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 5",
  resultGroups: {
    children: [
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
      {
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        children: [{ group: UrlbarUtils.RESULT_GROUP.GENERAL }],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [...makeIndexRange(2, 2), ...makeIndexRange(0, 1)],
});

add_resultGroupsLimit_tasks({
  testName: "multiple groups nested limit 6",
  resultGroups: {
    children: [
      {
        children: [
          {
            limit: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
      { group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION },
      { group: UrlbarUtils.RESULT_GROUP.GENERAL },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(2),
    ...makeHistoryResults(2),
  ],
  expectedResultIndexes: [
    ...makeIndexRange(2, 1),
    ...makeIndexRange(0, 2),
    ...makeIndexRange(3, 1),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 1",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (1 / (1 + 1))) = 5
    ...makeIndexRange(MAX_RESULTS, 5),
    // remote suggestions: round(10 * (1 / (1 + 1))) = 5
    ...makeIndexRange(0, 5),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 2",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (2 / 3)) = 7
    ...makeIndexRange(MAX_RESULTS, 7),
    // remote suggestions: round(10 * (1 / 3)) = 3
    ...makeIndexRange(0, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 3",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (1 / 3)) = 3
    ...makeIndexRange(MAX_RESULTS, 3),
    // remote suggestions: round(10 * (2 / 3)) = 7
    ...makeIndexRange(0, 7),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 4",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (1 / 3)) = 3, and then incremented to 4 so
    // that the total result span is 10 instead of 9. This group is incremented
    // because the fractional part of its unrounded ideal max result count is
    // 0.33 (since 10 * (1 / 3) = 3.33), the same as the other two groups, and
    // this group is first.
    ...makeIndexRange(2 * MAX_RESULTS, 4),
    // remote suggestions: round(10 * (1 / 3)) = 3
    ...makeIndexRange(MAX_RESULTS, 3),
    // form history: round(10 * (1 / 3)) = 3
    // The first three form history results dupe the three remote suggestions,
    // so they should not be included.
    ...makeIndexRange(3, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 5",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (2 / 4)) = 5
    ...makeIndexRange(2 * MAX_RESULTS, 5),
    // remote suggestions: round(10 * (1 / 4)) = 3
    ...makeIndexRange(MAX_RESULTS, 3),
    // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
    // The first three form history results dupe the three remote suggestions,
    // so they should not be included.
    ...makeIndexRange(3, 2),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 6",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (1 / 4)) = 3
    ...makeIndexRange(2 * MAX_RESULTS, 3),
    // remote suggestions: round(10 * (2 / 4)) = 5
    ...makeIndexRange(MAX_RESULTS, 5),
    // form history: round(10 * (1 / 4)) = 3, but context.maxResults is 10, so 2
    // The first five form history results dupe the five remote suggestions, so
    // they should not be included.
    ...makeIndexRange(5, 2),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex 7",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (1 / 4)) = 3
    ...makeIndexRange(2 * MAX_RESULTS, 3),
    // remote suggestions: round(10 * (1 / 4)) = 3, and then decremented to 2 so
    // that the total result span is 10 instead of 11. This group is decremented
    // because the fractional part of its unrounded ideal max result count is
    // 0.5 (since 10 * (1 / 4) = 2.5), the same as the previous group, and the
    // next group's fractional part is zero.
    ...makeIndexRange(MAX_RESULTS, 2),
    // form history: round(10 * (2 / 4)) = 5
    // The first 2 form history results dupe the three remote suggestions, so
    // they should not be included.
    ...makeIndexRange(2, 5),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex overfill 1",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (2 / (2 + 0 + 1))) = 7
    ...makeIndexRange(MAX_RESULTS, 7),
    // form history: round(10 * (1 / (2 + 0 + 1))) = 3
    ...makeIndexRange(0, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex overfill 2",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeRemoteSuggestionResults(1),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(9 * (2 / (2 + 0 + 1))) = 6
    ...makeIndexRange(MAX_RESULTS + 1, 6),
    // remote suggestions
    ...makeIndexRange(MAX_RESULTS, 1),
    // form history: round(9 * (1 / (2 + 0 + 1))) = 3
    // The first form history result dupes the remote suggestion, so it should
    // not be included.
    ...makeIndexRange(1, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested limit 1",
  resultGroups: {
    children: [
      {
        limit: 5,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(5 * (2 / (2 + 1))) = 3
    ...makeIndexRange(MAX_RESULTS, 3),
    // remote suggestions: round(5 * (1 / (2 + 1))) = 2
    ...makeIndexRange(0, 2),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested limit 2",
  resultGroups: {
    children: [
      {
        limit: 7,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general: round(7 * (1 / (2 + 1))) = 2
    ...makeIndexRange(MAX_RESULTS, 2),
    // remote suggestions: round(7 * (2 / (2 + 1))) = 5
    ...makeIndexRange(0, 5),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested limit 3",
  resultGroups: {
    children: [
      {
        limit: 7,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        limit: 3,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general: round(7 * (1 / (2 + 1))) = 2
    ...makeIndexRange(2 * MAX_RESULTS, 2),
    // remote suggestions: round(7 * (2 / (2 + 1))) = 5
    ...makeIndexRange(0, 5),
    // form history: round(3 * (2 / (2 + 1))) = 2
    // The first five form history results dupe the five remote suggestions, so
    // they should not be included.
    ...makeIndexRange(MAX_RESULTS + 5, 2),
    // general: round(3 * (1 / (2 + 1))) = 1
    ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested limit 4",
  resultGroups: {
    children: [
      {
        limit: 7,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        limit: 3,
        children: [
          { group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY },
          { group: UrlbarUtils.RESULT_GROUP.GENERAL },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general: round(7 * (1 / (2 + 1))) = 2
    ...makeIndexRange(2 * MAX_RESULTS, 2),
    // remote suggestions: round(7 * (2 / (2 + 1))) = 5
    ...makeIndexRange(0, 5),
    // form history: round(3 * (2 / (2 + 1))) = 2
    // The first five form history results dupe the five remote suggestions, so
    // they should not be included.
    ...makeIndexRange(MAX_RESULTS + 5, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested limit 5",
  resultGroups: {
    children: [
      {
        limit: 7,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        limit: 3,
        group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general: round(7 * (1 / (2 + 1))) = 2
    ...makeIndexRange(2 * MAX_RESULTS, 2),
    // remote suggestions: round(7 * (2 / (2 + 1))) = 5
    ...makeIndexRange(0, 5),
    // form history: round(3 * (2 / (2 + 1))) = 2
    // The first five form history results dupe the five remote suggestions, so
    // they should not be included.
    ...makeIndexRange(MAX_RESULTS + 5, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        flex: 1,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeFormHistoryResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
    // inner 1: general: round(7 * (1 / (2 + 1))) = 2
    ...makeIndexRange(2 * MAX_RESULTS, 2),
    // inner 2: remote suggestions: round(7 * (2 / (2 + 1))) = 5
    ...makeIndexRange(0, 5),

    // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
    // inner 1: form history: round(3 * (2 / (2 + 1))) = 2
    // The first five form history results dupe the five remote suggestions, so
    // they should not be included.
    ...makeIndexRange(MAX_RESULTS + 5, 2),
    // inner 2: general: round(3 * (1 / (2 + 1))) = 1
    ...makeIndexRange(2 * MAX_RESULTS + 2, 1),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested overfill 1",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        flex: 1,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeFormHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
    // inner 1: general: no results
    // inner 2: remote suggestions: round(7 * (2 / (2 + 0))) = 7
    ...makeIndexRange(0, 7),

    // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
    // inner 1: form history: round(3 * (2 / (2 + 0))) = 3
    // The first seven form history results dupe the seven remote suggestions,
    // so they should not be included.
    ...makeIndexRange(MAX_RESULTS + 7, 3),
    // inner 2: general: no results
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested overfill 2",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        flex: 1,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [...makeFormHistoryResults(MAX_RESULTS)],
  expectedResultIndexes: [
    // outer 1: general & remote suggestions: round(10 * (2 / (2 + 1))) = 7
    // inner 1: general: no results
    // inner 2: remote suggestions: no results

    // outer 2: form history & general: round(10 * (1 / (0 + 1))) = 10
    // inner 1: form history: round(10 * (2 / (2 + 0))) = 10
    ...makeIndexRange(0, MAX_RESULTS),
    // inner 2: general: no results
  ],
});

add_resultGroupsLimit_tasks({
  testName: "flex nested overfill 3",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        flex: 2,
        flexChildren: true,
        children: [
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
          },
        ],
      },
      {
        flex: 1,
        flexChildren: true,
        children: [
          {
            flex: 2,
            group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
          },
          {
            flex: 1,
            group: UrlbarUtils.RESULT_GROUP.GENERAL,
          },
        ],
      },
    ],
  },
  providerResults: [...makeRemoteSuggestionResults(MAX_RESULTS)],
  expectedResultIndexes: [
    // outer 1: general & remote suggestions: round(10 * (2 / (2 + 0))) = 10
    // inner 1: general: no results
    // inner 2: remote suggestions: round(10 * (2 / (2 + 0))) = 10
    ...makeIndexRange(0, MAX_RESULTS),

    // outer 2: form history & general: round(10 * (1 / (2 + 1))) = 3
    // inner 1: form history: no results
    // inner 2: general: no results
  ],
});

add_resultGroupsLimit_tasks({
  testName: "limit ignored with flex",
  resultGroups: {
    flexChildren: true,
    children: [
      {
        limit: 1,
        flex: 2,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        flex: 1,
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    ...makeHistoryResults(MAX_RESULTS),
  ],
  expectedResultIndexes: [
    // general/history: round(10 * (2 / (2 + 1))) = 7 -- limit ignored
    ...makeIndexRange(MAX_RESULTS, 7),
    // remote suggestions: round(10 * (1 / (2 + 1))) = 3
    ...makeIndexRange(0, 3),
  ],
});

add_resultGroupsLimit_tasks({
  testName: "resultSpan = 3 followed by others",
  resultGroups: {
    children: [
      {
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
      {
        group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
      },
    ],
  },
  providerResults: [
    // max results remote suggestions
    ...makeRemoteSuggestionResults(MAX_RESULTS),
    // 1 history with resultSpan = 3
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
  ],
  expectedResultIndexes: [
    // general/history: 1
    ...makeIndexRange(MAX_RESULTS, 1),
    // remote suggestions: maxResults - resultSpan of 3 = 10 - 3 = 7
    ...makeIndexRange(0, 7),
  ],
});

add_resultGroups_task({
  testName: "maxResultCount: 1, availableSpan: 3",
  resultGroups: {
    children: [
      {
        maxResultCount: 1,
        availableSpan: 3,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [...makeHistoryResults(MAX_RESULTS)],
  expectedResultIndexes: [0],
});

add_resultGroups_task({
  testName: "maxResultCount: 1, availableSpan: 3, resultSpan = 3",
  resultGroups: {
    children: [
      {
        maxResultCount: 1,
        availableSpan: 3,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 }),
  ],
  expectedResultIndexes: [0],
});

add_resultGroups_task({
  testName: "maxResultCount: 3, availableSpan: 1",
  resultGroups: {
    children: [
      {
        maxResultCount: 3,
        availableSpan: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [...makeHistoryResults(MAX_RESULTS)],
  expectedResultIndexes: [0],
});

add_resultGroups_task({
  testName: "maxResultCount: 3, availableSpan: 1, resultSpan = 3",
  resultGroups: {
    children: [
      {
        maxResultCount: 3,
        availableSpan: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
  expectedResultIndexes: [],
});

add_resultGroups_task({
  testName: "availableSpan: 1",
  resultGroups: {
    children: [
      {
        availableSpan: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [...makeHistoryResults(MAX_RESULTS)],
  expectedResultIndexes: [0],
});

add_resultGroups_task({
  testName: "availableSpan: 1, resultSpan = 3",
  resultGroups: {
    children: [
      {
        availableSpan: 1,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [Object.assign(makeHistoryResults(1)[0], { resultSpan: 3 })],
  expectedResultIndexes: [],
});

add_resultGroups_task({
  testName: "availableSpan: 3, resultSpan = 2 and resultSpan = 1",
  resultGroups: {
    children: [
      {
        availableSpan: 3,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [
    makeHistoryResults(1)[0],
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
    makeHistoryResults(1)[0],
  ],
  expectedResultIndexes: [0, 1],
});

add_resultGroups_task({
  testName: "availableSpan: 3, resultSpan = 1 and resultSpan = 2",
  resultGroups: {
    children: [
      {
        availableSpan: 3,
        group: UrlbarUtils.RESULT_GROUP.GENERAL,
      },
    ],
  },
  providerResults: [
    Object.assign(makeHistoryResults(1)[0], { resultSpan: 2 }),
    makeHistoryResults(1)[0],
    makeHistoryResults(1)[0],
  ],
  expectedResultIndexes: [0, 1],
});

/**
 * Adds a single test task.
 *
 * @param {object} options
 *   The options for the test
 * @param {string} options.testName
 *   This name is logged with `info` as the task starts.
 * @param {object} options.resultGroups
 *   browser.urlbar.resultGroups is set to this value as the task starts.
 * @param {Array} options.providerResults
 *   Array of result objects that the test provider will add.
 * @param {Array} options.expectedResultIndexes
 *   Array of indexes in `providerResults` of the expected final results.
 */
function add_resultGroups_task({
  testName,
  resultGroups,
  providerResults,
  expectedResultIndexes,
}) {
  let func = async () => {
    info(`Running resultGroups test: ${testName}`);
    info(`Setting result groups: ` + JSON.stringify(resultGroups));
    setResultGroups(resultGroups);
    let provider = registerBasicTestProvider(providerResults);
    let context = createContext("foo", { providers: [provider.name] });
    let controller = UrlbarTestUtils.newMockController();
    await UrlbarProvidersManager.startQuery(context, controller);
    UrlbarProvidersManager.unregisterProvider(provider);
    let expectedResults = expectedResultIndexes.map(i => providerResults[i]);
    Assert.deepEqual(context.results, expectedResults);
    setResultGroups(null);
  };
  Object.defineProperty(func, "name", { value: testName });
  add_task(func);
}

/**
 * Adds test tasks for each of the keys in `LIMIT_KEYS`.
 *
 * @param {object} options
 *   The options for the test
 * @param {string} options.testName
 *   The name of the test.
 * @param {object} options.resultGroups
 *   The resultGroups object to set.
 * @param {Array} options.providerResults
 *   The results to return from the test
 * @param {Array} options.expectedResultIndexes
 *   Indexes of the expected results within {@link providerResults}
 */
function add_resultGroupsLimit_tasks({
  testName,
  resultGroups,
  providerResults,
  expectedResultIndexes,
}) {
  for (let key of LIMIT_KEYS) {
    add_resultGroups_task({
      testName: `${testName} (limit: ${key})`,
      resultGroups: replaceLimitWithKey(resultGroups, key),
      providerResults,
      expectedResultIndexes,
    });
  }
}

function replaceLimitWithKey(group, key) {
  group = { ...group };
  if ("limit" in group) {
    group[key] = group.limit;
    delete group.limit;
  }
  for (let i = 0; i < group.children?.length; i++) {
    group.children[i] = replaceLimitWithKey(group.children[i], key);
  }
  return group;
}

function makeHistoryResults(count) {
  let results = [];
  for (let i = 0; i < count; i++) {
    results.push(
      new UrlbarResult(
        UrlbarUtils.RESULT_TYPE.URL,
        UrlbarUtils.RESULT_SOURCE.HISTORY,
        { url: "http://example.com/" + i }
      )
    );
  }
  return results;
}

function makeRemoteSuggestionResults(count) {
  let results = [];
  for (let i = 0; i < count; i++) {
    results.push(
      new UrlbarResult(
        UrlbarUtils.RESULT_TYPE.SEARCH,
        UrlbarUtils.RESULT_SOURCE.SEARCH,
        {
          engine: "test",
          query: "test",
          suggestion: "test " + i,
          lowerCaseSuggestion: "test " + i,
        }
      )
    );
  }
  return results;
}

function makeFormHistoryResults(count) {
  let results = [];
  for (let i = 0; i < count; i++) {
    results.push(
      new UrlbarResult(
        UrlbarUtils.RESULT_TYPE.SEARCH,
        UrlbarUtils.RESULT_SOURCE.HISTORY,
        {
          engine: "test",
          suggestion: "test " + i,
          lowerCaseSuggestion: "test " + i,
        }
      )
    );
  }
  return results;
}

function makeIndexRange(startIndex, count) {
  let indexes = [];
  for (let i = startIndex; i < startIndex + count; i++) {
    indexes.push(i);
  }
  return indexes;
}

function setResultGroups(resultGroups) {
  sandbox.restore();
  if (resultGroups) {
    sandbox.stub(UrlbarPrefs, "resultGroups").get(() => resultGroups);
  }
}
