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

WFRxHttpClientMock.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 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.

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

This mock is intended as a replacement for the WFRxHttpClient namespace.
It can be used when you want to simulate certain request / response patterns
that are difficult to emulate otherwise.

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

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.os
local KSTableUtils = require 'KSTableUtils'
local KSDate = require 'KSDate'
local WFQueueing = require 'WFQueueing'

	-- WF.web
local WFRxRateLimiter = require 'WFRxRateLimiter'

	-- WF.core.reactive
local WFRxScheduler = require "WFRxScheduler"

	-- WF.core.utils
local WFArguments = require 'WFArguments'

local WFMockHttpRequest = require 'WFMockHttpRequest'

local KSData = require "KSData"

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

local check = assert( WFArguments.check )

local scheduler = WFRxScheduler.sharedScheduler()

local _errorObserver

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

local function sortHeaders(options)

	if options and options.requestHeaders then
		table.sort( options.requestHeaders, function( a, b ) return a.header < b.header end )
	end

end

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

local function prepareForRequestMatch( url, data, options, requestInfo )

	local requestInfoData = requestInfo.data

	if data or requestInfoData then

		data = tostring( data )

		if requestInfo.dataFilter then
			data = requestInfo.dataFilter( data )
		end

		if type( data ) == 'string' then
			data = data:gsub( "client_id=[^&]+", "client_id=(mumble)" )
			data = data:gsub( "client_secret=[^&]+", "client_secret=(secret)" )
		end

	end

	if requestInfo.dataTransformation then
		data, requestInfoData = requestInfo.dataTransformation( data, requestInfoData )
	end

	if requestInfo.validationFn then
		if requestInfo.validationFn( data ) then
			requestInfoData = data
		else
			requestInfoData = "The actual request data failed validation"
		end
	end

	if requestInfoData == ':ignore:' then
		data = nil
		requestInfoData = nil
	end

	if requestInfo.urlFilter then
		url = requestInfo.urlFilter( url )
	end

	return url, data, requestInfoData

end

--============================================================================--

--[[!

	\brief Creates a WFRxHttpClientMock instance which can be used to create
		mocked HTTP request/response pairs for use in unit tests.

	\param rxTest (WFRxTestCaseRunner)

	\param expectedRequestResponsePairs (table, optional) A table of request/response pairs
		with the following optional parameters:

		shouldNotComplete: (boolean) If true, we will not count this request in the
			total number of expected requests.

		responseDelayBySeconds: (number) If set, delay the response by this many seconds.

		dataTransformation: (function) A function used to transform the request data.

		validationFn: (function) A function used to validated the request data.

	\param optRateLimiter (boolean, optional) Add rateLimiter if boolean is true

	\param enableIgnoreMockResponse (boolean, optional) If true, then if
		request options include ingoreMockResponse boolean flag set to true,
		then the request will not be compared to the expected response. Mostly
		useful to use WFRxHttpClientMock instance as a WFCatSpecSpy as a way to
		indicate a response is expected but that perhaps all of the details of
		the request and response are not known.

	\param unordered (boolean, optional) If true then do not require that the
		expectedRequestResponsePairs come in the order they are listed. Instead
		allow them to come in any order and annotate each table with a orderReceived
		value so that the calling test can confirm RELATIVE order without requiring
		an ABSOLUTE ordering.

	\return WFRxHttpClientMock object

--]]

return function( rxTest, expectedRequestResponsePairs, optRateLimiter, enableIgnoreMockResponse, unordered )

	check( 'WFRxTestCaseRunner', '?table', '?boolean', '?boolean' )

	expectedRequestResponsePairs = expectedRequestResponsePairs or {}

	local requestCount = 0
	local requestErrors = 0
	local lastRequestTime = KSDate.timeNowForAnimation()
	local prevRequestTime = lastRequestTime
	local requestsProcessed = Rx.BehaviorSubject( 0 )

	rxTest:finally( function()

		local numberOfExpectedRequestResponsePairs = #expectedRequestResponsePairs

		for _, requestResponsePair in ipairs( expectedRequestResponsePairs ) do

			if requestResponsePair.shouldNotComplete then
				numberOfExpectedRequestResponsePairs = numberOfExpectedRequestResponsePairs - 1
			end

		end

		if requestCount ~= numberOfExpectedRequestResponsePairs then
			error( string.format( "Expected test to make %d calls, only received %d",
								  numberOfExpectedRequestResponsePairs, requestCount ) )
		end

		if requestErrors ~= 0 then
			-- Handle case where error was trapped by a pcall.
			error( "Mock Http error encountered in test" )
		end

	end )

	local rateLimiter

	if optRateLimiter then
		rateLimiter = WFRxRateLimiter.makeRateLimiter( WFQueueing.makePriorityQueueNew, 10, 5 )
	end

	local function getPutImp( url, data, options, sequencing )

		prevRequestTime = lastRequestTime
		lastRequestTime = KSDate.timeNowForAnimation()
		requestCount = requestCount + 1

		rxTest:log( string.format( "Request #%s url %s", requestCount, url ) )

		local requestDescription

		if options then

			if options.requestHeaders then
				for _, header in ipairs( options.requestHeaders ) do
					if header.header == "Accept-Language" then
						header.value = "(language)"
					end
				end

				sortHeaders( options )
			end

			-- remove ignoreMockResponse when it is not enabled
			if not enableIgnoreMockResponse then
				options.ignoreMockResponse = nil
			end

			-- Remove some options that are specific to the rate limiter,
			-- and not the actual HTTP request.

			requestDescription = options.requestDescription or 'Unknown'
			options.requestDescription = nil

		end

		local requestInfo, urlComp, dataComp, requestInfoData

		if unordered then

			local firstUrlMatch, candidateData
			for i, candidate in ipairs(expectedRequestResponsePairs) do

				-- if orderReceived is set then we already used this request expectation
				if candidate and not candidate.orderReceived then

					-- in theory it would seem all this header sorting and value stripping could be done
					-- once in a loop for all these requests somewhere... but there are some tests that do
					-- goofy things like pass in expectedRequestResponsePairs table and then later swap out
					-- values in the table later, so just do it redundantly in this loop
					sortHeaders( candidate.options )

					-- remove ignoreMockResponse when it is not enabled
					if candidate.options and not enableIgnoreMockResponse then
						candidate.options.ignoreMockResponse = nil
					end

					urlComp, dataComp, candidateData = prepareForRequestMatch( url, data, options, candidate )

					if urlComp == candidate.url then

						firstUrlMatch = firstUrlMatch or { urlComp, dataComp, candidateData, candidate }

						-- don't compare options unless the mock request included options
						local compOptions = candidate.options and options

						-- if url, data and options all match, then we should have a good match
						if dataComp == candidateData and ( compOptions == nil or KSTableUtils.deepEquals( compOptions, candidate.options ) ) then
							requestInfo = candidate
							requestInfoData = candidateData
							requestInfo.orderReceived = requestCount
							break
						end

					end

				end

			end

			-- at this point if no candidate was chosen then use the first URL match to give some useful errors
			if not requestInfo and firstUrlMatch then
				urlComp, dataComp, requestInfoData, requestInfo = unpack( firstUrlMatch )
			end

			requestInfo = requestInfo or { url = "NO MATCHING REQUEST" }

		else

			requestInfo = expectedRequestResponsePairs[ requestCount ] or { url = "NO FURTHER REQUESTS" }
			urlComp, dataComp, requestInfoData = prepareForRequestMatch( url, data, options, requestInfo )

			sortHeaders( requestInfo.options )

			-- remove ignoreMockResponse when it is not enabled
			if requestInfo.options and not enableIgnoreMockResponse then
				requestInfo.options.ignoreMockResponse = nil
			end

		end

		local expectedRequestInfo = {
			URL = requestInfo.url,
			_count = requestCount,
			_traceback = requestInfo.traceback,
			options = requestInfo.options,
			data = requestInfoData,
		}

		local actualRequestInfo = {
			URL = urlComp,
			_count = requestCount,
			_traceback = requestInfo.traceback,
			-- don't compare options unless the mock request declared
			-- some to validate, because not all tests need to be pedantic
			options = requestInfo.options and options or nil,
			data = dataComp,
		}

		requestErrors = requestErrors + 1

		local ignoreMockResponse = options and options.ignoreMockResponse

		if not ignoreMockResponse then
			rxTest:expectEqual( expectedRequestInfo, actualRequestInfo )
		end

		requestErrors = requestErrors - 1

		local fail = requestInfo.fail
		local failFn = ( options and options.returnHttpErrorsAsData ) and Observable.just or Observable.throw
		if type( fail ) == 'table' then
			if _errorObserver then
				_errorObserver:onNext( unpack( fail, 1, #fail ) )
			end
			return failFn( unpack( fail, 1, #fail ) )
		elseif fail then
			if _errorObserver then
				_errorObserver:onNext( fail )
			end
			return failFn( fail )
		end

		local response = requestInfo.response

		if requestInfo.responseFn then
			response = requestInfo.responseFn( data, options )
		end

		if requestInfo.responseFilter then
			response = requestInfo.responseFilter( response )
		end

		if options and options.filePath then
			KSData.saveToUniquelyNamedFile( response, options.filePath )
			response = options.filePath
		end

		-- Schedule requestsProcessed's onNext to occur at the next idle time
		-- after getOrPut's result is sent.
		scheduler:schedule( requestCount, function( _, processedCount )
			requestsProcessed:onNext( processedCount )
		end )


		local result

		if rateLimiter and options and not options.bypassRateLimiter then

			local observableToLimit = Observable.just( true )
			:flatMap( function()

				local result = Observable.just( response, requestInfo.headers )

				if requestInfo.responseDelayBySeconds then
					result = result:delay( requestInfo.responseDelayBySeconds, scheduler )
				end

				return result

			end )

			if  options and options.notifyWhenHttpRequestIsInProgress then

				if requestInfo.sitInQueueForNSeconds then

					observableToLimit = Rx.Observable.timer( requestInfo.sitInQueueForNSeconds,
							WFRxScheduler.sharedScheduler() ):concat( observableToLimit )

				else

					observableToLimit = observableToLimit:startWith( 'requestInProgress' )

				end
			end

			local requestPriority = options and options.requestPriority
			local requestCost = options and options.requestCost

			result = rateLimiter:enqueueObservable( observableToLimit, requestPriority, requestCost, requestDescription )

		else

			result = Observable.just( response, requestInfo.headers )
			if requestInfo.responseDelayBySeconds then
				result = result:delay( requestInfo.responseDelayBySeconds, scheduler )
			end

			if options and options.notifyWhenHttpRequestIsInProgress then

				if requestInfo.sitInQueueForNSeconds then

					result = Rx.Observable.timer( requestInfo.sitInQueueForNSeconds,
							WFRxScheduler.sharedScheduler() ):concat( result )

				else

					result = result:startWith( 'requestInProgress' )

				end

			end

		end

		return result
	end

	return {

		get = function( url, options, sequencing )
			return getPutImp( url, nil, options, sequencing )
		end,

		put = function( url, data, options )
			return getPutImp( url, data, options )
		end,

		requestCount = function()
			return requestCount
		end,

		requestsProcessed = function()
			return requestsProcessed
		end,

		requestTraceback = function( optRequestNum )
			local requestNum = optRequestNum or requestCount
			return expectedRequestResponsePairs[ requestNum ].traceback
		end,

		getAttachedFunction = function( optRequestNum )
			local requestNum = optRequestNum or requestCount
			return expectedRequestResponsePairs[ requestNum ].attachedFn
		end,

		timeBetweenRequests = function()
			return lastRequestTime - prevRequestTime
		end,

		addHttpRequest = function( httpRequest )
			if WFMockHttpRequest:isClassOf( httpRequest ) then
				httpRequest = httpRequest:build()
			end
			table.insert( expectedRequestResponsePairs, httpRequest )
		end,

		watchHttpErrors = function()

			if not _errorObserver then
				_errorObserver = Rx.Subject()
			end

			return _errorObserver

		end,

		-- should only be used by unit tests for WFRxHttpClientMock/WFMockHttpRequest
		_debugClearRequestErrorCount = function()
			requestErrors = 0
		end,
	}

end
