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

WFRxTestCase.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 namespace should be used to create test cases that may have asynchronous
behaviors based on RxLua Observables. It may also be used for tests that are
not synchronous.

The primary premise is that you create test functions in a namespace that have
the prefix "rxTest". These functions get run in a coroutine environment that
monitors the function's execution (failing the test if an error gets thrown, etc.)
and allows you to wait inline for expected results from the Observable.

See WFRxTestCaseTest.lua and the many other files that now use this namespace
for examples of how to use this.

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

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

	-- WF.core.os
local KSDate = require 'KSDate'
local WFPlatformInfo = require 'WFPlatformInfo'

	-- WF.core.rxlua
local WFRxScheduler = require 'WFRxScheduler'

	-- WF.core.self_test
local WFTestMetrics = require 'WFTestMetrics'

	-- WF.core.table
local WFTableArchiving = require 'WFTableArchiving'

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local WFArguments = require 'WFArguments'
local WFCloseSupport = require 'WFCloseSupport'
local WFTableDiff = require 'WFTableDiff'

	-- WF.core.logger
local WFLogger = require 'WFLogger'

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

local check = assert( WFArguments.check )

local logger = WFLogger 'WFRxTestCase'

local handleResumeResponse -- forward declaration

local TIME_LIMIT_MODIFIER = WFPlatformInfo.isAndroid and 2 or 1
local NIGHTLY_TIME_LIMIT_MODIFIER = TIME_LIMIT_MODIFIER

local kDefaultNightlyRetryDelay = 30
	-- Number of seconds to delay before retrying a nightly test case.
	
local kDefaultStandardRetryDelay = 5
	-- Number of seconds to delay before retrying a standard test case.
	
local retryLimitByTestName = {}

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

local function pack( ... )

	return { ... }, select( '#', ... )

end

--============================================================================--
--[[!
	\class WFRxTestCase.testObserver

	\brief A TestObserver monitors the notifications from a single Observable
		object and validates that they match expected results.

	It is created via the \c :observe method on \c WFRxTestCaseRunner.
]]--

-- @mark WFRxTestCase.testObserver class

local testObserver = {

	Rx.AbstractObserver,

	_runner = false,
		-- (WFRxTestCaseRunner)

	_observable = false,
		-- (Rx.Observable)

	_unsubscribeHandle = false,
		-- (Disposable) handle to use for unsubscribing to the observable

	_isWaiting = false,
		-- (boolean) true if this observer is waiting

	_queuedResponses = false,
		-- (table) array of queued responses from data that was received
		-- while we weren't waiting

	_mostRecentValue = false,
		-- (table) most recent value from this observable
	
}

--------------------------------------------------------------------------------
-- Not to be called by test functions.

function testObserver:__init( runner, observable )

	check( 'WFRxTestCase.observer', 'WFRxTestCaseRunner', '_' )

	self._runner = runner
	self._observable = observable
	self._queuedResponses = {}

	self._unsubscribeHandle = observable:subscribe( self )

end

--------------------------------------------------------------------------------
--[[!
	\brief Releases the subscription to the Observable.

	Call this function only if you need to specify the lifetime of the subscription.

	If you do not call this function, the test infrastructure will clean up
	the subscription for you automatically at the end of the test.
]]--

function testObserver:dispose()

	check( 'WFRxTestCase.observer' )

	self._runner = nil
	self._observable = nil

	if self._unsubscribeHandle then
		self._unsubscribeHandle:dispose()
		self._unsubscribeHandle = nil
	end

	self._queuedResponses = nil
	self._mostRecentValue = nil

	self:Rx_AbstractObserver_dispose()

end

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

local function packResults( responseType, ... )

	local results = {
		type = responseType,
		n = select( '#', ... ),
		...
	}

	if responseType == 'error' then

		local result1 = results[ 1 ]

		if type( result1 ) == 'string' then
			results[ 1 ] = result1:gsub( "^%[[^]]+%]:%d+: ", "" )
		end

	end

	return results

end

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

local function continueTestWithResponse( self, responseType, ... )

	local testFnThread = self._runner._testFnThread

	-- It's possible to enter this state if notifications have been
	-- queued up (as below), but the test has already finished or failed.
	-- If this is the case, we simply discard any further notifications.

	if not testFnThread then
		return
	end

	local thisValue = packResults( responseType, ... )

	self._mostRecentValue = thisValue

	if( not self._isWaiting ) or coroutine.status( testFnThread ) == 'running' then

		-- An Observable that we're watching provided new results,
		-- but it isn't the one that we're waiting for Right Now.
		-- Queue up the result and send it later.

		self._queuedResponses[ #self._queuedResponses + 1 ] = thisValue

	else

		handleResumeResponse( self._runner,
							  coroutine.resume( self._runner._testFnThread,
												responseType, ... ) )

	end

end

--------------------------------------------------------------------------------
-- Not to be called by test functions.

function testObserver:onNext( ... )

	check( 'WFRxTestCase.observer' )

	continueTestWithResponse( self, 'data', ... )

end

--------------------------------------------------------------------------------
-- Not to be called by test functions.

function testObserver:onError( ... )

	check( 'WFRxTestCase.observer' )

	continueTestWithResponse( self, 'error', ... )

end

--------------------------------------------------------------------------------
-- Not to be called by test functions.

function testObserver:onCompleted()

	check( 'WFRxTestCase.observer' )

	continueTestWithResponse( self, 'completed' )

end

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

local function formatObservableResponse( packedResponse )

	if type( packedResponse ) ~= 'table' then
		return tostring( packedResponse )
	end

	local _, message = pcall( function()

		assert( packedResponse.n >= 0 )

		local responseArray = {}

		for i = 1, packedResponse.n do
			local value = packedResponse[ i ]
			if value == nil then
				responseArray[ i ] = '(nil)'
			else
				responseArray[ i ] = value
			end
		end

		local responseType = packedResponse.type

		if responseType == 'completed' and #packedResponse == 0 then

			return 'completed'

		else

			return string.format(
				"%s %s", responseType,
				WFTableArchiving.debugTableToString( responseArray ) )

		end

	end )

	return message

end

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

local function resultDeepEquals_helper( a, b, depth, depthLimit )

	if type( a ) == 'table' and type( b ) == 'table' then
	
		depth = depth + 1
		
		if depthLimit < depth then
			return false
		end
	
		-- All key/value pairs in a must match with b
	
		for k, av in pairs( a ) do
			local bv = b[ k ]
			if av ~= bv and not resultDeepEquals_helper( av, bv, depth, depthLimit )
			then
				return false
			end
		end
		
		-- b must contain no other pairs
		
		for k in pairs( b ) do
			if a[ k ] == nil then
				return false
			end
		end
		
		return true
	
	end
	
	return false

end

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

local function resultDeepEquals( a, b, depthLimit )
	
	if a == b then
		return true
	end
	
	depthLimit = depthLimit or 25
	
	return resultDeepEquals_helper( a, b, 1, depthLimit )

end

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

local function doesResultMatch( nativeTest, packedExpected, packedActual, verbose )

	local modifiedActual = KSTableUtils.shallowcopy( packedActual )

	for i = 1, #packedExpected do
		if packedExpected[ i ] == ':ignore:' then
			modifiedActual[ i ] = ':ignore:'
		end
	end

	if resultDeepEquals( packedExpected, modifiedActual, 25 ) then

		if verbose then

			local message = string.format( "\nmatched %s\n\n",
								formatObservableResponse( packedActual ) )

			debug.print( message )
			nativeTest:log( message )

		end

		return true

	end

	return false

end

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

local function failBecauseUnexpectedResults( nativeTest, packedExpected, packedActual,
											 sourceLocation )

	-- Results did not match expected. Provide error output and
	-- discontinue the test.

	local diff
	if ( packedExpected.n == 1 and packedActual.n == 1 ) and
		( type(packedExpected[1] ) == 'table' and type( packedActual[1] ) == 'table' ) then

		diff = WFTableDiff.diff( packedExpected[1], packedActual[1] )
	end


	local expectedStr = formatObservableResponse( packedExpected )
	local actualStr = formatObservableResponse( packedActual )

	local message =
		string.format( "\n\UNEXPECTED RESULTS AT %s\n\nexpected %s\n\nreceived %s\n\n",
					   sourceLocation,
					   expectedStr,
					   actualStr )

	if diff then
		-- plumb through pretty diff

		message = string.format("%s\nTABLE MISMATCH:\n\n%s", message, WFTableDiff.prettifyDiff( diff ))

	end

	nativeTest:log( message )

	error( "<<<WFRxTestCase FAIL>>>" .. message )

end

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

local function matchResultOrFail( rxTestCase, nativeTest, packedExpected, packedActual,
								  sourceLocation, verbose, expectFailure )

	local isMatch = doesResultMatch( nativeTest, packedExpected, packedActual, verbose )

	expectFailure = not not expectFailure

	if ( not isMatch and not expectFailure ) or
	   ( isMatch and expectFailure ) then

		failBecauseUnexpectedResults( nativeTest, packedExpected, packedActual,
									  sourceLocation )

	end

end

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

local function sourceLocationForDepth( stackDepth )

	local info

	repeat

		stackDepth = stackDepth + 1
		info = debug.getinfo( stackDepth )

	until not info or ( info.source ~= 'Class.lua' and info.source ~= 'WFRxTestCase.lua' )

	return info and string.format( "%s:%d", info.source, info.currentline )

end

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

local function expectResponseOfType( self, responseType, ... )

	local runner = self._runner
	local test = runner._test

	if not runner._hasYielded then

		runner._hasYielded = true

		local runnerTimeLimit = runner._timeLimit * runner._timeLimitModifier

		if runner._retryCount > 0 then
			runnerTimeLimit = ( runnerTimeLimit + runner._delayInBetweenRetryAttempts ) * runner._retryCount
		end

		test:waitForTestToFinish( runnerTimeLimit )

	end

	local sourceLocation = sourceLocationForDepth( 3 )

	local verbose = runner._verbose

	if verbose then

		local message = string.format( "\nWaiting for %s at %s ...\nelapsed time = %.2f s",
									   responseType, sourceLocation,
									   KSDate.timeNowForAnimation() - runner._startTime )

		debug.print( message )
		test:log( message )

	end

	local packedExpected = packResults( responseType, ... )
	local packedActual

	-- Do we have any data queued up from this Observable?

	local queuedResponses = self._queuedResponses

	if #queuedResponses > 0 then

		-- Yes: Process this response before waiting for more.

		packedActual = queuedResponses[ 1 ]

		table.remove( queuedResponses, 1 )

	else

		-- No: Wait until the Observable sends some notification.

		self._isWaiting = true

		packedActual = packResults( coroutine.yield() )

	end

	self._isWaiting = false

	matchResultOrFail( runner, test, packedExpected, packedActual,
					   sourceLocation, verbose )

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that the next notification from this Observable is \c onNext
		with specific values.

	\param ... values to expect with the \c onNext notification

	The test will wait (coroutine yield) for the next notification from
	this Observable.

	The test will fail if any other notification is received at this time.
]]--

function testObserver:expectData( ... )

	check( 'WFRxTestCase.observer' )

	expectResponseOfType( self, 'data', ... )

	return self
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that the next notification from this Observable is \c onError
		with specific values.

	\param ... values to expect with the \c onError notification

	The test will wait (coroutine yield) for the next notification from
	this Observable.

	The test will fail if any other notification is received at this time.
]]--

function testObserver:expectError( ... )

	check( 'WFRxTestCase.observer' )

	expectResponseOfType( self, 'error', ... )

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that the next notification from this Observable is \c onCompleted.

	The test will wait (coroutine yield) for the next notification from
	this Observable.

	The test will fail if any other notification is received at this time.
]]--

function testObserver:expectCompleted()

	check( 'WFRxTestCase.observer' )

	expectResponseOfType( self, 'completed' )

end

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

local function expectResponseOfTypeWithinTime( self, timeLimit, responseType, ... )

	local runner = self._runner
	local test = runner._test

	local runnerTimeLimit = runner._timeLimit * runner._timeLimitModifier

	if runner._retryCount > 0 then
		runnerTimeLimit = ( runnerTimeLimit + runner._delayInBetweenRetryAttempts ) * runner._retryCount
	end

	if not runner._hasYielded then

		runner._hasYielded = true

		test:waitForTestToFinish( runnerTimeLimit )
	
	end

	timeLimit = timeLimit * runner._timeLimitModifier

	local sourceLocation = sourceLocationForDepth( 3 )

	local verbose = runner._verbose

	if verbose then

		local message = string.format( "\nWaiting for %s at %s ...\nelapsed time = %.2f s",
									   responseType, sourceLocation,
									   KSDate.timeNowForAnimation() - runner._startTime )

		debug.print( message )
		test:log( message )

	end

	local packedExpected = packResults( responseType, ... )

	-- Set up the timeout.

	local timeRemaining = runnerTimeLimit - ( KSDate.timeNowForAnimation() - runner._startTime )

	if timeRemaining < timeLimit then

		-- Reduce the time waiting for this test as when a unit test exceeds the total test time
		-- we don't close out the old test before the new one starts.  This should also help see
		-- why the test failed by showing the most recent values.

		local newTimeLimit = math.max( timeRemaining - .2, 0 )

		local message = string.format( "\nExpected response time exceeds remaining unit test time, was %s reduced to %s\n\n",
			timeLimit, newTimeLimit )

		debug.print( message )
		test:log( message )

		timeLimit = newTimeLimit

	end

	local timeout = Observable.timer( timeLimit, WFRxScheduler.sharedScheduler() )

	local timeoutHandle = timeout:subscribe( function()

		handleResumeResponse( self._runner,
							  coroutine.resume( self._runner._testFnThread,
												'timeout', timeLimit ) )

	end )

	-- Shared code for logging if we succeed.

	local function tryThisValue( packedActual )

		if doesResultMatch( test, packedExpected, packedActual, verbose ) then

			timeoutHandle:dispose()

			if verbose then

				local message = string.format( "\nmatched %s\n\n",
											   formatObservableResponse( packedActual ) )

				debug.print( message )
				test:log( message )

			end

			return true

		else

			if packedActual.type == 'data' then

				if verbose then

					-- Results did not match expected. Provide log output but
					-- continue the test.

					local expectedStr = formatObservableResponse( packedExpected )
					local actualStr = formatObservableResponse( packedActual )

					local message =
						string.format( "\n\Ignoring unwanted results at %s\nelapsed time = %.2f s\n\nexpected %s\n\nreceived %s\n\n",
									   sourceLocation,
									   KSDate.timeNowForAnimation() - runner._startTime,
									   expectedStr,
									   actualStr )

					if verbose then
						debug.print( message )
					end

					test:log( message )

				end

			else

				-- If Observable is in terminal state and it wasn't what we expected,
				-- fail test immediately. (This includes timeout, which doesn't come
				-- directly from the Observable, but is processed through this same
				-- mechanism.)

				timeoutHandle:dispose()

				if packedActual.type == 'timeout' then

					-- Timeout: Log message should include most recent value.

					local expectedStr = formatObservableResponse( packedExpected )
					local actualStr = formatObservableResponse( self._mostRecentValue or packedActual )

					local message =
						string.format( "\n\TIMED OUT AT %s\nelapsed time = %.2f s\n\nexpected %s\n\nmost recent value received %s\n\n",
									   sourceLocation,
									   KSDate.timeNowForAnimation() - runner._startTime,
									   expectedStr,
									   actualStr )

					if verbose then
						debug.print( message )
					end

					test:log( message )

					error( "<<<WFRxTestCase FAIL>>>" .. message )

				else

					-- Some other terminal state: Common error handling will work here.

					failBecauseUnexpectedResults( test, packedExpected, packedActual,
												  sourceLocation )

				end

			end

		end

		return false
			-- Didn't match, but test may continue.

	end

	-- Run through all of the queued values to see if any of them match.

	local queuedResponses = self._queuedResponses

	while #queuedResponses > 0 do

		local queuedValue = queuedResponses[ 1 ]
		table.remove( queuedResponses, 1 )

		if tryThisValue( queuedValue ) then
			return
		end

	end

	-- Do we have an existing value to test?

	local mostRecentValue = self._mostRecentValue

	if mostRecentValue and tryThisValue( mostRecentValue ) then
		return
	end

	-- Keep waiting until the Observable sends some notification
	-- or the timeout occurs.

	while true do

		self._isWaiting = true

		local packedActual = packResults( coroutine.yield() )

		self._isWaiting = false

		if tryThisValue( packedActual ) then
			return
		end

	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that a specific \c onNext notification from this Observable
		arrives within a specified time limit.

	\param timeLimit (number) maximum time (in seconds) for this notification to arrive
	\param ... values to expect with the \c onNext notification

	The test will wait (coroutine yield) for the next notification from
	this Observable. It will ignore any \c onNext notification whose values
	do not match the specified values and continue to wait.

	The test will fail if \c onError or \c onCompleted is received before a
	matching \c onNext notification.

	The test will fail if no matching \c onNext is received within
	\c timeLimit seconds.

	The test will proceed as soon as the first matching \c onNext notification
	is received.
]]--

function testObserver:expectDataWithinTime( timeLimit, ... )

	if select( '#', ... ) == 0 then
		-- allow this form rxTest:expectDataWithinTime( 1 ) { ... }
		return function( ... )
			return self:expectDataWithinTime( timeLimit, ... )
		end
	end

	check( 'WFRxTestCase.observer', 'number' )

	expectResponseOfTypeWithinTime( self, timeLimit, 'data', ... )

	return self
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that a specific \c onError notification from this Observable
		arrives within a specified time limit.

	\param timeLimit (number) maximum time (in seconds) for this notification to arrive
	\param ... error values to expect with the \c onError notification

	The test will wait (coroutine yield) for the next error notification from
	this Observable.

	The test will fail if no matching \c onError is received within
	\c timeLimit seconds.

	The test will proceed as soon as a matching \c onError notification
	is received.
]]--
function testObserver:expectErrorWithinTime( timeLimit, ... )

	check( 'WFRxTestCase.observer', 'number' )

	expectResponseOfTypeWithinTime( self, timeLimit, 'error', ... )
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that a specific \c onCompleted notification from this Observable
		arrives within a specified time limit.

	\param timeLimit (number) maximum time (in seconds) for this notification to arrive

	The test will wait (coroutine yield) for the next completed notification from
	this Observable.

	The test will fail if no \c onCompleted is received within
	\c timeLimit seconds.

	The test will proceed as soon as a \c onCompleted notification
	is received.
]]--
function testObserver:expectCompletedWithinTime( timeLimit )
	
	check( 'WFRxTestCase.observer', 'number' )

	expectResponseOfTypeWithinTime( self, timeLimit, 'completed' )
	
end

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

testObserver = Rx.Class( 'WFRxTestCase.observer', testObserver )


--============================================================================--
--[[!
	\class WFRxTestCaseRunner

	\brief A WFRxTestCaseRunner monitors the execution of an RxLua-based
		asynchronous test case and determines whether the test has passed
		or failed.

	This is the `rxTest` object that is passed to test functions wrapped via
	\c WFRxTestCase.wrapTest.
]]--

-- @mark WFRxTestCaseRunner class

local WFRxTestCaseRunner = {

	_test = false,
		-- (native test object)
		
	_testName = false,
		-- (string) testName
		
	_luaClassName = false,
		-- (string) luaClassName

	_testFnThread = false,
		-- (Lua thread) coroutine in which test fn runs

	_startTime = false,
		-- (number) time stamp when test was started

	_verbose = false,
		-- (boolean) true for extra logging (goes to both test log and console)

	_hasYielded = false,
		-- (boolean) true if we have done our first yield

	_timeLimit = 2,
		-- (number) number of seconds to allow test to run

	_timeLimitModifier = 1,
		-- (number) used to modify the time limit across different platforms

	_observers = false,
		-- (weak set of Observer objects)

	_finallyFns = false,
		-- (array of function)

	_retryCount = false,
		-- (number) current test retry count

	_delayInBetweenRetryAttempts = false,
		-- (number) time to wait in between retry attempts.

	WFCloseSupport.addCloseSupportMethods,

}

--------------------------------------------------------------------------------
--[[!
	\brief Construct a WFRxTestCaseRunner object.

	\param test (WF native test object)
	\param fn (test function) test function to be wrapped
]]--

function WFRxTestCaseRunner:__init( test, fn, luaClassName, testName, retryCount )

	check( 'WFRxTestCaseRunner', '_', 'function', '?string', '?string', '?number' )

	self._test = test

	local function fnWrapper( ... )
	
		if self._retryCount > 0 then
		
			logger:warnf( "RETRYING TEST CASE: %s", self._testName )
			self:sleep( self._delayInBetweenRetryAttempts )
			
		end
		
		return fn( ... )
	
	end

	self._testFnThread = coroutine.create( fnWrapper )

	self._startTime = KSDate.timeNowForAnimation()

	self._observers = setmetatable( {}, { __mode = 'kv' } )

	self._finallyFns = {}

	self._timeLimitModifier = TIME_LIMIT_MODIFIER
	
	self._testName = testName
	
	self._luaClassName = luaClassName
	
	self._delayInBetweenRetryAttempts = kDefaultStandardRetryDelay
	
	retryLimitByTestName[ testName ] = 2
	
	self._retryCount = retryCount or 0
	
	if testName and testName:match( 'testNightly' ) then
	
		self._timeLimitModifier = NIGHTLY_TIME_LIMIT_MODIFIER
			-- Nightly tests can take a long time depending on Oz load
			-- so we use a larger time limit modifier to help with
			-- test stability.
			
		retryLimitByTestName[ testName ] = 3
		self._delayInBetweenRetryAttempts = kDefaultNightlyRetryDelay
		
	elseif testName and testName:match( 'WFRxTestCaseTest' ) then
	
		self._timeLimitModifier = 1
			-- The WFRxTestCaseTest test suite includes tests that check
			-- the timings of WFRxTestCase. Setting the modifier to 1 for
			-- this particular suite allows the tests to complete quickly
			-- and precisely.

	end
	
end

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

function WFRxTestCaseRunner:nativeTest()

	check( 'WFRxTestCaseRunner' )
	
	return self._test
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Retry a failing test with numberOfRetries times.
	
	\param numberOfRetries(number) number of times to retry the test.
	
	\param delayInBetweenRetryAttempts(optional number) delay seconds between each Retry run.
]]--

function WFRxTestCaseRunner:setRetryLimit( numberOfRetries, delayInBetweenRetryAttempts )
	
	check( 'WFRxTestCaseRunner', 'number', '?number' )
	
	if delayInBetweenRetryAttempts then
		self._delayInBetweenRetryAttempts = delayInBetweenRetryAttempts
	end

	local testName = self._testName
	
	retryLimitByTestName[ testName ] = numberOfRetries
	
end
	
--------------------------------------------------------------------------------
	
local function run( self )

	local testFnThread = self._testFnThread

	handleResumeResponse( self, coroutine.resume( testFnThread, self ) )

end

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

local function callFinallyFns( self )

	local ffns = self._finallyFns

	local finallyObservables = {}

	finallyObservables[ #finallyObservables + 1 ] = Observable.empty()

	-- Iterate through the finally functions. If the return value is an Observable,
	-- add it to a list of observables. If it is not an observable, create an Observable
	-- out of the return value and add that to the list.

	for i = #ffns, 1, -1 do

		local success, maybeObservableOrMessage = pcall( ffns[ i ] )

		if success and Observable:isClassOf( maybeObservableOrMessage ) then

			finallyObservables[ #finallyObservables + 1 ] = maybeObservableOrMessage

		elseif not success then
		
			local message = maybeObservableOrMessage
			finallyObservables[ #finallyObservables + 1 ] = Observable.throw( message )

		end

	end

	return Observable.mergeDelayError( unpack( finallyObservables ) )

end

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

-- This function is called when the test function coroutine is resumed because
-- an Observable we are monitoring has signaled new data.

local subscriptions = {}

handleResumeResponse = function( self, success, message )

	local test = self._test

	local status = coroutine.status( self._testFnThread )
	local testDidSucceed = false

	-- Signal success or failure as appropriate.

	if success == true and status == 'suspended' then

		return
			-- Not done yet, wait for something to resume test.

	elseif success == true and status == 'dead' then

		testDidSucceed = true

	elseif type( message ) == 'string' and message:match( '<<<WFRxTestCase FAIL>>>' ) then

		-- We've already logged a suitable failure message.
		-- No need to repeat it.

	else

		-- Test failed due to an error being thrown, not due to
		-- an expected value mismatch. Make sure that error is visible.

		test:log( message )

	end

	-- Test is done. Start the tear-down process.

	self._testFnThread = nil

	for observer in pairs( self._observers ) do
		observer:dispose()
	end

	self._observers = nil

	local closeAndCleanFn = function( didFinallyFnsSucceed, finallyFnsErr )

		subscriptions[ self ] = nil
	
		-- Close self and any other objects that might have been attached
		-- to this test runner.

		self:close()

		-- Drop reference to native test object.

		self._test = nil

		-- Report status to native test system.

		if testDidSucceed then

			if didFinallyFnsSucceed then

				test:notifyTestDidSucceed()

			else

				test:notifyTestDidFail( finallyFnsErr )

			end

		else

			if type( message ) == 'string' then

				message = message:gsub( "^.*<<<WFRxTestCase FAIL>>>", '' )

				test:notifyTestDidFail( tostring( message ) )

			end

		end

	end

	-- Call finally functions.

	local source = callFinallyFns( self )
		:timeout( 30, "timeout", WFRxScheduler.sharedScheduler() )

	local subscription = source:subscribe(

		function()
			--noop
		end,

		function( err )

			-- A finally method failed due to an error being thrown.
			-- Make sure that error is visible.
			test:log( string.format( "A finally function failed\n%s", err ) )
			closeAndCleanFn( false, err )

		end,

		function()
			closeAndCleanFn( true )
		end )
		
	subscriptions[ self ] = subscription

end

--------------------------------------------------------------------------------
--[[!
	\brief Ensure that a function is executed after the test completes, regardless
		of success or failure of the test.

	\param fn (function) function to be called

	The functions are called in reverse order of their registration via \c :finally.
	This is to allow nested resources to be torn down in the correct sequence.

	Each function may return an Observable object. If it does so, the test completion
	is delayed until each of the Observables has completed, but will still be
	limited to whatever timeout was initially specified for this test.

	If any function registered via \c :finally throws an error, the test will be
	marked as a failure. If the test had not already failed, the error message
	from the first such function will be used as the reason for the test failure.

	Every Observable object returned via `:finally` must send an \c onCompleted
	notification within the overall test timeout. If any of the Observables instead
	sends an \c onError notification, the test will fail. If the test had not
	already failed, the error message from the \c onError notification will be used
	as the reason for the test failure.
]]--

function WFRxTestCaseRunner:finally( fn )

	check( 'WFRxTestCaseRunner', 'function' )

	local ffns = self._finallyFns

	ffns[ #ffns + 1 ] = fn

end

--------------------------------------------------------------------------------
--[[!
	\brief Causes the test runner to produce extra output while running the test.

	As each test step is executed and values produced, information about those
	steps will be printed to the debug console.

	Do not leave this in any test that is checked in to master, but this can be
	useful while debugging.
]]--

function WFRxTestCaseRunner:makeVerbose()

	check( 'WFRxTestCaseRunner' )

	self._verbose = true

 end

--------------------------------------------------------------------------------
--[[!
	\brief Set the maximum time allowed for this test to execute.

	\param timeout (number) maximum number of seconds for this test to execute

	The test will fail if it has not passed within this time limit.

	If this call is not made, the default time limit is 2 seconds.

	It is an error to call this function after the first operation that
	causes the coroutine to yield.
]]--

function WFRxTestCaseRunner:setTimeLimit( timeout )

	check( 'WFRxTestCaseRunner', 'number' )

	if self._hasYielded and self._retryCount == 0 then
		error( "WFRxTestCase:setTimeLimit must be called before first yield", 2 )
	end

	self._timeLimit = timeout

end

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

local function observeImp( self, observable )

	if not Rx.Observable:isClassOf( observable ) then
		error( "WFRxTestCaseRunner:observe: observable is not an Rx.Observable", 3 )
	end

	local observer = testObserver( self, observable )

	self._observers[ observer ] = observer

	return observer

end

--------------------------------------------------------------------------------
--[[!
	\brief Create an Observer object which can be used later in the test to
		specify expected results from an Observable.

	\param observable (Rx.Observable) the Observable to monitor

	\return (WFRxTestCase.testObserver) a test Observer object tied to \c observable
]]--

function WFRxTestCaseRunner:observe( observable )

	return observeImp( self, observable )

end

--------------------------------------------------------------------------------
--[[!
	\brief Assert that a value provided immediately matches an expected value.

	\param expected (any) expected value
	\param actual (any) actual value

	The test will fail if expected ~= actual.
]]--

function WFRxTestCaseRunner:expectEqual( expected, actual )

	check( 'WFRxTestCaseRunner' )

	local packedExpected = packResults( 'immediate', expected )

	local packedActual = packResults( 'immediate', actual )

	local sourceLocation = sourceLocationForDepth( 2 )

	matchResultOrFail( self, self._test, packedExpected, packedActual,
					   sourceLocation, self._verbose )

end

--------------------------------------------------------------------------------
--[[!
	\brief Assert that a value provided immediately does not match an specific  value.

	\param incorrect (any) incorrect value
	\param actual (any) actual value

	The test will fail if incorrect == actual.
]]--

function WFRxTestCaseRunner:expectNotEqual( incorrect, actual )

	check( 'WFRxTestCaseRunner' )

	local packedIncorrect = packResults( 'immediate', incorrect )

	local packedActual = packResults( 'immediate', actual )

	local sourceLocation = sourceLocationForDepth( 2 )

	matchResultOrFail( self, self._test, packedIncorrect, packedActual,
					   sourceLocation, self._verbose, true )

end

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

local function startClock( test, optName )

	local startTime = KSDate.timeNowForAnimation()

	-- Generating test metrics, to be used by dashboard

	local testMetrics = WFTestMetrics.populateTestMetrics( test, optName )
	return startTime, testMetrics

end

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

local function stopClock( test, startTime, testMetrics )

	testMetrics[ "wfp_execution_time" ] = ( KSDate.timeNowForAnimation() - startTime )

	WFTestMetrics.writeTestMetrics( testMetrics )

end

--------------------------------------------------------------------------------
--[[!
	TODO: Pallavi, can you describe this?
]]--

function WFRxTestCaseRunner:measurePerformance( fn, optName )

	check( 'WFRxTestCaseRunner', 'function' )

	local startTime, testMetrics = startClock( self._test, optName )

	fn()

	stopClock( self._test, startTime, testMetrics )

end

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

function WFRxTestCaseRunner:recordPerformanceReport()

	check( 'WFRxTestCaseRunner' )
	
	local WFPerformanceReport = require 'WFPerformanceReport'
	
	local fileName = self._test:testName()
	local className = self._test:luaClassName()
	
	self:expectOnlyCompleted( 
	
		WFPerformanceReport.newPerformanceReport( 
			string.format( '%s.%s', className, fileName ) ) )
			
end

--------------------------------------------------------------------------------
--[[!
	\brief Run a function and assert that it throws a specific Lua error message.

	\param expectedError (string) error message to match
	\param fn (function) function to call
	\param ... parameters to pass to function

	This function may not yield.

	If the function does not throw an error, the test will fail.

	If the function throws an error that doesn't match \c expectedError, the
	test will fail. (Note that the test infrastructure will strip the standard
	file name and line number prefix generated by Lua for purposes of the match.)
]]--

function WFRxTestCaseRunner:expectSpecificError( expectedError, fn, ... )

	check( 'WFRxTestCaseRunner', 'string', 'function' )

	local packedExpected = packResults( 'immediate', false,
										expectedError or ':ignore:' )

	local packedActual = packResults( 'immediate', pcall( fn, ... ) )

	-- Remove source file/line prefix from string error messages.

	local packedActual2 = packedActual[ 2 ]

	if packedActual[ 1 ] == false and type( packedActual2 ) == 'string' then

		-- Ugh. Errors may be rethrown repeatedly leading to multiple
		-- file/line prefixes.

		local prevPA2
		repeat
			prevPA2 = packedActual2
			packedActual2 = packedActual2:gsub( "^%[[^%]]+%]:%d+: ", "" )
		until prevPA2 == packedActual2

		packedActual[ 2 ] = packedActual2

	end

	local sourceLocation = sourceLocationForDepth( 2 )

	matchResultOrFail( self, self._test, packedExpected, packedActual,
					   sourceLocation, self._verbose )

end

--------------------------------------------------------------------------------
--[[!
	\brief Run a function and assert that it throws any Lua error.

	\param fn (function) function to call
	\param ... parameters to pass to function

	This function may not yield.

	If the function does not throw an error, the test will fail.
]]--

function WFRxTestCaseRunner:expectError( fn, ... )

	check( 'WFRxTestCaseRunner', 'function' )

	return self:expectSpecificError( ':ignore:', fn, ... )

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that an Observable passes a single \c onNext notification
		and then sends its \c onCompleted notification.

	\param observable (Rx.Observable) the Observable to monitor
	\param ... (any) values that should be provided in the \c onNext notification

	If the Observable sends any notifications that do not match \c onNext( ... )
	followed by \c onCompleted, the test will fail.
]]--

function WFRxTestCaseRunner:expectSingleValue( observable, ... )

	check( 'WFRxTestCaseRunner', '_' )

	local o = observeImp( self, observable )

	o:expectData( ... )
	o:expectCompleted()

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that an Observable passes a single \c onNext notification
		and then sends its \c onCompleted notification. Within a specified
		duration of time

	\param minTime (number) Test should take at least this long to complete.
	\param maxTime (number) Test should take less than this amount of time to complete.
	\param observable (Rx.Observable) the Observable to monitor
	\param ... (any) values that should be provided in the \c onNext notification

	If the Observable sends any notifications that do not match \c onNext( ... )
	followed by \c onCompleted, the test will fail.
]]--

function WFRxTestCaseRunner:expectSingleValueWithinTimeLimits( minTime, maxTime, observable, ... )

	check( 'WFRxTestCaseRunner', 'number', 'number', '_' )

	local o = observeImp( self, observable )

	local startTime = KSDate.timeNowForAnimation()

	o:expectData( ... )
	o:expectCompleted()

	local actualTime = KSDate.timeNowForAnimation() - startTime

	if actualTime < minTime or actualTime > maxTime then

		local sourceLocation = sourceLocationForDepth( 2 )

		local message =
			string.format( "\n\TIME LIMITS EXCEEDED AT %s\n\nminTime %s maxTime %s actualTime = %s\n\n",
				sourceLocation,
				minTime,
				maxTime,
				actualTime )

		self:nativeTest():log( message )

		error( "<<<WFRxTestCase FAIL>>>" .. message )

	end

end


--------------------------------------------------------------------------------
--[[!
	\brief Verify that an Observable passes an \c onError notification without
		sending any \c onNext notifications.

	\param observable (Rx.Observable) the Observable to monitor
	\param ... (any) values that should be provided in the \c onError notification

	If the Observable sends any notifications that do not match \c onError( ... ),
	the test will fail.
]]--

function WFRxTestCaseRunner:expectOnlyError( observable, ... )

	check( 'WFRxTestCaseRunner', '_' )

	local o = observeImp( self, observable )
	o:expectError( ... )

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that an Observable passes an \c onCompleted notification without
		sending any \c onNext notifications.

	\param observable (Rx.Observable) the Observable to monitor

	If the Observable sends any notifications other than \c onCompleted, the
	test will fail.
]]--

function WFRxTestCaseRunner:expectOnlyCompleted( observable )

	check( 'WFRxTestCaseRunner', '_' )

	local o = observeImp( self, observable )
	o:expectCompleted()

end

--------------------------------------------------------------------------------
--[[!
	\brief Verify that an Observable passes an \c onCompleted notification without
		sending any \c onNext notifications. Within a specified duration of time

	\param minTime (number) Test should take at least this long to complete.
	\param maxTime (number) Test should take less than this amount of time to complete.
	\param observable (Rx.Observable) the Observable to monitor

	If the Observable sends any notifications other than \c onCompleted, the
	test will fail.
]]--

function WFRxTestCaseRunner:expectOnlyCompletedWithinTimeLimits( minTime, maxTime, observable, ... )

	check( 'WFRxTestCaseRunner', 'number', 'number', '_' )

	local o = observeImp( self, observable )

	local startTime = KSDate.timeNowForAnimation()

	o:expectCompleted()

	local actualTime = KSDate.timeNowForAnimation() - startTime

	if actualTime < minTime or actualTime > maxTime then

		local sourceLocation = sourceLocationForDepth( 2 )

		local message =
			string.format( "\n\TIME LIMITS EXCEEDED AT %s\n\nminTime %s maxTime %s actualTime = %s\n\n",
				sourceLocation,
				minTime,
				maxTime,
				actualTime )

		self:nativeTest():log( message )

		error( "<<<WFRxTestCase FAIL>>>" .. message )

	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Run an Observable and wait for it to complete before continuing to ensure any
		work done in that Observable has finished before moving on to the next
		step in the test. For single value Observables this would be equivalent to 
		\c rxTest:expectSingleValue(o,':ignore:') or for other Observables 
		\c rxTest:expectOnlyCompleted(o:takeLast(0)) but is easier to read 
		than both.
	
	\param Observable (Rx.Observable) the Observable to monitor
	
]]--

function WFRxTestCaseRunner:waitFor( observable )
	
	check( 'WFRxTestCaseRunner', '_' )
	
	local o = observeImp( self, observable:takeLast( 0 ) )
		
	o:expectCompleted()

end

--------------------------------------------------------------------------------
--[[!
	\brief Run an Observable, wait for it to complete and return the first
		data event that the observable fired.
	
	\param Observable (Rx.Observable) the Observable to monitor
	
]]--

function WFRxTestCaseRunner:getFirstValue( observable )
	
	check( 'WFRxTestCaseRunner', '_' )
	
	local result
	
	observable = observable
	
		:map( function( ... )

			if not result then

				result = pack( ... )
		
			end
		
		end )

	local o = observeImp( self, observable:takeLast( 0 ) )
		
	o:expectCompleted()
	
	return unpack( result or {} )

end

--------------------------------------------------------------------------------
--[[!
	\brief Run an Observable, wait for it to complete and return the last
		data event that the observable fired.
	
	\param Observable (Rx.Observable) the Observable to monitor
	
]]--

function WFRxTestCaseRunner:getLastValue( observable )
	
	check( 'WFRxTestCaseRunner', '_' )
	
	local result
	
	observable = observable
	
		:map( function( ... )

			result = pack( ... )
		
		end )
		
	local o = observeImp( self, observable:takeLast( 0 ) )
		
	o:expectCompleted()
	
	return unpack( result or {} )

end

--------------------------------------------------------------------------------
--[[!
	\brief Causes the test to pause for a specified time interval.

	\param interval (number) how many seconds to pause
]]--

function WFRxTestCaseRunner:sleep( interval )

	check( 'WFRxTestCaseRunner', 'number' )

	self:expectSingleValue( Observable.timer( interval, WFRxScheduler.sharedScheduler() ), 0 )

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns the name of the test function.

	\return (string) name of the test function
]]--

function WFRxTestCaseRunner:testName()

	check( 'WFRxTestCaseRunner' )

	local testName = self._testName

	testName = testName:gsub( '^test', 'rxTest' )

	return testName

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns the name of the test suite.

	\return (string) name of the test suite
]]--

function WFRxTestCaseRunner:luaClassName()

	check( 'WFRxTestCaseRunner' )

	return self._luaClassName

end

--------------------------------------------------------------------------------
--[[!
	\brief Returns the retry count of the test.

	\return (number) Count of retries
]]--

function WFRxTestCaseRunner:retryCount()

	check( 'WFRxTestCaseRunner' )

	return self._retryCount

end

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

function WFRxTestCaseRunner:__tostring()

	check( 'WFRxTestCaseRunner' )

	return string.format( "WFRxTestCaseRunner( \"%s\" )", self:testName() )

end

--------------------------------------------------------------------------------
--[[!
	\brief Writes a message to the test console log.

	\param ... (any) all messages are converted to strings and printed in a
		single message to the log
]]--

function WFRxTestCaseRunner:log( ... )

	check( 'WFRxTestCaseRunner' )

	self._test:log( ... )

end

--------------------------------------------------------------------------------
--[[!
	\brief Writes a formatted message to the test console log. This is
	just a shortcut for rxTest:log( string.format( str, ... ) )

	\param ... (string) the string.format string that will be logged

	\param ... (any) the positional string.format varargs
]]--

function WFRxTestCaseRunner:logf( formatStr, ... )

	check( 'WFRxTestCaseRunner', 'string' )

	self._test:log( string.format( formatStr, ... ) )

end

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

WFRxTestCaseRunner = Rx.Class( 'WFRxTestCaseRunner', WFRxTestCaseRunner )


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

-- @mark WFRxTestCase namespace

local WFRxTestCase = {}

--------------------------------------------------------------------------------
--[[!
	\brief Wraps a single function in a coroutine which will monitor the test
		execution.

	\param testFn (function) test function. This function takes one parameter,
		typically named \c rxTest, which will be an instance of \c WFRxTestRunner.
		If this coroutine function finishes successfully (and any functions
		invoked via \c rxTest:finally() also finish successfully), the test
		passes. If any errors occur, the test fails.

	\param testName (optional string) The test name.
	
	\param testShouldFail (optional boolean) Only used in nativeTest.

	\return (function) a new function which will create an instance of
		\c WFRxTestRunner and use that instance to run \c testFn
]]--

function WFRxTestCase.wrapTest( testFn, luaClassName, testName, testShouldFail )

	check( 'function', 'string', '?string', '?boolean' )

	return function( test )
	
		local previousRunner
	
		local function runTest( testClass, retries )
		
			local runner = WFRxTestCaseRunner( testClass, testFn, luaClassName, testName, retries or 0 )
			
			if previousRunner then
			
				-- SPAM [jandrew 2021-11-03]: In the event of a retry, the test will immediately
				-- sleep for a little bit when `run` is executed. However, the sleep will occur
				-- before the test code runs that sets the time limit and retry delay. To fix this,
				-- we just grab the values from the first run of the test.
			
				runner._timeLimit = previousRunner._timeLimit
				runner._delayInBetweenRetryAttempts = previousRunner._delayInBetweenRetryAttempts
				
			end
			
			previousRunner = runner
			
			run( runner )
			
		end
		
		local MAX_RETRIES = 1
		
		local testName = test:testName()
			
		local wrappedTest = {
			retries = 1, 
			
			log = function( self, ... )
				return test:log( ... )
			end,
			
			waitForTestToFinish = function( self, ... )
				return test:waitForTestToFinish( ... )
			end,
			
			waitForTimeout = function( self, ... )
				return test:waitForTimeout( ... )
			end,
			
			notifyTestDidSucceed = function( self, ... )

				retryLimitByTestName[ testName ] = nil
			
				return test:notifyTestDidSucceed()
				
			end,
			
			notifyTestDidFail = function( self, ... )
			
				if retryLimitByTestName[ testName ] ~= nil then
					MAX_RETRIES = retryLimitByTestName[ testName ]
				end
				
				retryLimitByTestName[ testName ] = nil
				
				if testShouldFail or self.retries >= MAX_RETRIES then
					return test:notifyTestDidFail( ... )
				else
					self.retries = self.retries + 1
					runTest( self, self.retries )
				end

			end,
			
			expectFailure = function( self, ... )
				return test:expectFailure( ... )
			end,
			
			testName = function( self, ... )
				return test:testName( ... )
			end,
			
			luaClassName = function( self, ... )
				return test:luaClassName( ... )
			end,
			
			retryCount = function( self, ... )
				return test:retryCount( ... )
			end
			
		}
		
		runTest( wrappedTest )
		
	end

end

--------------------------------------------------------------------------------
--[[!
	\brief Wraps a table of test functions in coroutines which will monitor the
		execution of each test.

	\param testSuite (table) test functions. Any entries with the prefix \c rxTest
		will be renamed to \c test(suffix) and will be wrapped as per \c wrapTest
		above. All other entries will be passed through unmodified.

	\return (table) a table of test functions suitable for use with WF's native
		test infrastructure
]]--

function WFRxTestCase.wrapSuite( luaClassName, testSuite )

	check( '+string', 'table' )

	local resultSuite = {}

	for k, fn in pairs( testSuite ) do

		if type( k ) == 'string' and k:match( '^rxTest' ) then

			k = k:gsub( '^rxTest', 'test' )
			fn = WFRxTestCase.wrapTest( fn, luaClassName, k )

		end

		resultSuite[ k ] = fn
		

	end

	return resultSuite

end

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

return WFRxTestCase
