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

// Tests the UrlbarProvider.onEngagement() method.

"use strict";

add_task(async function abandonment() {
  await doTest({
    expectedEndState: "abandonment",
    endEngagement: async () => {
      await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
    },
  });
});

add_task(async function engagement() {
  await BrowserTestUtils.withNewTab("about:blank", async () => {
    await doTest({
      expectedEndState: "engagement",
      endEngagement: async () => {
        let result, element;
        await UrlbarTestUtils.promisePopupClose(window, () => {
          EventUtils.synthesizeKey("KEY_ArrowDown");
          result = gURLBar.view.selectedResult;
          element = gURLBar.view.selectedElement;
          EventUtils.synthesizeKey("KEY_Enter");
        });
        return { result, element };
      },
      expectedEndDetails: {
        selIndex: 0,
        selType: "history",
        provider: "",
        searchSource: "urlbar",
        isSessionOngoing: false,
      },
    });
  });
});

add_task(async function privateWindow_abandonment() {
  let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
  await doTest({
    win,
    expectedEndState: "abandonment",
    expectedIsPrivate: true,
    endEngagement: async () => {
      await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur());
    },
  });
  await BrowserTestUtils.closeWindow(win);
});

add_task(async function privateWindow_engagement() {
  let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
  await doTest({
    win,
    expectedEndState: "engagement",
    expectedIsPrivate: true,
    endEngagement: async () => {
      let result, element;
      await UrlbarTestUtils.promisePopupClose(win, () => {
        EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
        result = win.gURLBar.view.selectedResult;
        element = win.gURLBar.view.selectedElement;
        EventUtils.synthesizeKey("KEY_Enter", {}, win);
      });
      return { result, element };
    },
    expectedEndDetails: {
      selIndex: 0,
      selType: "history",
      provider: "",
      searchSource: "urlbar",
      isSessionOngoing: false,
    },
  });
  await BrowserTestUtils.closeWindow(win);
});

/**
 * Performs an engagement test.
 *
 * @param {object} options
 *   Options object.
 * @param {string} options.expectedEndState
 *   The expected state at the end of the engagement.
 * @param {Function} options.endEngagement
 *   A function that should end the engagement. If the expected end state is
 *   "engagement", the function should return `{ result, element }` with the
 *   expected engaged result and element.
 * @param {window} [options.win]
 *   The window to perform the test in.
 * @param {boolean} [options.expectedIsPrivate]
 *   Whether the engagement and query context are expected to be private.
 * @param {object} [options.expectedEndDetails]
 *   The expected `details` at the end of the engagement.  `searchString` is
 *   automatically included since it's always present.  If `provider` is
 *   expected, then include it and set it to any value; this function will
 *   replace it with the name of the test provider.
 */
async function doTest({
  expectedEndState,
  endEngagement,
  win = window,
  expectedIsPrivate = false,
  expectedEndDetails = {},
}) {
  let provider = new TestProvider();
  UrlbarProvidersManager.registerProvider(provider);

  let startPromise = provider.promiseEngagement();
  await UrlbarTestUtils.promiseAutocompleteResultPopup({
    window: win,
    value: "test",
    fireInputEvent: true,
  });

  let [isPrivate, state, queryContext, details] = await startPromise;
  Assert.equal(isPrivate, expectedIsPrivate, "Start isPrivate");
  Assert.equal(state, "start", "Start state");

  // `queryContext` isn't always defined for `start`, and `onEngagement`
  // shouldn't rely on it being defined on start, but there's no good reason to
  // assert that it's not defined here.

  // Similarly, `details` is never defined for `start`, but there's no good
  // reason to assert that it's not defined.

  let endPromise = provider.promiseEngagement();
  let { result, element } = (await endEngagement()) ?? {};

  [isPrivate, state, queryContext, details] = await endPromise;
  Assert.equal(isPrivate, expectedIsPrivate, "End isPrivate");
  Assert.equal(state, expectedEndState, "End state");
  Assert.ok(queryContext, "End queryContext");
  Assert.equal(
    queryContext.isPrivate,
    expectedIsPrivate,
    "End queryContext.isPrivate"
  );

  let detailsDefaults = {
    searchString: "test",
    searchSource: "urlbar",
    provider: undefined,
    selIndex: -1,
  };
  if ("provider" in expectedEndDetails) {
    detailsDefaults.provider = provider.name;
    delete expectedEndDetails.provider;
  }

  if (expectedEndState == "engagement") {
    Assert.ok(
      result,
      "endEngagement() should have returned the expected engaged result"
    );
    Assert.ok(
      element,
      "endEngagement() should have returned the expected engaged element"
    );
    expectedEndDetails.result = result;
    expectedEndDetails.element = element;
  }

  Assert.deepEqual(
    details,
    Object.assign(detailsDefaults, expectedEndDetails),
    "End details"
  );

  UrlbarProvidersManager.unregisterProvider(provider);
}

/**
 * Test provider that resolves promises when onEngagement is called.
 */
class TestProvider extends UrlbarTestUtils.TestProvider {
  _resolves = [];

  constructor() {
    super({
      priority: Infinity,
      results: [
        new UrlbarResult(
          UrlbarUtils.RESULT_TYPE.URL,
          UrlbarUtils.RESULT_SOURCE.HISTORY,
          { url: "http://example.com/" }
        ),
      ],
    });
  }

  onEngagement(...args) {
    let resolve = this._resolves.shift();
    if (resolve) {
      resolve(args);
    }
  }

  promiseEngagement() {
    return new Promise(resolve => this._resolves.push(resolve));
  }
}
