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

/**
 * This file tests urlbar telemetry related to the zero-prefix view, i.e., when
 * the search string is empty.
 */

"use strict";

const HISTOGRAM_DWELL_TIME = "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS";
const SCALARS = {
  ABANDONMENT: "urlbar.zeroprefix.abandonment",
  ENGAGEMENT: "urlbar.zeroprefix.engagement",
  EXPOSURE: "urlbar.zeroprefix.exposure",
};

add_setup(async function () {
  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();
  Services.telemetry.clearScalars();

  await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
  await updateTopSitesAndAwaitChanged();
});

// zero prefix engagement
add_task(async function engagement() {
  let dwellHistogram =
    TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);

  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await showZeroPrefix();
    checkScalars({
      [SCALARS.EXPOSURE]: 1,
    });
    checkAndClearHistogram(dwellHistogram, false);

    info("Finding row with result type URL");
    let foundURLRow = false;
    let count = UrlbarTestUtils.getResultCount(window);
    for (let i = 0; i < count && !foundURLRow; i++) {
      EventUtils.synthesizeKey("KEY_ArrowDown");
      let index = UrlbarTestUtils.getSelectedRowIndex(window);
      Assert.equal(index, i, "The expected row index should be selected");
      let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
      info(`Checked row at index ${i}, result type is: ${details.type}`);
      if (details.type == UrlbarUtils.RESULT_TYPE.URL) {
        foundURLRow = true;
      }
    }
    Assert.ok(foundURLRow, "Should have found a row with result type URL");

    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    EventUtils.synthesizeKey("KEY_Enter");
    await loadPromise;
  });

  checkScalars({
    [SCALARS.ENGAGEMENT]: 1,
  });
  checkAndClearHistogram(dwellHistogram, true);
});

// zero prefix abandonment
add_task(async function abandonment() {
  let dwellHistogram =
    TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);

  // Open and close the view twice. The second time the view will used a cached
  // query context and that shouldn't interfere with telemetry.
  for (let i = 0; i < 2; i++) {
    await showZeroPrefix();
    checkScalars({
      [SCALARS.EXPOSURE]: 1,
    });
    checkAndClearHistogram(dwellHistogram, false);

    await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
    checkScalars({
      [SCALARS.ABANDONMENT]: 1,
    });
    dwellHistogram = checkAndClearHistogram(dwellHistogram, true);
  }
});

// Shows the zero-prefix view, does some searches, then shows it again by doing
// a search for an empty string.
add_task(async function searches() {
  let dwellHistogram =
    TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);

  info("Show zero prefix");
  await showZeroPrefix();
  checkScalars({
    [SCALARS.EXPOSURE]: 1,
  });
  checkAndClearHistogram(dwellHistogram, false);

  info("Search for 't'");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "t",
  });
  checkScalars({});
  dwellHistogram = checkAndClearHistogram(dwellHistogram, true);

  info("Search for 'te'");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "te",
  });
  checkScalars({});
  checkAndClearHistogram(dwellHistogram, false);

  info("Search for 't'");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "t",
  });
  checkScalars({});
  checkAndClearHistogram(dwellHistogram, false);

  info("Search for ''");
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "",
  });
  checkScalars({
    [SCALARS.EXPOSURE]: 1,
  });
  checkAndClearHistogram(dwellHistogram, false);

  info("Blur urlbar and close view");
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
  checkScalars({
    [SCALARS.ABANDONMENT]: 1,
  });
  checkAndClearHistogram(dwellHistogram, true);
});

// A zero prefix engagement should not be recorded when the view isn't showing
// zero prefix.
add_task(async function notZeroPrefix_engagement() {
  let dwellHistogram =
    TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);

  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: "test",
    });
    let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
    EventUtils.synthesizeKey("KEY_Enter");
    await loadPromise;
  });

  checkScalars({});
  checkAndClearHistogram(dwellHistogram, false);
});

// A zero prefix abandonment should not be recorded when the view isn't showing
// zero prefix.
add_task(async function notZeroPrefix_abandonment() {
  let dwellHistogram =
    TelemetryTestUtils.getAndClearHistogram(HISTOGRAM_DWELL_TIME);

  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window,
    value: "test",
  });
  await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());

  checkScalars({});
  checkAndClearHistogram(dwellHistogram, false);
});

function checkScalars(expected) {
  let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
  for (let scalar of Object.values(SCALARS)) {
    if (expected.hasOwnProperty(scalar)) {
      TelemetryTestUtils.assertScalar(scalars, scalar, expected[scalar]);
    } else {
      Assert.ok(
        !scalars.hasOwnProperty(scalar),
        "Scalar should not be recorded: " + scalar
      );
    }
  }
}

function checkAndClearHistogram(histogram, expected) {
  if (expected) {
    Assert.deepEqual(
      Object.values(histogram.snapshot().values).filter(v => v > 0),
      [1],
      "Dwell histogram should be updated"
    );
  } else {
    Assert.strictEqual(
      histogram.snapshot().sum,
      0,
      "Dwell histogram should not be updated"
    );
  }

  return TelemetryTestUtils.getAndClearHistogram(histogram.name());
}

async function showZeroPrefix() {
  let { promise, cleanup } = waitForQueryFinished();
  await SimpleTest.promiseFocus(window);
  await UrlbarTestUtils.promisePopupOpen(window, () =>
    document.getElementById("Browser:OpenLocation").doCommand()
  );
  await promise;
  cleanup();

  Assert.greater(
    UrlbarTestUtils.getResultCount(window),
    0,
    "There should be at least one row in the zero prefix view"
  );
}

/**
 * Returns a promise that's resolved on the next `onQueryFinished()`. It's
 * important to wait for `onQueryFinished()` because that's when the view checks
 * whether it's showing zero prefix.
 *
 * @returns {object}
 *   An object with the following properties:
 *     {Promise} promise
 *       Resolved when `onQueryFinished()` is called.
 *     {Function} cleanup
 *       This should be called to remove the listener.
 */
function waitForQueryFinished() {
  let deferred = PromiseUtils.defer();
  let listener = {
    onQueryFinished: () => deferred.resolve(),
  };
  gURLBar.controller.addQueryListener(listener);

  return {
    promise: deferred.promise,
    cleanup() {
      gURLBar.controller.removeQueryListener(listener);
    },
  };
}

async function updateTopSitesAndAwaitChanged() {
  let url = "http://mochi.test:8888/topsite";
  for (let i = 0; i < 5; i++) {
    await PlacesTestUtils.addVisits(url);
  }

  info("Updating top sites and awaiting newtab-top-sites-changed");
  let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
    () => info("Observed newtab-top-sites-changed")
  );
  await updateTopSites(sites => sites?.length);
  await changedPromise;
}
