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

/**
 * This file tests urlbar autofill telemetry.
 */

"use strict";

const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";

function assertSearchTelemetryEmpty(search_hist) {
  const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
  Assert.ok(
    !(SCALAR_URLBAR in scalars),
    `Should not have recorded ${SCALAR_URLBAR}`
  );

  // SEARCH_COUNTS should not contain any engine counts at all. The keys in this
  // histogram are search engine telemetry identifiers.
  Assert.deepEqual(
    Object.keys(search_hist.snapshot()),
    [],
    "SEARCH_COUNTS is empty"
  );

  // Also check events.
  let events = Services.telemetry.snapshotEvents(
    Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
    false
  );
  events = (events.parent || []).filter(
    e => e[1] == "navigation" && e[2] == "search"
  );
  Assert.deepEqual(
    events,
    [],
    "Should not have recorded any navigation search events"
  );
}

function snapshotHistograms() {
  Services.telemetry.clearScalars();
  Services.telemetry.clearEvents();
  return {
    resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
      "FX_URLBAR_SELECTED_RESULT_METHOD"
    ),
    search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
  };
}

function assertTelemetryResults(histograms, type, index, method) {
  TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1);

  TelemetryTestUtils.assertKeyedScalar(
    TelemetryTestUtils.getProcessScalars("parent", true, true),
    `urlbar.picked.${type}`,
    index,
    1
  );
}

/**
 * Performs a search and picks the first result.
 *
 * @param {string} searchString
 *   The search string. Assumed to trigger an autofill result
 * @param {string} autofilledValue
 *   The input's expected value after autofill occurs.
 * @param {string} unpickResult
 *   Optional: If true, do not pick any result. Default value is false.
 * @param {string} urlToSelect
 *   Optional: If want to select result except autofill, pass the URL.
 */
async function triggerAutofillAndPickResult(
  searchString,
  autofilledValue,
  unpickResult = false,
  urlToSelect = null
) {
  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: searchString,
      fireInputEvent: true,
    });

    let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
    Assert.ok(details.autofill, "Result is autofill");
    Assert.equal(gURLBar.value, autofilledValue, "gURLBar.value");
    Assert.equal(gURLBar.selectionStart, searchString.length, "selectionStart");
    Assert.equal(gURLBar.selectionEnd, autofilledValue.length, "selectionEnd");

    if (urlToSelect) {
      for (let row = 0; row < UrlbarTestUtils.getResultCount(window); row++) {
        const result = await UrlbarTestUtils.getDetailsOfResultAt(window, row);
        if (result.url === urlToSelect) {
          UrlbarTestUtils.setSelectedRowIndex(window, row);
          break;
        }
      }
    }

    if (unpickResult) {
      // Close popup without any action.
      await UrlbarTestUtils.promisePopupClose(window);
      return;
    }

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

    let url;
    if (urlToSelect) {
      url = urlToSelect;
    } else {
      url = autofilledValue.includes(":")
        ? autofilledValue
        : "http://" + autofilledValue;
    }
    Assert.equal(gBrowser.currentURI.spec, url, "Loaded URL is correct");
  });
}

function createOtherAutofillProvider(searchString, autofilledValue) {
  return new UrlbarTestUtils.TestProvider({
    priority: Infinity,
    type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC,
    results: [
      Object.assign(
        new UrlbarResult(
          UrlbarUtils.RESULT_TYPE.URL,
          UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
          {
            title: "Test",
            url: "http://example.com/",
          }
        ),
        {
          heuristic: true,
          autofill: {
            value: autofilledValue,
            selectionStart: searchString.length,
            selectionEnd: autofilledValue.length,
            // Leave out `type` to trigger "other"
          },
        }
      ),
    ],
  });
}

// 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 () {
  // The following initialization code is necessary to avoid a frequent
  // intermittent failure in verify-fission where, due to timings, we may or
  // may not import default bookmarks.
  info("Ensure Places init is complete");
  let placesInitCompleteObserved = TestUtils.topicObserved(
    "places-browser-init-complete"
  );
  Cc["@mozilla.org/browser/browserglue;1"]
    .getService(Ci.nsIObserver)
    .observe(null, "browser-glue-test", "places-browser-init-complete");
  await placesInitCompleteObserved;

  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesUtils.history.clear();
  await PlacesTestUtils.clearInputHistory();

  // Enable local telemetry recording for the duration of the tests.
  const originalCanRecord = Services.telemetry.canRecordExtended;
  Services.telemetry.canRecordExtended = true;

  registerCleanupFunction(async () => {
    Services.telemetry.canRecordExtended = originalCanRecord;
    await PlacesTestUtils.clearInputHistory();
    await PlacesUtils.history.clear();
  });
});

// Checks adaptive history, origin, and URL autofill.
add_task(async function history() {
  const testData = [
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "ex",
      autofilled: "example.com/",
      expected: "autofill_origin",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "exa",
      autofilled: "example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "exam",
      autofilled: "example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.com",
      autofilled: "example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.com/",
      autofilled: "example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.com/test",
      autofilled: "example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test", "http://example.org/test"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.org",
      autofilled: "example.org/",
      expected: "autofill_origin",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/test", "http://example.com/test/url"],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.com/test/",
      autofilled: "example.com/test/",
      expected: "autofill_url",
    },
    {
      useAdaptiveHistory: true,
      visitHistory: [{ uri: "http://example.com/test" }],
      inputHistory: [
        { uri: "http://example.com/test", input: "http://example.com/test" },
      ],
      userInput: "http://example.com/test",
      autofilled: "http://example.com/test",
      expected: "autofill_adaptive",
    },
    {
      useAdaptiveHistory: false,
      visitHistory: [{ uri: "http://example.com/test" }],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example",
      autofilled: "example.com/",
      expected: "autofill_origin",
    },
    {
      useAdaptiveHistory: false,
      visitHistory: [{ uri: "http://example.com/test" }],
      inputHistory: [{ uri: "http://example.com/test", input: "exa" }],
      userInput: "example.com/te",
      autofilled: "example.com/test",
      expected: "autofill_url",
    },
  ];

  for (const {
    useAdaptiveHistory,
    visitHistory,
    inputHistory,
    userInput,
    autofilled,
    expected,
  } of testData) {
    const histograms = snapshotHistograms();

    await PlacesTestUtils.addVisits(visitHistory);
    for (const { uri, input } of inputHistory) {
      await UrlbarUtils.addToInputHistory(uri, input);
    }

    UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory);

    await triggerAutofillAndPickResult(userInput, autofilled);

    assertSearchTelemetryEmpty(histograms.search_hist);
    assertTelemetryResults(
      histograms,
      expected,
      0,
      UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
    );

    UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");
    await PlacesTestUtils.clearInputHistory();
    await PlacesUtils.history.clear();
  }
});

// Checks about-page autofill (e.g., "about:about").
add_task(async function about() {
  let histograms = snapshotHistograms();
  await triggerAutofillAndPickResult("about:abou", "about:about");

  assertSearchTelemetryEmpty(histograms.search_hist);
  assertTelemetryResults(
    histograms,
    "autofill_about",
    0,
    UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
  );

  await PlacesUtils.history.clear();
});

// Checks the "other" fallback, which shouldn't normally happen.
add_task(async function other() {
  let searchString = "exam";
  let autofilledValue = "example.com/";
  let provider = createOtherAutofillProvider(searchString, autofilledValue);
  UrlbarProvidersManager.registerProvider(provider);

  let histograms = snapshotHistograms();
  await triggerAutofillAndPickResult(searchString, autofilledValue);

  assertSearchTelemetryEmpty(histograms.search_hist);
  assertTelemetryResults(
    histograms,
    "autofill_other",
    0,
    UrlbarTestUtils.SELECTED_RESULT_METHODS.enter
  );

  await PlacesUtils.history.clear();
  UrlbarProvidersManager.unregisterProvider(provider);
});

// Checks impression telemetry.
add_task(async function impression() {
  const testData = [
    {
      description: "Adaptive history autofill and pick it",
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
      userInput: "exa",
      autofilled: "example.com/first",
      expected: "autofill_adaptive",
    },
    {
      description: "Adaptive history autofill but pick another result",
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
      userInput: "exa",
      urlToSelect: "http://example.com/second",
      autofilled: "example.com/first",
      expected: "autofill_adaptive",
    },
    {
      description: "Adaptive history autofill but not pick any result",
      unpickResult: true,
      useAdaptiveHistory: true,
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      inputHistory: [{ uri: "http://example.com/first", input: "exa" }],
      userInput: "exa",
      autofilled: "example.com/first",
    },
    {
      description: "Origin autofill and pick it",
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "exa",
      autofilled: "example.com/",
      expected: "autofill_origin",
    },
    {
      description: "Origin autofill but pick another result",
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "exa",
      urlToSelect: "http://example.com/second",
      autofilled: "example.com/",
      expected: "autofill_origin",
    },
    {
      description: "Origin autofill but not pick any result",
      unpickResult: true,
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "exa",
      autofilled: "example.com/",
    },
    {
      description: "URL autofill and pick it",
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "example.com/",
      autofilled: "example.com/",
      expected: "autofill_url",
    },
    {
      description: "URL autofill but pick another result",
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "example.com/",
      urlToSelect: "http://example.com/second",
      autofilled: "example.com/",
      expected: "autofill_url",
    },
    {
      description: "URL autofill but not pick any result",
      unpickResult: true,
      visitHistory: ["http://example.com/first", "http://example.com/second"],
      userInput: "example.com/",
      autofilled: "example.com/",
    },
    {
      description: "about page autofill and pick it",
      userInput: "about:a",
      autofilled: "about:about",
      expected: "autofill_about",
    },
    {
      description: "about page autofill but pick another result",
      userInput: "about:a",
      urlToSelect: "about:addons",
      autofilled: "about:about",
      expected: "autofill_about",
    },
    {
      description: "about page autofill but not pick any result",
      unpickResult: true,
      userInput: "about:a",
      autofilled: "about:about",
    },
    {
      description: "Other provider's autofill and pick it",
      useOtherProvider: true,
      userInput: "example",
      autofilled: "example.com/",
      expected: "autofill_other",
    },
    {
      description: "Other provider's autofill but not pick any result",
      unpickResult: true,
      useOtherProvider: true,
      userInput: "example",
      autofilled: "example.com/",
    },
  ];

  for (const {
    description,
    useAdaptiveHistory = false,
    useOtherProvider = false,
    unpickResult = false,
    visitHistory,
    inputHistory,
    userInput,
    select,
    autofilled,
    expected,
  } of testData) {
    info(description);

    UrlbarPrefs.set("autoFill.adaptiveHistory.enabled", useAdaptiveHistory);
    let otherProvider;
    if (useOtherProvider) {
      otherProvider = createOtherAutofillProvider(userInput, autofilled);
      UrlbarProvidersManager.registerProvider(otherProvider);
    }

    if (visitHistory) {
      await PlacesTestUtils.addVisits(visitHistory);
    }
    if (inputHistory) {
      for (const { uri, input } of inputHistory) {
        await UrlbarUtils.addToInputHistory(uri, input);
      }
    }

    await triggerAutofillAndPickResult(
      userInput,
      autofilled,
      unpickResult,
      select
    );

    const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
    if (unpickResult) {
      TelemetryTestUtils.assertScalarUnset(
        scalars,
        "urlbar.impression.autofill_adaptive"
      );
      TelemetryTestUtils.assertScalarUnset(
        scalars,
        "urlbar.impression.autofill_origin"
      );
      TelemetryTestUtils.assertScalarUnset(
        scalars,
        "urlbar.impression.autofill_url"
      );
      TelemetryTestUtils.assertScalarUnset(
        scalars,
        "urlbar.impression.autofill_about"
      );
    } else {
      TelemetryTestUtils.assertScalar(
        scalars,
        `urlbar.impression.${expected}`,
        1
      );
    }

    UrlbarPrefs.clear("autoFill.adaptiveHistory.enabled");

    if (otherProvider) {
      UrlbarProvidersManager.unregisterProvider(otherProvider);
    }

    await PlacesTestUtils.clearInputHistory();
    await PlacesUtils.history.clear();
  }
});

// Checks autofill deletion telemetry.
add_task(async function deletion() {
  await PlacesTestUtils.addVisits(["http://example.com/"]);

  info("Delete autofilled value by DELETE key");
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      EventUtils.synthesizeKey("KEY_Delete");
      Assert.equal(gURLBar.value, "exa");
    },
    expectedScalar: 1,
  });

  info("Delete autofilled value by BACKSPACE key");
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      EventUtils.synthesizeKey("KEY_Backspace");
      Assert.equal(gURLBar.value, "exa");
    },
    expectedScalar: 1,
  });

  info("Delete autofilled value twice");
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      // Delete autofilled string.
      EventUtils.synthesizeKey("KEY_Delete");
      Assert.equal(gURLBar.value, "exa");

      // Re-autofilling.
      EventUtils.synthesizeKey("m");
      Assert.equal(gURLBar.value, "example.com/");
      Assert.equal(gURLBar.selectionStart, "exam".length);
      Assert.equal(gURLBar.selectionEnd, "example.com/".length);

      // Delete autofilled string again.
      EventUtils.synthesizeKey("KEY_Backspace");
      Assert.equal(gURLBar.value, "exam");
    },
    expectedScalar: 2,
  });

  info("Delete one char after unselecting autofilled string");
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      // Cancel selection.
      EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
      Assert.equal(gURLBar.selectionStart, "example.com/".length);
      Assert.equal(gURLBar.selectionEnd, "example.com/".length);

      EventUtils.synthesizeKey("KEY_Backspace");
      Assert.equal(gURLBar.value, "example.com");
    },
    expectedScalar: 0,
  });

  info("Delete autofilled value after unselecting autofilled string");
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      // Cancel selection.
      EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
      Assert.equal(gURLBar.selectionStart, "example.com/".length);
      Assert.equal(gURLBar.selectionEnd, "example.com/".length);

      // Delete autofilled string one by one.
      for (let i = 0; i < "mple.com/".length; i++) {
        EventUtils.synthesizeKey("KEY_Backspace");
      }
      Assert.equal(gURLBar.value, "exa");
    },
    expectedScalar: 0,
  });

  info(
    "Delete autofilled value after unselecting autofilled string then selecting them manually again"
  );
  await doDeletionTest({
    firstSearchString: "exa",
    firstAutofilledValue: "example.com/",
    trigger: () => {
      // Cancel selection.
      const previousSelectionStart = gURLBar.selectionStart;
      const previousSelectionEnd = gURLBar.selectionEnd;
      EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
      Assert.equal(gURLBar.selectionStart, "example.com/".length);
      Assert.equal(gURLBar.selectionEnd, "example.com/".length);

      // Select same range again.
      gURLBar.selectionStart = previousSelectionStart;
      gURLBar.selectionEnd = previousSelectionEnd;

      EventUtils.synthesizeKey("KEY_Backspace");
      Assert.equal(gURLBar.value, "exa");
    },
    expectedScalar: 1,
  });

  await PlacesUtils.history.clear();
});

async function doDeletionTest({
  firstSearchString,
  firstAutofilledValue,
  trigger,
  expectedScalar,
}) {
  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await UrlbarTestUtils.promiseAutocompleteResultPopup({
      window,
      value: firstSearchString,
      fireInputEvent: true,
    });
    const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
    Assert.ok(details.autofill, "Result is autofill");
    Assert.equal(gURLBar.value, firstAutofilledValue, "gURLBar.value");
    Assert.equal(
      gURLBar.selectionStart,
      firstSearchString.length,
      "selectionStart"
    );
    Assert.equal(
      gURLBar.selectionEnd,
      firstAutofilledValue.length,
      "selectionEnd"
    );

    await trigger();

    const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
    if (expectedScalar) {
      TelemetryTestUtils.assertScalar(
        scalars,
        "urlbar.autofill_deletion",
        expectedScalar
      );
    } else {
      TelemetryTestUtils.assertScalarUnset(scalars, "urlbar.autofill_deletion");
    }

    await UrlbarTestUtils.promisePopupClose(window);
  });
}
