/* 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 that old results from UrlbarProviderAutofill do not overwrite results
 * from UrlbarProviderHeuristicFallback after the autofillable query is
 * cancelled. See bug 1653436.
 */

const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

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

/**
 * A test provider that waits before returning results to simulate a slow DB
 * lookup.
 */
class SlowHeuristicProvider extends TestProvider {
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
  }

  async startQuery(context, add) {
    this._context = context;
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, 300));
    for (let result of this.results) {
      add(this, result);
    }
  }
}

/**
 * A fast provider that alerts the test when it has added its results.
 */
class FastHeuristicProvider extends TestProvider {
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
  }

  async startQuery(context, add) {
    this._context = context;
    for (let result of this.results) {
      add(this, result);
    }
    Services.obs.notifyObservers(null, "results-added");
  }
}

add_setup(async function () {
  registerCleanupFunction(async () => {
    Services.prefs.clearUserPref("browser.urlbar.suggest.searches");
  });

  Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
});

/**
 * Tests that UrlbarProvidersManager._heuristicProviderTimer is cancelled when
 * a query is cancelled.
 */
add_task(async function timerIsCancelled() {
  let context = createContext("m", { isPrivate: false });
  await PlacesTestUtils.promiseAsyncUpdates();
  info("Manually set up query and then overwrite it.");
  // slowProvider is a stand-in for a slow UrlbarProviderPlaces returning a
  // non-heuristic result.
  let slowProvider = new SlowHeuristicProvider({
    results: [
      makeVisitResult(context, {
        uri: `http://mozilla.org/`,
        title: `mozilla.org/`,
      }),
    ],
  });
  UrlbarProvidersManager.registerProvider(slowProvider);

  // fastProvider is a stand-in for a fast Autofill returning a heuristic
  // result.
  let fastProvider = new FastHeuristicProvider({
    results: [
      makeVisitResult(context, {
        uri: `http://mozilla.com/`,
        title: `mozilla.com/`,
        heuristic: true,
      }),
    ],
  });
  UrlbarProvidersManager.registerProvider(fastProvider);
  let firstContext = createContext("m", {
    providers: [slowProvider.name, fastProvider.name],
  });
  let secondContext = createContext("ma", {
    providers: [slowProvider.name, fastProvider.name],
  });

  let controller = UrlbarTestUtils.newMockController();
  let queryRecieved, queryCancelled;
  const controllerListener = {
    onQueryResults(queryContext) {
      Assert.equal(
        queryContext,
        secondContext,
        "Only the second query should finish."
      );
      queryRecieved = true;
    },
    onQueryCancelled(queryContext) {
      Assert.equal(
        queryContext,
        firstContext,
        "The first query should be cancelled."
      );
      Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
      queryCancelled = true;
    },
  };
  controller.addQueryListener(controllerListener);

  // Wait until FastProvider sends its results to the providers manager.
  // Then they will be queued up in a _heuristicProvidersTimer, waiting for
  // the results from SlowProvider.
  let resultsAddedPromise = new Promise(resolve => {
    let observe = async () => {
      Services.obs.removeObserver(observe, "results-added");
      // Fire the second query to cancel the first.
      await controller.startQuery(secondContext);
      resolve();
    };

    Services.obs.addObserver(observe, "results-added");
  });

  controller.startQuery(firstContext);
  await resultsAddedPromise;

  Assert.ok(queryCancelled, "At least one query was cancelled.");
  Assert.ok(queryRecieved, "At least one query finished.");
  controller.removeQueryListener(controllerListener);
});

/**
 * Tests that old autofill results aren't displayed after a query is cancelled.
 * See bug 1653436.
 */
add_task(async function autofillIsCleared() {
  /**
   * Steps:
   *   1. Start query.
   *   2. Allow UrlbarProviderAutofill to start _getAutofillResult.
   *   3. Execute a new query with no autofill match, cancelling the first
   *      query.
   *   4. Test that the old result from UrlbarProviderAutofill isn't displayed.
   */
  await PlacesTestUtils.addVisits("http://example.com");

  let firstContext = createContext("e", {
    providers: ["Autofill", "HeuristicFallback"],
  });
  let secondContext = createContext("em", {
    providers: ["Autofill", "HeuristicFallback"],
  });

  info("Sanity check: The first query autofills and the second does not.");
  await check_results({
    firstContext,
    autofilled: "example.com",
    completed: "http://example.com/",
    matches: [
      makeVisitResult(firstContext, {
        uri: "http://example.com/",
        title: "example.com",
        heuristic: true,
      }),
    ],
  });

  await check_results({
    secondContext,
    matches: [
      makeSearchResult(secondContext, {
        engineName: (await Services.search.getDefault()).name,
        providerName: "HeuristicFallback",
        heuristic: true,
      }),
    ],
  });

  // Refresh our queries
  firstContext = createContext("e", {
    providers: ["Autofill", "HeuristicFallback"],
  });
  secondContext = createContext("em", {
    providers: ["Autofill", "HeuristicFallback"],
  });

  // Set up controller to observe queries.
  let controller = UrlbarTestUtils.newMockController();
  let queryRecieved, queryCancelled;
  const controllerListener = {
    onQueryResults(queryContext) {
      Assert.equal(
        queryContext,
        secondContext,
        "Only the second query should finish."
      );
      queryRecieved = true;
    },
    onQueryCancelled(queryContext) {
      Assert.equal(
        queryContext,
        firstContext,
        "The first query should be cancelled."
      );
      Assert.ok(
        !UrlbarProviderAutofill._autofillData,
        "The first result should not have populated autofill data."
      );
      Assert.ok(!queryCancelled, "No more than one query should be cancelled.");
      queryCancelled = true;
    },
  };
  controller.addQueryListener(controllerListener);

  // Intentionally do not await this first query.
  controller.startQuery(firstContext);
  await controller.startQuery(secondContext);

  Assert.ok(queryCancelled, "At least one query was cancelled.");
  Assert.ok(queryRecieved, "At least one query finished.");
  controller.removeQueryListener(controllerListener);
  await cleanupPlaces();
});
