/* 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 dynamic Rust suggestions.

const REMOTE_SETTINGS_RECORDS = [
  // nonsponsored, no `bypassSuggestAll`
  {
    type: "dynamic-suggestions",
    suggestion_type: "aaa",
    score: 0.9,
    attachment: [
      {
        keywords: ["aaa keyword", "aaa bbb keyword", "wikipedia"],
        data: {
          result: {
            payload: {
              title: "aaa title",
              url: "https://example.com/aaa",
            },
          },
        },
      },
    ],
  },

  // sponsored, no `bypassSuggestAll`
  {
    type: "dynamic-suggestions",
    suggestion_type: "bbb",
    score: 0.1,
    attachment: [
      {
        keywords: ["bbb keyword", "aaa bbb keyword", "wikipedia"],
        dismissal_key: "bbb-dismissal-key",
        data: {
          result: {
            isBestMatch: true,
            suggestedIndex: 1,
            isSuggestedIndexRelativeToGroup: false,
            isRichSuggestion: true,
            payload: {
              title: "bbb title",
              url: "https://example.com/bbb",
              isSponsored: true,
              telemetryType: "bbb_telemetry_type",
            },
          },
        },
      },
    ],
  },

  // nonsponsored, `bypassSuggestAll: true`
  {
    type: "dynamic-suggestions",
    suggestion_type: "ccc",
    score: 0.9,
    attachment: [
      {
        keywords: ["ccc keyword", "ccc ddd keyword"],
        data: {
          result: {
            bypassSuggestAll: true,
            payload: {
              title: "ccc title",
              url: "https://example.com/ccc",
            },
          },
        },
      },
    ],
  },

  // sponsored, `bypassSuggestAll: true`
  {
    type: "dynamic-suggestions",
    suggestion_type: "ddd",
    score: 0.9,
    attachment: [
      {
        keywords: ["ddd keyword", "ccc ddd keyword"],
        data: {
          result: {
            bypassSuggestAll: true,
            payload: {
              title: "ddd title",
              url: "https://example.com/ddd",
              isSponsored: true,
            },
          },
        },
      },
    ],
  },

  {
    type: QuickSuggestTestUtils.RS_TYPE.WIKIPEDIA,
    attachment: [QuickSuggestTestUtils.wikipediaRemoteSettings()],
  },
];

const EXPECTED_AAA_RESULT = makeExpectedResult({
  title: "aaa title",
  url: "https://example.com/aaa",
  telemetryType: "aaa",
  suggestionType: "aaa",
});

const EXPECTED_BBB_RESULT = makeExpectedResult({
  title: "bbb title",
  url: "https://example.com/bbb",
  isSponsored: true,
  telemetryType: "bbb_telemetry_type",
  suggestionType: "bbb",
  isBestMatch: true,
  suggestedIndex: 1,
  isSuggestedIndexRelativeToGroup: false,
  isRichSuggestion: true,
});

const EXPECTED_CCC_RESULT = makeExpectedResult({
  title: "ccc title",
  url: "https://example.com/ccc",
  telemetryType: "ccc",
  suggestionType: "ccc",
});

const EXPECTED_DDD_RESULT = makeExpectedResult({
  title: "ddd title",
  url: "https://example.com/ddd",
  isSponsored: true,
  telemetryType: "ddd",
  suggestionType: "ddd",
});

add_setup(async function () {
  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    remoteSettingsRecords: REMOTE_SETTINGS_RECORDS,
    prefs: [
      ["quicksuggest.dynamicSuggestionTypes", "aaa,bbb,ccc,ddd"],
      ["suggest.quicksuggest.all", true],
      ["suggest.quicksuggest.sponsored", true],
      ["quicksuggest.ampTopPickCharThreshold", 0],
    ],
  });
});

// When a dynamic suggestion doesn't include `telemetryType`, its
// `suggestionType` should be used as the telemetry type.
add_task(async function telemetryType_default() {
  Assert.equal(
    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
      suggestionType: "abcdefg",
    }),
    "abcdefg",
    "Telemetry type should be correct when using default"
  );
});

// When a dynamic suggestion includes `telemetryType`, it should be used as the
// telemetry type.
add_task(async function telemetryType_override() {
  Assert.equal(
    QuickSuggest.getFeature("DynamicSuggestions").getSuggestionTelemetryType({
      suggestionType: "abcdefg",
      data: {
        result: {
          payload: {
            telemetryType: "telemetry_type_override",
          },
        },
      },
    }),
    "telemetry_type_override",
    "Telemetry type should be correct when overridden"
  );
});

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 "aaa" suggestion has a higher score than "bbb".
      expected: [EXPECTED_AAA_RESULT],
    },
    {
      query: "ccc keyword",
      expected: [EXPECTED_CCC_RESULT],
    },
    {
      query: "ddd keyword",
      expected: [EXPECTED_DDD_RESULT],
    },
    {
      query: "ccc ddd keyword",
      // The "ccc" suggestion has a higher score than "ddd".
      expected: [EXPECTED_CCC_RESULT],
    },
  ];

  await doQueries(queries);
});

// When only one dynamic 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("bbb", async () => {
    await doQueries([
      {
        query: "aaa keyword",
        expected: [],
      },
      {
        query: "bbb keyword",
        expected: [EXPECTED_BBB_RESULT],
      },
      {
        query: "aaa bbb keyword",
        expected: [EXPECTED_BBB_RESULT],
      },
      {
        query: "doesn't match",
        expected: [],
      },
    ]);
  });
});

// When no dynamic 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: [],
      }))
    );
  });
});

// Dynamic suggestions shouldn't be added when `all` is disabled unless they
// define `bypassSuggestAll`.
add_task(async function allDisabled() {
  UrlbarPrefs.set("suggest.quicksuggest.all", false);

  // The sponsored pref shouldn't matter.
  for (let sponsoredEnabled of [true, false]) {
    UrlbarPrefs.set("suggest.quicksuggest.sponsored", sponsoredEnabled);

    await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => {
      await doQueries([
        {
          query: "aaa keyword",
          expected: [],
        },
        {
          query: "bbb keyword",
          expected: [],
        },
        {
          query: "aaa bbb keyword",
          expected: [],
        },

        {
          query: "ccc keyword",
          expected: [EXPECTED_CCC_RESULT],
        },
        {
          query: "ddd keyword",
          expected: [EXPECTED_DDD_RESULT],
        },
        {
          query: "ccc ddd keyword",
          // The "ccc" suggestion has a higher score than "ddd".
          expected: [EXPECTED_CCC_RESULT],
        },
      ]);
    });
  }

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

// Dynamic suggestions that are sponsored shouldn't be added when sponsored
// suggestions are disabled unless they define `bypassSuggestAll`.
add_task(async function sponsoredDisabled() {
  UrlbarPrefs.set("suggest.quicksuggest.all", true);
  UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);

  await withSuggestionTypesPref("aaa,bbb,ccc,ddd", async () => {
    await doQueries([
      {
        query: "aaa keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "bbb keyword",
        expected: [],
      },
      {
        query: "aaa bbb keyword",
        expected: [EXPECTED_AAA_RESULT],
      },

      {
        query: "ccc keyword",
        expected: [EXPECTED_CCC_RESULT],
      },
      {
        query: "ddd keyword",
        expected: [EXPECTED_DDD_RESULT],
      },
      {
        query: "ccc ddd keyword",
        // The "ccc" suggestion has a higher score than "ddd".
        expected: [EXPECTED_CCC_RESULT],
      },
    ]);
  });

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

// Tests the `quickSuggestDynamicSuggestionTypes` Nimbus variable.
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: "aaa,bbb",
    });
    await QuickSuggestTestUtils.forceSync();
    await doQueries([
      {
        query: "aaa keyword",
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "bbb keyword",
        expected: [EXPECTED_BBB_RESULT],
      },
      {
        query: "aaa bbb keyword",
        // The "aaa" suggestion has a higher score than "bbb".
        expected: [EXPECTED_AAA_RESULT],
      },
      {
        query: "doesn't match",
        expected: [],
      },
    ]);

    await cleanup();
  });
});

// Tests dismissals. Note that dynamic suggestions must define a `dismissal_key`
// in order to be dismissable.
add_task(async function dismissal() {
  // Do a search and get the actual result that's returned.
  let context = createContext("bbb keyword", {
    providers: [UrlbarProviderQuickSuggest.name],
    isPrivate: false,
  });
  await check_results({
    context,
    matches: [EXPECTED_BBB_RESULT],
  });

  let result = context.results[0];
  let { suggestionObject } = result.payload;
  let { dismissalKey } = suggestionObject;
  Assert.equal(
    dismissalKey,
    "bbb-dismissal-key",
    "The suggestion should have the expected dismissal key"
  );

  // It shouldn't be dismissed yet.
  Assert.ok(
    !(await QuickSuggest.isResultDismissed(result)),
    "The result should not be dismissed yet"
  );
  Assert.ok(
    !(await QuickSuggest.rustBackend.isRustSuggestionDismissed(
      suggestionObject
    )),
    "The suggestion should not be dismissed yet"
  );
  Assert.ok(
    !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)),
    "The dismissal key should not be registered yet"
  );

  // Dismiss it. It should be dismissed by its dismissal key.
  await QuickSuggest.dismissResult(result);

  Assert.ok(
    await QuickSuggest.isResultDismissed(result),
    "The result should be dismissed"
  );
  Assert.ok(
    await QuickSuggest.rustBackend.isRustSuggestionDismissed(suggestionObject),
    "The suggestion should be dismissed"
  );

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

  // Clear dismissals and check again.
  await QuickSuggest.clearDismissedSuggestions();

  await check_results({
    context: createContext("bbb keyword", {
      providers: [UrlbarProviderQuickSuggest.name],
      isPrivate: false,
    }),
    matches: [EXPECTED_BBB_RESULT],
  });

  Assert.ok(
    !(await QuickSuggest.isResultDismissed(result)),
    "The result should not be dismissed after clearing dismissals"
  );
  Assert.ok(
    !(await QuickSuggest.rustBackend.isRustSuggestionDismissed(
      suggestionObject
    )),
    "The suggestion should not be dismissed after clearing dismissals"
  );
  Assert.ok(
    !(await QuickSuggest.rustBackend.isDismissedByKey(dismissalKey)),
    "The dismissal key should not be registered after clearing dismissals"
  );
});

// Tests whether the prefs DynamicSuggestion handles clears.
add_task(async function clearDismissedSuggestions() {
  let feature = QuickSuggest.getFeature("DynamicSuggestions");
  let sandbox = sinon.createSandbox();
  sinon
    .stub(feature, "primaryUserControlledPreferences")
    .get(() => ["suggest.realtimeOptIn", "autoFill", "closeOtherPanelsOnOpen"]);

  UrlbarPrefs.set("suggest.realtimeOptIn", false);
  UrlbarPrefs.set("autoFill", false);
  UrlbarPrefs.set("closeOtherPanelsOnOpen", false);

  Assert.ok(await QuickSuggest.canClearDismissedSuggestions());
  await QuickSuggest.clearDismissedSuggestions();

  Assert.ok(UrlbarPrefs.get("suggest.realtimeOptIn"));
  Assert.ok(UrlbarPrefs.get("autoFill"));
  Assert.ok(UrlbarPrefs.get("closeOtherPanelsOnOpen"));

  sandbox.restore();
});

// Tests some suggestions with bad data that desktop ignores.
add_task(async function badSuggestions() {
  await QuickSuggestTestUtils.setRemoteSettingsRecords([
    {
      type: "dynamic-suggestions",
      suggestion_type: "bad",
      attachment: [
        // Include a good suggestion so we can verify this record was actually
        // ingested. Change the keyword so we don't confuse ourselves by
        // searching for an "aaa" keyword and getting a urlbar result whose
        // telemetry type and dynamic suggestion type is "bad".
        {
          ...REMOTE_SETTINGS_RECORDS[0].attachment[0],
          keywords: ["good actually"],
        },
        // `data` is missing -- Rust actually allows this since `data` is
        // defined as `Option<serde_json::Value>`, but desktop doesn't.
        {
          keywords: ["bad"],
        },
        // `data` isn't an object
        {
          data: 123,
          keywords: ["bad"],
        },
        // `data.result` is missing
        {
          data: {},
          keywords: ["bad"],
        },
        // `data.result` isn't an object
        {
          data: {
            result: 123,
          },
          keywords: ["bad"],
        },
        // `data.result.payload` isn't an object
        {
          data: {
            result: {
              payload: 123,
            },
          },
          keywords: ["bad"],
        },
      ],
    },
  ]);

  await withSuggestionTypesPref("bad", async () => {
    // Verify the good suggestion was ingested.
    await check_results({
      context: createContext("good actually", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [
        {
          ...EXPECTED_AAA_RESULT,
          payload: {
            ...EXPECTED_AAA_RESULT.payload,
            telemetryType: "bad",
            suggestionType: "bad",
          },
        },
      ],
    });

    // No "bad" suggestions should be matched.
    await check_results({
      context: createContext("bad", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [],
    });
  });

  // Clean up.
  await QuickSuggestTestUtils.setRemoteSettingsRecords(REMOTE_SETTINGS_RECORDS);
});

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({
  title,
  url,
  telemetryType,
  suggestionType,
  isSponsored = false,
  isBestMatch = false,
  suggestedIndex = -1,
  isSuggestedIndexRelativeToGroup = true,
  isRichSuggestion = undefined,
}) {
  return {
    type: UrlbarUtils.RESULT_TYPE.URL,
    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    heuristic: false,
    isBestMatch,
    suggestedIndex,
    isRichSuggestion,
    isSuggestedIndexRelativeToGroup,
    payload: {
      title,
      url,
      isSponsored,
      telemetryType,
      suggestionType,
      source: "rust",
      provider: "Dynamic",
      isManageable: true,
      helpUrl: QuickSuggest.HELP_URL,
    },
  };
}
