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

PixelProviderRenderTaskTest.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.

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

	-- If ACR is not supported, disable test before we attempt to
	-- require WFAcr. (That will fail.)
if not require 'WFPlatformInfo'.hasACR then return {} end

	-- wf.core
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFFileUtils = require 'WFFileUtils'
local WFFiniteStateMachine = require 'WFFiniteStateMachine'
local WFRxTestCase = require 'WFRxTestCase'

	-- Rx
local Rx = require 'Rx'

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

local PixelProviderRenderTask = require "PixelProviderRenderTask"
local PixelPathProviderScheduler = require "PixelPathProviderScheduler"
local PixelPathProviderRequestTypes = require "PixelPathProviderRequestTypes"
local PixelPathProviderStateMachine = require "PixelPathProviderStateMachine"
local PixelPathProviderSchedulerPrivateMethods =
		require "PixelPathProviderSchedulerPrivateMethods"

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

local PixelPathProviderRequestTypeNames = PixelPathProviderRequestTypes.names

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

local function makeSchedulerForTest( rxTest )

	local catalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )

	local scheduler = PixelPathProviderScheduler( catalog )

	rxTest:finally( function() scheduler:close() end )

	return scheduler, catalog

end

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

local function digest( i )

	return string.format( "digest%s", i )

end

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

local function renderSize( i )

	return 10 + i

end

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

local function localAssetId( i )

	return i

end

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

local function pixStreamId( i )

	return string.format( "pixStreamId%d", i )

end

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

local function justReturn( ... )

	return ...

end

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

local function getNFromTestInfoStream( testStream, n, filterFunc, 
										maybeTransformFunc )

	local transformTestData = maybeTransformFunc or justReturn

	return testStream:
			where( filterFunc ):
			map( transformTestData ):
			take( n):
			toArray()

end

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

local function stripTestMessageAndMakeTuple( _, ... )

	return { ... }

end

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

local function sortArray( arr, maybeSortFn )

	local sorted = KSTableUtils.deepCopy( arr )

	table.sort( sorted, maybeSortFn )

	return sorted

end

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

local function compareArrays( a1, a2 )

	if #a1 ~= #a2 then

		return #a1 > #a2

	end

	for i = 1, math.min( #a1, #a2 ) do

		if a1[ i ] ~= a2[ i ] then

			return a1[ i ] > a2 [i ]

		end

	end

	return false

end

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

local function sortTupleArray( tupleArray )

	return sortArray( tupleArray, compareArrays )

end

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

local function isTestMessage( msg )

	return function( testMsg, ... )

		return testMsg == msg

	end

end

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

local function makeSavePath( i )

	local fname = string.format( "PixelPathProviderStateMachineTest%d", i )

	local realFileDir = KSPathUtils.getStandardFilePath( "temp" )

	return WFFileUtils.ensureFileNameIsUnique( KSPathUtils.child( realFileDir, 
																	fname ) )

end

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

local function takeFirstN( n )

	return function( t )

		local ret = {}

		for i = 1, n do

			ret[ i ] = t[ i ]

		end

		return ret

	end

end

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

local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ),
										"import-samples" )

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

local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )

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

local _validRenderPath = KSPathUtils.child( testDataFolder1, "IMG_7763.jpg" )

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

local function rxTestRenderTask( rxTest )

	rxTest:setTimeLimit( 8 )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local needsRenderStates =
			{ WFFiniteStateMachine.makeRetryableStates(
					PixelPathProviderStateMachine.RenderRetryCount,
					PixelPathProviderStateMachine.RenderStates.NeedsRender ) }

	for i = 1, #needsRenderStates do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

	end

	for _, testWithBasicRenderInfo in ipairs( { false, true } ) do

		for _, testWithActiveRenderInfo in ipairs( { false, true } ) do

			for _, testWithOriginal in ipairs( { false, true } ) do

				for _, testWithSuitableProxy in ipairs( { false, true } ) do

					for _, testWithUsableRenderSize in ipairs( { false, true } ) do

						local expectedTestDataTuples = {}

						local paths = {}

						local renderableSize = {}

						local expectedTestMessage = "didRender"

						local expectedState = 
								PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender
						
						if not testWithBasicRenderInfo then

							expectedTestMessage = "skipRenderNoPath"

							expectedState = 
								PixelPathProviderStateMachine.DocstoreInfoStates.NeedAssetInfoForRender

						elseif not testWithActiveRenderInfo then

							expectedTestMessage = "skipRenderNoSettings"

							expectedState = 
								PixelPathProviderStateMachine.DocstoreInfoStates.NeedAssetInfoForRender

						elseif not testWithOriginal and not testWithSuitableProxy then

							expectedTestMessage = "skipRenderNoSuitablePath"

							expectedState = 
								PixelPathProviderStateMachine.DocstoreInfoStates.NeedAssetInfoForRender

						elseif not testWithUsableRenderSize then

							expectedTestMessage = "skipRenderNoRenderSize"

							expectedState = 
								PixelPathProviderStateMachine.SendDataStates.SendPermanentError

						end

						local watchPixelRenders = rxTest:observe( 
														getNFromTestInfoStream(
																scheduler._testData.testInfoStream,
																#needsRenderStates,
																isTestMessage( expectedTestMessage ),
																stripTestMessageAndMakeTuple )
															:map( sortTupleArray ) )

						for i, needRenderState in ipairs( needsRenderStates ) do

							local croppedLongSide = 4096 + i

							if testWithBasicRenderInfo then

								scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
														localAssetId( i ), 
													{ ozAssetId = string.format( "%s", localAssetId( i ) ), 
														changeCounter = i, 
														revId = digest( i ),
														digest = digest( i ), 
														croppedLongSide = croppedLongSide,
														originalPath = testWithOriginal and _validRenderPath or nil, 
														proxyPath = _validRenderPath, 
														renditions = {} } )

							end

							if testWithActiveRenderInfo then

								renderableSize[ i ] = ( ( testWithOriginal or testWithSuitableProxy )
															and 1024 or 3000 ) + i

								if expectedTestMessage ~= "skipRenderNoSuitablePath" then

									renderableSize[ i ] = testWithUsableRenderSize and
															renderableSize[ i ] or "full"

								end

								paths[ i ] = makeSavePath( i )

								rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

								scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
									pixStreamId( i ), localAssetId( i ),
									{ renditionPath = paths[ i ], 
										digest = digest( i ), 
										revId = digest( i ), 
										renderSize = renderableSize[ i ],
										croppedLongSide = testWithUsableRenderSize and croppedLongSide or nil,
										userOrientation = 1, 
										devSettings = "<xmp />" } )

							end

							SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
														needRenderState )

							expectedTestDataTuples[ #expectedTestDataTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

						end

						local testDesc = string.format( "%s ( %s, %s, %s, %s, %s )", 
															expectedTestMessage,
															testWithBasicRenderInfo,
															testWithActiveRenderInfo,
															testWithOriginal,
															testWithSuitableProxy,
															testWithUsableRenderSize )

						rxTest:log( "Scheduling a test expecting " .. testDesc )

						SchedulerTestAPI.runScheduledRequests( scheduler )

						watchPixelRenders:expectData( sortTupleArray( expectedTestDataTuples ) )

						rxTest:log( "Finishing a test expecting " .. testDesc )

						for _, state in ipairs( { 
								PixelPathProviderStateMachine.DocstoreInfoStates.NeedAssetInfoForRender,
								PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender,
								PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
								} ) do

							local expectedTuples = state == expectedState and 
														expectedTestDataTuples or {}

							rxTest:log( "Checking if '" .. state .. "' tuples are correct for expected state '" .. expectedState .. "'" )

							rxTest:expectEqual( 
								sortTupleArray( expectedTuples ),
								sortTupleArray( 
									scheduler:_mapAllRequestsWithState( state,
									takeFirstN( 2 ) ) ) )

						end


						if expectedTestMessage == "didRender" then

							-- check to make sure all the render blobs are cleaned up
							for i = 1, #needsRenderStates do

								local renderBlob =
									scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:popBlob(
										localAssetId( i ), 'rendition', renderableSize[ i ],
										digest( i ) )

								rxTest:expectEqual( true, renderBlob ~= nil )

								renderBlob:close()

								renderBlob = nil

							end

						end

						if testWithActiveRenderInfo then

							for i = 1, #needsRenderStates do

								scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:destroyCacheItem(
									pixStreamId( i ), localAssetId( i ) )

							end

						end

					end

				end

			end

		end

	end

end

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

local function makeMockWFCrContext( rxTest )

	local MockWFCrContext_renderStreams = {}

	local MockWFCrContext_close = { count = 0 }

	local function MockWFCrContextPsuedoClass( negPath )

		rxTest:logf( "setting up mock render context of path %s", negPath )

		return {

			render = 

				function( _, devSettings, orientation, longSide, quality ) 

					rxTest:logf( "setting up mock render of size %s orientation %s quality %s at path %s with settings %s", 
									longSide, orientation, quality, negPath, devSettings )

					MockWFCrContext_renderStreams[ #MockWFCrContext_renderStreams + 1 ] = Rx.Subject()

					return MockWFCrContext_renderStreams[ #MockWFCrContext_renderStreams ]
					
				end,

			close = 

				function() 
					
					MockWFCrContext_close.count = MockWFCrContext_close.count + 1
					
				end,

		}

	end

	return MockWFCrContext_renderStreams, MockWFCrContext_close,
				MockWFCrContextPsuedoClass

end

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

local function rxTestRenderStreamFail( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local expectedTestDataTuples = {}

	local needsRenderStates =
			{ WFFiniteStateMachine.makeRetryableStates(
					PixelPathProviderStateMachine.RenderRetryCount,
					PixelPathProviderStateMachine.RenderStates.NeedsRender ) }

	for i = 1, #needsRenderStates do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		for _ = i, #needsRenderStates do

			expectedTestDataTuples[ #expectedTestDataTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

		end

	end

	local MockWFCrContext_renderStreams, MockWFCrContext_close,
				MockWFCrContext = makeMockWFCrContext( rxTest )

	scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext =
		MockWFCrContext

	local expectedErrorStateTuples = {}

	local paths = {}

	local expectedTestMessage = "renderFail"
	
	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											#expectedTestDataTuples,
											isTestMessage( expectedTestMessage ),
											stripTestMessageAndMakeTuple )
										:map( sortTupleArray ) )

	for i, needRenderState in ipairs( needsRenderStates ) do

		local croppedLongSide = 200 + i

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
								localAssetId( i ), 
							{ ozAssetId = string.format( "%s", localAssetId( i ) ), 
								changeCounter = i, 
								revId = digest( i ),
								digest = digest( i ), 
								croppedLongSide = croppedLongSide,
								originalPath = _validRenderPath, 
								renditions = {} } )

		paths[ i ] = makeSavePath( i )

		rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			pixStreamId( i ), localAssetId( i ),
			{ renditionPath = paths[ i ], 
				digest = digest( i ), 
				revId = digest( i ), 
				renderSize = renderSize( i ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
									needRenderState )

		expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

	end

	rxTest:log( "Scheduling a test expecting " .. expectedTestMessage )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	for i, renderStream in ipairs( MockWFCrContext_renderStreams ) do

		renderStream:onError( string.format( "fale wail #%s", i ) )

	end

	watchPixelRenders:expectData( sortTupleArray( expectedTestDataTuples ) )

	rxTest:log( "Finishing a test expecting " .. expectedTestMessage )

	rxTest:expectEqual( 
		sortTupleArray( expectedErrorStateTuples ),
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( 
		{},
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( #expectedTestDataTuples, MockWFCrContext_close.count )

end

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

local function makeMockWFCrContextRenderMethodFail( rxTest )

	local MockWFCrContext_close = { count = 0 }

	local function MockWFCrContextPsuedoClass( negPath )

		rxTest:logf( "setting up mock render context of path %s", negPath )

		return {

			render = 

				function()

					error( "render method fail" )
					
				end,

			close = 

				function() 
					
					MockWFCrContext_close.count = MockWFCrContext_close.count + 1
					
				end,

		}

	end

	return MockWFCrContext_close, MockWFCrContextPsuedoClass

end

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

local function rxTestRenderMethodFail( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local expectedTestDataTuples = {}

	local needsRenderStates =
			{ PixelPathProviderStateMachine.RenderStates.NeedsRender }

	for i = 1, #needsRenderStates do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		for _ = i, #needsRenderStates do

			expectedTestDataTuples[ #expectedTestDataTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

		end

	end

	local MockWFCrContext_close, MockWFCrContext = makeMockWFCrContextRenderMethodFail( rxTest )

	scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext =
		MockWFCrContext

	local expectedErrorStateTuples = {}

	local paths = {}

	local expectedTestMessage = "renderMethodFail"
	
	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											#expectedTestDataTuples,
											isTestMessage( expectedTestMessage ),
											stripTestMessageAndMakeTuple )
										:map( sortTupleArray ) )

	for i, needRenderState in ipairs( needsRenderStates ) do

		local croppedLongSide = 200 + i

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
								localAssetId( i ), 
							{ ozAssetId = string.format( "%s", localAssetId( i ) ), 
								changeCounter = i, 
								revId = digest( i ),
								digest = digest( i ), 
								croppedLongSide = croppedLongSide,
								originalPath = _validRenderPath, 
								renditions = {} } )

		paths[ i ] = makeSavePath( i )

		rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			pixStreamId( i ), localAssetId( i ),
			{ renditionPath = paths[ i ], 
				digest = digest( i ), 
				revId = digest( i ), 
				renderSize = renderSize( i ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
									needRenderState )

		expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

	end

	rxTest:log( "Scheduling a test expecting " .. expectedTestMessage )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	watchPixelRenders:expectData( sortTupleArray( expectedTestDataTuples ) )

	rxTest:log( "Finishing a test expecting " .. expectedTestMessage )

	rxTest:expectEqual( 
		sortTupleArray( expectedErrorStateTuples ),
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( 
		{},
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( #expectedTestDataTuples, MockWFCrContext_close.count )

	rxTest:expectEqual(
		nil,
		next( scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.activeRenders ) )

end

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

local function rxTestAbandonedRender( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local needsRenderStates = { 
			PixelPathProviderStateMachine.RenderStates.NeedsRender }

	SchedulerTestAPI.runScheduledRequests( scheduler )

	for _, renderFail in ipairs( { true, false } ) do

		local MockWFCrContext_renderStreams, MockWFCrContext_close,
					MockWFCrContext = makeMockWFCrContext( rxTest )

		scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext =
			MockWFCrContext

		local expectedErrorStateTuples = {}

		local paths = {}

		for i, needRenderState in ipairs( needsRenderStates ) do

			scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
										PixelPathProviderRequestTypeNames.grid_visible, 1 )

			local croppedLongSide = 200 + i

			scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
									localAssetId( i ), 
								{ ozAssetId = string.format( "%s", localAssetId( i ) ),
									subtype = "image",
									changeCounter = i, 
									revId = digest( i ),
									digest = digest( i ), 
									croppedLongSide = croppedLongSide,
									originalPath = _validRenderPath, 
									renditions = {} } )

			paths[ i ] = makeSavePath( i )

			rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

			scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
				pixStreamId( i ), localAssetId( i ),
				{ renditionPath = paths[ i ], 
					digest = digest( i ), 
					revId = digest( i ), 
					renderSize = renderSize( i ),
					croppedLongSide = croppedLongSide,
					userOrientation = 1, 
					devSettings = "<xmp />" } )

			SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
										needRenderState )

			expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

		end

		SchedulerTestAPI.runScheduledRequests( scheduler )

		for i in ipairs( needsRenderStates ) do

			scheduler:abandonAllPixStreamRequests( pixStreamId( i ) )

		end

		local fakeBlob = require 'KSData'.make( 'wut?' )

		rxTest:log( "about to loop renderstreams" )

		for _, renderStream in ipairs( MockWFCrContext_renderStreams ) do

			rxTest:logf( "about to stream %s", 
							renderFail and "onError" or "onNext" )

			local streamFunc = renderFail and renderStream.onError or 
												renderStream.onNext

			streamFunc( renderStream, fakeBlob )

		end

		rxTest:expectEqual( 
			not renderFail, 
			require 'WFCloseSupport'.hasBeenOrWillBeClosed( fakeBlob ) )

		rxTest:logf( "about to check close count of %srender", 
						renderFail and "failed " or "" )

		rxTest:expectEqual( #MockWFCrContext_renderStreams,
								MockWFCrContext_close.count )

		if renderFail then

			fakeBlob:close()

		end

	end

end

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

local function rxTestJoinRender( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.maxConcurrentRenders = 1

	local maxConcurrentRenders = scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.maxConcurrentRenders

	for _, joinInProcessRequests in ipairs( { true, false } ) do

		local MockWFCrContext_renderStreams, _, MockWFCrContext =
				makeMockWFCrContext( rxTest )

		scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext =
			MockWFCrContext

		-- add req A
		scheduler:addRequest( pixStreamId( 1 ), localAssetId( 1 ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		local croppedLongSide = 200 + 1

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
								localAssetId( 1 ), 
							{ ozAssetId = string.format( "%s", localAssetId( 1 ) ), 
								changeCounter = 1, 
								revId = digest( 1 ),
								subtype = "image",
								digest = digest( 1 ), 
								croppedLongSide = croppedLongSide,
								originalPath = _validRenderPath, 
								renditions = {} } )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			pixStreamId( 1 ), localAssetId( 1 ),
			{ renditionPath = makeSavePath( 1 ),
				digest = digest( 1 ), 
				revId = digest( 1 ), 
				renderSize = renderSize( 1 ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
									PixelPathProviderStateMachine.RenderStates.NeedsRender )

		SchedulerTestAPI.runScheduledRequests( scheduler )

		rxTest:log( "Testing first render with joinInProcessRequests = " .. tostring( joinInProcessRequests ) )

		rxTest:expectEqual( 0, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.NeedsRender ) )

		rxTest:expectEqual( 1, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.RenderScheduled ) )

		local joinPixstreamId = pixStreamId( 2 )

		-- add req B, which should join to A
		scheduler:addRequest( joinPixstreamId, localAssetId( 1 ),
										PixelPathProviderRequestTypeNames.grid_visible, 1 )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			joinPixstreamId, localAssetId( 1 ),
			{ renditionPath = makeSavePath( 1 ),
				digest = digest( 1 ), 
				revId = digest( 1 ), 
				renderSize = renderSize( 1 ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		local notJoinPixstreamId = pixStreamId( 3 )

		-- add req C which should not join
		scheduler:addRequest( notJoinPixstreamId, localAssetId( 1 ),
										PixelPathProviderRequestTypeNames.grid_visible, 4000 )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			notJoinPixstreamId, localAssetId( 1 ),
			{ renditionPath = makeSavePath( 1 ),
				digest = digest( 1 ), 
				revId = digest( 1 ), 
				renderSize = renderSize( 4000 ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		rxTest:expectEqual( 1, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.RenderScheduled ) )

		SchedulerTestAPI.setState( scheduler, joinPixstreamId, localAssetId( 1 ), 
									PixelPathProviderStateMachine.RenderStates.NeedsRender )

		SchedulerTestAPI.setState( scheduler, notJoinPixstreamId, localAssetId( 1 ), 
									PixelPathProviderStateMachine.RenderStates.NeedsRender )

		if joinInProcessRequests then

			SchedulerTestAPI.runScheduledRequests( scheduler )

		end

		rxTest:expectEqual( joinInProcessRequests and 1 or 2, 
								PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.NeedsRender ) )

		rxTest:expectEqual( joinInProcessRequests and 2 or 1, 
								PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.RenderScheduled ) )

		rxTest:expectEqual( joinInProcessRequests and maxConcurrentRenders or 1, #MockWFCrContext_renderStreams )

		local renderStream = MockWFCrContext_renderStreams[ 1 ]

		if renderStream then

			local fakeBlob = require 'KSData'.make( 'wut?' )

			renderStream:onNext( fakeBlob )

		end

		rxTest:expectEqual( 0, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.NeedsRender ) )

		rxTest:expectEqual( 1, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.RenderStates.RenderScheduled ) )

		rxTest:expectEqual( 2, PixelPathProviderSchedulerPrivateMethods.countRequestsWithState( scheduler,  
									PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender ) )

		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:destroyAll()

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:destroyAll()

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:destroyAll()

		scheduler:abandonAllPixStreamRequests( pixStreamId( 1 ) )

		scheduler:abandonAllPixStreamRequests( joinPixstreamId )

		scheduler:abandonAllPixStreamRequests( notJoinPixstreamId )

		for i = 2, #MockWFCrContext_renderStreams do

			MockWFCrContext_renderStreams[ i ]:onError( "skip this" )

		end

	end

end

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

local function rxTestRenderContextFail( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local expectedTestDataTuples = {}

	local needsRenderStates =
			{ WFFiniteStateMachine.makeRetryableStates(
					PixelPathProviderStateMachine.RenderRetryCount,
					PixelPathProviderStateMachine.RenderStates.NeedsRender ) }

	for i = 1, #needsRenderStates do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		expectedTestDataTuples[ #expectedTestDataTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

	end

	scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext = error

	local expectedErrorStateTuples = {}

	local paths = {}

	local expectedTestMessage = "renderContextFail"
	
	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											#expectedTestDataTuples,
											isTestMessage( expectedTestMessage ),
											stripTestMessageAndMakeTuple )
										:map( sortTupleArray ) )

	for i, needRenderState in ipairs( needsRenderStates ) do

		local croppedLongSide = 200 + i

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
								localAssetId( i ), 
							{ ozAssetId = string.format( "%s", localAssetId( i ) ), 
								changeCounter = i, 
								revId = digest( i ),
								subtype = "image",
								digest = digest( i ), 
								croppedLongSide = croppedLongSide,
								originalPath = _validRenderPath, 
								renditions = {} } )

		paths[ i ] = makeSavePath( i )

		rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			pixStreamId( i ), localAssetId( i ),
			{ renditionPath = paths[ i ], 
				digest = digest( i ), 
				revId = digest( i ), 
				renderSize = renderSize( i ),
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
									needRenderState )

		expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

	end

	rxTest:log( "Scheduling a test expecting " .. expectedTestMessage )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	watchPixelRenders:expectData( sortTupleArray( expectedTestDataTuples ) )

	rxTest:log( "Finishing a test expecting " .. expectedTestMessage )

	rxTest:expectEqual( 
		sortTupleArray( expectedErrorStateTuples ),
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( 
		{},
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender,
						takeFirstN( 2 ) ) ) )

end

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

local function rxTestRenderTaskBacklog( rxTest )

	rxTest:setTimeLimit( 2 )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local numRequests = 50
	
	for i = 1, numRequests do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
									PixelPathProviderStateMachine.SaveRenderStates.SavingRender )

	end

	SchedulerTestAPI.setState( scheduler, 
								pixStreamId( numRequests ), 
								localAssetId( numRequests ), 
								PixelPathProviderStateMachine.RenderStates.NeedsRender )


	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											1,
											isTestMessage( "skipRenderSaveBacklog" ) ) )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	watchPixelRenders:expectData( { "skipRenderSaveBacklog" } )

end

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

local function createCacheEntryForRenderPreempt( scheduler, cachePixStreamId, cacheLocalAssetId, savePath )

	scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
															cacheLocalAssetId, 
															{ ozAssetId = string.format( "%s", cacheLocalAssetId ), 
															changeCounter = 1, 
															revId = digest( 1 ),
															digest = digest( 1 ), 
															croppedLongSide = 4096,
															originalPath = _validRenderPath, 
															proxyPath = _validRenderPath, 
															renditions = {} } )

	scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
		cachePixStreamId, cacheLocalAssetId,
		{ renditionPath = savePath,
			digest = digest( 1 ), 
			revId = digest( 1 ), 
			renderSize = 1024,
			croppedLongSide = 4096,
			userOrientation = 1, 
			devSettings = "<xmp />" } )

end

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


local function rxTestRenderTaskPreempt( rxTest )

	rxTest:setTimeLimit( 2 )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	-- Add low priority requests that needs to render

	local maxConcurrentRenders = scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.maxConcurrentRenders

	for i = 1, maxConcurrentRenders do

		scheduler:addRequest( pixStreamId( 1 ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_prefetch, 1 )

		createCacheEntryForRenderPreempt( 
			scheduler, pixStreamId( 1 ), localAssetId( i ), makeSavePath( i ) )

		SchedulerTestAPI.setState( scheduler, 
									pixStreamId( 1 ), 
									localAssetId( i ), 
									PixelPathProviderStateMachine.RenderStates.NeedsRender )

	end

	SchedulerTestAPI.runScheduledRequests( scheduler )

	-- A higher priority request now needs to render. It should pre-empt a existing render.

	local preemptSeqNum = maxConcurrentRenders + 1

	local preemptAssetId = localAssetId( preemptSeqNum )

	scheduler:addRequest( pixStreamId( 2 ), preemptAssetId,
									PixelPathProviderRequestTypeNames.highest, 1 )

	createCacheEntryForRenderPreempt( 
		scheduler, pixStreamId( 2 ), preemptAssetId, makeSavePath( preemptSeqNum ) )

	SchedulerTestAPI.setState( scheduler, 
								pixStreamId( 2 ), 
								preemptAssetId, 
								PixelPathProviderStateMachine.RenderStates.NeedsRender )

	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											1,
											isTestMessage( "renderPreempted" ) ) )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	rxTest:log( "start waiting for renderPreempted" )

		require "WFFunctionUtils".map(
			function( reqStat )

				rxTest:logf( "%s = %s", unpack( reqStat ) )

			end,
			scheduler._requestPriorityQueue:_DEBUG_getRequestCountsByState().all )

	watchPixelRenders:expectData( { "renderPreempted" } )

end

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

local function rxTestQuitWithActiveRender( rxTest )

	-- quit the task before we've finished rendering the blob

	local catalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )

	local scheduler = PixelPathProviderScheduler( catalog )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	scheduler:addRequest( pixStreamId( 1 ), localAssetId( 1 ),
								PixelPathProviderRequestTypeNames.grid_visible, 1 )

	local MockWFCrContext_renderStreams, MockWFCrContext_close,
				MockWFCrContext = makeMockWFCrContext( rxTest )

	scheduler._testData.runLoopTaskStates.PixelProviderRenderTask.MockWFCrContext =
		MockWFCrContext

	local croppedLongSide = 200

	scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
							localAssetId( 1 ), 
						{ ozAssetId = string.format( "%s", localAssetId( 1 ) ), 
							changeCounter = 1, 
							revId = digest( 1 ),
							subtype = "image",
							digest = digest( 1 ), 
							croppedLongSide = croppedLongSide,
							originalPath = _validRenderPath, 
							renditions = {} } )


	local paths = { makeSavePath( 1 ) }

	rxTest:expectEqual( false, WFFileUtils.exists( paths[ 1 ] ) )

	scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
		pixStreamId( 1 ), localAssetId( 1 ),
		{ renditionPath = paths[ 1 ], 
			digest = digest( 1 ), 
			revId = digest( 1 ), 
			renderSize = renderSize( 1 ),
			croppedLongSide = croppedLongSide,
			userOrientation = 1, 
			devSettings = "<xmp />" } )

	SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
								PixelPathProviderStateMachine.RenderStates.NeedsRender )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	scheduler:close()

	rxTest:expectEqual( 1, #MockWFCrContext_renderStreams )

	rxTest:expectEqual( 1, MockWFCrContext_close.count )

end

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

local function rxTestBogusCroppedLongSide( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderRenderTask" )

	local expectedTestDataTuples = {}

	local needsRenderStates =
			{ WFFiniteStateMachine.makeRetryableStates(
					PixelPathProviderStateMachine.RenderRetryCount,
					PixelPathProviderStateMachine.RenderStates.NeedsRender ) }

	for i = 1, #needsRenderStates do

		scheduler:addRequest( pixStreamId( i ), localAssetId( i ),
									PixelPathProviderRequestTypeNames.grid_visible, 1 )

		for _ = i, #needsRenderStates do

			expectedTestDataTuples[ #expectedTestDataTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

		end

	end

	local expectedErrorStateTuples = {}

	local paths = {}

	local expectedTestMessage = "renderFail"
	
	local watchPixelRenders = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											#expectedTestDataTuples,
											isTestMessage( expectedTestMessage ),
											stripTestMessageAndMakeTuple )
										:map( sortTupleArray ) )

	for i, needRenderState in ipairs( needsRenderStates ) do

		local croppedLongSide = 1 / ( 200 + i )

		scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:pushCacheItem( 
								localAssetId( i ), 
							{ ozAssetId = string.format( "%s", localAssetId( i ) ), 
								changeCounter = i, 
								revId = digest( i ),
								digest = digest( i ), 
								croppedLongSide = croppedLongSide,
								originalPath = _validRenderPath, 
								renditions = {} } )

		paths[ i ] = makeSavePath( i )

		rxTest:expectEqual( false, WFFileUtils.exists( paths[ i ] ) )

		scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache:pushCacheItem(
			pixStreamId( i ), localAssetId( i ),
			{ renditionPath = paths[ i ], 
				digest = digest( i ), 
				revId = digest( i ), 
				renderSize = "full",
				croppedLongSide = croppedLongSide,
				userOrientation = 1, 
				devSettings = "<xmp />" } )

		SchedulerTestAPI.setState( scheduler, pixStreamId( i ), localAssetId( i ), 
									needRenderState )

		expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = { pixStreamId( i ), localAssetId( i ) }

	end

	rxTest:log( "Scheduling a test expecting " .. expectedTestMessage )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	watchPixelRenders:expectData( sortTupleArray( expectedTestDataTuples ) )

	rxTest:log( "Finishing a test expecting " .. expectedTestMessage )

	rxTest:expectEqual( 
		sortTupleArray( expectedErrorStateTuples ),
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
						takeFirstN( 2 ) ) ) )

	rxTest:expectEqual( 
		{},
		sortTupleArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.SaveRenderStates.NeedToSaveRender,
						takeFirstN( 2 ) ) ) )

end

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

local function rxTestMaybeGetUnusedRenderSlot( rxTest )

	for maxConcurrent = 1, 3 do

		local oneButNotAllActives, oneButNotAllInactives, allActives

		for currSlot = 1, maxConcurrent do

			local oneButNotAllActive, oneButNotAllInactive = {}, {}

			local allActive = not allActives and {}

			for emptySlot = 1, maxConcurrent do

				if allActive then

					allActive[ emptySlot ] = true

				end

				if currSlot ~= emptySlot then

					oneButNotAllInactive[ emptySlot ] = true

				elseif maxConcurrent > 1 then

					oneButNotAllActive[ emptySlot ] = true

				end

			end

			allActives = allActives or { allActive }

			if next( oneButNotAllActive ) then

				oneButNotAllActives = oneButNotAllActives or {}

				oneButNotAllActives[ #oneButNotAllActives + 1 ] = oneButNotAllActive

			end

			if next( oneButNotAllInactive ) then

				oneButNotAllInactives = oneButNotAllInactives or {}

				oneButNotAllInactives[ #oneButNotAllInactives + 1 ] = oneButNotAllInactive

			end

		end
	
		local noActives = { {} }

		for _, priorityName in ipairs( PixelPathProviderRequestTypes.priorities) do

			for _, activeRendersArray in ipairs( { noActives, oneButNotAllActives, oneButNotAllInactives, allActives } ) do

				for _, activeRenders in ipairs( activeRendersArray ) do
			
					for _, blackoutStatus in ipairs( { false, true } ) do

						local requestPriority = PixelPathProviderRequestTypes.getPriority( priorityName )

						-- we expect to get a slot if there are no active
						-- renders or if the the request is higher priority
						-- than idle and all the renders are not active.

						local availableSlotIsExpectedForPriorityWhenActiveRenders =
									not blackoutStatus and
									PixelPathProviderRequestTypes.comparePriorities( 
											requestPriority, 
											PixelPathProviderRequestTypes.getPriority(
												PixelPathProviderRequestTypes.names.idle ) ) > 0 and
									activeRendersArray ~= allActives

						local availableSlotIsExpected =
								activeRendersArray == noActives or
									availableSlotIsExpectedForPriorityWhenActiveRenders

						rxTest:logf( "Checking we %sget slot when %s, %s, %s, maxRenders %s",
										availableSlotIsExpected and "" or "DO NOT ",
										priorityName,
										( activeRendersArray == noActives and "noActives" ) or
											( activeRendersArray == oneButNotAllActives and "oneButNotAllActives" ) or
											( activeRendersArray == oneButNotAllInactives and "oneButNotAllInactives" ) or
											( activeRendersArray == allActives and "allActives" ),
										blackoutStatus and "is blacked out" or "no blackout", 
										maxConcurrent )

						rxTest:expectEqual( 
							availableSlotIsExpected, 
							PixelProviderRenderTask.privateUnitTestExports.maybeGetUnusedRenderSlot(
								blackoutStatus, requestPriority, maxConcurrent, activeRenders ) ~= nil )

					end

				end

			end

		end

	end

end

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

return WFRxTestCase.wrapSuite {

	rxTestRenderTask = rxTestRenderTask,

	rxTestJoinRender = rxTestJoinRender,

	rxTestRenderStreamFail = rxTestRenderStreamFail,

	rxTestRenderMethodFail = rxTestRenderMethodFail,

	rxTestAbandonedRender = rxTestAbandonedRender,

	rxTestRenderContextFail = rxTestRenderContextFail,

	rxTestRenderTaskBacklog = rxTestRenderTaskBacklog,

	rxTestQuitWithActiveRender = rxTestQuitWithActiveRender,
	
	rxTestBogusCroppedLongSide = rxTestBogusCroppedLongSide,
	
	rxTestRenderTaskPreempt = rxTestRenderTaskPreempt,

	rxTestMaybeGetUnusedRenderSlot = rxTestMaybeGetUnusedRenderSlot,

}
