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

WFAsyncTestCase.lua

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

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

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

DEPRECATION NOTICE: This class should only be used when testing code that relies
on the classic reactive system (KSPromises and KSReactive), which is also deprecated.

Please use WFRxTestCase whenever possible for new tests.

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

	-- RxLua
local Rx = require 'Rx'

	-- WF.core.lua.threading
local WFIdle = require 'WFIdle'
local WFTimer = require 'WFTimer'

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

	-- WF.core.reactive
local KSPromises = require 'KSPromises'
local KSReactive = require 'KSReactive'

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

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


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

local check = assert( WFArguments.check )
local dpcall = assert( KSLuaUtils.dpcall )

local WFAsyncTestCase = 'WFAsyncTestCase'


--============================================================================--
--[[!
	\class WFAsyncTestCase

	\brief This namespace allows you to test code that makes
		asynchronous calls through event streams, behaviors, and promises.

	Create a \c WFAsyncTestCase object and attach a number of conditions
	that the test must pass in order to be marked as successful.

	Specify a condition in a <code>:waitFor( function )</code> block.
	The conditions are tested in the sequence in which you attach them;
	this roughly approximates the code you would write if you were using
	thread-blocking code.

	For example:

\code
function KSReactivePromiseBridgeTest.testPromiseToBehaviorWithRequest( test )

	WFAsyncTestCase( test )

		local requestTestHandler = WFWorkerThread( 'WFRequestTestHandler.lua' )
	
		:waitFor( function()

			local p = requestTestHandler:requestP( 'echo', "The data value" )
			
			return KSReactivePromiseBridge.promiseToBehavior( p, "Not Yet!" )
		
		end )
		:continueWhenValueMatches( 1, "The data value" )  -- condition

		requestTestHandler:close()

end
\endcode

	You can attach any number of \c :waitFor blocks to the test.
	Each block can return a timeout value (a hard number of seconds to wait),
	and an event stream, behavior, or promise.
	
	After each \c :waitFor block, you can specify a condition that
	must be satisfied in order for that step to pass. The typical case is
	<code>:continueWhenValueMatches</code>, which tests the reactive node's
	value against the expected values before passing the step.
	
	You can also use <code>:whenResolvedDo</code> (for promises) if you can't
	provide an exact expected value set.
	
	If no validation steps are applied, the step passes if the event
	stream, behavior, or promise returns \b any non-error value.

	If all of the <code>:waitFor</code> steps  are completed without
	an error being raised or the timeout being reached, then the test is marked
	as successful.
--]]

-- @mark WFAsyncTestCase class

local reactiveTestCaseMethods = {

	_test = false,
		-- (KSLuaTestCase native object)
	
	_steps = false,
		-- (table) array of step descriptions
	
	_startTime = false,
		-- (number) timestamp when test was created
	
	_verbose = false,
		-- (boolean) true to generate extra log output
	
	_currentStepIndex = false,
		-- (number) index into _steps array; which step are we now waiting for?
	
	_failureMessage = false,
		-- (string) if present, this is the failure message for this test
	
	_currentReactiveNode = false,
		-- (event stream or behavior) the node we're currently waiting on
	
	_promiseActionHolder = false,
		-- (table) used to hold :whenResolvedDo actions used by calling code
		-- alive for the life of this test case
	
	_baselinePromiseLimboCount = false,
		-- (number) baseline result for KSPromises.DEBUG_getLimboCount().
		-- We use this to make a test fail if the count increases.
	
	_matchValueActionHolder = false,
		-- (table) used to hold :whenResolvedDo actions used by this code
		-- alive for the life of this test case

	_beforeRunning = false,
		-- (function) used to hold a function to be called just before
		-- this async test cases

	_reactiveNodes = false,
		-- (table) set of reactive nodes (behaviors and event streams) that
		-- we've added to this test case
		
	WFCloseSupport.addCloseSupportMethods
	
}

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

local continueTest -- forward declaration
local sCurrentTest -- prevent the current async testcase from being GC'd

--------------------------------------------------------------------------------
--[[!
	\brief Constructs an async test-case object.

	\param test (WFTestCase) A generic test-case object.
	\param waitTime (optional, number) A timeout for the test in seconds.
		Default is 3 seconds ( 9 seconds for Android )
		
	\return The new async test-case object.
]]--

local platformTimeoutModifier = 1

function reactiveTestCaseMethods:__init( --[[WFTestCase]] test,
					 --[[optional number]] waitTime )
	
	check( WFAsyncTestCase, '_', '?number' )
		-- FIXME [scouten 2012-08-05]: We don't have a checker for native
		-- type WFTestCase.
	
	sCurrentTest = self

	self._test = assert( test )
	
	self._startTime = KSDate.timeNowForAnimation()
	
	self._steps = {}
	self._currentStepIndex = 0
	
	self._promiseActionHolder = {}
	self._matchValueActionHolder = {}
	
	self._reactiveNodes = {}
	
	self._baselinePromiseLimboCount = KSPromises.DEBUG_getLimboCount()
	
	local defaultWaitTime = 3
	
	if WFPlatformInfo.isAndroid then
		
		-- PERFORMANCE_ANDROID
		-- Android takes longer to run most tests, so we account for that here,
		-- as opposed to at the test-level.
		platformTimeoutModifier = 3
		
	end
	
	if waitTime then
		waitTime = waitTime * platformTimeoutModifier
	else
		defaultWaitTime = defaultWaitTime * platformTimeoutModifier
	end
	
	test:waitForTestToFinish( waitTime or defaultWaitTime )
	
	WFIdle.onIdleDo( function()
		
		continueTest( self )
	
	end )

end

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

local function debugPrintValue( test, verbose, ... )

	local values = { ... }

	for i = 1, select( '#', ... ) do

		local value = values[ i ]
		
		if type( value ) == 'table' then
			values[ i ] = WFTableArchiving.tableToString( value )
		else
			values[ i ] = tostring( values[ i ] )
		end
	
	end
	
	if verbose then
		debug.print( unpack( values, 1, select( '#', ... ) ) )
	end
	
	local joinedValues = table.concat( values, " " ) .. "\n"
	
	test:log( joinedValues )

	return joinedValues

end

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

local testMetrics = nil
local startTimes = {}
local executionTimes = {}
local WFTestMetrics = require 'WFTestMetrics'

local timeNow = KSDate.timeInIso8601FormatNearestSecond

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

local function startClock( test )
	startTimes[ test:testName() ] = KSDate.timeNowForAnimation()
	-- Generating test metrics, to be used by dashboard
	testMetrics = WFTestMetrics.populateTestMetrics( test )
end
	
--------------------------------------------------------------------------------
	
local function stopClock( test )
	local testName = test:testName()
	if startTimes[ testName ] then
		executionTimes[ testName ] = ( KSDate.timeNowForAnimation() - startTimes[ testName ] )
		testMetrics[ "wfp_execution_time" ] = executionTimes[ testName ]
	end
	WFTestMetrics.writeTestMetrics( testMetrics )
	startTimes = {}
end

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

continueTest = function( self, ... )
	
	local test = self._test
	
	if startTimes[ test:testName() ]  then 
		stopClock( test )
	end 

	if self._beforeRunning then
		self:_beforeRunning() -- one time only
		self._beforeRunning = false
	end

	collectgarbage()
	collectgarbage()

	local verbose = self._verbose

	self._currentReactiveNode = false

	-- If we've logged a failure message, short-circuit out now.

	if self._failureMessage then
		test:log( self._failureMessage )
		test:notifyTestDidFail( self._failureMessage )
		self:close()
		sCurrentTest = nil
		return
	end
	
	-- Make sure no new promises or promise actions got left in limbo.
	-- What would be really nice is if this detected promise limbo happening
	-- on other threads, but that doesn't work yet. :-(
	
	if KSPromises.DEBUG_getLimboCount() > self._baselinePromiseLimboCount then
		self._failureMessage = "One or more promises or promise actions were left in limbo: Failing test"
		test:log( self._failureMessage )
		test:notifyTestDidFail( self._failureMessage )
		self:close()
		sCurrentTest = nil
		return
	end
	
	-- What's the next step?

	self._currentStepIndex = self._currentStepIndex + 1

	local currentStep = self._steps[ self._currentStepIndex ]
	debugPrintValue( test, verbose,
					 string.rep( '-', 52 ),
					 timeNow(),
					 " -----\n\nTEST STEP",
					 self._currentStepIndex,
					 "(", currentStep and currentStep.whereDefined or 'END OF TEST', ")",
					 "\nelapsed time = ",
					 string.format( "%.2f", KSDate.timeNowForAnimation() - self._startTime ) )
	
	if not currentStep then
		debugPrintValue( test, verbose, "Test succeeded" )
		self._test:notifyTestDidSucceed()
		self:close()
		sCurrentTest = nil
		return
	end
	
	local success, messageOrNode, wasGeneratedByFunc
	
	if type( currentStep.eventOrGenFunc ) == 'function' then
		
		if currentStep.isWaitForPerf then 
			startClock( test )
		end 
		success, messageOrNode = dpcall( string.format( 'step %d (defined at %s)',
														self._currentStepIndex,
														currentStep.whereDefined ),
										 currentStep.eventOrGenFunc, ... )
		
		wasGeneratedByFunc = true

	else

		success, messageOrNode = true, currentStep.eventOrGenFunc

	end

	if success and not messageOrNode then
		success = false
		messageOrNode = ":waitFor at this step returned nil"
		debug.print( ":waitFor at this step returned nil" )
	end

	if success then
		
		-- Event gen function didn't fail out, configure the node
		-- so we can continue after this.
		
		local reactiveNode = messageOrNode
		
		if type( reactiveNode ) == 'number' then
		
			local timeout = reactiveNode
			reactiveNode = WFTimer.afterDelayP( timeout )
			
		end
		
		if reactiveNode and WFCloseSupport.hasBeenOrWillBeClosed( reactiveNode ) then
			error( "Node has already been closed." )
		end
		
		self._currentReactiveNode = reactiveNode or false
		
		local isReactiveNode, isBehavior = KSReactive.isEventNode( reactiveNode )
		local isPromise = ( not isReactiveNode )
						  and KSPromises.isPromise( reactiveNode )

		-- Make sure we close any behaviors that get passed to us when the
		-- test itself complete. We have separate logic for closing event streams
		-- and promises. Only do this if the behavior is generated dynamically.
		-- Otherwise, the :setOwned call in our :waitFor method will do it.

		if isBehavior
			and not self._reactiveNodes[ reactiveNode ]
			and wasGeneratedByFunc
		then
			self._reactiveNodes[ reactiveNode ] = true
			self:addCloseTarget( reactiveNode )
		end

		-- FIXME [scouten 2012-08-14]: Q&D hack: Convert things that are neither
		-- reactive nor promise to a resolved promise. Formalize and document this.

		if not isReactiveNode and not isPromise then
			isPromise = true
			reactiveNode = KSPromises.resolvedPromiseP( 'data', reactiveNode )
			self._currentReactiveNode = reactiveNode
			messageOrNode = reactiveNode
		end

		-- FIXME [scouten 2011-03-30]: Add some options to expect
		-- failure, etc., here.
	
		if currentStep.onDataE then

			assert( isReactiveNode )
			reactiveNode:ownedOnDataE( self, currentStep.onDataE )
		
		end

		local wrdFinishedP = reactiveNode
		local resolver
		
		-- Sometimes we have multiple functions that need to respond
		-- to the whenResolvedDo condition.  However, Promises make
		-- no guarantees about what order multiple functions attached
		-- in this way will be called.  So we will attach only a single
		-- function in this manner, and then run all of our funcs
		-- in sequence in the order in which they were attached
		
		-- What's more, we run the risk that we are waiting for a promise that
		-- has already resolved in which case the action will execute immediately
		-- and future additions to the list of functions to execute will never
		-- execute. So, we need to reset the list each time we process it.
		
		-- Worse, we really weren't focused on running the functions at this point
		-- in the process. So, if the node is already resolved then we need to actually
		-- attach to a version that fires later. We do this by always deferring the
		-- result from the reactive node.
				
		local whenResolvedDoFuncs = nil
		local function addWhenResolvedDoFn( actionFunc )
			if not whenResolvedDoFuncs then
				whenResolvedDoFuncs = { actionFunc }
				WFIdle.asapP():mapP( reactiveNode )
					:whenResolvedDo( function( ... )
						local existingFuncs = whenResolvedDoFuncs
						whenResolvedDoFuncs = nil
						for _, f in ipairs( existingFuncs ) do
							f( ... )
						end
					end )
					:attachTo( nil, self._promiseActionHolder, self._currentStepIndex )
			else
				whenResolvedDoFuncs[ #whenResolvedDoFuncs + 1 ] = actionFunc
			end
		end

		local function foundMismatch( message )
			
			self._test:log( message )

			-- Event streams and promises must match the expected
			-- value first time around. Behaviors can evolve to the
			-- expected value, provided they do it within the alloted
			-- time. Fail test immediately if not a behavior.

			if not isBehavior then

				self._failureMessage = message
				if isReactiveNode then
					reactiveNode:close()
				end

				self._test:notifyTestDidFail( self._failureMessage )
				self:close()
				
			end
		
		end

		if currentStep.whenResolvedDo then
		
			assert( isPromise, "whenResolvedDo can only operate on Promise nodes" )
			
			local actionFunc = currentStep.whenResolvedDo
			
			-- Ensure that next step is always executed after the
			-- whenResolvedDo function from this step.
			
			if currentStep.expectedValue then
			
				reactiveNode:runUntilDone()
					-- FIXME [scouten 2011-10-14]: Ugh. Hacky way to
					-- silence the promise limbo warnings.
			
				wrdFinishedP, resolver = KSPromises.promiseP()
				
				actionFunc = function( ... )
					currentStep.whenResolvedDo( ... )
					resolver( ... )
				end
				
				wrdFinishedP:runUntilDone()
					-- FIXME [scouten 2011-10-14]: Ugh. Hacky way to
					-- silence the promise limbo warnings.
			
			end
			
			addWhenResolvedDoFn( actionFunc )
		
		end

		local matchCount = 0

		if currentStep.expectedValue then

			local expectedValue = currentStep.expectedValue
			local continueWhenCount = currentStep.continueWhenCount
			
			if expectedValue.evaluate then
				local function packn( ... )
					return { n = select( '#', ... ), ... }
				end
				expectedValue = packn( expectedValue.evaluate() )
			end

			-- Guard against repeated calls to this onDataE
			-- function after we've satisfied this criteria.

			local hasMatched = false

			local currentStepForMatch = self._currentStepIndex

			local function matchValue( ... )
			
				if currentStepForMatch ~= self._currentStepIndex then
					return
				end
			
				debugPrintValue( test, verbose,
								 "Attempting to match output for test step ",
								 self._currentStepIndex )

				local actualValuesStr = debugPrintValue( test, verbose, "actual", ... )
				local expectedValuesStr = debugPrintValue( test, verbose, "expected",
								 unpack( expectedValue, 1,
								 expectedValue.n ) )

				if hasMatched then return end

				-- Note that we expect that some behaviors may
				-- have other values before they get to the exact
				-- desired value, so we simply bail out (albeit
				-- with a log) if there's a mismatch.
				
				local count = select( '#', ... )
				
				-- We can handle ignores via nil extension
				
				local checkCount = count
				
				while checkCount < expectedValue.n do
					if expectedValue[ checkCount + 1 ] == ':ignore:' then
						checkCount = checkCount + 1
					else
						break
					end
				end

				if checkCount ~= expectedValue.n then

					local moreInfo = {}
					for i = 1, count do
						moreInfo[ i ] = tostring( select( i, ... ) )
					end
					moreInfo = table.concat( moreInfo, ", " )
					return foundMismatch( "Wrong number of results: Expected "
						.. tostring( expectedValue.n ) .. " got "
						.. tostring( count ) .. " : "
						.. moreInfo)

				end
				
				for i = 1, count do
			
					local paramI = select( i, ... )
					
					if expectedValue[ i ] ~= ':ignore:' then
					
						local mismatch = expectedValue[ i ] ~= paramI
						
						if mismatch
							and type( expectedValue[ i ] ) == 'table'
							and type( paramI ) == 'table'
							and KSTableUtils.deepEquals( expectedValue[ i ],
														 paramI, 20, verbose )
						then
							mismatch = false
						end
						
						if mismatch
							and type( paramI ) == 'string'
							and expectedValue[ i ] == ':*:'
						then
							mismatch = false
						end
				
						if mismatch then
						
							local mismatchInfo = "Result mismatch at position "
												.. tostring( i ) .. ": "
												.. actualValuesStr
												.. expectedValuesStr

							return foundMismatch( mismatchInfo )
							
						end
					
					end

				end

				-- Parameters are a complete match. Test can now proceed.
				
				hasMatched = true
				matchCount = matchCount + 1
				
				if isReactiveNode and matchCount == continueWhenCount then
					continueTest( self, ... )
				end
			
			end
			
			if isReactiveNode then

				-- It's possible we already have this value, in which case
				-- we just proceed right away. (Only applies to behaviors.)
				
				if isBehavior then
					if currentStep.expectError then
						if reactiveNode:isErrorNow() then
							matchValue( "Error triggered immediately, can't capture value" )
						end
					else
						matchValue( reactiveNode:valueNow() )
					end
				end
				
				if not hasMatched then
					if currentStep.expectError then
						reactiveNode:ownedOnErrorE( self, matchValue )
					else
						reactiveNode:ownedOnDataE( self, matchValue )
					end
				end
			
			elseif isPromise then
			
				addWhenResolvedDoFn( matchValue )

			end
		
		end
		
		if currentStep.expectedModelFormat then

			local expectedModelFormat = currentStep.expectedModelFormat
			local continueWhenCount = currentStep.continueWhenCount
			
			local currentStepForMatch = self._currentStepIndex
			
			local hasMatched = false
			
			-- Generates a table of keys and data types
			local function generateModelFormat( tbl )
			
				local modelFormat = {}
				
				for k, v in pairs( tbl ) do
  					modelFormat[ k ] = type( v )
				end
				
				-- If optional fields are not present in the model data,
				-- go ahead and add them since it makes the comparison
				-- logic very easy. While we're doing this, we also modify
				-- the expectedModelFormat data structure to remove informatino
				-- about optional fields.
				for k, v in pairs( expectedModelFormat ) do
				
					if type( v ) == "table" then
					
						if not modelFormat[ k ] and v.optional then
							modelFormat[ k ] = v.type
						end
						
						expectedModelFormat[ k ] = v.type
						
					end
					
				end
				
				return modelFormat
					
			end

			local function matchFormat( ... )
			
				if currentStepForMatch ~= self._currentStepIndex or hasMatched then
					return
				end
			
				debugPrintValue( test, verbose,
								 "Attempting to match model format for test step ",
								 self._currentStepIndex )

				local actualModelFormat = generateModelFormat( ... )

				local actualStr = debugPrintValue( test, verbose, "actual",
								actualModelFormat )
				
				local expectedStr = debugPrintValue( test, verbose, "expected",
								expectedModelFormat )

				-- Check the size
				
				if #expectedModelFormat ~= #actualModelFormat then

					return foundMismatch( "Wrong number of fields: Expected "
						.. tostring( #expectedModelFormat) .. " got "
						.. tostring( #actualModelFormat ) )

				end
				
				if not KSTableUtils.deepEquals( expectedModelFormat, actualModelFormat, 20, verbose ) then
				
					local mismatchInfo = "Result mismatch: \n\n"
												.. actualStr .. "\n"
												.. expectedStr

					return foundMismatch( mismatchInfo )
					
				end
				
				hasMatched = true
				matchCount = matchCount + 1
				
				if isReactiveNode and matchCount == continueWhenCount then
					continueTest( self, ... )
				end
			
			end
			
			if isReactiveNode and isBehavior then
			
				matchFormat( reactiveNode:valueNow() )
				
				if not hasMatched then
					reactiveNode:ownedOnDataE( self, matchFormat )
				end
				
			end
			
		end
		
		if currentStep.expectedFieldValues then

			local expectedFieldValues = currentStep.expectedFieldValues
			local continueWhenCount = currentStep.continueWhenCount
			
			local currentStepForMatch = self._currentStepIndex
			
			local hasMatched = false
			
			local function matchFields( ... )
			
				if currentStepForMatch ~= self._currentStepIndex or hasMatched then
					return
				end
			
				local data = ...
			
				if data and type( data ) ~= 'table' then
					local mismatchInfo = "Expected a table, got a " .. type( data )
					return foundMismatch( mismatchInfo )
				end
			
				local newMatchCount = matchCount
			
				for expectedField, expectedValueOrFuncAndModifierFunc in pairs( expectedFieldValues ) do
				
					debugPrintValue( test, verbose,
									 "Attempting to match field value for test step ",
									 self._currentStepIndex )
	
					local function noopModifierFunc( ... )
						return ...
					end
	
					local expectedValueOrFunc = expectedValueOrFuncAndModifierFunc[ 1 ]
					local modifierFunc = expectedValueOrFuncAndModifierFunc[ 2 ] or noopModifierFunc
	
					local actualValue
					
					-- Make sure field is present
					if not data or not data[ expectedField ] then
						actualValue = nil
					else
						actualValue = modifierFunc( data[ expectedField ] )
					end

					local expectedValue
					
					-- If the expected value is a function, get the result
					if type( expectedValueOrFunc ) == 'function' then
						expectedValue = modifierFunc( expectedValueOrFunc() )
					else
						expectedValue = modifierFunc( expectedValueOrFunc )
					end
						
					local actualStr = debugPrintValue( test, verbose, "Field: " .. expectedField .. " Actual:",
									actualValue )
					
					local expectedStr = debugPrintValue( test, verbose, "Field: " .. expectedField .. " Expected:",
									expectedValue )
					
					if actualValue ~= expectedValue then
					
						local mismatchInfo = "Result mismatch: \n\n"
													.. actualStr .. "\n"
													.. expectedStr
	
						return foundMismatch( mismatchInfo )
						
					end
			
					newMatchCount = newMatchCount + 1
			
				end
			
				matchCount = newMatchCount
				hasMatched = true
				
				if isReactiveNode and matchCount == continueWhenCount then
					continueTest( self, ... )
				end
				
			end
			
			if isReactiveNode and isBehavior then
				
				matchFields( reactiveNode:valueNow() )
					
				if not hasMatched then
					reactiveNode:ownedOnDataE( self, matchFields )
				end
					
			end
			
		end
		
		if isReactiveNode and not currentStep.expectError
			and not WFCloseSupport.hasBeenOrWillBeClosed( self )
		then

			local currentStepIndex = self._currentStepIndex

			reactiveNode:ownedOnErrorE( self, function( message )

				if currentStepIndex == self._currentStepIndex then
			
					local annotatedMessage = string.format(
						"reactive node from step %d failed with message %s",
						self._currentStepIndex, message )
				
					debugPrintValue( test, verbose, annotatedMessage )
					self._failureMessage = annotatedMessage
					
					reactiveNode:close()
				
				end
			
			end )
		
		end
		
		if isReactiveNode and currentStep.onDoneE == nil
			and not currentStep.expectedValue
			and not currentStep.expectedFieldValues
			and not currentStep.expectedModelFormat
			and not WFCloseSupport.hasBeenOrWillBeClosed( self )
		then
		
			reactiveNode:ownedOnDoneE( self, function()
			
				continueTest( self )
			
			end )
		
		end
		
		if isPromise then
		
			addWhenResolvedDoFn( function( status, message, ... )

				if not currentStep.expectedValue and not currentStep.whenResolvedDo then
				
					if status ~= 'data' then
						self._failureMessage =
							string.format( "promise node failed with status = %s and message = %s",
											tostring( status ), tostring( message ) )
					end
					
				end
				
				continueTest( self, status, message, ... )

			end )

		end
	
	else
	
		local annotatedMessage = string.format(
			"step %d failed to generate new node with message %s",
			self._currentStepIndex, messageOrNode or "(return value was nil)" )
	
		debugPrintValue( test, verbose, annotatedMessage )
		
		self._test:log( annotatedMessage )
		self._test:notifyTestDidFail( annotatedMessage )
		self:close()
	
	end

	-- Throw in extra GC to ensure that we don't succeed by chance if things
	-- are weakly held.

	collectgarbage()
	collectgarbage()

end

--------------------------------------------------------------------------------
--[[!
	\brief Common code for waitFor and waitForPrep
	
	\param step An event stream, behavior, promise, or number; or a
		function that returns one of those types.
	\param self object 
	
	\return whereDefined line number for test step
--]]

local function prepWaitFor( step, self )
	
	local whereDefined
	local thread = coroutine.running()
	local level = 3
	
	while true do

		local debugInfo = debug and debug.getinfo and debug.getinfo( thread, level )
		-- 1 : This stack frame
		-- 2 : Caller
		if not debugInfo then
			break
		end
		
		-- Enable testing helper classes; so long as they are implemented in files
		-- named something + "TestCase" we won't use those file names and line numbers.
		
		if debugInfo and debugInfo.source and debugInfo.currentline and debugInfo.currentline >= 0 and not string.find( debugInfo.source, "TestCase" ) then
			whereDefined = string.format( '%s:%s', debugInfo.source, debugInfo.currentline )
			break
		end
		
		level = level + 1

	end
	
	if KSReactive.isEventNode( step ) then
	
		step:setOwnedBy( self )
		
	else
		-- any Promise value that's passed to a waitFor step ought
		-- to just be kept alive by the test, so we'll test for that
		-- here  -- arahn.
		local isPromise = ( step )
						  and KSPromises.isPromise( step )
		if isPromise then
			step:runUntilDone()
		end
	end

	return whereDefined
	
end

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

--[[!
	\brief Adds a step to this test case.
	
	\param step An event stream, behavior, promise, or number; or a
		function that returns one of those types.
	
	If the ultimate result is an event stream or behavior, the default behavior
	is to wait until one non-error value is generated. If the ultimate result is
	a promise, the default behavior is to wait for that promise to resolve
	successfully. In either case, if an error result is generated, the default
	behavior is to fail the test.
	
	You can override these defaults with <code>:continueWhenValueMatches</code>
	or another method to evaluate the results of this node.
	
	If the ultimate result is a number, the behavior is to wait for that number
	of seconds. (This behavior cannot be overridden.)

	\return This object (useful for chaining).
--]]

function reactiveTestCaseMethods:waitFor( --[[reactive node or number]] step )

	check( WFAsyncTestCase, '_' )
	local steps = self._steps
	local whereDefined = prepWaitFor( step, self )
	steps[ #steps + 1 ] = {
		eventOrGenFunc = assert( step, "waitFor was given no step" ),
		whereDefined = whereDefined or 'source unknown',
	}
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Adds a step to this test case.
	
	\param model An opened model object

	\return This object (useful for chaining).
--]]

function reactiveTestCaseMethods:waitForModel( --[[model]] model )

	return self:waitFor( model:valueB() )

end

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

--[[!
	\brief Adds a step to this test case. This is useful if you want to 
	measure the execution time for this test step and write this into a 
	file on disk along with other test metrics.
	
	\param step An event stream, behavior, promise, or number; or a
		function that returns one of those types.
	
	If the ultimate result is an event stream or behavior, the default behavior
	is to wait until one non-error value is generated. If the ultimate result is
	a promise, the default behavior is to wait for that promise to resolve
	successfully. In either case, if an error result is generated, the default
	behavior is to fail the test.
	
	You can override these defaults with <code>:continueWhenValueMatches</code>
	or another method to evaluate the results of this node.
	
	If the ultimate result is a number, the behavior is to wait for that number
	of seconds. (This behavior cannot be overridden.)

	\return This object (useful for chaining).
--]]

function reactiveTestCaseMethods:waitForPerf( --[[reactive node or number]] step )

	check( WFAsyncTestCase, '_' )
	
	local steps = self._steps
	local whereDefined = prepWaitFor( step, self )
	
	steps[ #steps + 1 ] = {
		eventOrGenFunc = assert( step, "waitFor was given no step" ),
		whereDefined = whereDefined or 'source unknown',
		isWaitForPerf = 1,
	}
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Attaches a function to the promise from the most recent
		<code>:waitFor</code> step via its <code>:whenResolvedDo</code> hook.
	
	\param resolvedFunc (function) A function that can evaluate the promise's
		results. It can cause the test to fail by raising an error.
	
	\return This object (useful for chaining).
]]--

function reactiveTestCaseMethods:whenResolvedDo( --[[function]] resolvedFunc )

	check( WFAsyncTestCase, 'function' )

	local steps = self._steps
	assert( #steps > 0 )
	
	steps[ #steps ].whenResolvedDo = function( ... )
	
		local success, message = pcall( resolvedFunc, ... )
		
		if not success then
			self._failureMessage = message
			debug.print( message )
			self._test:log( message )
		end
	
	end
	
	return self

end

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

local function addToContinueWhenCount( steps )

	if not steps[ #steps ].continueWhenCount then
		steps[ #steps ].continueWhenCount = 0
	end
	
	steps[ #steps ].continueWhenCount = steps[ #steps ].continueWhenCount + 1
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Provides expected values for the event stream, behavior, or promise
		from the most recent <code>:waitFor</code> step.
	
	\param ... The expected value or values.
		<ul><li>For an event stream, the very next <code>:onDataE</code>
		call \b must match this value or the test fails. </li>
		
		<li>For a behavior, other values can be permitted but the behavior
		must eventually reach the expected value or the test fails. </li>
		
		<li>For a promise, the status (\c 'data' or \c 'error') is the first parameter.
		The status and all returned values must match or the test fails.
		
		You can use the string \c ':ignore:' to skip matching any given parameter.
		For example:
<code> :continueWhenValueMatches( 'data', ':ignore:', ':ignore:', catalogId, true )</code></li>
		</ul>
	
	\return This object (useful for chaining).

--]]

function reactiveTestCaseMethods:continueWhenValueMatches( ... )

	local steps = self._steps
	assert( #steps > 0 )
	
	steps[ #steps ].expectedValue = { n = select( '#', ... ), ... }
	addToContinueWhenCount( steps )
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Provides the expected value or values for the event stream,
		behavior, or promise from the most recent <code>:waitFor</code> step.
	
	\param fn A function that returns the value or values to match,
		to be executed immediately before evaluating the match.
	
		<ul><li>For an event stream, the very next <code>:onDataE</code>
		call \b must match this value or the test fails. </li>
		
		<li>For a behavior, other values can be permitted but the behavior
		must eventually reach the expected value or the test fails. </li>
		
		<li>For a promise, the status (\c 'data' or \c 'error') is the first parameter.
		The status and all returned values must match or the test fails.
		
		You can use the string \c ':ignore:' to skip matching any given parameter.</li>
		</ul>

	\return This object (useful for chaining).

	\sa reactiveTestCaseMethods:continueWhenValueMatches

--]]

function reactiveTestCaseMethods:continueWhenValueMatchesFunction( --[[function]] fn )

	local steps = self._steps
	assert( #steps > 0 )
	
	steps[ #steps ].expectedValue = { evaluate = fn }
	addToContinueWhenCount( steps )
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Provides expected \b error values for the event stream or behavior
		from the most recent <code>:waitFor</code> step.
		
		Use to verify that the API under test returns an error
		condition when expected. The event stream or behavior
		must call <code>:onErrorE</code> with the expected value or values,
		or the test fails.
	
	\param ... The expected error values.
		<ul><li>For an event stream, the very next <code>:onErrorE</code>
		call \b must match this value or the test fails. </li>
		
		<li>For a behavior, other values can be permitted but the behavior
		must eventually reach the expected value or the test fails. </li>
		
		<li>For a promise, the status (\c 'data' or \c 'error') is the first parameter.
		The status and all returned values must match or the test fails.
		
		You can use the string \c ':ignore:' to skip matching any given parameter.</li>
		</ul>
 	
		\return This object (useful for chaining).
	
--]]

function reactiveTestCaseMethods:continueWhenValueMatchesError( ... )

	local steps = self._steps
	assert( #steps > 0 )
	
	steps[ #steps ].expectedValue = { n = select( '#', ... ), ... }
	steps[ #steps ].expectError = true
	addToContinueWhenCount( steps )
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Provides expected values for a specified field of the data from a
		behavior from the most recent <code>:waitForModel</code> step.
	
	\param field The expected field of a table or object
	\param valueOrFunc The expected value for the specified field, or a function that returns the
		expected value.
	\param modifierFunc An optional function that can be specified and used to modify the values
		before they're compared. An example function would be string.lower for doing case insensitive
		comparisons.
	
	\return This object (useful for chaining).

--]]

function reactiveTestCaseMethods:continueWhenFieldMatches( field, valueOrFunc, modifierFunc )

	local steps = self._steps
	assert( #steps > 0 )
	
	if not steps[ #steps ].expectedFieldValues then
		steps[ #steps ].expectedFieldValues = {}
	end
	
	steps[ #steps ].expectedFieldValues[ field ] = { valueOrFunc, modifierFunc }
	addToContinueWhenCount( steps )
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Provides the expected model format for the behavior from the most
		recent <code>:waitForModel</code> step. The model format is simply
		the field names and expected data types of the model.
	
	\param expectedModelFormat The expected field of a table or object

	\c expectedModelFormat should look something like the following:
		{
			field1 = "number"
			field2 = "string"
			field3 = "boolean"
			optionalField4 = { type = "string", optional = true }
		}
	
	\c Note that optional fields are wrapped in tables.
	
	\return This object (useful for chaining).

--]]

function reactiveTestCaseMethods:continueWhenModelFormatMatches( expectedModelFormat )

	local steps = self._steps
	assert( #steps > 0 )
	
	steps[ #steps ].expectedModelFormat = expectedModelFormat
	addToContinueWhenCount( steps )
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Sets a callback to be invoked just before running this test.
	
	Use this to append additional common steps so they don't have to be duplicated.
	
	\return This object (useful for chaining).
--]]

function reactiveTestCaseMethods:beforeRunning( fn )

	check( WFAsyncTestCase, 'function' )

	self._beforeRunning = fn
	
	return self

end

--------------------------------------------------------------------------------
--[[!
	\brief Generates extra debug output to the console log while running the test.
	
	Use while debugging your unit test, but remove before checking in.
	
	\return This object (useful for chaining).
--]]

function reactiveTestCaseMethods:verbose()

	self._verbose = true
	
	return self

end

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

return Rx.Class 'WFAsyncTestCase'( reactiveTestCaseMethods )
