/* 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 addon quick suggest results.

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
  ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
});

AddonTestUtils.init(this, false);
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "42",
  "42"
);

// TODO: Firefox no longer uses `rating` and `number_of_ratings` but they are
// still present in Merino and RS suggestions, so they are included here for
// greater accuracy. We should remove them from Merino, RS, and tests.
const MERINO_SUGGESTIONS = [
  {
    provider: "amo",
    icon: "icon",
    url: "https://example.com/merino-addon",
    title: "title",
    description: "description",
    custom_details: {
      amo: {
        rating: "5",
        number_of_ratings: "1234567",
        guid: "test@addon",
      },
    },
  },
];

const REMOTE_SETTINGS_RESULTS = [
  {
    type: "amo-suggestions",
    attachment: [
      {
        url: "https://example.com/first-addon",
        guid: "first@addon",
        icon: "https://example.com/first-addon.svg",
        title: "First Addon",
        rating: "4.7",
        keywords: ["first", "1st", "two words", "aa b c"],
        description: "Description for the First Addon",
        number_of_ratings: 1256,
        score: 0.25,
      },
      {
        url: "https://example.com/second-addon",
        guid: "second@addon",
        icon: "https://example.com/second-addon.svg",
        title: "Second Addon",
        rating: "1.7",
        keywords: ["second", "2nd"],
        description: "Description for the Second Addon",
        number_of_ratings: 256,
        score: 0.25,
      },
      {
        url: "https://example.com/third-addon",
        guid: "third@addon",
        icon: "https://example.com/third-addon.svg",
        title: "Third Addon",
        rating: "3.7",
        keywords: ["third", "3rd"],
        description: "Description for the Third Addon",
        number_of_ratings: 3,
        score: 0.25,
      },
      {
        url: "https://example.com/fourth-addon?utm_medium=aaa&utm_source=bbb",
        guid: "fourth@addon",
        icon: "https://example.com/fourth-addon.svg",
        title: "Fourth Addon",
        rating: "4.7",
        keywords: ["fourth", "4th"],
        description: "Description for the Fourth Addon",
        number_of_ratings: 4,
        score: 0.25,
      },
    ],
  },
];

add_setup(async function init() {
  await AddonTestUtils.promiseStartupManager();

  // Disable search suggestions so we don't hit the network.
  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);

  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    remoteSettingsRecords: REMOTE_SETTINGS_RESULTS,
    merinoSuggestions: MERINO_SUGGESTIONS,
    prefs: [["suggest.quicksuggest.all", true]],
  });
});

add_task(async function telemetryType() {
  Assert.equal(
    QuickSuggest.getFeature("AddonSuggestions").getSuggestionTelemetryType({}),
    "amo",
    "Telemetry type should be 'amo'"
  );
});

// When quick suggest prefs are disabled, addon suggestions should be disabled.
add_task(async function prefsDisabled() {
  let prefs = [
    "quicksuggest.enabled",
    "addons.featureGate",
    "suggest.quicksuggest.all",
    "suggest.addons",
  ];
  for (let pref of prefs) {
    info("Testing pref: " + pref);

    // Before disabling the pref, first make sure the suggestion is added.
    await check_results({
      context: createContext("test", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [
        makeExpectedResult({
          suggestion: MERINO_SUGGESTIONS[0],
          source: "merino",
          provider: "amo",
        }),
      ],
    });

    // Now disable the pref.
    UrlbarPrefs.set(pref, false);
    await check_results({
      context: createContext("test", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [],
    });

    UrlbarPrefs.set(pref, true);
    await QuickSuggestTestUtils.forceSync();
  }
});

// Check wheather the addon suggestions will be shown by the setup of Nimbus
// variable.
add_task(async function nimbus() {
  // Disable the fature gate.
  UrlbarPrefs.set("addons.featureGate", false);
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [],
  });

  // Enable by Nimbus.
  const cleanUpNimbusEnable = await UrlbarTestUtils.initNimbusFeature({
    addonsFeatureGate: true,
  });
  await QuickSuggestTestUtils.forceSync();
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [
      makeExpectedResult({
        suggestion: MERINO_SUGGESTIONS[0],
        source: "merino",
        provider: "amo",
      }),
    ],
  });
  await cleanUpNimbusEnable();

  // Enable locally.
  UrlbarPrefs.set("addons.featureGate", true);
  await QuickSuggestTestUtils.forceSync();

  // Disable by Nimbus.
  const cleanUpNimbusDisable = await UrlbarTestUtils.initNimbusFeature({
    addonsFeatureGate: false,
  });
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [],
  });
  await cleanUpNimbusDisable();

  // Revert.
  UrlbarPrefs.clear("addons.featureGate");
  await QuickSuggestTestUtils.forceSync();
});

add_task(async function hideIfAlreadyInstalled() {
  // Show suggestion.
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [
      makeExpectedResult({
        suggestion: MERINO_SUGGESTIONS[0],
        source: "merino",
        provider: "amo",
      }),
    ],
  });

  // Install an addon for the suggestion.
  const xpi = ExtensionTestCommon.generateXPI({
    manifest: {
      browser_specific_settings: {
        gecko: { id: "test@addon" },
      },
    },
  });
  const addon = await AddonManager.installTemporaryAddon(xpi);

  // Show suggestion for the addon installed.
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [],
  });

  await addon.uninstall();
  xpi.remove(false);
});

add_task(async function remoteSettings() {
  const testCases = [
    {
      input: "f",
      expected: null,
    },
    {
      input: "fi",
      expected: null,
    },
    {
      input: "fir",
      expected: null,
    },
    {
      input: "firs",
      expected: null,
    },
    {
      input: "first",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "1st",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "t",
      expected: null,
    },
    {
      input: "tw",
      expected: null,
    },
    {
      input: "two",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two ",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two w",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two wo",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two wor",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two word",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "two words",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "aa",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "aa ",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "aa b",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "aa b ",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "aa b c",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
      }),
    },
    {
      input: "second",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
      }),
    },
    {
      input: "2nd",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
      }),
    },
    {
      input: "third",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2],
      }),
    },
    {
      input: "3rd",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[2],
      }),
    },
    {
      input: "fourth",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[3],
        setUtmParams: false,
      }),
    },
    {
      input: "FoUrTh",
      expected: makeExpectedResult({
        suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[3],
        setUtmParams: false,
      }),
    },
  ];

  // Disable Merino so we trigger only remote settings suggestions.
  UrlbarPrefs.set("quicksuggest.online.enabled", false);

  for (let { input, expected } of testCases) {
    await check_results({
      context: createContext(input, {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: expected ? [expected] : [],
    });
  }

  UrlbarPrefs.clear("quicksuggest.online.enabled");
});

add_task(async function merinoIsTopPick() {
  const suggestion = JSON.parse(JSON.stringify(MERINO_SUGGESTIONS[0]));

  // is_top_pick is specified as false.
  suggestion.is_top_pick = false;
  MerinoTestUtils.server.response.body.suggestions = [suggestion];
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [
      makeExpectedResult({
        suggestion,
        source: "merino",
        provider: "amo",
      }),
    ],
  });

  // is_top_pick is undefined.
  delete suggestion.is_top_pick;
  MerinoTestUtils.server.response.body.suggestions = [suggestion];
  await check_results({
    context: createContext("test", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [
      makeExpectedResult({
        suggestion,
        source: "merino",
        provider: "amo",
      }),
    ],
  });
});

// Tests the "Dismiss" command: a dismissed suggestion shouldn't be added.
add_task(async function dismiss() {
  // Disable Merino suggestions to make this task simpler.
  UrlbarPrefs.set("quicksuggest.online.enabled", false);

  await doDismissOneTest({
    result: makeExpectedResult({
      suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
    }),
    command: "dismiss",
    feature: QuickSuggest.getFeature("AddonSuggestions"),
    queriesForDismissals: [
      {
        query: REMOTE_SETTINGS_RESULTS[0].attachment[0].keywords[0],
      },
    ],
    queriesForOthers: [
      {
        query: REMOTE_SETTINGS_RESULTS[0].attachment[1].keywords[0],
        expectedResults: [
          makeExpectedResult({
            suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
          }),
        ],
      },
    ],
  });

  UrlbarPrefs.clear("quicksuggest.online.enabled");
});

// Tests the "Not interested" command: all addon suggestions should be disabled
// and not added anymore.
add_task(async function notInterested() {
  await doDismissAllTest({
    result: makeExpectedResult({
      suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
    }),
    command: "not_interested",
    feature: QuickSuggest.getFeature("AddonSuggestions"),
    pref: "suggest.addons",
    queries: [
      {
        query: REMOTE_SETTINGS_RESULTS[0].attachment[0].keywords[0],
      },
      {
        query: REMOTE_SETTINGS_RESULTS[0].attachment[1].keywords[0],
        expectedResults: [
          makeExpectedResult({
            suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[1],
          }),
        ],
      },
    ],
  });
});

// Tests the "show less frequently" behavior.
add_task(async function showLessFrequently() {
  await doShowLessFrequentlyTest({
    feature: QuickSuggest.getFeature("AddonSuggestions"),
    keyword: "two words",
    minKeywordLengthPref: "addons.minKeywordLength",
    showLessFrequentlyCountPref: "addons.showLessFrequentlyCount",
    expectedResult: makeExpectedResult({
      suggestion: REMOTE_SETTINGS_RESULTS[0].attachment[0],
    }),
  });
});

// The `Amo` Rust provider should be passed to the Rust component when querying
// depending on whether addon suggestions are enabled.
add_task(async function rustProviders() {
  await doRustProvidersTests({
    searchString: "first",
    tests: [
      {
        prefs: {
          "suggest.addons": true,
        },
        expectedUrls: ["https://example.com/first-addon"],
      },
      {
        prefs: {
          "suggest.addons": false,
        },
        expectedUrls: [],
      },
    ],
  });

  UrlbarPrefs.clear("suggest.addons");
  await QuickSuggestTestUtils.forceSync();
});

function makeExpectedResult({
  suggestion,
  source,
  provider,
  setUtmParams = true,
}) {
  return QuickSuggestTestUtils.amoResult({
    source,
    provider,
    setUtmParams,
    title: suggestion.title,
    description: suggestion.description,
    url: suggestion.url,
    originalUrl: suggestion.url,
    icon: suggestion.icon,
  });
}

/**
 * Tests the `show_less_frequently` command for a given feature.
 *
 * @param {object} options
 *   Options object.
 * @param {SuggestFeature} options.feature
 *   The feature being tested.
 * @param {SuggestFeature} options.keyword
 *   The keyword to search for. Its length must be at least 3, and each prefix
 *   starting at `keyword.length - 3` must match a suggestion.
 * @param {object} options.expectedResult
 *   The result that is expected to match the keyword and its prefixes.
 * @param {string} options.minKeywordLengthPref
 *   The name of the pref for the min keyword length being tested.
 * @param {string} options.showLessFrequentlyCountPref
 *   The name of the pref for the "show less frequently" count being tested.
 */
async function doShowLessFrequentlyTest({
  feature,
  keyword,
  expectedResult,
  minKeywordLengthPref,
  showLessFrequentlyCountPref,
}) {
  let showLessFrequentlyCap = 3;

  if (keyword.length < showLessFrequentlyCap) {
    throw new Error(
      "keyword must be long enough to 'Show less frequently' enough times"
    );
  }

  await QuickSuggestTestUtils.withConfig({
    config: { show_less_frequently_cap: showLessFrequentlyCap },
    callback: async () => {
      // Do `showLessFrequentlyCap` searches and trigger the
      // `show_less_frequently` command after each one. In each iteration, the
      // length of `searchString` increases by 1.
      for (let count = 0; count < showLessFrequentlyCap; count++) {
        let searchString = keyword.substring(
          0,
          keyword.length - showLessFrequentlyCap + count + 1
        );

        info(
          "Doing search: " +
            JSON.stringify({
              count,
              searchString,
            })
        );

        // Check prefs and other values before searching.
        Assert.equal(
          UrlbarPrefs.get(minKeywordLengthPref),
          count == 0 ? 0 : searchString.length,
          "minKeywordLength pref should be correct before search " + count
        );
        Assert.equal(
          feature.showLessFrequentlyCount,
          count,
          "showLessFrequentlyCount should be correct before search " + count
        );
        Assert.equal(
          UrlbarPrefs.get(showLessFrequentlyCountPref),
          count,
          "showLessFrequentlyCount pref should be correct before search " +
            count
        );
        Assert.ok(
          feature.canShowLessFrequently,
          "canShowLessFrequently should be correct before search " + count
        );

        // Do the search.
        await check_results({
          context: createContext(searchString, {
            providers: [UrlbarProviderQuickSuggest.name],
            isPrivate: false,
          }),
          matches: [expectedResult],
        });

        // Trigger the command.
        triggerCommand({
          feature,
          searchString,
          command: "show_less_frequently",
          result: expectedResult,
          expectedCountsByCall: {
            acknowledgeFeedback: 1,
            invalidateResultMenuCommands:
              count == showLessFrequentlyCap - 1 ? 1 : 0,
          },
        });

        // The same search should now match nothing.
        await check_results({
          context: createContext(searchString, {
            providers: [UrlbarProviderQuickSuggest.name],
            isPrivate: false,
          }),
          matches: [],
        });
      }

      // Check prefs and other values now that all searches are done.
      Assert.equal(
        UrlbarPrefs.get(minKeywordLengthPref),
        keyword.length + 1,
        "minKeywordLength pref should be correct after all searches"
      );
      Assert.equal(
        feature.showLessFrequentlyCount,
        showLessFrequentlyCap,
        "showLessFrequentlyCount should be correct after all searches"
      );
      Assert.equal(
        UrlbarPrefs.get(showLessFrequentlyCountPref),
        showLessFrequentlyCap,
        "showLessFrequentlyCap pref should be correct after all searches"
      );
      Assert.ok(
        !feature.canShowLessFrequently,
        "canShowLessFrequently should be correct after all searches"
      );
    },
  });

  UrlbarPrefs.clear(minKeywordLengthPref);
  UrlbarPrefs.clear(showLessFrequentlyCountPref);
}
