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

/*
 * Initialization of Form Autofill tests shared between all frameworks.
 *
 * A copy of this file is installed in each of the framework subfolders, this
 * means it becomes a sibling of the test files in the final layout.  This is
 * determined by how manifest "support-files" installation works.
 *
 * This file expects the globals below to be defined in the scope it is
 * imported into.
 */

 /* global add_termination_task, add_task_in_both_processes,
           add_task_in_parent_process, Output */

"use strict";

// The requestAutocomplete framework is available at this point, you can add
// mochitest-chrome specific test initialization here.  If you need shared
// functions or initialization that are not specific to mochitest-chrome,
// consider adding them to "head_common.js" in the parent folder instead.

XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                  "resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                  "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
                                  "resource://gre/modules/FormAutofill.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                  "resource://gre/modules/osfile.jsm");

/* --- Global helpers --- */

// Some of these functions are already implemented in other parts of the source
// tree, see bug 946708 about sharing more code.

var TestUtils = {
  /**
   * Waits for at least one tick of the event loop.  This means that all pending
   * events at the time of this call will have been processed.  Other events may
   * be processed before the returned promise is resolved.
   *
   * @return {Promise}
   * @resolves When pending events have been processed.
   * @rejects Never.
   */
  waitForTick() {
    return new Promise(resolve => executeSoon(resolve));
  },

  /**
   * Waits for the specified timeout.
   *
   * @param aTimeMs
   *        Minimum time to wait from the moment of this call, in milliseconds.
   *        The actual wait may be longer, due to system timer resolution and
   *        pending events being processed before the promise is resolved.
   *
   * @return {Promise}
   * @resolves When the specified time has passed.
   * @rejects Never.
   */
  waitMs(aTimeMs) {
    return new Promise(resolve => setTimeout(resolve, aTimeMs));
  },

  /**
   * Allows waiting for an observer notification once.
   *
   * @param aTopic
   *        Notification topic to observe.
   *
   * @return {Promise}
   * @resolves The array [aSubject, aData] from the observed notification.
   * @rejects Never.
   */
  waitForNotification(aTopic) {
    Output.print("Waiting for notification: '" + aTopic + "'.");

    return new Promise(resolve => Services.obs.addObserver(
      function observe(aSubject, aTopic, aData) {
        Services.obs.removeObserver(observe, aTopic);
        resolve([aSubject, aData]);
      }, aTopic));
  },

  /**
   * Waits for a DOM event on the specified target.
   *
   * @param aTarget
   *        The DOM EventTarget on which addEventListener should be called.
   * @param aEventName
   *        String with the name of the event.
   * @param aUseCapture
   *        This parameter is passed to the addEventListener call.
   *
   * @return {Promise}
   * @resolves The arguments from the observed event.
   * @rejects Never.
   */
  waitForEvent(aTarget, aEventName, aUseCapture = false) {
    Output.print("Waiting for event: '" + aEventName + "' on " + aTarget + ".");

    return new Promise(resolve => aTarget.addEventListener(aEventName,
      function(...aArgs) {
        resolve(...aArgs);
      }, {capture: aUseCapture, once: true}));
  },

  // While the previous test file should have deleted all the temporary files it
  // used, on Windows these might still be pending deletion on the physical file
  // system.  Thus, start from a new base number every time, to make a collision
  // with a file that is still pending deletion highly unlikely.
  _fileCounter: Math.floor(Math.random() * 1000000),

  /**
   * Returns a reference to a temporary file, that is guaranteed not to exist,
   * and to have never been created before.
   *
   * @param aLeafName
   *        Suggested leaf name for the file to be created.
   *
   * @return {Promise}
   * @resolves Path of a non-existent file in a temporary directory.
   *
   * @note It is not enough to delete the file if it exists, or to delete the
   *       file after calling nsIFile.createUnique, because on Windows the
   *       delete operation in the file system may still be pending, preventing
   *       a new file with the same name to be created.
   */
  getTempFile: Task.async(function* (aLeafName) {
    // Prepend a serial number to the extension in the suggested leaf name.
    let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
    let leafName = base + "-" + this._fileCounter + ext;
    this._fileCounter++;

    // Get a file reference under the temporary directory for this test file.
    let path = OS.Path.join(OS.Constants.Path.tmpDir, leafName);
    Assert.ok(!(yield OS.File.exists(path)));

    // Ensure the file is deleted whe the test terminates.
    add_termination_task(function* () {
      if (yield OS.File.exists(path)) {
        yield OS.File.remove(path);
      }
    });

    return path;
  }),
};

/* --- Local helpers --- */

var FormAutofillTest = {
  /**
   * Stores the response that the next call to the mock requestAutocomplete UI
   * will return to the requester, or null to enable displaying the default UI.
   */
  requestAutocompleteResponse: null,

  /**
   * Displays the requestAutocomplete user interface using the specified data.
   *
   * @param aFormAutofillData
   *        Serializable object containing the set of requested fields.
   *
   * @return {Promise}
   * @resolves An object with the following properties:
   *           {
   *             uiWindow: Reference to the initialized window.
   *             promiseResult: Promise resolved by the UI when it closes.
   *           }
   */
  showUI: Task.async(function* (aFormAutofillData) {
    Output.print("Opening UI with data: " + JSON.stringify(aFormAutofillData));

    // Wait for the initialization event before opening the window.
    let promiseUIWindow =
        TestUtils.waitForNotification("formautofill-window-initialized");
    let ui = yield FormAutofill.integration.createRequestAutocompleteUI(
                                                         aFormAutofillData);
    let promiseResult = ui.show();

    // The window is the subject of the observer notification.
    return {
      uiWindow: (yield promiseUIWindow)[0],
      promiseResult,
    };
  }),
};

var TestData = {
  /**
   * Autofill UI request for the e-mail field only.
   */
  get requestEmailOnly() {
    return {
      sections: [{
        name: "",
        addressSections: [{
          addressType: "",
          fields: [{
            fieldName: "email",
            contactType: "",
          }],
        }],
      }],
    };
  },
};

/* --- Initialization and termination functions common to all tests --- */

add_task_in_parent_process(function* () {
  // If required, we return a mock response instead of displaying the UI.
  let mockIntegrationFn = base => ({
    createRequestAutocompleteUI: Task.async(function* () {
      // Call the base method to display the UI if override is not requested.
      if (FormAutofillTest.requestAutocompleteResponse === null) {
        return yield base.createRequestAutocompleteUI.apply(this, arguments);
      }

      // Return a mock RequestAutocompleteUI object.
      return {
        show: Task.async(function* () {
          let response = FormAutofillTest.requestAutocompleteResponse;
          Output.print("Mock UI response: " + JSON.stringify(response));
          return response;
        }),
      };
    }),
  });

  FormAutofill.registerIntegration(mockIntegrationFn);
  add_termination_task(function* () {
    FormAutofill.unregisterIntegration(mockIntegrationFn);
  });
});

add_task_in_both_processes(function* () {
  // We must manually enable the feature while testing.
  Services.prefs.setBoolPref("dom.forms.requestAutocomplete", true);
  add_termination_task(function* () {
    Services.prefs.clearUserPref("dom.forms.requestAutocomplete");
  });
});
