/* 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 for ML Suggest backend.

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  MLSuggest: "moz-src:///browser/components/urlbar/private/MLSuggest.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

const ML_PREFS = ["browser.ml.enable", "quicksuggest.mlEnabled"];

let gSandbox;
let gInitializeStub;
let gShutdownStub;

add_setup(async function init() {
  // Stub `MLSuggest`.
  gSandbox = sinon.createSandbox();
  gInitializeStub = gSandbox.stub(MLSuggest, "initialize");
  gShutdownStub = gSandbox.stub(MLSuggest, "shutdown");

  // Enable Suggest but not the ML backend yet.
  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    prefs: [
      ["suggest.quicksuggest.all", true],
      ["suggest.quicksuggest.sponsored", true],
    ],
  });
});

function enableMl(enable) {
  if (enable) {
    for (let p of ML_PREFS) {
      UrlbarPrefs.set(p, true);
    }
  } else {
    // This assumes the prefs are false by default, i.e., the ML backend is
    // disabled by default.
    for (let p of ML_PREFS) {
      UrlbarPrefs.clear(p);
    }
  }
}

// Init shouldn't be delayed by default.
add_task(async function zeroDelay() {
  // The init timer is always used, its timeout is just set to zero when init
  // shouldn't be delayed. To test that there's no large delay, get the current
  // time, enable ML, wait for `initialize()` to be called, and then make sure
  // the elapsed time is small. Some CI machines are really slow, so this might
  // take longer than you think. Use a generous expected upper bound of 1s
  // (1000ms).
  let startMs = ChromeUtils.now();
  enableMl(true);
  await TestUtils.waitForCondition(
    () => gInitializeStub.callCount > 0,
    "Waiting for initialize to be called"
  );

  let elapsedMs = ChromeUtils.now() - startMs;
  Assert.less(
    elapsedMs,
    1000,
    "initialize should have been called quickly after enabling the backend"
  );

  enableMl(false);

  gInitializeStub.resetHistory();
  gShutdownStub.resetHistory();
});

// Delaying init should work properly.
add_task(async function delay() {
  // Delay by 4s. We use a large timeout because some CI machines are slow.
  let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
    quickSuggestMlInitDelaySeconds: 4,
  });

  enableMl(true);

  Assert.equal(
    gInitializeStub.callCount,
    0,
    "initialize should not have been called immediately after enabling ML"
  );

  await waitSeconds(2);

  Assert.equal(
    gInitializeStub.callCount,
    0,
    "initialize should not have been called after waiting 2s"
  );

  // Wait a generous 4 more seconds for a total of 6s.
  await waitSeconds(4);

  Assert.equal(
    gInitializeStub.callCount,
    1,
    "initialize should have been called after waiting a total of 6s"
  );

  enableMl(false);
  Assert.equal(
    gShutdownStub.callCount,
    1,
    "shutdown should have been called after disabling ML"
  );

  gInitializeStub.resetHistory();
  gShutdownStub.resetHistory();

  await cleanUpNimbus();
});

// `initialize` should not be called if the backend is disabled before the init
// timer fires.
add_task(async function disabledBeforeTimerFires() {
  let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
    quickSuggestMlInitDelaySeconds: 3,
  });

  enableMl(true);

  // Wait a little for better realism.
  await waitSeconds(1);

  enableMl(false);
  Assert.equal(
    gShutdownStub.callCount,
    1,
    "shutdown should have been called after disabling ML"
  );

  // Wait a generous 3 more seconds for a total of 4s.
  await waitSeconds(3);

  Assert.equal(
    gInitializeStub.callCount,
    0,
    "initialize should not have been called after waiting 2s"
  );

  gInitializeStub.resetHistory();
  gShutdownStub.resetHistory();

  await cleanUpNimbus();
});

async function waitSeconds(s) {
  info(`Waiting ${s} seconds...`);
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(r => setTimeout(r, s * 1000));
}
