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

/*
 * Shared functions for tests related to invoking external handler applications.
 */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { Assert } from "resource://testing-common/Assert.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gExternalProtocolService",
  "@mozilla.org/uriloader/external-protocol-service;1",
  "nsIExternalProtocolService"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gMIMEService",
  "@mozilla.org/mime;1",
  "nsIMIMEService"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gHandlerService",
  "@mozilla.org/uriloader/handler-service;1",
  "nsIHandlerService"
);

export var HandlerServiceTestUtils = {
  /**
   * Retrieves the names of all the MIME types and protocols configured in the
   * handler service instance currently under testing.
   *
   * @return Array of strings like "example/type" or "example-scheme", sorted
   *         alphabetically regardless of category.
   */
  getAllHandlerInfoTypes() {
    return Array.from(
      lazy.gHandlerService.enumerate(),
      info => info.type
    ).sort();
  },

  /**
   * Retrieves all the configured handlers for MIME types and protocols.
   *
   * @note The nsIHandlerInfo instances returned by the "enumerate" method
   *       cannot be used for testing because they incorporate information from
   *       the operating system and also from the default nsIHandlerService
   *       instance, independently from what instance is under testing.
   *
   * @return Array of nsIHandlerInfo instances sorted by their "type" property.
   */
  getAllHandlerInfos() {
    return this.getAllHandlerInfoTypes().map(type => this.getHandlerInfo(type));
  },

  /**
   * Retrieves an nsIHandlerInfo for the given MIME type or protocol, which
   * incorporates information from the operating system and also from the
   * handler service instance currently under testing.
   *
   * @note If the handler service instance currently under testing is not the
   *       default one and the requested type is a MIME type, the returned
   *       nsIHandlerInfo will include information from the default
   *       nsIHandlerService instance. This cannot be avoided easily because the
   *       getMIMEInfoFromOS method is not exposed to JavaScript.
   *
   * @param type
   *        MIME type or scheme name of the nsIHandlerInfo to retrieve.
   *
   * @return The populated nsIHandlerInfo instance.
   */
  getHandlerInfo(type) {
    if (type.includes("/")) {
      // We have to use the getFromTypeAndExtension method because we don't have
      // access to getMIMEInfoFromOS. This means that we have to reset the data
      // that may have been imported from the default nsIHandlerService instance
      // and is not overwritten by fillHandlerInfo later.
      let handlerInfo = lazy.gMIMEService.getFromTypeAndExtension(type, null);
      if (AppConstants.platform == "android") {
        // On Android, the first handler application is always the internal one.
        while (handlerInfo.possibleApplicationHandlers.length > 1) {
          handlerInfo.possibleApplicationHandlers.removeElementAt(1);
        }
      } else {
        handlerInfo.possibleApplicationHandlers.clear();
      }
      handlerInfo.setFileExtensions("");
      // Populate the object from the handler service instance under testing.
      if (lazy.gHandlerService.exists(handlerInfo)) {
        lazy.gHandlerService.fillHandlerInfo(handlerInfo, "");
      }
      return handlerInfo;
    }

    // Populate the protocol information from the handler service instance under
    // testing, like the nsIExternalProtocolService::GetProtocolHandlerInfo
    // method does on the default nsIHandlerService instance.
    let osDefaultHandlerFound = {};
    let handlerInfo =
      lazy.gExternalProtocolService.getProtocolHandlerInfoFromOS(
        type,
        osDefaultHandlerFound
      );
    if (lazy.gHandlerService.exists(handlerInfo)) {
      lazy.gHandlerService.fillHandlerInfo(handlerInfo, "");
    } else {
      lazy.gExternalProtocolService.setProtocolHandlerDefaults(
        handlerInfo,
        osDefaultHandlerFound.value
      );
    }
    return handlerInfo;
  },

  /**
   * Creates an nsIHandlerInfo for the given MIME type or protocol, initialized
   * to the default values for the current platform.
   *
   * @note For this method to work, the specified MIME type or protocol must not
   *       be configured in the default handler service instance or the one
   *       under testing, and must not be registered in the operating system.
   *
   * @param type
   *        MIME type or scheme name of the nsIHandlerInfo to create.
   *
   * @return The blank nsIHandlerInfo instance.
   */
  getBlankHandlerInfo(type) {
    let handlerInfo = this.getHandlerInfo(type);

    let preferredAction, preferredApplicationHandler;
    let alwaysAskBeforeHandling = true;

    if (AppConstants.platform == "android") {
      // On Android, the default preferredAction for MIME types is useHelperApp.
      // For protocols, we always behave as if an operating system provided
      // handler existed, and as such we initialize them to useSystemDefault.
      // This is because the AndroidBridge is not available in xpcshell tests.
      preferredAction = type.includes("/")
        ? Ci.nsIHandlerInfo.useHelperApp
        : Ci.nsIHandlerInfo.useSystemDefault;
      // On Android, the default handler application is always the internal one.
      preferredApplicationHandler = {
        name: "Android chooser",
      };
    } else {
      // On Desktop, the default preferredAction for MIME types is saveToDisk,
      // while for protocols it is alwaysAsk. Since Bug 1735843, for new MIME
      // types we default to not asking before handling unless a pref is set.
      alwaysAskBeforeHandling = Services.prefs.getBoolPref(
        "browser.download.always_ask_before_handling_new_types",
        false
      );

      if (type.includes("/")) {
        preferredAction = Ci.nsIHandlerInfo.saveToDisk;
      } else {
        preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
        // we'll always ask before handling protocols
        alwaysAskBeforeHandling = true;
      }
      preferredApplicationHandler = null;
    }

    this.assertHandlerInfoMatches(handlerInfo, {
      type,
      preferredAction,
      alwaysAskBeforeHandling,
      preferredApplicationHandler,
    });
    return handlerInfo;
  },

  /**
   * Checks whether an nsIHandlerInfo instance matches the provided object.
   */
  assertHandlerInfoMatches(handlerInfo, expected) {
    let expectedInterface = expected.type.includes("/")
      ? Ci.nsIMIMEInfo
      : Ci.nsIHandlerInfo;
    Assert.ok(handlerInfo instanceof expectedInterface);
    Assert.equal(handlerInfo.type, expected.type);

    if (!expected.preferredActionOSDependent) {
      Assert.equal(handlerInfo.preferredAction, expected.preferredAction);
      Assert.equal(
        handlerInfo.alwaysAskBeforeHandling,
        expected.alwaysAskBeforeHandling
      );
    }

    if (expectedInterface == Ci.nsIMIMEInfo) {
      let fileExtensionsEnumerator = handlerInfo.getFileExtensions();
      for (let expectedFileExtension of expected.fileExtensions || []) {
        Assert.equal(fileExtensionsEnumerator.getNext(), expectedFileExtension);
      }
      Assert.ok(!fileExtensionsEnumerator.hasMore());
    }

    if (expected.preferredApplicationHandler) {
      this.assertHandlerAppMatches(
        handlerInfo.preferredApplicationHandler,
        expected.preferredApplicationHandler
      );
    } else {
      Assert.equal(handlerInfo.preferredApplicationHandler, null);
    }

    let handlerAppsArrayEnumerator =
      handlerInfo.possibleApplicationHandlers.enumerate();
    if (AppConstants.platform == "android") {
      // On Android, the first handler application is always the internal one.
      this.assertHandlerAppMatches(handlerAppsArrayEnumerator.getNext(), {
        name: "Android chooser",
      });
    }
    for (let expectedHandlerApp of expected.possibleApplicationHandlers || []) {
      this.assertHandlerAppMatches(
        handlerAppsArrayEnumerator.getNext(),
        expectedHandlerApp
      );
    }
    Assert.ok(!handlerAppsArrayEnumerator.hasMoreElements());
  },

  /**
   * Checks whether an nsIHandlerApp instance matches the provided object.
   */
  assertHandlerAppMatches(handlerApp, expected) {
    Assert.ok(handlerApp instanceof Ci.nsIHandlerApp);
    Assert.equal(handlerApp.name, expected.name);
    if (expected.executable) {
      Assert.ok(handlerApp instanceof Ci.nsILocalHandlerApp);
      Assert.ok(expected.executable.equals(handlerApp.executable));
    } else if (expected.uriTemplate) {
      Assert.ok(handlerApp instanceof Ci.nsIWebHandlerApp);
      Assert.equal(handlerApp.uriTemplate, expected.uriTemplate);
    } else if (expected.service) {
      Assert.ok(handlerApp instanceof Ci.nsIDBusHandlerApp);
      Assert.equal(handlerApp.service, expected.service);
      Assert.equal(handlerApp.method, expected.method);
      Assert.equal(handlerApp.dBusInterface, expected.dBusInterface);
      Assert.equal(handlerApp.objectPath, expected.objectPath);
    }
  },
};
