text: module.exports = function init(jsUtil, cookieHandler, messages, base64) {
    var validSerializers = ['urlencoded', 'json', 'utf8'];
    var validCertModes = ['default', 'nocheck', 'pinned', 'legacy'];
    var validClientAuthModes = ['none', 'systemstore', 'buffer'];
    var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download'];
    var validResponseTypes = ['text', 'arraybuffer', 'blob'];
  
    var interface = {
      b64EncodeUnicode: b64EncodeUnicode,
      checkSerializer: checkSerializer,
      checkSSLCertMode: checkSSLCertMode,
      checkClientAuthMode: checkClientAuthMode,
      checkClientAuthOptions: checkClientAuthOptions,
      checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey,
      checkForInvalidHeaderValue: checkForInvalidHeaderValue,
      checkTimeoutValue: checkTimeoutValue,
      checkFollowRedirectValue: checkFollowRedirectValue,
      injectCookieHandler: injectCookieHandler,
      injectRawResponseHandler: injectRawResponseHandler,
      injectFileEntryHandler: injectFileEntryHandler,
      getMergedHeaders: getMergedHeaders,
      getProcessedData: getProcessedData,
      handleMissingCallbacks: handleMissingCallbacks,
      handleMissingOptions: handleMissingOptions
    };
  
    // expose all functions for testing purposes
    if (init.debug) {
      interface.mergeHeaders = mergeHeaders;
      interface.checkForValidStringValue = checkForValidStringValue;
      interface.checkKeyValuePairObject = checkKeyValuePairObject;
      interface.checkHttpMethod = checkHttpMethod;
      interface.checkResponseType = checkResponseType;
      interface.checkHeadersObject = checkHeadersObject;
      interface.checkParamsObject = checkParamsObject;
      interface.resolveCookieString = resolveCookieString;
      interface.createFileEntry = createFileEntry;
      interface.getCookieHeader = getCookieHeader;
      interface.getMatchingHostHeaders = getMatchingHostHeaders;
      interface.getAllowedDataTypes = getAllowedDataTypes;
    }
  
    return interface;
  
    // Thanks Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22
    function b64EncodeUnicode(str) {
      return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
        return String.fromCharCode('0x' + p1);
      }));
    }
  
    function mergeHeaders(globalHeaders, localHeaders) {
      var globalKeys = Object.keys(globalHeaders);
      var key;
  
      for (var i = 0; i < globalKeys.length; i++) {
        key = globalKeys[i];
  
        if (!localHeaders.hasOwnProperty(key)) {
          localHeaders[key] = globalHeaders[key];
        }
      }
  
      return localHeaders;
    }
  
    function checkForValidStringValue(list, value, onInvalidValueMessage) {
      if (jsUtil.getTypeOf(value) !== 'String') {
        throw new Error(onInvalidValueMessage + ' ' + list.join(', '));
      }
  
      value = value.trim().toLowerCase();
  
      if (list.indexOf(value) === -1) {
        throw new Error(onInvalidValueMessage + ' ' + list.join(', '));
      }
  
      return value;
    }
  
    function checkKeyValuePairObject(obj, allowedChildren, onInvalidValueMessage) {
      if (jsUtil.getTypeOf(obj) !== 'Object') {
        throw new Error(onInvalidValueMessage);
      }
  
      var keys = Object.keys(obj);
  
      for (var i = 0; i < keys.length; i++) {
        if (allowedChildren.indexOf(jsUtil.getTypeOf(obj[keys[i]])) === -1) {
          throw new Error(onInvalidValueMessage);
        }
      }
  
      return obj;
    }
  
    function checkHttpMethod(method) {
      return checkForValidStringValue(validHttpMethods, method, messages.INVALID_HTTP_METHOD);
    }
  
    function checkResponseType(type) {
      return checkForValidStringValue(validResponseTypes, type, messages.INVALID_RESPONSE_TYPE);
    }
  
    function checkSerializer(serializer) {
      return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER);
    }
  
    function checkSSLCertMode(mode) {
      return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE);
    }
  
    function checkClientAuthMode(mode) {
      return checkForValidStringValue(validClientAuthModes, mode, messages.INVALID_CLIENT_AUTH_MODE);
    }
  
    function checkClientAuthOptions(mode, options) {
      options = options || {};
  
      // none
      if (mode === validClientAuthModes[0]) {
        return {
          alias: null,
          rawPkcs: null,
          pkcsPassword: ''
        };
      }
  
      if (jsUtil.getTypeOf(options) !== 'Object') {
        throw new Error(messages.INVALID_CLIENT_AUTH_OPTIONS);
      }
  
      // systemstore
      if (mode === validClientAuthModes[1]) {
        if (jsUtil.getTypeOf(options.alias) !== 'String'
          && jsUtil.getTypeOf(options.alias) !== 'Undefined') {
          throw new Error(messages.INVALID_CLIENT_AUTH_ALIAS);
        }
  
        return {
          alias: jsUtil.getTypeOf(options.alias) === 'Undefined' ? null : options.alias,
          rawPkcs: null,
          pkcsPassword: ''
        };
      }
  
      // buffer
      if (mode === validClientAuthModes[2]) {
        if (jsUtil.getTypeOf(options.rawPkcs) !== 'ArrayBuffer') {
          throw new Error(messages.INVALID_CLIENT_AUTH_RAW_PKCS);
        }
  
        if (jsUtil.getTypeOf(options.pkcsPassword) !== 'String') {
          throw new Error(messages.INVALID_CLIENT_AUTH_PKCS_PASSWORD);
        }
  
        return {
          alias: null,
          rawPkcs: options.rawPkcs,
          pkcsPassword: options.pkcsPassword
        }
      }
    }
  
    function checkForBlacklistedHeaderKey(key) {
      if (key.toLowerCase() === 'cookie') {
        throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED);
      }
  
      return key;
    }
  
    function checkForInvalidHeaderValue(value) {
      if (jsUtil.getTypeOf(value) !== 'String') {
        throw new Error(messages.INVALID_HEADERS_VALUE);
      }
  
      return value;
    }
  
    function checkTimeoutValue(timeout) {
      if (jsUtil.getTypeOf(timeout) !== 'Number' || timeout < 0) {
        throw new Error(messages.INVALID_TIMEOUT_VALUE);
      }
  
      return timeout;
    }
  
    function checkFollowRedirectValue(follow) {
      if (jsUtil.getTypeOf(follow) !== 'Boolean') {
        throw new Error(messages.INVALID_FOLLOW_REDIRECT_VALUE);
      }
  
      return follow;
    }
  
    function checkHeadersObject(headers) {
      return checkKeyValuePairObject(headers, ['String'], messages.INVALID_HEADERS_VALUE);
    }
  
    function checkParamsObject(params) {
      return checkKeyValuePairObject(params, ['String', 'Array'], messages.INVALID_PARAMS_VALUE);
    }
  
    function resolveCookieString(headers) {
      var keys = Object.keys(headers || {});
  
      for (var i = 0; i < keys.length; ++i) {
        if (keys[i].match(/^set-cookie$/i)) {
          return headers[keys[i]];
        }
      }
  
      return null;
    }
  
    function createFileEntry(rawEntry) {
      var entry = new (require('cordova-plugin-file.FileEntry'))();
  
      entry.isDirectory = rawEntry.isDirectory;
      entry.isFile = rawEntry.isFile;
      entry.name = rawEntry.name;
      entry.fullPath = rawEntry.fullPath;
      entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == window.PERSISTENT ? 'persistent' : 'temporary'));
      entry.nativeURL = rawEntry.nativeURL;
  
      return entry;
    }
  
    function injectCookieHandler(url, cb) {
      return function (response) {
        console.log(`injectCookieHandler(): url=${url}`)
        console.log(`injectCookieHandler(): headers=${JSON.stringify(response)}`)
        cookieHandler.setCookieFromString(url, resolveCookieString(response.headers));
        cb(response);
      }
    }
  
    function injectRawResponseHandler(responseType, cb) {
      return function (response) {
        var dataType = jsUtil.getTypeOf(response.data);
  
        // don't need post-processing if it's already binary type (on browser platform)
        if (dataType === 'ArrayBuffer' || dataType === 'Blob') {
          return cb(response);
        }
  
        // arraybuffer
        if (responseType === validResponseTypes[1]) {
          var buffer = base64.toArrayBuffer(response.data);
          response.data = buffer;
        }
  
        // blob
        if (responseType === validResponseTypes[2]) {
          var buffer = base64.toArrayBuffer(response.data);
          var type = response.headers['content-type'] || '';
          var blob = new Blob([buffer], { type: type });
          response.data = blob;
        }
  
        cb(response);
      }
    }
  
    function injectFileEntryHandler(cb) {
      return function (response) {
        cb(createFileEntry(response.file));
      }
    }
  
    function getCookieHeader(url) {
      var cookieString = cookieHandler.getCookieString(url);
  
      if (cookieString.length) {
        return { Cookie: cookieHandler.getCookieString(url) };
      }
  
      return {};
    }
  
    function getMatchingHostHeaders(url, headersList) {
      var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
      var domain = matches && matches[1];
  
      return headersList[domain] || null;
    }
  
    function getMergedHeaders(url, requestHeaders, predefinedHeaders) {
      var globalHeaders = predefinedHeaders['*'] || {};
      var hostHeaders = getMatchingHostHeaders(url, predefinedHeaders) || {};
      var mergedHeaders = mergeHeaders(globalHeaders, hostHeaders);
  
      mergedHeaders = mergeHeaders(mergedHeaders, requestHeaders);
      mergedHeaders = mergeHeaders(mergedHeaders, getCookieHeader(url));
  
      return mergedHeaders;
    }
  
    function getAllowedDataTypes(dataSerializer) {
      switch (dataSerializer) {
        case 'utf8':
          return ['String'];
        case 'urlencoded':
          return ['Object'];
        default:
          return ['Array', 'Object'];
      }
    }
  
    function getProcessedData(data, dataSerializer) {
      var currentDataType = jsUtil.getTypeOf(data);
      var allowedDataTypes = getAllowedDataTypes(dataSerializer);
  
      if (allowedDataTypes.indexOf(currentDataType) === -1) {
        throw new Error(messages.DATA_TYPE_MISMATCH + ' ' + allowedDataTypes.join(', '));
      }
  
      if (dataSerializer === 'utf8') {
        data = { text: data };
      }
  
      return data;
    }
  
    function handleMissingCallbacks(successFn, failFn) {
      if (jsUtil.getTypeOf(successFn) !== 'Function') {
        throw new Error(messages.MANDATORY_SUCCESS);
      }
  
      if (jsUtil.getTypeOf(failFn) !== 'Function') {
        throw new Error(messages.MANDATORY_FAIL);
      }
    }
  
    function handleMissingOptions(options, globals) {
      options = options || {};
  
      return {
        method: checkHttpMethod(options.method || validHttpMethods[0]),
        responseType: checkResponseType(options.responseType || validResponseTypes[0]),
        serializer: checkSerializer(options.serializer || globals.serializer),
        timeout: checkTimeoutValue(options.timeout || globals.timeout),
        followRedirect: checkFollowRedirectValue(options.followRedirect || globals.followRedirect),
        headers: checkHeadersObject(options.headers || {}),
        params: checkParamsObject(options.params || {}),
        data: jsUtil.getTypeOf(options.data) === 'Undefined' ? null : options.data,
        filePath: options.filePath || '',
        name: options.name || ''
      };
    }
  };
  