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

// Tests group labels in the view.

"use strict";

const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst";
const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches";

const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");

const TOP_SITES = [
  "http://example-1.com/",
  "http://example-2.com/",
  "http://example-3.com/",
];

const FIREFOX_SUGGEST_LABEL = "Firefox Suggest";

// %s is replaced with the engine name.
const ENGINE_SUGGESTIONS_LABEL = "%s suggestions";

// Allow more time for Mac machines so they don't time out in verify mode.
if (AppConstants.platform == "macosx") {
  requestLongerTimeout(3);
}

add_setup(async function () {
  Assert.ok(
    UrlbarPrefs.get("showSearchSuggestionsFirst"),
    "Precondition: Search suggestions shown first by default"
  );

  // Add some history.
  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();
  await UrlbarTestUtils.formHistory.clear();
  await addHistory();

  // Make sure we have some top sites.
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.urlbar.suggest.topsites", true],
      ["browser.urlbar.suggest.quickactions", false],
      ["browser.newtabpage.activity-stream.default.sites", TOP_SITES.join(",")],
    ],
  });
  // Waiting for all top sites to be added intermittently times out, so just
  // wait for any to be added. We're not testing top sites here; we only need
  // the view to open in top-sites mode.
  await updateTopSites(sites => sites && sites.length);

  // Add a mock engine so we don't hit the network.
  await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });

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

// The Firefox Suggest label should not appear when the labels pref is disabled.
add_task(async function prefDisabled() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.urlbar.groupLabels.enabled", false]],
  });
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  await checkLabels(MAX_RESULTS, {});
  await UrlbarTestUtils.promisePopupClose(window);
  await SpecialPowers.popPrefEnv();
});

// The Firefox Suggest label should not appear when the view shows top sites.
add_task(async function topSites() {
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "",
  });
  await checkLabels(-1, {});
  await UrlbarTestUtils.promisePopupClose(window);
});

// The Firefox Suggest label should appear when the search string is non-empty
// and there are only general results.
add_task(async function general() {
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  await checkLabels(MAX_RESULTS, {
    1: FIREFOX_SUGGEST_LABEL,
  });
  await UrlbarTestUtils.promisePopupClose(window);
});

// The Firefox Suggest label should appear when the search string is non-empty
// and there are suggestions followed by general results.
add_task(async function suggestionsBeforeGeneral() {
  await withSuggestions(async () => {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    await checkLabels(MAX_RESULTS, {
      3: FIREFOX_SUGGEST_LABEL,
    });
    await UrlbarTestUtils.promisePopupClose(window);
  });
});

// Both the Firefox Suggest and Suggestions labels should appear when the search
// string is non-empty, general results are shown before suggestions, and there
// are general and suggestion results.
add_task(async function generalBeforeSuggestions() {
  await withSuggestions(async engine => {
    Assert.ok(engine.name, "Engine name is non-empty");
    await SpecialPowers.pushPrefEnv({
      set: [[SUGGESTIONS_FIRST_PREF, false]],
    });
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    await checkLabels(MAX_RESULTS, {
      1: FIREFOX_SUGGEST_LABEL,
      [MAX_RESULTS - 2]: engineSuggestionsLabel(engine.name),
    });
    await UrlbarTestUtils.promisePopupClose(window);
  });
});

// Neither the Firefox Suggest nor Suggestions label should appear when the
// search string is non-empty, general results are shown before suggestions, and
// there are only suggestion results.
add_task(async function generalBeforeSuggestions_suggestionsOnly() {
  await PlacesUtils.history.clear();

  await withSuggestions(async () => {
    await SpecialPowers.pushPrefEnv({
      set: [[SUGGESTIONS_FIRST_PREF, false]],
    });
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    await checkLabels(3, {});
    await UrlbarTestUtils.promisePopupClose(window);
  });

  // Add back history so subsequent tasks run with this test's initial state.
  await addHistory();
});

// The Suggestions label should be updated when the default engine changes.
add_task(async function generalBeforeSuggestions_defaultChanged() {
  // Install both test engines, one after the other. Engine 2 will be the final
  // default engine.
  await withSuggestions(async engine1 => {
    await withSuggestions(async engine2 => {
      Assert.ok(engine2.name, "Engine 2 name is non-empty");
      Assert.notEqual(engine1.name, engine2.name, "Engine names are different");
      Assert.equal(
        Services.search.defaultEngine.name,
        engine2.name,
        "Engine 2 is default"
      );
      await SpecialPowers.pushPrefEnv({
        set: [[SUGGESTIONS_FIRST_PREF, false]],
      });
      await UrlbarTestUtils.promiseAutocompleteResultPopup({
        window,
        value: "test",
      });
      await checkLabels(MAX_RESULTS, {
        1: FIREFOX_SUGGEST_LABEL,
        [MAX_RESULTS - 2]: engineSuggestionsLabel(engine2.name),
      });
      await UrlbarTestUtils.promisePopupClose(window);
    }, TEST_ENGINE_2_BASENAME);
  });
});

// The Firefox Suggest label should appear above a suggested-index result when
// the result is the only result with that label.
add_task(async function suggestedIndex_only() {
  // Clear history, add a provider that returns a result with suggestedIndex =
  // -1, set up an engine with suggestions, and start a query. The suggested-
  // index result will be the only result with a label.
  await PlacesUtils.history.clear();

  let index = -1;
  let provider = new SuggestedIndexProvider(index);
  UrlbarProvidersManager.registerProvider(provider);

  await withSuggestions(async () => {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 3);
    Assert.equal(
      result.element.row.result.suggestedIndex,
      index,
      "Sanity check: Our suggested-index result is present"
    );
    await checkLabels(4, {
      3: FIREFOX_SUGGEST_LABEL,
    });
    await UrlbarTestUtils.promisePopupClose(window);
  });

  UrlbarProvidersManager.unregisterProvider(provider);

  // Add back history so subsequent tasks run with this test's initial state.
  await addHistory();
});

// The Firefox Suggest label should appear above a suggested-index result when
// the result is the first but not the only result with that label.
add_task(async function suggestedIndex_first() {
  let index = 1;
  let provider = new SuggestedIndexProvider(index);
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
  Assert.equal(
    result.element.row.result.suggestedIndex,
    index,
    "Sanity check: Our suggested-index result is present"
  );
  await checkLabels(MAX_RESULTS, {
    [index]: FIREFOX_SUGGEST_LABEL,
  });
  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

// The Firefox Suggest label should not appear above a suggested-index result
// when the result is not the first with that label.
add_task(async function suggestedIndex_notFirst() {
  let index = -1;
  let provider = new SuggestedIndexProvider(index);
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  let result = await UrlbarTestUtils.getDetailsOfResultAt(
    window,
    MAX_RESULTS + index
  );
  Assert.equal(
    result.element.row.result.suggestedIndex,
    index,
    "Sanity check: Our suggested-index result is present"
  );
  await checkLabels(MAX_RESULTS, {
    1: FIREFOX_SUGGEST_LABEL,
  });
  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

// Labels that appear multiple times but not consecutively should be shown.
add_task(async function repeatLabels() {
  let engineName = Services.search.defaultEngine.name;
  let results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      suggestedIndex: 0,
      payload: { url: "http://example.com/1" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      suggestedIndex: 1,
      payload: { suggestion: "test1", engine: engineName },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      suggestedIndex: 2,
      payload: { url: "http://example.com/2" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      suggestedIndex: 3,
      payload: { suggestion: "test2", engine: engineName },
    }),
  ];

  let provider = new UrlbarTestUtils.TestProvider({
    results,
    priority: Infinity,
  });
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  await checkLabels(results.length, {
    0: FIREFOX_SUGGEST_LABEL,
    1: engineSuggestionsLabel(engineName),
    2: FIREFOX_SUGGEST_LABEL,
    3: engineSuggestionsLabel(engineName),
  });
  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

// Clicking a row label shouldn't do anything.
add_task(async function clickLabel() {
  await BrowserTestUtils.withNewTab("about:blank", async () => {
    // Do a search. The mock history added in init() should appear with the
    // Firefox Suggest label at index 1.
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    await checkLabels(MAX_RESULTS, {
      1: FIREFOX_SUGGEST_LABEL,
    });

    // Check the result at index 2.
    let result2 = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
    Assert.ok(result2.url, "Result at index 2 has a URL");
    let url2 = result2.url;
    Assert.ok(
      url2.startsWith("http://example.com/"),
      "Result at index 2 is one of our mock history results"
    );

    // Get the row at index 3 and click above it. The click should hit the row
    // at index 2 and load its URL. We do this to make sure our click code
    // here in the test works properly and that performing a similar click
    // relative to index 1 (see below) would hit the row at index 0 if not for
    // the label at index 1.
    let result3 = await UrlbarTestUtils.getDetailsOfResultAt(window, 3);
    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

    info("Performing click relative to index 3");
    await UrlbarTestUtils.promisePopupClose(window, () =>
      click(result3.element.row, { y: -2 })
    );
    info("Waiting for load after performing click relative to index 3");
    await loadPromise;
    Assert.equal(gBrowser.currentURI.spec, url2, "Loaded URL at index 2");
    // Now do the search again.
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });

    await checkLabels(MAX_RESULTS, {
      1: FIREFOX_SUGGEST_LABEL,
    });

    // Check the result at index 1, the one with the label.
    let result1 = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
    Assert.ok(result1.url, "Result at index 1 has a URL");
    let url1 = result1.url;
    Assert.ok(
      url1.startsWith("http://example.com/"),
      "Result at index 1 is one of our mock history results"
    );
    Assert.notEqual(url1, url2, "URLs at indexes 1 and 2 are different");

    // Do a click on the row at index 1 in the same way as before. This time
    // nothing should happen because the click should hit the label, not the
    // row at index 0.
    info("Clicking row label at index 1");
    click(result1.element.row, { y: -2 });
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 500));
    Assert.ok(UrlbarTestUtils.isPopupOpen(window), "View remains open");
    Assert.equal(
      gBrowser.currentURI.spec,
      url2,
      "Current URL is still URL from index 2"
    );

    // Now click the main part of the row at index 1. Its URL should load.
    loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    let { height } = result1.element.row.getBoundingClientRect();
    info(`Clicking main part of the row at index 1, height=${height}`);
    await UrlbarTestUtils.promisePopupClose(window, () =>
      click(result1.element.row)
    );
    info("Waiting for load after clicking row at index 1");
    await loadPromise;
    Assert.equal(gBrowser.currentURI.spec, url1, "Loaded URL at index 1");
  });
});

add_task(async function ariaLabel() {
  const helpUrl = "http://example.com/help";
  const results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      suggestedIndex: 0,
      payload: { url: "http://example.com/1", helpUrl },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      suggestedIndex: 1,
      payload: { url: "http://example.com/2", helpUrl },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      suggestedIndex: 2,
      payload: { url: "http://example.com/3" },
    }),
  ];

  const provider = new UrlbarTestUtils.TestProvider({
    results,
    priority: Infinity,
  });
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  await checkLabels(results.length, {
    0: FIREFOX_SUGGEST_LABEL,
  });

  const expectedRows = [
    { hasGroupAriaLabel: true, ariaLabel: FIREFOX_SUGGEST_LABEL },
    { hasGroupAriaLabel: false },
    { hasGroupAriaLabel: false },
  ];
  await checkGroupAriaLabels(expectedRows);

  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

add_task(async function hideRowLabel() {
  let engineName = Services.search.defaultEngine.name;
  const results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      hideRowLabel: true,
      isBestMatch: true,
      payload: { url: "http://example.com/1" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      payload: { url: "http://example.com/2" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      payload: { url: "http://example.com/3" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.RESTRICT,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      hideRowLabel: true,
      payload: { keyword: "*" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      hideRowLabel: true,
      payload: {
        engine: engineName,
        keyword: "@keyword",
      },
    }),
  ];
  const provider = new UrlbarTestUtils.TestProvider({
    results,
    priority: Infinity,
  });
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });

  const expectedRows = [
    { hasGroupAriaLabel: false },
    { hasGroupAriaLabel: true, ariaLabel: FIREFOX_SUGGEST_LABEL },
    { hasGroupAriaLabel: false },
    { hasGroupAriaLabel: false },
    { hasGroupAriaLabel: false },
  ];
  await checkGroupAriaLabels(expectedRows);

  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

add_task(async function previousRowLabelIsHidden_then_searchResults() {
  let engineName = Services.search.defaultEngine.name;
  const results = [
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.URL,
      source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
      hideRowLabel: true,
      suggestedIndex: 1,
      payload: { url: "http://example.com/1" },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      suggestedIndex: 2,
      payload: { suggestion: "test1", engine: engineName },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      hideRowLabel: true,
      suggestedIndex: 3,
      payload: { suggestion: "test2", engine: engineName },
    }),
    new UrlbarResult({
      type: UrlbarUtils.RESULT_TYPE.SEARCH,
      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
      suggestedIndex: 4,
      payload: { suggestion: "test3", engine: engineName },
    }),
  ];
  const provider = new UrlbarTestUtils.TestProvider({
    results,
    priority: Infinity,
  });
  UrlbarProvidersManager.registerProvider(provider);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });

  await checkLabels(4, {
    1: engineSuggestionsLabel(engineName),
  });

  await UrlbarTestUtils.promisePopupClose(window);

  UrlbarProvidersManager.unregisterProvider(provider);
});

/**
 * Provider that returns a suggested-index result.
 */
class SuggestedIndexProvider extends UrlbarTestUtils.TestProvider {
  constructor(suggestedIndex) {
    super({
      results: [
        new UrlbarResult({
          type: UrlbarUtils.RESULT_TYPE.URL,
          source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
          suggestedIndex,
          payload: { url: "http://example.com/" },
        }),
      ],
    });
  }
}

async function addHistory() {
  for (let i = 0; i < MAX_RESULTS; i++) {
    await PlacesTestUtils.addVisits("http://example.com/" + i);
  }
}

/**
 * Asserts that each result in the view does or doesn't have a label, depending
 * on `labelsByIndex`.
 *
 * @param {number} resultCount
 *   The expected number of results. Pass -1 to use the max index in
 *   `labelsByIndex` or the actual result count if `labelsByIndex` is empty.
 * @param {object} labelsByIndex
 *   A mapping from indexes to expected labels.
 */
async function checkLabels(resultCount, labelsByIndex) {
  if (resultCount >= 0) {
    Assert.equal(
      UrlbarTestUtils.getResultCount(window),
      resultCount,
      "Expected result count"
    );
  } else {
    // This `else` branch is only necessary because waiting for all top sites to
    // be added intermittently times out. Don't let the test fail for such a
    // dumb reason.
    let indexes = Object.keys(labelsByIndex);
    if (indexes.length) {
      resultCount = indexes.sort((a, b) => b - a)[0] + 1;
    } else {
      resultCount = UrlbarTestUtils.getResultCount(window);
      Assert.greater(resultCount, 0, "Actual result count is > 0");
    }
  }
  for (let i = 0; i < resultCount; i++) {
    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
    let { row } = result.element;
    let before = getComputedStyle(row, "::before");
    if (labelsByIndex.hasOwnProperty(i)) {
      Assert.equal(
        before.content,
        "attr(label)",
        `::before.content is correct at index ${i}`
      );
      Assert.equal(
        row.getAttribute("label"),
        labelsByIndex[i],
        `Row has correct label at index ${i}`
      );
    } else {
      Assert.equal(
        before.content,
        "none",
        `::before.content is 'none' at index ${i}`
      );
      Assert.ok(
        !row.hasAttribute("label"),
        `Row does not have label attribute at index ${i}`
      );
    }
  }
}

/**
 * Asserts that an element for group aria label.
 *
 * @param {Array} expectedRows The expected rows.
 */
async function checkGroupAriaLabels(expectedRows) {
  Assert.equal(
    UrlbarTestUtils.getResultCount(window),
    expectedRows.length,
    "Expected result count"
  );

  for (let i = 0; i < expectedRows.length; i++) {
    const result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
    const { row } = result.element;
    const groupAriaLabel = row.querySelector(".urlbarView-group-aria-label");

    const expected = expectedRows[i];

    Assert.equal(
      !!groupAriaLabel,
      expected.hasGroupAriaLabel,
      `Group aria label exists as expected in the results[${i}]`
    );

    if (expected.hasGroupAriaLabel) {
      Assert.equal(
        groupAriaLabel.getAttribute("aria-label"),
        expected.ariaLabel,
        `Content of aria-label attribute in the element for group aria label in the results[${i}] is correct`
      );
    }
  }
}

function engineSuggestionsLabel(engineName) {
  return ENGINE_SUGGESTIONS_LABEL.replace("%s", engineName);
}

/**
 * Adds a search engine that provides suggestions, calls your callback, and then
 * remove the engine.
 *
 * @param {Function} callback
 *   Your callback function.
 * @param {string} [engineBasename]
 *   The basename of the engine file.
 */
async function withSuggestions(
  callback,
  engineBasename = TEST_ENGINE_BASENAME
) {
  await SpecialPowers.pushPrefEnv({
    set: [[SUGGESTIONS_PREF, true]],
  });
  let engine = await SearchTestUtils.installOpenSearchEngine({
    url: getRootDirectory(gTestPath) + engineBasename,
  });
  let oldDefaultEngine = await Services.search.getDefault();
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
  try {
    await callback(engine);
  } finally {
    await Services.search.setDefault(
      oldDefaultEngine,
      Ci.nsISearchService.CHANGE_REASON_UNKNOWN
    );
    await Services.search.removeEngine(engine);
    await SpecialPowers.popPrefEnv();
  }
}

function click(element, { x = undefined, y = undefined } = {}) {
  let { width, height } = element.getBoundingClientRect();
  if (typeof x != "number") {
    x = width / 2;
  }
  if (typeof y != "number") {
    y = height / 2;
  }
  EventUtils.synthesizeMouse(element, x, y, { type: "mousedown" });
  EventUtils.synthesizeMouse(element, x, y, { type: "mouseup" });
}
