text: /**
   * @module http-transport
   */
  'use strict';
  
  /*jshint -W079 */// Suppress warning about redefiniton of `Promise`
  var Promise = require( 'es6-promise' ).Promise;
  
  var agent = require( 'superagent' );
  var parseLinkHeader = require( 'li' ).parse;
  var url = require( 'url' );
  
  var WPRequest = require( './constructors/wp-request' );
  var checkMethodSupport = require( './util/check-method-support' );
  var extend = require( 'node.extend' );
  var objectReduce = require( './util/object-reduce' );
  var isEmptyObject = require( './util/is-empty-object' );
  
  /**
   * Set any provided headers on the outgoing request object. Runs after _auth.
   *
   * @method _setHeaders
   * @private
   * @param {Object} request A superagent request object
   * @param {Object} options A WPRequest _options object
   * @param {Object} A superagent request object, with any available headers set
   */
  function _setHeaders( request, options ) {
  	// If there's no headers, do nothing
  	if ( ! options.headers ) {
  		return request;
  	}
  
  	return objectReduce( options.headers, function( request, value, key ) {
  		return request.set( key, value );
  	}, request );
  }
  
  /**
   * Conditionally set basic authentication on a server request object.
   *
   * @method _auth
   * @private
   * @param {Object} request A superagent request object
   * @param {Object} options A WPRequest _options object
   * @param {Boolean} forceAuthentication whether to force authentication on the request
   * @param {Object} A superagent request object, conditionally configured to use basic auth
   */
  function _auth( request, options, forceAuthentication ) {
  	// If we're not supposed to authenticate, don't even start
  	if ( ! forceAuthentication && ! options.auth && ! options.nonce ) {
  		return request;
  	}
  
  	// Enable nonce in options for Cookie authentication http://wp-api.org/guides/authentication.html
  	if ( options.nonce ) {
  		request.set( 'X-WP-Nonce', options.nonce );
  		return request;
  	}
  
  	// Retrieve the username & password from the request options if they weren't provided
  	var username = username || options.username;
  	var password = password || options.password;
  
  	// If no username or no password, can't authenticate
  	if ( ! username || ! password ) {
  		return request;
  	}
  
  	// Can authenticate: set basic auth parameters on the request
  	return request.auth( username, password );
  }
  
  // Pagination-Related Helpers
  // ==========================
  
  /**
   * Combine the API endpoint root URI and link URI into a valid request URL.
   * Endpoints are generally a full path to the JSON API's root endpoint, such
   * as `website.com/wp-json`: the link headers, however, are returned as root-
   * relative paths. Concatenating these would generate a URL such as
   * `website.com/wp-json/wp-json/posts?page=2`: we must intelligently merge the
   * URI strings in order to generate a valid new request URL.
   *
   * @private
   * @param endpoint {String} The endpoint URL for the REST API root
   * @param linkPath {String} A root-relative link path to an API request
   * @returns {String} The full URL path to the provided link
   */
  function mergeUrl( endpoint, linkPath ) {
  	var request = url.parse( endpoint );
  	linkPath = url.parse( linkPath, true );
  
  	// Overwrite relevant request URL object properties with the link's values:
  	// Setting these three values from the link will ensure proper URL generation
  	request.query = linkPath.query;
  	request.search = linkPath.search;
  	request.pathname = linkPath.pathname;
  
  	// Reassemble and return the merged URL
  	return url.format( request );
  }
  
  /**
   * Extract the body property from the superagent response, or else try to parse
   * the response text to get a JSON object.
   *
   * @private
   * @param {Object} response      The response object from the HTTP request
   * @param {String} response.text The response content as text
   * @param {Object} response.body The response content as a JS object
   * @returns {Object} The response content as a JS object
   */
  function extractResponseBody( response ) {
  	var responseBody = response.body;
  	if ( isEmptyObject( responseBody ) && response.type === 'text/html' ) {
  		// Response may have come back as HTML due to caching plugin; try to parse
  		// the response text into JSON
  		try {
  			responseBody = JSON.parse( response.text );
  		} catch ( e ) {
  			// Swallow errors, it's OK to fall back to returning the body
  		}
  	}
  	return responseBody;
  }
  
  /**
   * If the response is not paged, return the body as-is. If pagination
   * information is present in the response headers, parse those headers into
   * a custom `_paging` property on the response body. `_paging` contains links
   * to the previous and next pages in the collection, as well as metadata
   * about the size and number of pages in the collection.
   *
   * The structure of the `_paging` property is as follows:
   *
   * - `total` {Integer} The total number of records in the collection
   * - `totalPages` {Integer} The number of pages available
   * - `links` {Object} The parsed "links" headers, separated into individual URI strings
   * - `next` {WPRequest} A WPRequest object bound to the "next" page (if page exists)
   * - `prev` {WPRequest} A WPRequest object bound to the "previous" page (if page exists)
   *
   * @private
   * @param {Object} result           The response object from the HTTP request
   * @param {Object} options          The options hash from the original request
   * @param {String} options.endpoint The base URL of the requested API endpoint
   * @param {Object} httpTransport    The HTTP transport object used by the original request
   * @returns {Object} The pagination metadata object for this HTTP request, or else null
   */
  function createPaginationObject( result, options, httpTransport ) {
  	var _paging = null;
  
  	if ( ! result.headers || ! result.headers[ 'x-wp-totalpages' ] ) {
  		// No headers: return as-is
  		return _paging;
  	}
  
  	var totalPages = result.headers[ 'x-wp-totalpages' ];
  
  	if ( ! totalPages || totalPages === '0' ) {
  		// No paging: return as-is
  		return _paging;
  	}
  
  	// Decode the link header object
  	var links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
  
  	// Store pagination data from response headers on the response collection
  	_paging = {
  		total: result.headers[ 'x-wp-total' ],
  		totalPages: totalPages,
  		links: links
  	};
  
  	// Re-use any options from the original request, updating only the endpoint
  	// (this ensures that request properties like authentication are preserved)
  	var endpoint = options.endpoint;
  
  	// Create a WPRequest instance pre-bound to the "next" page, if available
  	if ( links.next ) {
  		_paging.next = new WPRequest( extend( {}, options, {
  			transport: httpTransport,
  			endpoint: mergeUrl( endpoint, links.next )
  		}));
  	}
  
  	// Create a WPRequest instance pre-bound to the "prev" page, if available
  	if ( links.prev ) {
  		_paging.prev = new WPRequest( extend( {}, options, {
  			transport: httpTransport,
  			endpoint: mergeUrl( endpoint, links.prev )
  		}));
  	}
  
  	return _paging;
  }
  
  // HTTP-Related Helpers
  // ====================
  
  /**
   * Submit the provided superagent request object, invoke a callback (if it was
   * provided), and return a promise to the response from the HTTP request.
   *
   * @private
   * @param {Object} request A superagent request object
   * @param {Function} callback A callback function (optional)
   * @param {Function} transform A function to transform the result data
   * @returns {Promise} A promise to the superagent request
   */
  function invokeAndPromisify( request, callback, transform ) {
  	return new Promise(function( resolve, reject ) {
  		// Fire off the result
  		request.end(function( err, result ) {
  
  			// Return the results as a promise
  			if ( err || result.error ) {
  				reject( err || result.error );
  			} else {
  				resolve( result );
  			}
  		});
  	}).then( transform ).then(function( result ) {
  		// If a node-style callback was provided, call it, but also return the
  		// result value for use via the returned Promise
  		if ( callback && typeof callback === 'function' ) {
  			callback( null, result );
  		}
  		return result;
  	}, function( err ) {
  		// If the API provided an error object, it will be available within the
  		// superagent response object as response.body (containing the response
  		// JSON). If that object exists, it will have a .code property if it is
  		// truly an API error (non-API errors will not have a .code).
  		if ( err.response && err.response.body && err.response.body.code ) {
  			// Forward API error response JSON on to the calling method: omit
  			// all transport-specific (superagent-specific) properties
  			err = err.response.body;
  		}
  		// If a callback was provided, ensure it is called with the error; otherwise
  		// re-throw the error so that it can be handled by a Promise .catch or .then
  		if ( callback && typeof callback === 'function' ) {
  			callback( err );
  		} else {
  			throw err;
  		}
  	});
  }
  
  /**
   * Return the body of the request, augmented with pagination information if the
   * result is a paged collection.
   *
   * @private
   * @param {WPRequest} wpreq The WPRequest representing the returned HTTP response
   * @param {Object} result The results from the HTTP request
   * @returns {Object} The "body" property of the result, conditionally augmented with
   *                  pagination information if the result is a partial collection.
   */
  function returnBody( wpreq, result ) {
  	var body = extractResponseBody( result );
  	var _paging = createPaginationObject( result, wpreq._options, wpreq.transport );
  	if ( _paging ) {
  		body._paging = _paging;
  	}
  
  	if ( typeof body === 'object' && body ) {
  		if ( result.headers['allow'] ) {
  			Object.defineProperty( body, '_allow', {
  				value: result.headers['allow'].split( ', ' ),
  			} );
  		}
  
  		if ( result.headers['link'] ) {
  			Object.defineProperty( body, '_headerLinks', {
  				value: parseLinkHeader( result.headers['link'], { extended: true } ).map( function ( link ) {
  					link.href = link.link;
  					link.rel = link.rel[0];
  
  					return link;
  				} )
  			} );
  		}
  	}
  
  	return body;
  }
  
  /**
   * Extract and return the headers property from a superagent response object
   *
   * @private
   * @param {Object} result The results from the HTTP request
   * @returns {Object} The "headers" property of the result
   */
  function returnHeaders( result ) {
  	return result.headers;
  }
  
  // HTTP Methods: Private HTTP-verb versions
  // ========================================
  
  /**
   * @method get
   * @async
   * @param {WPRequest} wpreq A WPRequest query object
   * @param {Function} [callback] A callback to invoke with the results of the GET request
   * @returns {Promise} A promise to the results of the HTTP request
   */
  function _httpGet( wpreq, callback ) {
  	checkMethodSupport( 'get', wpreq );
  	var url = wpreq.toString();
  
  	var request = _auth( agent.get( url ), wpreq._options );
  	request = _setHeaders( request, wpreq._options );
  
  	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  }
  
  /**
   * Invoke an HTTP "POST" request against the provided endpoint
   * @method post
   * @async
   * @param {WPRequest} wpreq A WPRequest query object
   * @param {Object} data The data for the POST request
   * @param {Function} [callback] A callback to invoke with the results of the POST request
   * @returns {Promise} A promise to the results of the HTTP request
   */
  function _httpPost( wpreq, data, callback ) {
  	checkMethodSupport( 'post', wpreq );
  	var url = wpreq.toString();
  	data = data || {};
  	var request = _auth( agent.post( url ), wpreq._options, true );
  	request = _setHeaders( request, wpreq._options );
  
  	if ( wpreq._attachment ) {
  		// Data must be form-encoded alongside image attachment
  		request = objectReduce( data, function( req, value, key ) {
  			return req.field( key, value );
  		}, request.attach( 'file', wpreq._attachment, wpreq._attachmentName ) );
  	} else {
  		request = request.send( data );
  	}
  
  	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  }
  
  /**
   * @method put
   * @async
   * @param {WPRequest} wpreq A WPRequest query object
   * @param {Object} data The data for the PUT request
   * @param {Function} [callback] A callback to invoke with the results of the PUT request
   * @returns {Promise} A promise to the results of the HTTP request
   */
  function _httpPut( wpreq, data, callback ) {
  	checkMethodSupport( 'put', wpreq );
  	var url = wpreq.toString();
  	data = data || {};
  
  	var request = _auth( agent.put( url ), wpreq._options, true ).send( data );
  	request = _setHeaders( request, wpreq._options );
  
  	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  }
  
  /**
   * @method delete
   * @async
   * @param {WPRequest} wpreq A WPRequest query object
   * @param {Object} [data] Data to send along with the DELETE request
   * @param {Function} [callback] A callback to invoke with the results of the DELETE request
   * @returns {Promise} A promise to the results of the HTTP request
   */
  function _httpDelete( wpreq, data, callback ) {
  	if ( ! callback && typeof data === 'function' ) {
  		callback = data;
  		data = null;
  	}
  	checkMethodSupport( 'delete', wpreq );
  	var url = wpreq.toString();
  	var request = _auth( agent.del( url ), wpreq._options, true ).send( data );
  	request = _setHeaders( request, wpreq._options );
  
  	return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  }
  
  /**
   * @method head
   * @async
   * @param {WPRequest} wpreq A WPRequest query object
   * @param {Function} [callback] A callback to invoke with the results of the HEAD request
   * @returns {Promise} A promise to the header results of the HTTP request
   */
  function _httpHead( wpreq, callback ) {
  	checkMethodSupport( 'head', wpreq );
  	var url = wpreq.toString();
  	var request = _auth( agent.head( url ), wpreq._options );
  	request = _setHeaders( request, wpreq._options );
  
  	return invokeAndPromisify( request, callback, returnHeaders );
  }
  
  module.exports = {
  	delete: _httpDelete,
  	get: _httpGet,
  	head: _httpHead,
  	post: _httpPost,
  	put: _httpPut
  };
  