--[[----------------------------------------------------------------------------

WFMockHttpRequest.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2018 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

local Rx = require 'Rx'

local WFArguments = require 'WFArguments'

local check = WFArguments.check

--[[!
	\class WFMockHttpRequest
	
	\brief This class provides an interface to simplify the creation 
	of HTTP request/response pairs for WFRxHttpClientMock (and it's common wrapper 
	WFCatSpecMock) so that tests can effectively mock and validate HTTP requests 
	and responses from WF components without having to create an unreadable list of tables 
	and options used by WFRxHttpClientMock.
	
	For example, if you want to mock a request for an Oz catalog with a successful response
	you might have
	<pre>
		WFMockHttpRequest()
			:get( "/v2/catalogs/" .. catalogId )
			:respond( '200', \[\[{
				"base": "https://photos-stage.adobe.io/v2/",
				"id": "\]\] .. catalogId .. \[\[",
				"type": "catalog"
			}\]\] )
	</pre>
	
	This class uses the builder pattern so each function should return the 
	WFMockHttpRequest so you can easily stack additional calls to WFMockHttpRequest
	functions.
	
	The resulting request can then be added to the WFRxHttpClientMock 
	<pre>
		httpMock:addHttpRequest( request )
	</pre>
	
	or, more commonly, to a WFCatSpecMock
	<pre>
		catSpec:expectHttpRequest( request )
	</pre>
	
	and will set the expectation that a GET request should be made to the catalogs URL
	and a 200 response with the associated JSON data should be returned.

]]--
local WFMockHttpRequest = {
	
	_method = false,
	_url = false,
	_urlTransformationFn = false,
	_expectedRequestData = false,
	_requestValidationFn = false,
	_actualRequestDataTransformationFn = false,
	_requestDataTransformationFn = false,
	_responseFn = false,
	_responseFilterFn = false,
	_expectedRequestHeaders = false,
	_requestOptions = false,
	_responseData = false,
	_responseCode = false,
	_responseHeaders = false,
	_attachedFunction = false,
	_traceback = false,

}

--[[
	Constructs a WFMockHttpRequest object.
 --]]
function WFMockHttpRequest:__init()
	self._traceback = debug.traceback( "", 4 )
end

--[[
	Make the mock request a GET request for the specified URL. The URL must exactly match
	the actual URL that is requested.
	
	\param url (string) The url of the request
	
	\return WFMockHttpRequest
 --]]
function WFMockHttpRequest:get( url )
	check( 'WFMockHttpRequest', 'string' )
	
	self._url = url
	self._method = 'GET'
	return self
end

--[[
	Make the mock request a POST request for the specified URL. The URL must exactly match
	the actual URL that is requested.

	\param url (string) The url of the request
	
	\return WFMockHttpRequest
 --]]
function WFMockHttpRequest:post( url )
	check( 'WFMockHttpRequest', 'string' )

	self._url = url
	self._method = 'POST'
	return self
end

--[[
	Make the mock request a PUT request for the specified URL. The URL must exactly match
	the actual URL that is requested.

	\param url (string) The url of the request
	
	\return WFMockHttpRequest
 --]]
function WFMockHttpRequest:put( url )
	check( 'WFMockHttpRequest', 'string' )
	
	self._url = url
	self._method = 'PUT'
	return self
end

--[[
	Set the data expected in the POST or PUT request. If you don't care to validate the
	actual data sent in the request, you should use \c ':ignore:' and the request data
	will not be compared by WFRxHttpClientMock.
	
	Note: The expected data can be a table, but if a table is passed in you must also
	use \c :requestDataTransformation to transform the actual HTTP request data into a 
	table or else the comparison will always fail. Unless you need to modify the expected
	data table as well as transforming the actual request data into a table, using 
	\c :expectRequestDataAfterTransformation is probably a better choice to make it clear
	that the data is expected to be transformed.
	
	\param data (string|table) expected POST/PUT data
	
	\return WFMockHttpRequest
 --]]
function WFMockHttpRequest:expectRequestData( data )
	check( 'WFMockHttpRequest', 'string|table' )
	
	self._expectedRequestData = data
	return self
end

--[[
	Sets the data expected in the POST or PUT request after the request data from the 
	actual HTTP request is run through the provided transformation function.
	
	A data transformation function can be useful to strip out pieces of request data, like 
	timestamps, that might not be predictable ahead of time.
	
	This function should take a single \c string parameter that contains the actual 
	HTTP request data and return a single \c string parameter containing the transformed
	request data. e.g.
	<pre>
		function( actualRequestData )
			...
			return transformedRequestData
		end
	</pre>
	
	This transformed data will then be compared to the \c data parameter of
	\c expectRequestDataAfterTransformation
	
	Similar to \c expectRequestData, \c ':ignore:' will skip validation of the request
	data, but you should just use \c expectRequestData in these cases because there's 
	no reason to transform the real HTTP request data if it's not going to be validated.
	
	\param data (string) expected POST/PUT data
	\param transformFn (function) function which will take the actual HTTP request data and
		modify it. Once modified, it is expected to match the data parameter in order for the
		mock validation to pass.
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:expectRequestDataAfterTransformation( data, transformFn )
	check( 'WFMockHttpRequest', 'string|table', '?function' )
	
	-- SPAM [matthewj 2018-04-27]: the transformation function is run on the data 
	-- coming in from the actual put/post call. The output of this will then be compared
	-- to the expected request data
	self._expectedRequestData = data
	self._actualRequestDataTransformationFn = transformFn
	return self
end

--[[
	Adds a transformation function for the request URL. This function will be passed the 
	request URL and should return a transformed URL. e.g.
	
	<pre>
		function( actualUrl )
			...
			return transformedUrl
		end
	</pre>
	
	The returned \c transformedUrl will then be compared against the \c url specified in the
	\c :get, \c :post, or \c :put call
	
	A url transformation function can be useful to strip out parameters, like timestamps, that
	cannot be predicted ahead of time.
	
	\param urlTransformFn (function) function that will take the actual request URL and return 
		a transformed URL
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:requestUrlTransformation( urlTransformFn )
	check( 'WFMockHttpRequest', 'function' )

	self._urlTransformationFn = urlTransformFn
	return self
end

--[[
	Add a data transformation function that can transform both the actual request data
	and the expected request data before they are compared. e.g.
	<pre>
		function( actualRequestData, expectedRequestData )
			...
			return transformedActualData, transformedExpectedData
		end
	</pre>
	
	Note: this is similar to the transform function in \c expectRequestDataAfterTransformation
	but can act on both the actual HTTP request data and the expected request data.
	
	IMPORTANT: If both \c requestDataTransformation and \c expectRequestDataAfterTransformation
	are set for an HTTP request, the transform function from \c expectRequestDataAfterTransformation
	is executed first.
	
	\param transformFn (function) function that takes both the actual and expected request data and
		returns modified actual and expected request data.
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:requestDataTransformation( transformFn )
	check( 'WFMockHttpRequest', 'function' )
	
	-- transformFn can transform both expected and actual data
	self._requestDataTransformationFn = transformFn
	return self
end

--[[
	Adds a validation function that takes the actual request data and should return \c true 
	if it is valid request data and \c false otherwise. 
	<pre>
		function( requestData )
			...
			return isRequestDataValid
		end
	</pre>
	
	If the request data passes this validation check it will NOT be compared to the expected
	request data. If it fails the validation check, the request data is changed to an string 
	indicating an error which - when compared to the expected data - should fail and display 
	the message.
	
	IMPORTANT: Using this with \c expectRequestData( ':ignore:' ) will not work as expected 
	and may fail the validation but pass the data check and incorrectly succeed.
	
	\param validationFn (function)

	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:validateRequestData( validationFn )
	check( 'WFMockHttpRequest', 'function' )
	
	self._requestValidationFn = validationFn
	return self
end

--[[
	Adds a table of HTTP headers that are expected with the request. If any headers are 
	specified here, then the actual request headers must match exactly. You cannot currently
	specify only a partial list of expected request headers.
	
	Request headers should be specified in a table format like this:
	<pre>
	{
		{
			header = "X-Generate-Renditions",
			value = "all",
		},
		{
			header = "Content-Type",
			value = "application/json",
		},
		{
			header = "Accept",
			value = "application/json",
		},
	}
	</pre>
	
	\param requestHeaders (table) table of request headers
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:withRequestHeaders( requestHeaders )
	check( 'WFMockHttpRequest', 'table' )
	
	self._expectedRequestHeaders = requestHeaders
	return self
end

--[[

	\deprecated Use \c withRequestHeaders and other individual functions instead
	
	Adds a full options table to the mock request to be compared to the actual 
	request options. See the documentation for \c WFRxHttpClient.get/put for full
	documentation on what this table entails, but a small sample would be:
	
	<pre>
	local options = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	</pre>
	
	\deprecated Use \c withRequestHeaders and other individual functions instead
	
	\param options (table) matching the options table format of WFRxHttpClient.get/put
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:withRequestOptions( options )
	check( 'WFMockHttpRequest', 'table' )
	
	self._requestOptions = options
	return self
end

--[[
	Adds the basic content of the mock response to the HTTP request including the HTTP 
	status code and any actual data in the response.
	
	The \c dataOrFn can be either a string of data or it can be a function. 
	
	Using a function will allow the response to be more dynamically tailored to the actual
	request data and options than may be possible by using a simple string of data. Any 
	function specified should take two parameters - the request data and the request 
	options and return the response data.
	
	<pre>
		function( requestData, requestOptions )
			...
			return responseData
		end
	</pre>
	
	\param code (string) HTTP response code to return in response to the HTTP request
	\param dataOrFn (string|userdata|function) data, or a function that will produce a string|userdata, to 
		to respond to the HTTP request with

	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:respond( code, dataOrFn )
	check( 'WFMockHttpRequest', 'string', 'string|userdata|function' )
	
	self._responseCode = code
	
	if type( dataOrFn ) == 'function' then
		self._responseFn = dataOrFn
	else
		self._responseData = dataOrFn
	end
	
	return self
end

--[[
	Provide specific response headers to include with the mock HTTP response. 
	
	This table should be a simple table of key/value pairs. e.g.
	<pre>
		{
			[ "Content-Length" ] = 13
		}
	</pre>
	
	\param responseHeaders (table) table of response header key/value pairs
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:withResponseHeaders( responseHeaders )
	check( 'WFMockHttpRequest', '?table' )
	
	self._responseHeaders = responseHeaders
	return self
end

--[[
	Attaches a function to the HTTP request which can later be retrieved by the creating test
	and used as part of additional verification steps outside of the basic mock HTTP testing.
	See \c WFCatTrashTestUtils for the lone usage of this
	
	This function is never executed by the mock testing framework so it's parameters and return
	values can be defined entirely by the client.
	
	\deprecated This function should not be used if at all possible. There is currently
	no replacement, but a different solution/API should really be created to replace this
	as it provides functionality that isn't strictly necessary for supporting mock testing 
	of HTTP requests.
	
	\param aFunction (function) a function that can piggyback along with the request/response
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:attachFunctionToRequest( aFunction )
	check( 'WFMockHttpRequest', '?function' )
	
	self._attachedFunction = aFunction
	return self
end

--[[
	Provides an additional transformation function that operates on the response data.
	
	<pre>
	function( responseData )
		...
		return modifiedResponseData
	end
	</pre>
	
	This function is passed the response data or the result of the response function from
	\c :respond
	
	\param transformFn (function) function that takes a response and returns a modified response
	
	\return WFMockHttpRequest
]]--
function WFMockHttpRequest:withResponseTransformationFn( transformFn )
	check( 'WFMockHttpRequest', 'function' )
	
	self._responseFilterFn = transformFn
	return self
end

--[[
	Builds a table in the format \c WFRxHttpClientMock expects with all of the configuration
	previously configured on the \c WFMockHttpRequest object.
	
	Users of \c WFMockHttpRequest typically do not have to call this and can pass the actual
	\c WFMockHttpRequest object directly to \c WFCatSpecMock:expectHttpRequest or 
	\c WFRxHttpClientMock:addHttpRequest 
	
	OR 
	
	you can call \c build() and pass the resulting table to these functions.
	
	\return table in \c WFRxHttpClientMock's expected format
]]--
function WFMockHttpRequest:build()
	check( 'WFMockHttpRequest' )
	
	local request = {}
	if self._url then
		request.url = self._url
	end
	
	if self._urlTransformationFn then
		request.urlFilter = self._urlTransformationFn
	end
	
	if self._expectedRequestData then
		request.data = self._expectedRequestData
	end
	
	if self._requestValidationFn then
		request.validationFn = self._requestValidationFn
	end
	
	if self._actualRequestDataTransformationFn then
		request.dataFilter = self._actualRequestDataTransformationFn	
	end

	if self._requestDataTransformationFn then
		request.dataTransformation = self._requestDataTransformationFn
	end

	if self._requestOptions then
		request.options = self._requestOptions
	end
	
	if self._expectedRequestHeaders then
		request.options = request.options or {}
		request.options.requestHeaders = self._expectedRequestHeaders
	end
	
	if self._responseData then
		request.response = self._responseData
	end

	if self._responseFn then
		request.responseFn = self._responseFn
	end
	
	if self._responseHeaders then
		request.headers = self._responseHeaders
	end

	if self._responseCode then
		
		request.headers = request.headers or {}
		request.headers.status_code = self._responseCode or '200'
		if self._responseCode >= '400' then
			local fullResponseHeaders = self._responseHeaders or {}
			fullResponseHeaders.status_code = self._responseCode
			request.fail = {
				self._responseData or {},
				fullResponseHeaders,
			}
		end
	end

	if self._attachedFunction then
		request.attachedFn = self._attachedFunction
	end
	
	if self._responseFilterFn then
		request.responseFilter = self._responseFilterFn
	end

	request.traceback = self._traceback

	return request
end

return Rx.Class 'WFMockHttpRequest'( WFMockHttpRequest )