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

/**
 * Tests tab-to-search results. See also
 * browser/components/urlbar/tests/unit/test_providerTabToSearch.js.
 */

"use strict";

const TEST_ENGINE_NAME = "Test";
const TEST_ENGINE_DOMAIN = "example.com";

const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";

ChromeUtils.defineESModuleGetters(this, {
  UrlbarProviderTabToSearch:
    "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
});

add_setup(async function () {
  await PlacesUtils.history.clear();
  await SpecialPowers.pushPrefEnv({
    set: [
      // Disable onboarding results for general tests. They are enabled in tests
      // that specifically address onboarding.
      ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0],
    ],
  });

  await SearchTestUtils.installSearchExtension({
    name: TEST_ENGINE_NAME,
    search_url: `https://${TEST_ENGINE_DOMAIN}/`,
  });

  for (let i = 0; i < 3; i++) {
    await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]);
  }

  registerCleanupFunction(async () => {
    await PlacesUtils.history.clear();
  });
});

// Tests that tab-to-search results preview search mode when highlighted. These
// results are worth testing separately since they do not set the
// payload.keyword parameter.
add_task(async function basic() {
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
  Assert.ok(autofillResult.autofill);
  Assert.equal(
    autofillResult.url,
    `https://${TEST_ENGINE_DOMAIN}/`,
    "The autofilled URL matches the engine domain."
  );

  let tabToSearchResult = (
    await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
  ).result;
  Assert.equal(
    tabToSearchResult.providerName,
    "TabToSearch",
    "The second result is a tab-to-search result."
  );
  Assert.equal(
    tabToSearchResult.payload.engine,
    TEST_ENGINE_NAME,
    "The tab-to-search result is for the correct engine."
  );
  let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt(
    window,
    1
  );
  let [actionTabToSearch] = await document.l10n.formatValues([
    {
      id: Services.search.getEngineByName(
        tabToSearchDetails.searchParams.engine
      ).isGeneralPurposeEngine
        ? "urlbar-result-action-tabtosearch-web"
        : "urlbar-result-action-tabtosearch-other-engine",
      args: { engine: tabToSearchDetails.searchParams.engine },
    },
  ]);
  Assert.equal(
    tabToSearchDetails.displayed.title,
    `Search with ${tabToSearchDetails.searchParams.engine}`,
    "The result's title is set correctly."
  );
  Assert.equal(
    tabToSearchDetails.displayed.action,
    actionTabToSearch,
    "The correct action text is displayed in the tab-to-search result."
  );

  EventUtils.synthesizeKey("KEY_ArrowDown");
  Assert.equal(
    UrlbarTestUtils.getSelectedRowIndex(window),
    1,
    "Sanity check: The second result is selected."
  );
  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch",
    isPreview: true,
  });

  await UrlbarTestUtils.exitSearchMode(window);
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
});

// Tests that we do not set aria-activedescendant after tabbing to a
// tab-to-search result when the pref
// browser.urlbar.accessibility.tabToSearch.announceResults is true. If that
// pref is true, the result was already announced while the user was typing.
add_task(async function activedescendant_tab() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.accessibility.tabToSearch.announceResults", true]],
  });

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  Assert.equal(
    UrlbarTestUtils.getResultCount(window),
    2,
    "There should be two results."
  );
  let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
    window,
    1
  );
  Assert.equal(
    tabToSearchRow.result.providerName,
    "TabToSearch",
    "The second result is a tab-to-search result."
  );

  EventUtils.synthesizeKey("KEY_Tab");

  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch",
    isPreview: true,
  });
  let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(aadID, null, "aria-activedescendant was not set.");

  // Cycle through all the results then return to the tab-to-search result. It
  // should be announced.
  EventUtils.synthesizeKey("KEY_Tab");
  aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  let firstRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
  Assert.equal(
    aadID,
    firstRow._content.id,
    "aria-activedescendant was set to the row after the tab-to-search result."
  );
  EventUtils.synthesizeKey("KEY_Tab");
  aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(
    aadID,
    tabToSearchRow._content.id,
    "aria-activedescendant was set to the tab-to-search result."
  );

  // Now close and reopen the view, then do another search that yields a
  // tab-to-search result. aria-activedescendant should not be set when it is
  // selected.
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
  Assert.equal(
    tabToSearchRow.result.providerName,
    "TabToSearch",
    "The second result is a tab-to-search result."
  );

  EventUtils.synthesizeKey("KEY_Tab");

  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch",
    isPreview: true,
  });
  aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(aadID, null, "aria-activedescendant was not set.");

  await UrlbarTestUtils.exitSearchMode(window);
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  await SpecialPowers.popPrefEnv();
});

// Tests that we set aria-activedescendant after accessing a tab-to-search
// result with the arrow keys.
add_task(async function activedescendant_arrow() {
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(
    window,
    1
  );
  Assert.equal(
    tabToSearchRow.result.providerName,
    "TabToSearch",
    "The second result is a tab-to-search result."
  );

  EventUtils.synthesizeKey("KEY_ArrowDown");

  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch",
    isPreview: true,
  });
  let aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(
    aadID,
    tabToSearchRow._content.id,
    "aria-activedescendant was set to the tab-to-search result."
  );

  // Move selection away from the tab-to-search result then return. It should
  // be announced.
  EventUtils.synthesizeKey("KEY_ArrowDown");
  aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(
    aadID,
    UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.id,
    "aria-activedescendant was moved to the first one-off."
  );
  EventUtils.synthesizeKey("KEY_ArrowUp");
  aadID = gURLBar.inputField.getAttribute("aria-activedescendant");
  Assert.equal(
    aadID,
    tabToSearchRow._content.id,
    "aria-activedescendant was set to the tab-to-search result."
  );

  await UrlbarTestUtils.exitSearchMode(window);
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
});

add_task(async function tab_key_race() {
  // Mac Debug tinderboxes are just too slow and fail intermittently
  // even if the EventBufferer timeout is set to an high value.
  if (AppConstants.platform == "macosx" && AppConstants.DEBUG) {
    return;
  }
  info(
    "Test typing a letter followed shortly by down arrow consistently selects a tab-to-search result"
  );
  Assert.equal(gURLBar.value, "", "Sanity check urlbar is empty");
  let promiseQueryStarted = new Promise(resolve => {
    /**
     * A no-op test provider.
     * We use this to wait for the query to start, because otherwise TAB will
     * move to the next widget since the panel is closed and there's no running
     * query. This means waiting for the UrlbarProvidersManager to at least
     * evaluate the isActive status of providers.
     * In the future we should try to reduce this latency, to defer user events
     * even more efficiently.
     */
    class ListeningTestProvider extends UrlbarProvider {
      constructor() {
        super();
      }
      get name() {
        return "ListeningTestProvider";
      }
      get type() {
        return UrlbarUtils.PROVIDER_TYPE.PROFILE;
      }
      isActive(context) {
        executeSoon(resolve);
        return false;
      }
      isRestricting(context) {
        return false;
      }
      async startQuery(context, addCallback) {
        // Nothing to do.
      }
    }
    let provider = new ListeningTestProvider();
    UrlbarProvidersManager.registerProvider(provider);
    registerCleanupFunction(async function () {
      UrlbarProvidersManager.unregisterProvider(provider);
    });
  });
  gURLBar.focus();
  info("Type the beginning of the search string to get tab-to-search");
  EventUtils.synthesizeKey(TEST_ENGINE_DOMAIN.slice(0, 1));
  info("Awaiting for the query to start");
  await promiseQueryStarted;
  EventUtils.synthesizeKey("KEY_ArrowDown");
  await UrlbarTestUtils.promiseSearchComplete(window);
  await TestUtils.waitForCondition(
    () => UrlbarTestUtils.getSelectedRowIndex(window) == 1,
    "Wait for down arrow key to be handled"
  );
  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch",
    isPreview: true,
  });

  await UrlbarTestUtils.exitSearchMode(window);
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
});

// Test that large-style onboarding results appear and have the correct
// properties.
add_task(async function onboard() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
  });

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
  Assert.ok(autofillResult.autofill);
  Assert.equal(
    autofillResult.url,
    `https://${TEST_ENGINE_DOMAIN}/`,
    "The autofilled URL matches the engine domain."
  );
  EventUtils.synthesizeKey("KEY_ArrowDown");
  Assert.equal(
    UrlbarTestUtils.getSelectedRowIndex(window),
    1,
    "Sanity check: The second result is selected."
  );

  // Now check the properties of the onboarding result.
  let onboardingElement = await UrlbarTestUtils.waitForAutocompleteResultAt(
    window,
    1
  );
  Assert.equal(
    onboardingElement.result.payload.dynamicType,
    DYNAMIC_RESULT_TYPE,
    "The tab-to-search result is an onboarding result."
  );
  Assert.equal(
    onboardingElement.result.resultSpan,
    2,
    "The correct resultSpan was set."
  );
  Assert.ok(
    onboardingElement
      .querySelector(".urlbarView-row-inner")
      .hasAttribute("selected"),
    "The onboarding element set the selected attribute."
  );

  let [titleOnboarding, actionOnboarding, descriptionOnboarding] =
    await document.l10n.formatValues([
      {
        id: "urlbar-result-action-search-w-engine",
        args: {
          engine: onboardingElement.result.payload.engine,
        },
      },
      {
        id: Services.search.getEngineByName(
          onboardingElement.result.payload.engine
        ).isGeneralPurposeEngine
          ? "urlbar-result-action-tabtosearch-web"
          : "urlbar-result-action-tabtosearch-other-engine",
        args: { engine: onboardingElement.result.payload.engine },
      },
      {
        id: "urlbar-tabtosearch-onboard",
      },
    ]);
  let onboardingDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
  Assert.equal(
    onboardingDetails.displayed.title,
    titleOnboarding,
    "The correct title was set."
  );
  Assert.equal(
    onboardingDetails.displayed.action,
    actionOnboarding,
    "The correct action text was set."
  );
  Assert.equal(
    onboardingDetails.element.row.querySelector(
      ".urlbarView-dynamic-onboardTabToSearch-description"
    ).textContent,
    descriptionOnboarding,
    "The correct description was set."
  );

  // Check that the onboarding result enters search mode.
  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch_onboard",
    isPreview: true,
  });
  let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
  EventUtils.synthesizeKey("KEY_Enter");
  await searchPromise;
  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch_onboard",
  });
  await UrlbarTestUtils.exitSearchMode(window);
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
  delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
  await SpecialPowers.popPrefEnv();
});

// Tests that we show the onboarding result until the user interacts with it
// `browser.urlbar.tabToSearch.onboard.interactionsLeft` times.
add_task(async function onboard_limit() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
  });

  Assert.equal(
    UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
    3,
    "Sanity check: interactionsLeft is 3."
  );

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  let onboardingResult = (
    await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
  ).result;
  Assert.equal(
    onboardingResult.payload.dynamicType,
    DYNAMIC_RESULT_TYPE,
    "The second result is an onboarding result."
  );
  await EventUtils.synthesizeKey("KEY_ArrowDown");
  await UrlbarTestUtils.assertSearchMode(window, {
    engineName: TEST_ENGINE_NAME,
    entry: "tabtosearch_onboard",
    isPreview: true,
  });
  Assert.equal(UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), 2);
  await UrlbarTestUtils.exitSearchMode(window);

  // We don't increment the counter if we showed the onboarding result less than
  // 5 minutes ago.
  for (let i = 0; i < 5; i++) {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: TEST_ENGINE_DOMAIN.slice(0, 4),
      fireInputEvent: true,
    });
    onboardingResult = (
      await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
    ).result;
    Assert.equal(
      onboardingResult.payload.dynamicType,
      DYNAMIC_RESULT_TYPE,
      "The second result is an onboarding result."
    );
    await EventUtils.synthesizeKey("KEY_ArrowDown");
    await UrlbarTestUtils.assertSearchMode(window, {
      engineName: TEST_ENGINE_NAME,
      entry: "tabtosearch_onboard",
      isPreview: true,
    });
    Assert.equal(
      UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
      2,
      "We shouldn't decrement interactionsLeft if an onboarding result was just shown."
    );
    await UrlbarTestUtils.exitSearchMode(window);
  }

  // If the user doesn't interact with the result, we don't increment the
  // counter.
  for (let i = 0; i < 5; i++) {
    delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: TEST_ENGINE_DOMAIN.slice(0, 4),
      fireInputEvent: true,
    });
    let tabToSearchResult = (
      await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
    ).result;
    Assert.equal(
      tabToSearchResult.providerName,
      "TabToSearch",
      "The second result is a tab-to-search result."
    );
    Assert.equal(
      tabToSearchResult.type,
      UrlbarUtils.RESULT_TYPE.DYNAMIC,
      "The tab-to-search result is an onboarding result."
    );
    Assert.equal(
      UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
      2,
      "We shouldn't decrement interactionsLeft if the user doesn't interact with onboarding."
    );
  }

  // Test that we increment the counter if the user interacts with the result
  // and it's been 5+ minutes since they last interacted with it.
  for (let i = 1; i >= 0; i--) {
    delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: TEST_ENGINE_DOMAIN.slice(0, 4),
      fireInputEvent: true,
    });
    let tabToSearchResult = (
      await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
    ).result;
    Assert.equal(
      tabToSearchResult.providerName,
      "TabToSearch",
      "The second result is a tab-to-search result."
    );
    Assert.equal(
      onboardingResult.payload.dynamicType,
      DYNAMIC_RESULT_TYPE,
      "The second result is an onboarding result."
    );
    await EventUtils.synthesizeKey("KEY_ArrowDown");
    await UrlbarTestUtils.assertSearchMode(window, {
      engineName: TEST_ENGINE_NAME,
      entry: "tabtosearch_onboard",
      isPreview: true,
    });
    Assert.equal(
      UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"),
      i,
      "We decremented interactionsLeft."
    );
    await UrlbarTestUtils.exitSearchMode(window);
  }

  delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });
  let tabToSearchResult = (
    await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
  ).result;
  Assert.equal(
    tabToSearchResult.providerName,
    "TabToSearch",
    "The second result is a tab-to-search result."
  );
  Assert.notEqual(
    tabToSearchResult.type,
    UrlbarUtils.RESULT_TYPE.DYNAMIC,
    "Now that interactionsLeft is 0, we don't show onboarding results."
  );

  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
  delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
  await SpecialPowers.popPrefEnv();
});

// Tests that we show at most one onboarding result at a time. See
// tests/unit/test_providerTabToSearch.js:multipleEnginesForHostname for a test
// that ensures only one normal tab-to-search result is shown in this scenario.
add_task(async function onboard_multipleEnginesForHostname() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]],
  });

  let extension = await SearchTestUtils.installSearchExtension(
    {
      name: `${TEST_ENGINE_NAME}Maps`,
      search_url: `https://${TEST_ENGINE_DOMAIN}/maps/`,
    },
    { skipUnload: true }
  );

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: TEST_ENGINE_DOMAIN.slice(0, 4),
    fireInputEvent: true,
  });

  Assert.equal(
    UrlbarTestUtils.getResultCount(window),
    2,
    "Only two results are shown."
  );
  let firstResult = (
    await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0)
  ).result;
  Assert.notEqual(
    firstResult.providerName,
    "TabToSearch",
    "The first result is not from TabToSearch."
  );
  let secondResult = (
    await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)
  ).result;
  Assert.equal(
    secondResult.providerName,
    "TabToSearch",
    "The second result is from TabToSearch."
  );
  Assert.equal(
    secondResult.type,
    UrlbarUtils.RESULT_TYPE.DYNAMIC,
    "The tab-to-search result is the only onboarding result."
  );
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  await extension.unload();
  UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3);
  delete UrlbarProviderTabToSearch.onboardingInteractionAtTime;
  await SpecialPowers.popPrefEnv();
});
