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

PixelPathProviderSchedulerMainLoopTest.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 KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFRxTestCase = require 'WFRxTestCase'
local WFFileUtils = require 'WFFileUtils'
local KSPathUtils = require 'KSPathUtils'

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

local PixelPathProviderScheduler = require "PixelPathProviderScheduler"
local PixelPathProviderRequestTypes = require "PixelPathProviderRequestTypes"
local PixelPathProviderStateMachine = require "PixelPathProviderStateMachine"
local NmbLocalRenditionDigest = require "NmbLocalRenditionDigest"

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

local PixelPathProviderRequestTypeNames = PixelPathProviderRequestTypes.names

local testPath = KSPathUtils.getStandardFilePath( 'carouselDocuments' )
local volumeGuid = WFFileUtils.getVolumeGuidForPath( testPath )

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

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 generateLocalAssetId( i )

	return i

end

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

local function generatePixStreamId( i )

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

end

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

local function extractLocalAssetIdFromRequest( request )

	return request[ 2 ]

end

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

local function addRecordToDocStore( rxTest, catalog, i )

	rxTest:expectSingleValue(

		catalog:docStore():bulkSetDocValue {
			{
				_id = KSIdGenerator.uuidInLowercaseHex(),
				_localId = generateLocalAssetId( i ),
				subtype = "image",
				_localOnly = {
					files = {
						original = {
							master = {
								relativePath = string.format( 'local path %s', i ),
								volumeGuid = volumeGuid
							}
						}
					}
				},
				develop = {
					croppedHeight = 100 * i,
					croppedWidth = 100 * i,
					xmpCameraRaw = '<xmp />',
					userOrientation = i % 4,
				},
				rating = i % 5,
			},
		},

		':ignore:' )

end

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

local function rxTestRunLoopShutdown( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	rxTest:expectEqual( "suspended", coroutine.status( scheduler._runLoopCoroutineThread ) )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	rxTest:expectEqual( "suspended", coroutine.status( scheduler._runLoopCoroutineThread ) )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	rxTest:expectEqual( "suspended", coroutine.status( scheduler._runLoopCoroutineThread ) )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	rxTest:expectEqual( "suspended", coroutine.status( scheduler._runLoopCoroutineThread ) )

	scheduler:close()

	rxTest:expectEqual( "dead", coroutine.status( scheduler._runLoopCoroutineThread ) )

end

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

local function isTestMessage( msg )

	return function( testMsg, ... )

		return testMsg == msg

	end

end

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

local function takeNth( n )
	
	return function( ... )

		return select( n, ... )

	end

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 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 rxTestPixelCacheCheckTask( rxTest )

	local scheduler, catalog = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderDocstoreInfoTask" )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderPixelCacheCheckTask" )

	local newRecordCount = 5

	for i = 1, newRecordCount do

		addRecordToDocStore( rxTest, catalog, i )

	end

	local pixStreamCount = 3

	local watchPixelCacheChecks = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											newRecordCount * pixStreamCount,
											isTestMessage( "didPixelCacheCheck" ),
											stripTestMessageAndMakeTuple )
										:map( sortTupleArray ) )

	local expectedNewRecords = {}

	local expectedPixelCacheCheckMessages = {}

	for i = 1, newRecordCount do

		for j = 1, pixStreamCount do

			expectedPixelCacheCheckMessages[ #expectedPixelCacheCheckMessages + 1 ] = 
				{ generatePixStreamId( j ), generateLocalAssetId( i ), 'no_local_rendition' }

			scheduler:addRequest( generatePixStreamId( j ), generateLocalAssetId( i ), 
									PixelPathProviderRequestTypeNames.grid_visible, 1 )
		
			expectedNewRecords[ #expectedNewRecords + 1 ] = generateLocalAssetId( i )

		end
	
	end

	rxTest:log( "before watchPixelCacheChecks" )

	-- make sure we got doc store data for all the records
	watchPixelCacheChecks:expectData( sortTupleArray( expectedPixelCacheCheckMessages ) )

	rxTest:log( "after watchPixelCacheChecks" )

	-- all requests are now on NeedCheckForPixelPathInCache
	rxTest:expectEqual( 
		sortArray( expectedNewRecords ),
		sortArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.PixelCacheCheckStates.ChooseRenditionStrategy,
						extractLocalAssetIdFromRequest ) ) )


end

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

local function rxTestChooseRenditionStrategyNeedCheckAnyLocalNegativeForRender( rxTest )

	local scheduler, catalog = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( 
						scheduler, "PixelProviderDocstoreInfoTask" )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( 
						scheduler, "PixelProviderPixelCacheCheckTask" )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( 
						scheduler, "PixelProviderChooseRenditionStrategyTask" )

	local newRecordCount = 5

	for i = 1, newRecordCount do

		addRecordToDocStore( rxTest, catalog, i )

	end

	local pixStreamCount = 3

	local watchChooseRenditionStrategy = rxTest:observe( 
									getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											newRecordCount * pixStreamCount,
											isTestMessage( "chooseRenditionStrategy" ),
											takeNth( 2 ) ) )

	local expectedNewRecords = {}

	local expectedMessages = {}

	for i = 1, newRecordCount do

		for j = 1, pixStreamCount do

			expectedMessages[ #expectedMessages + 1 ] = "NeedCheckAnyLocalNegativeForRender"

			scheduler:addRequest( generatePixStreamId( j ), generateLocalAssetId( i ), 
									PixelPathProviderRequestTypeNames.grid_visible, 1 )
		
			expectedNewRecords[ #expectedNewRecords + 1 ] = generateLocalAssetId( i )

		end
	
	end

	-- make sure we got doc store data for all the records
	watchChooseRenditionStrategy:expectData( expectedMessages )

	-- all requests are now on NeedCheckForPixelPathInCache
	rxTest:expectEqual( 
		sortArray( expectedNewRecords ),
		sortArray( 
			scheduler:_mapAllRequestsWithState( 
						PixelPathProviderStateMachine.LocalBinaryPathCheckStates.NeedCheckAnyLocalNegativeForRender,
						extractLocalAssetIdFromRequest ) ) )


end

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

local function rxTestChooseRenditionStrategyBestMatchCanMaybeRender( rxTest )

	local revId = KSIdGenerator.uuidInLowercaseHex()

	local digest = 'digest'

	local userOrientation = 1
	
	local pixStreamId = "tesssst"

	local localAssetId = 1

	local croppedLongSide = 4000

	local renditionPath = 
			WFFileUtils.ensureFileNameIsUnique( 
					KSPathUtils.child( 
						KSPathUtils.getStandardFilePath( "temp" ),
						"fake" ) )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( 
						scheduler, "PixelProviderChooseRenditionStrategyTask" )

	-- runScheduledRequests to bootstrap scheduler._testData.runLoopCaches
	SchedulerTestAPI.runScheduledRequests( scheduler )

	for _, downloadableRenditionState in ipairs( { "none", "valid", "invalid" } ) do

		for _, downloadableRenditionType in ipairs( { "image", "video" } ) do
		
			local renderSizes = {}
			
			if downloadableRenditionType == "image" then
				renderSizes = {
					640,
					2048,
				}
			else
				renderSizes = {
					640,
					"720p",
					"1080p",
				}
			
			end
			
			for _, renderSize in ipairs( renderSizes ) do 
				for _, haveNegatives in ipairs( { "none", "localAny", "localProxy", "localOrig", "serverBoth", "serverProxy", "serverOrig" } ) do

					for _, xmpCameraRawType in ipairs( { "sharedAssetViewOnly", "renderableInternal", "missingExternal" } ) do

						local renditions, invalid_renditions

						if downloadableRenditionState ~= "none" then

							local rawRenditionList = 
								( downloadableRenditionType == "image" and
									{
										{ size = "thumbnail2x", href = "fakeThumb" },
										{ size = "640", href = "fake640" },
										{ size = "1280", href = "fake1280" },
										{ size = "2048", href = "fake2048" },
									} ) or
								( downloadableRenditionType == "video" and
									{
										{ size = "thumbnail2x", href = "fakeThumb" },
										{ size = "2048", href = "fake2048" },
										{ size = "360p", href = "fake360p" },
										{ size = "720p", href = "fake720p" },
										{ size = "1080p", href = "fake1080p" },
										{ size = "1080p709", href = "fake1080p709" },
									} )

							if downloadableRenditionState == "valid" then

								renditions = rawRenditionList

							elseif downloadableRenditionState == "invalid" then
								
								invalid_renditions = rawRenditionList

							end

						end

						-- set up xmpCameraRaw for various states
						local xmpCameraRaw 

						if xmpCameraRawType ~= "missingExternal" then

							xmpCameraRaw =
								xmpCameraRawType == "sharedAssetViewOnly" and
									NmbLocalRenditionDigest.privateUnitTestExports.makeUniqueEmptySettingsForDownloadOnlyAssetImp( revId ) or
									"<xmp />" 

						end

						-- set up negative paths for various states
						local proxyPath, originalPath, proxies, ozValidOriginalLink 

						if haveNegatives == "localAny" or haveNegatives == "localProxy" then

							proxyPath = "proxyPath"

						end

						if haveNegatives == "localAny" or haveNegatives == "localOrig" then

							originalPath = "originalPath"

						end

						if haveNegatives == "serverBoth" or haveNegatives == "serverProxy" then

							proxies = { { href = "proxyHref", size = 2560 } }

						end

						if haveNegatives == "serverBoth" or haveNegatives == "serverOrig" then

							ozValidOriginalLink = "masterHref"

						end

						scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache
							:pushCacheItem( 
								localAssetId,
								{ 
									ozAssetId = "ozAssetId",
									changeCounter = 1, 
									revId = revId,
									digest = digest,
									croppedLongSide = croppedLongSide,
									renditions = renditions,
									invalid_renditions = invalid_renditions,
									xmpCameraRaw = xmpCameraRaw,
									proxyPath = proxyPath,
									originalPath = originalPath,
									proxies = proxies,
									ozValidOriginalLink = ozValidOriginalLink, 

								} )

						scheduler._testData.runLoopCaches.sharedRenderCaches.activeRenditionInfoCache
							:pushCacheItem(
								pixStreamId, localAssetId,
								{ 
									renditionPath = renditionPath,
									digest = digest,
									revId = revId,
									renderSize = renderSize,
									croppedLongSide = croppedLongSide,
									userOrientation = userOrientation, 
									devSettings = xmpCameraRaw,
								} )

						scheduler._testData.runLoopCaches.sharedRenderCaches.currRenderSettingsCache
							:pushCacheItem( 
								localAssetId, 
								{ digest, revId, xmpCameraRaw, userOrientation } )

						local watchChooseRenditionStrategy = 
								rxTest
									:observe( 
										getNFromTestInfoStream(
											scheduler._testData.testInfoStream,
											1,
											isTestMessage( "chooseRenditionStrategy" ),
											takeNth( 2 ) ) )

						scheduler
							:addRequest( 
								pixStreamId, localAssetId,
								PixelPathProviderRequestTypeNames.grid_visible, 
								renderSize,
								PixelPathProviderStateMachine.PixelCacheCheckStates.ChooseRenditionStrategy )

						SchedulerTestAPI.runScheduledRequests( scheduler )

						local expectedMessages = { "unhandled permutation!" }

						if downloadableRenditionState == "none" then

							-- we can't download, test all the render scenarios
							expectedMessages = 
								-- no render, no download
								( haveNegatives == "none" and
									{ "SendNoDownloadableRenderOrBinary" } ) or
								-- can render, but missing external xmp
								( xmpCameraRawType == "missingExternal" and
									{ "RenditionStrategyNeedCheckIfExternalXmpDevelopIsLocal"} ) or
								-- can render, but not allowed in album (should be impossible)
								( xmpCameraRawType == "sharedAssetViewOnly" and
									{ "SendNoDownloadableRenderOrBinary"} ) or
								-- can render using local negative
								( ( haveNegatives == "localAny" or 
										haveNegatives == "localProxy" or 
										haveNegatives == "localOrig" ) and
									{ "NeedCheckAnyLocalNegativeForRender" } ) or
								-- can render after downloading proxy negative
								( ( haveNegatives == "serverBoth" or 
										haveNegatives == "serverProxy" ) and
									{ "NeedProxyDownloadForRender" } ) or
									{ "NeedOriginalDownloadForRender" }
						elseif downloadableRenditionType == "video" and renderSize == 640 then

							-- Note: for video, we are requesting a 640 rendition
							-- but none are availaible online (only thumbnail2x and
							-- 2048 are available in those cases). If we can render
							-- locally we'll create a 640, otherwise we will
							-- download a 2048 which will result in a new long side
							-- for the request
							expectedMessages = 
								-- no negatives, must download a 2048 which will
								-- reset long side and restart request state machine
								( haveNegatives == "none" and 
									{ "NeedAssetInfoForRender" } ) or
								-- can't render due to album policy must download a
								-- 2048 which will reset long side and restart
								-- request state machine
								( xmpCameraRawType == "sharedAssetViewOnly" and
									{ "NeedAssetInfoForRender"} ) or
								-- can render, but missing external xmp
								( xmpCameraRawType == "missingExternal" and
									{ "RenditionStrategyNeedCheckIfExternalXmpDevelopIsLocal"} ) or
								-- can render using local negative
								( ( haveNegatives == "localAny" or 
										haveNegatives == "localProxy" or 
										haveNegatives == "localOrig" ) and
									{ "NeedCheckAnyLocalNegativeForRender" } ) or
								-- can render after downloading proxy negative
								( ( haveNegatives == "serverBoth" or 
										haveNegatives == "serverProxy" ) and
									{ "NeedProxyDownloadForRender" } ) or
									{ "NeedOriginalDownloadForRender" }

						-- we CAN download, test all rest of the scenarios
						elseif ( xmpCameraRawType == "sharedAssetViewOnly" or 
										haveNegatives == "none" ) then

							-- we have an image which cannot be rendered
							expectedMessages = 
								downloadableRenditionState == "invalid" and
								{ "NeedInvalidRenderDownload" } or
								{ "NeedRenderDownload" }
							
						-- we can download, maybe we can render
						elseif downloadableRenditionState == "invalid" then

							expectedMessages = 
								-- cannot render, must download
								( haveNegatives == "none" and 
									{ "NeedInvalidRenderDownload" } ) or
								-- can render, but missing external xmp
								( xmpCameraRawType == "missingExternal" and
									{ "RenditionStrategyNeedCheckIfExternalXmpDevelopIsLocal"} ) or
								-- can render using local negative
								( ( haveNegatives == "localAny" or 
										haveNegatives == "localProxy" or 
										haveNegatives == "localOrig" ) and
									{ "NeedCheckAnyLocalNegativeForRender" } ) or
								-- can render after downloading proxy negative
								( ( haveNegatives == "serverBoth" or 
										haveNegatives == "serverProxy" ) and
									{ "NeedProxyDownloadForRender" } ) or
									{ "NeedOriginalDownloadForRender" }

						elseif downloadableRenditionState == "valid"  then

							expectedMessages = { "NeedRenderDownload" }

						end
				
						rxTest:logf( "Expecting %s when renderSize is (%s), downloadableRenditionState (%s), downloadableRenditionType (%s), haveNegatives (%s), xmpCameraRawType (%s)",
										expectedMessages[ 1 ],
										renderSize,
										downloadableRenditionState,
										downloadableRenditionType,
										haveNegatives,
										xmpCameraRawType )

						watchChooseRenditionStrategy:expectData( expectedMessages )

						scheduler:abandonAllPixStreamRequests( pixStreamId )

					end
			
				end
			end
			
		end

	end

end

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

return WFRxTestCase.wrapSuite( 'PixelPathProviderSchedulerMainLoopTest', {

	rxTestRunLoopShutdown = rxTestRunLoopShutdown,
	
	rxTestPixelCacheCheckTask = rxTestPixelCacheCheckTask,

	rxTestChooseRenditionStrategyNeedCheckAnyLocalNegativeForRender = 
		rxTestChooseRenditionStrategyNeedCheckAnyLocalNegativeForRender,

	rxTestChooseRenditionStrategyBestMatchCanMaybeRender = 
		rxTestChooseRenditionStrategyBestMatchCanMaybeRender,


} )
