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

// Tests Suggest exposure suggestions.

// Notes on the following mock suggestions:
//
// * `rsSuggestionType` echoes the `suggestion_type` in the RS record so that
//   the test can verify that matched results correspond to the expected
//   suggestions/records. It's not a necessary or special property for exposure
//   suggestions.
// * `score` ensures a stable sort vs. other suggestion types for the test. It's
//   not necessary for exposure suggestions.
const REMOTE_SETTINGS_RECORDS = [
  {
    type: "dynamic-suggestions",
    suggestion_type: "test-exposure-aaa",
    score: 1.0,
    attachment: [
      {
        keywords: ["aaa keyword", "aaa bbb keyword", "wikipedia"],
        data: {
          result: {
            isHiddenExposure: true,
            payload: {
              rsSuggestionType: "test-exposure-aaa",
            },
          },
        },
      },
    ],
  },
  {
    type: "dynamic-suggestions",
    suggestion_type: "test-exposure-bbb",
    score: 1.0,
    attachment: {
      keywords: ["bbb keyword", "aaa bbb keyword", "wikipedia"],
      data: {
        result: {
          isHiddenExposure: true,
          payload: {
            rsSuggestionType: "test-exposure-bbb",
            telemetryType: "bbb_telemetry_type",
          },
        },
      },
    },
  },
  {
    type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA,
    attachment: [QuickSuggestTestUtils.wikipediaRemoteSettings()],
  },
];

const EXPECTED_AAA_RESULT = makeExpectedResult({
  rsSuggestionType: "test-exposure-aaa",
});

const EXPECTED_BBB_RESULT = makeExpectedResult({
  rsSuggestionType: "test-exposure-bbb",
  telemetryType: "bbb_telemetry_type",
});

const EXPECTED_WIKIPEDIA_RESULT = {
  ...QuickSuggestTestUtils.wikipediaResult(),
  exposureTelemetry: UrlbarUtils.EXPOSURE_TELEMETRY.NONE,
};

add_setup(async function () {
  // Add many exposure and AMP suggestions that have the "maxresults" keyword.
  let maxResults = UrlbarPrefs.get("maxRichResults");
  Assert.greater(maxResults, 0, "This test expects maxRichResults to be > 0");
  let ampRecord = {
    collection: QuickSuggestTestUtils.RS_COLLECTION.AMP,
    type: QuickSuggestTestUtils.RS_TYPE.AMP,
    attachment: [],
  };
  REMOTE_SETTINGS_RECORDS.push(ampRecord);
  for (let i = 0; i < maxResults; i++) {
    ampRecord.attachment.push(
      QuickSuggestTestUtils.ampRemoteSettings({
        keywords: ["maxresults"],
        title: "maxresults " + i,
        url: "https://example.com/maxresults/" + i,
      })
    );
    REMOTE_SETTINGS_RECORDS.push({
      type: "dynamic-suggestions",
      suggestion_type: "test-exposure-maxresults-" + i,
      score: 1.0,
      attachment: [
        {
          keywords: ["maxresults"],
          data: {
            result: {
              isHiddenExposure: true,
              payload: {
                rsSuggestionType: "test-exposure-maxresults-" + i,
              },
            },
          },
        },
      ],
    });
  }

  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    remoteSettingsRecords: REMOTE_SETTINGS_RECORDS,
    prefs: [
      [
        "quicksuggest.dynamicSuggestionTypes",
        "test-exposure-aaa,test-exposure-bbb",
      ],
      ["suggest.quicksuggest.all", true],
      ["quicksuggest.ampTopPickCharThreshold", 0],
    ],
  });
});

add_task(async function telemetryType_default() {
  Assert.equal(
    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
      data: {
        result: {
          isHiddenExposure: true,
        },
      },
    }),
    "exposure",
    "Telemetry type should be correct when using default"
  );
});

add_task(async function telemetryType_override() {
  Assert.equal(
    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
      data: {
        result: {
          isHiddenExposure: true,
          payload: {
            telemetryType: "telemetry_type_override",
          },
        },
      },
    }),
    "telemetry_type_override",
    "Telemetry type should be correct when overridden"
  );
});

// Results for enabled exposure suggestions should always be added and have
// hidden `exposureTelemetry` no matter the value of the `exposureResults` or
// `showExposureResults` prefs.
add_task(async function basic() {
  let queries = [
    {
      query: "no match",
      expected: [],
    },
    {
      query: "aaa keyword",
      expected: [EXPECTED_AAA_RESULT],
    },
    {
      query: "bbb keyword",
      expected: [EXPECTED_BBB_RESULT],
    },
    {
      query: "aaa bbb keyword",
      // The order of the results is reversed since they have the same suggested
      // index, due to how the muxer handles that.
      expected: [EXPECTED_BBB_RESULT, EXPECTED_AAA_RESULT],
    },
  ];

  let exposureResultsList = [
    undefined,
    "",
    "some_other_result",
    "exposure",
    "some_other_result,exposure",
  ];

  for (let exposureResults of exposureResultsList) {
    if (exposureResults === undefined) {
      UrlbarPrefs.clear("exposureResults");
    } else {
      UrlbarPrefs.set("exposureResults", exposureResults);
    }

    for (let showExposureResults of [undefined, true, false]) {
      if (showExposureResults === undefined) {
        UrlbarPrefs.clear("showExposureResults");
      } else {
        UrlbarPrefs.set("showExposureResults", showExposureResults);
      }

      info(
        "Doing basic subtest: " +
          JSON.stringify({
            exposureResults,
            showExposureResults,
          })
      );

      await doQueries(queries);
    }
  }

  UrlbarPrefs.clear("exposureResults");
  UrlbarPrefs.clear("showExposureResults");
});

// When only one exposure suggestion type is enabled, only its result should be
// returned. This task assumes multiples types were added to remote settings in
// the setup task.
add_task(async function oneSuggestionType() {
  await withSuggestionTypesPref("test-exposure-bbb", async () => {
    await doQueries([
      {
        query: "aaa keyword",
        expected: [],
      },
      {
        query: "bbb keyword",
        expected: [EXPECTED_BBB_RESULT],
      },
      {
        query: "aaa bbb keyword",
        expected: [EXPECTED_BBB_RESULT],
      },
    ]);
  });
});

// When no exposure suggestion types are enabled, no results should be added.
add_task(async function disabled() {
  await withSuggestionTypesPref("", async () => {
    await doQueries(
      ["aaa keyword", "bbb keyword", "aaa bbb keyword"].map(query => ({
        query,
        expected: [],
      }))
    );
  });
});

// When another visible suggestion also matches, both it and the exposure
// suggestion should be added.
add_task(async function otherVisibleSuggestionsAlsoMatched_1() {
  await withSuggestionTypesPref("test-exposure-aaa", async () => {
    await doQueries([
      {
        query: "aaa keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "wikipedia",
        expected: [EXPECTED_WIKIPEDIA_RESULT, EXPECTED_AAA_RESULT],
      },
    ]);
  });
});

// When another visible suggestion also matches, both it and all specified
// exposure suggestions should be added.
add_task(async function otherVisibleSuggestionsAlsoMatched_2() {
  await withSuggestionTypesPref(
    "test-exposure-aaa,test-exposure-bbb",
    async () => {
      await doQueries([
        {
          query: "aaa keyword",
          expected: [EXPECTED_AAA_RESULT],
        },
        {
          query: "bbb keyword",
          expected: [EXPECTED_BBB_RESULT],
        },
        {
          query: "aaa bbb keyword",
          expected: [EXPECTED_BBB_RESULT, EXPECTED_AAA_RESULT],
        },
        {
          query: "wikipedia",
          expected: [
            EXPECTED_WIKIPEDIA_RESULT,
            EXPECTED_BBB_RESULT,
            EXPECTED_AAA_RESULT,
          ],
        },
      ]);
    }
  );
});

// Tests with `maxResults` exposures. All should be added.
add_task(async function maxResults_exposuresOnly() {
  let maxResults = UrlbarPrefs.get("maxRichResults");
  let exposureResults = [];
  for (let i = 0; i < maxResults; i++) {
    exposureResults.unshift(
      makeExpectedResult({ rsSuggestionType: "test-exposure-maxresults-" + i })
    );
  }

  await doMaxResultsTest({
    expectedResults: [...exposureResults],
  });
});

// Tests with `maxResults` exposures and and `maxResults` history. All exposures
// and history should be added since exposures are hidden.
add_task(async function maxResults_exposuresHistory() {
  let maxResults = UrlbarPrefs.get("maxRichResults");
  let exposureResults = [];
  let historyResults = [];
  for (let i = 0; i < maxResults; i++) {
    exposureResults.unshift(
      makeExpectedResult({ rsSuggestionType: "test-exposure-maxresults-" + i })
    );
    historyResults.push(
      new UrlbarResult({
        type: UrlbarUtils.RESULT_TYPE.URL,
        source: UrlbarUtils.RESULT_SOURCE.HISTORY,
        payload: { url: "http://example.com/history/" + i },
      })
    );
  }

  await doMaxResultsTest({
    includeHistory: true,
    expectedResults: [...historyResults, ...exposureResults],
  });
});

// Tests with `maxResults` exposures and and `maxResults` AMP suggestions. The
// first AMP result should be added since it's visible and only one visible
// Suggest result should be added. All exposures should be added since exposures
// are hidden.
add_task(async function maxResults_exposuresAmp() {
  let maxResults = UrlbarPrefs.get("maxRichResults");
  let exposureResults = [];
  for (let i = 0; i < maxResults; i++) {
    exposureResults.unshift(
      makeExpectedResult({ rsSuggestionType: "test-exposure-maxresults-" + i })
    );
  }

  await doMaxResultsTest({
    includeAmp: true,
    expectedResults: [
      QuickSuggestTestUtils.ampResult({
        keyword: "maxresults",
        title: "maxresults 0",
        url: "https://example.com/maxresults/0",
        suggestedIndex: -1,
      }),
      ...exposureResults,
    ],
  });
});

// Tests with `maxResults` exposures, history, and AMP suggestions. The first
// AMP result should be added since it's visible and only one visible Suggest
// result should be added. `maxResults` - 1 history results should be added. All
// exposures should be added since exposures are hidden.
add_task(async function maxResults_exposuresHistoryAmp() {
  let maxResults = UrlbarPrefs.get("maxRichResults");
  let exposureResults = [];
  let historyResults = [];
  for (let i = 0; i < maxResults; i++) {
    exposureResults.unshift(
      makeExpectedResult({ rsSuggestionType: "test-exposure-maxresults-" + i })
    );
    historyResults.push(
      new UrlbarResult({
        type: UrlbarUtils.RESULT_TYPE.URL,
        source: UrlbarUtils.RESULT_SOURCE.HISTORY,
        payload: { url: "http://example.com/history/" + i },
      })
    );
  }

  await doMaxResultsTest({
    includeAmp: true,
    includeHistory: true,
    expectedResults: [
      ...historyResults.slice(0, maxResults - 1),
      QuickSuggestTestUtils.ampResult({
        keyword: "maxresults",
        title: "maxresults 0",
        url: "https://example.com/maxresults/0",
        suggestedIndex: -1,
      }),
      ...exposureResults,
    ],
  });
});

async function doMaxResultsTest({
  expectedResults,
  includeAmp = false,
  includeHistory = false,
}) {
  let maxResults = UrlbarPrefs.get("maxRichResults");
  let providerNames = [UrlbarProviderQuickSuggest.name];

  // If history results should be included, register a test provider that adds a
  // bunch of history results.
  let historyProvider;
  let historyResults = [];
  if (includeHistory) {
    for (let i = 0; i < maxResults; i++) {
      historyResults.push(
        new UrlbarResult({
          type: UrlbarUtils.RESULT_TYPE.URL,
          source: UrlbarUtils.RESULT_SOURCE.HISTORY,
          payload: { url: "http://example.com/history/" + i },
        })
      );
    }
    historyProvider = new UrlbarTestUtils.TestProvider({
      results: historyResults,
    });
    UrlbarProvidersManager.registerProvider(historyProvider);
    providerNames.push(historyProvider.name);
  }

  // Enable sponsored suggestions according to whether AMP results should be
  // included.
  UrlbarPrefs.set("suggest.quicksuggest.sponsored", includeAmp);
  await QuickSuggestTestUtils.forceSync();

  // Generate the list of exposure suggestion types so we can trigger the
  // exposure suggestions.
  let exposureTypes = [];
  for (let i = 0; i < maxResults; i++) {
    exposureTypes.push("test-exposure-maxresults-" + i);
  }

  await withSuggestionTypesPref(exposureTypes.join(","), async () => {
    await check_results({
      context: createContext("maxresults", {
        providers: providerNames,
        isPrivate: false,
      }),
      matches: expectedResults,
    });
  });

  if (historyProvider) {
    UrlbarProvidersManager.unregisterProvider(historyProvider);
  }
  UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
  await QuickSuggestTestUtils.forceSync();
}

// Exposure suggestions should automatically bypass the usual `all` and
// sponsored prefs.
add_task(async function bypassPrefs() {
  UrlbarPrefs.set("suggest.quicksuggest.all", false);
  UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);

  await withSuggestionTypesPref("test-exposure-aaa", async () => {
    await doQueries([
      {
        query: "aaa keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "aaa bbb keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "wikipedia",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "doesn't match",
        expected: [],
      },
    ]);
  });

  UrlbarPrefs.set("suggest.quicksuggest.all", true);
  UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
  await QuickSuggestTestUtils.forceSync();
});

// Tests the `quickSuggestDynamicSuggestionTypes` Nimbus variable with exposure
// suggestions.
add_task(async function nimbus() {
  // Clear `dynamicSuggestionTypes` to make sure the value comes from the Nimbus
  // variable and not the pref.
  await withSuggestionTypesPref("", async () => {
    let cleanup = await UrlbarTestUtils.initNimbusFeature({
      quickSuggestDynamicSuggestionTypes: "test-exposure-aaa,test-exposure-bbb",
    });
    await QuickSuggestTestUtils.forceSync();
    await doQueries([
      {
        query: "aaa keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "aaa bbb keyword",
        expected: [EXPECTED_BBB_RESULT, EXPECTED_AAA_RESULT],
      },
      {
        query: "wikipedia",
        expected: [
          EXPECTED_WIKIPEDIA_RESULT,
          EXPECTED_BBB_RESULT,
          EXPECTED_AAA_RESULT,
        ],
      },
      {
        query: "doesn't match",
        expected: [],
      },
    ]);

    await cleanup();
  });
});

async function doQueries(queries) {
  for (let { query, expected } of queries) {
    info(
      "Doing query: " +
        JSON.stringify({
          query,
          expected,
        })
    );

    await check_results({
      context: createContext(query, {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: expected,
    });
  }
}

async function withSuggestionTypesPref(prefValue, callback) {
  // Use `Services` to get the original pref value since `UrlbarPrefs` will
  // parse the string value into a `Set`.
  let originalPrefValue = Services.prefs.getCharPref(
    "browser.urlbar.quicksuggest.dynamicSuggestionTypes"
  );

  // Changing the pref (or Nimbus variable) to a different value will trigger
  // ingest, so force sync afterward (or at least wait for ingest to finish).
  UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", prefValue);
  await QuickSuggestTestUtils.forceSync();

  await callback();

  UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", originalPrefValue);
  await QuickSuggestTestUtils.forceSync();
}

function makeExpectedResult({ rsSuggestionType, telemetryType = "exposure" }) {
  return {
    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    heuristic: false,
    exposureTelemetry: UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN,
    payload: {
      telemetryType,
      rsSuggestionType,
      source: "rust",
      dynamicType: "exposure",
      provider: "Dynamic",
      suggestionType: rsSuggestionType,
      isSponsored: false,
    },
  };
}
