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

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 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 check = assert( WFArguments.check )

local scheduler = WFRxScheduler.sharedScheduler()

local _errorObserver

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

--[[!

	\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) 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
	
	\return WFRxHttpClientMock object
	
--]]

return function( rxTest, expectedRequestResponsePairs, optRateLimiter )

	check( 'WFRxTestCaseRunner', '?table', '?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 requestInfo = expectedRequestResponsePairs[ requestCount ]
						or { URL = "NO FURTHER REQUESTS" }

		if data or requestInfo.data 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 options then
		
			if options.requestHeaders then
				for _, header in ipairs( options.requestHeaders ) do
					if header.header == "Accept-Language" then
						header.value = "(language)"
					end
				end
			end
			
			-- Remove some options that are specific to the rate limiter,
			-- and not the actual HTTP request.
			
			options.requestDescription = nil
			
		end
		
		if requestInfo.dataTransformation then
			data, requestInfo.data = requestInfo.dataTransformation( data, requestInfo.data )
		end
		
		if requestInfo.validationFn then
			if requestInfo.validationFn( data ) then
				requestInfo.data = data
			else
				requestInfo.data = "The actual request data failed validation" 
			end
		end

		if requestInfo.urlFilter then
			url = requestInfo.urlFilter( url )
		end
		
		if requestInfo.options and requestInfo.options.requestHeaders then
			table.sort( requestInfo.options.requestHeaders, function( a, b ) return a.header < b.header end )
		end
		
		if options and options.requestHeaders then
			table.sort( options.requestHeaders, function( a, b ) return a.header < b.header end )
		end
		
		local expectedRequestInfo = {
			URL = requestInfo.url,
			_count = requestCount,
			_traceback = requestInfo.traceback,
			options = requestInfo.options,
			data = requestInfo.data,
		}

		local actualRequestInfo = {
			URL = url,
			_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 = data,
		}
	
		if expectedRequestInfo.data == ':ignore:' then
			expectedRequestInfo.data = nil
			actualRequestInfo.data = nil
		end
		
		requestErrors = requestErrors + 1
		rxTest:expectEqual( expectedRequestInfo, actualRequestInfo )
		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

		-- 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
			local requestDescription = options and options.requestDescription or 'Unknown'
			
			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
