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

PixelProviderSaveDownloadedTaskTest.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 KSData = require 'KSData'
local KSIdGenerator = require 'KSIdGenerator'
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFFileUtils = require 'WFFileUtils'
local WFRxTestCase = require 'WFRxTestCase'
local WFCloseSupport = require 'WFCloseSupport'
local WFTableMerge = require 'WFTableMerge'
local WFTableArchiving = require 'WFTableArchiving'
local WFCrypto = require 'WFCrypto'

	-- Rx
local Rx = require 'Rx'

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

local PixelPathProviderScheduler = require 'PixelPathProviderScheduler'
local PixelPathProviderRequestTypes = require 'PixelPathProviderRequestTypes'
local PixelPathProviderStateMachine = require 'PixelPathProviderStateMachine'
local PixelProviderSharedBlobCache = require 'PixelProviderSharedBlobCache'
local PixelProviderSaveDownloadedTask = require 'PixelProviderSaveDownloadedTask'

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

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 _ids = {}

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

local function id( i )

	_ids[ i ] = _ids[ i ] or KSIdGenerator.uuidInLowercaseHex()

	return _ids[ i ]

end

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

local function localAssetId( i )

	return i

end

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

local function pixStreamId( i )

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

end

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

local function makeSaveDataStr( i )

	return string.format( "PixelPathProviderStateMachineTest fake data %d", i )

end

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

local function justReturn( ... )

	return ...

end

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

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

	local transformTestData = maybeTransformFunc or justReturn

	local i

	return testStream
			:map( 
				function( a, b, ... )

					i = ( i or 0 ) + 1

					rxTest:logf( "raw getNFromTestInfoStream %s %s of %s",
									desc, i, n )

					return a, b, ...

				end )
			: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 runningSaveFileCount = 0

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

local function makeUniqueSavePath( i )

	runningSaveFileCount = runningSaveFileCount + 1

	local fname = string.format( "PixelPathProviderStateMachineTest%d_%d", 
									runningSaveFileCount, 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 function extractPixStreamIdLocalAssetIdState( request )

	local aPixStreamId, aLocalAssetId, _, _, state = unpack( request )

	return { aPixStreamId, aLocalAssetId, state }

end

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

local _uuids = {}

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

local function uuid( i )

	_uuids[ i ] = _uuids[ i ] or KSIdGenerator.uuidInLowercaseHex()

	return _uuids[ i ]

end

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

local function makeFakeDocValue( i )

	return {
		_id = uuid( i ),
		_localId = localAssetId( i ),
		subtype = "image",
	}

end

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

local function docValueSha256( i )

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

end

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

local function makeRealishDocValue( i )

	return {
			_id = KSIdGenerator.uuidInLowercaseHex(),
			_localId = localAssetId( 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,
		}

end

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

local function addRecordToDocStore( rxTest, catalog, i )

	rxTest:expectSingleValue(

		catalog:docStore():bulkSetDocValue( { makeRealishDocValue( i ) } ),

		':ignore:' )

end

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

local function makeOzAssetId( i )

	return string.format( "ozAssetIdFor%s", localAssetId( i ) )

end

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

local function digest( i )

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

end

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

local function testSha256( i )

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

end

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

local function addBaseRenderInfoCacheEntry( baseRenderInfoCache, i )

	baseRenderInfoCache
		:pushCacheItem( 
			localAssetId( i ), 
			{ 
				sha256 = testSha256( i ),
				ozAssetId = makeOzAssetId( i ), 
				changeCounter = i, 
				revId = digest( i ),
				digest = digest( i ), 
				proxies = { { size="2560", href="notAProxy" } },
				ozValidOriginalLink = "yeahNope",
				ozXmpDevelopLink = 'uh_huh_NOPE',
				ozXmpLink = 'justNope',
				renditions = { { size = 640, href = "fakeURI" } },
			} )

end

--------------------------------------------------------------------------------
-- rxTestSaveDownloadedNegative 
-- 
-- 		test a real save
--
--------------------------------------------------------------------------------

local function blobSha256( i ) 

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

end

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

local function makeMockDocstoreForSaveTask() -- rxTest

	local docValuesPerId = {}

	local mockDocStore = {

		applyOverlay = 
			function( _, overlay ) 

				local assetId = 
					assert( 
						KSTableUtils.derefOpt( overlay, '_localId' ) or
							KSTableUtils.derefOpt( overlay, '_id' ), 
						"missing _id" )

				overlay._id = nil

				overlay._localId = nil

				docValuesPerId[ assetId ] = 
					WFTableMerge.applyOverlay( 
						docValuesPerId[ assetId ] or {}, overlay )

				return Rx.Observable.just( assetId )

			end,

		docValue =
			function( _, assetId )

				local docValue = docValuesPerId[ assetId ]

				if not docValue then

					return Rx.Observable.throw( string.format( "no docValue for %s", localAssetId ) )

				end

				return Rx.Observable.just( docValue )

			end,
	}

	return mockDocStore, docValuesPerId

end

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

local function rxTestSaveDownloadedNegative( rxTest )

	rxTest:setTimeLimit( 5 )
	
	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.runScheduledRequests( scheduler )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )
	
	local mockDocStore, docValuesPerId = makeMockDocstoreForSaveTask()

	SchedulerTestAPI.setTaskState( 
		scheduler, "PixelProviderSaveDownloadedTask",
		"docStore", mockDocStore )

	local expectedHappyPathEndState = {

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginalForRender ]=
				PixelPathProviderStateMachine.RenderStates.NeedsRender,

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxyForRender ]=
				PixelPathProviderStateMachine.RenderStates.NeedsRender,

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginal ]=
				PixelPathProviderStateMachine.SendDataStates.HaveLocalNegativePath,

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxy ]=
				PixelPathProviderStateMachine.SendDataStates.HaveLocalNegativePath,

		}

	for _, hasDocValueSha256 in ipairs( { false, true } ) do
		
	for _, hasBlobSha256 in ipairs( { false, true } ) do
		
	for _, needSaveNegativeState in ipairs( 
					{
						PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginalForRender,
						PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxyForRender,
						PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginal,
						PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxy 
					} ) do

		local needSaveDownloadStates =
				{ needSaveNegativeState }

		for i = 1, #needSaveDownloadStates do

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

			docValuesPerId[ i ] = makeFakeDocValue( i )

			if hasDocValueSha256 then

				docValuesPerId[ i ].importSource = 
					{ 
						sha256 = docValueSha256( i ) 
					} 

			end

			-- save task relies on baseRenderInfoCache for saving
			addBaseRenderInfoCacheEntry( 
				scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache, i )

		end

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

			local expectedTestDataTuples = {}

			local expectedErrorStateTuples = {}

			local expectedEndSaveRequestTuples = {}

			local strings = {}

			local ksDatas = {}

			local expectedTestMessage = testWithNegativeBlob and 
											"didPixelCacheSave" or "skipSaveNoBlob"

			local watchNegativeSaves = rxTest:observe( 
											getNFromTestInfoStream(
													rxTest, 
													"watching for " .. expectedTestMessage,
													scheduler._testData.testInfoStream,
													#needSaveDownloadStates,
													isTestMessage( expectedTestMessage ),
													stripTestMessageAndMakeTuple )
												:map( sortTupleArray ) )

			for i, needSaveDownloadState in ipairs( needSaveDownloadStates ) do

				local ozBinaryType = PixelProviderSharedBlobCache.isProxyDownloadState( 
											needSaveDownloadState ) and
												'proxy' or 'original'
										
				local ozBinarySubtype = ozBinaryType == 'original' and 'master' or '2560'

				if testWithNegativeBlob then

					strings[ i ] = makeSaveDataStr( i )

					ksDatas[ i ] = KSData.make( strings[ i ] )

					if ozBinaryType == 'original' then

						if hasBlobSha256 then

							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithSha256(
								localAssetId( i ), ksDatas[ i ], blobSha256( i ) )

						else

							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithoutSha256(
								localAssetId( i ), ksDatas[ i ] )

						end

					elseif ozBinaryType == 'xmp_develop' then

							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushXmpDevelopBlob(
								localAssetId( i ), ksDatas[ i ] )

					else

						scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushBlob(
							localAssetId( i ), ksDatas[ i ], ozBinaryType, ozBinarySubtype )

					end

				end

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

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

				expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = 
					not( testWithNegativeBlob ) and
						{ pixStreamId( i ), localAssetId( i ) } or nil

				expectedEndSaveRequestTuples[ #expectedEndSaveRequestTuples + 1 ] = 
					testWithNegativeBlob and 
							{ pixStreamId( i ), localAssetId( i ) } or nil

			end

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

			SchedulerTestAPI.runScheduledRequests( scheduler )

			watchNegativeSaves: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( expectedEndSaveRequestTuples ),
				sortTupleArray( 
					scheduler:_mapAllRequestsWithState( 
								expectedHappyPathEndState[ needSaveNegativeState ],
								takeFirstN( 2 ) ) ) )

			if testWithNegativeBlob then

				-- check to make sure all the render blobs are cleaned up
				for i, needSaveDownloadState in ipairs( needSaveDownloadStates ) do

					local ozBinaryType = PixelProviderSharedBlobCache.isProxyDownloadState( 
												needSaveDownloadState ) and
													'proxy' or 'original'
										
					local ozBinarySubtype = ozBinaryType == 'original' and 'master' or '2560'

					rxTest:expectEqual( nil,
						scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
							localAssetId( i ), ozBinaryType, ozBinarySubtype ) )

					local path = unpack(
						scheduler._testData.runLoopCaches.sharedCaches.localBinaryPathCache:popCacheItem( 
											pixStreamId( i ), localAssetId( i ) ) or {} )

					rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( ksDatas[ i ] ) )

					rxTest:log( "Making sure saved negative exists at " .. path )

					rxTest:expectEqual( "file", WFFileUtils.exists( path ) )

					local f = io.open( path, 'r' )

					local contents = f:read( "*all" )

					f:close()

					WFFileUtils.delete( path )

					rxTest:expectEqual( strings[ i ], contents )

					rxTest:logf( "Checking docValue._localOnly.files %s", 
									WFTableArchiving.debugTableToString(
										KSTableUtils.derefOpt( docValuesPerId[ i ], "_localOnly", "files" ) ) )

					rxTest:expectEqual( 
						"number",
						type( KSTableUtils.derefOpt( docValuesPerId[ i ], "_localOnly", "files", ozBinaryType, ozBinarySubtype, "size" ) ) )

					rxTest:expectEqual( 
						"string",
						type( KSTableUtils.derefOpt( docValuesPerId[ i ], "_localOnly", "files", ozBinaryType, ozBinarySubtype, "relativePath" ) ) )

					if ozBinaryType == "original" then

						local expectedSha256 =
								( hasBlobSha256 and blobSha256( i ) ) or
								( hasDocValueSha256 and docValueSha256( i ) ) or
								nil

						rxTest:logf( "checking for matching sha %s for %s", expectedSha256, needSaveDownloadState )

						rxTest:expectEqual( 
							expectedSha256,
							KSTableUtils.derefOpt( docValuesPerId[ i ], "_localOnly", "files", ozBinaryType, ozBinarySubtype, "sha256" ) )

					end

				end

			end

		end

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

	end

	end

end

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

local function makeFakeSaveThread( rxTest, desc, fakeSaveResultsStreams )

	local i

	return {

		savePixels = 

			function( _, _, blob ) 

				blob:close()

				i = ( i or 0 ) + 1

				rxTest:logf( "fakeSaveThread %s is emitting result %s", 
								desc, i )

				return fakeSaveResultsStreams[ i ] or Rx.Subject()
				
			end,

		close = function() end,

	}

end

--------------------------------------------------------------------------------
-- rxTestSaveDownloadedNegativeFail 
-- 
-- 	test a save failure (failures are faked by mocking the save thread)
--
--------------------------------------------------------------------------------

local function rxTestSaveDownloadedNegativeFail( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )

	for _, needSaveNegativeState in ipairs( 
				{
					PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginalForRender,
					PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxyForRender,
					PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginal,
					PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveProxy 
				} ) do

		local needSaveDownloadStates = { needSaveNegativeState }

		local k 

		local fakeSaveResultsStreams = {}

		local expectedTestDataTuples = {}

		for i = 1, #needSaveDownloadStates do

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

			-- save task relies on baseRenderInfoCache for saving
			addBaseRenderInfoCacheEntry( 
				scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache, i )

			for _ = i, #needSaveDownloadStates do

				k = ( k or 0 ) + 1

				expectedTestDataTuples[ k ] = { pixStreamId( i ), localAssetId( i ) }

				fakeSaveResultsStreams[ k ] = Rx.Subject()

			end

		end

		scheduler._testData.runLoopTaskStates.PixelProviderSaveDownloadedTask.saveToDiskThread =
			makeFakeSaveThread( rxTest, "rxTestSaveDownloadedNegativeFail", fakeSaveResultsStreams )

		local expectedSendPermanentErrorTuples = {}

		local strings = {}

		local ksDatas = {}

		local paths = {}

		local watchNegativeSaves = rxTest:observe( 
										getNFromTestInfoStream(
												rxTest,
												"watching for saveFailed",
												scheduler._testData.testInfoStream,
												#fakeSaveResultsStreams,
												isTestMessage( 'saveFailed' ),
												stripTestMessageAndMakeTuple )
											:map( sortTupleArray ) )

		for i, needSaveDownloadState in ipairs( needSaveDownloadStates ) do

			local ozBinaryType = PixelProviderSharedBlobCache.isProxyDownloadState( 
										needSaveDownloadState ) and
											'proxy' or 'original'
									
			local ozBinarySubtype = ozBinaryType == 'original' and 'master' or '2560'

			strings[ i ] = makeSaveDataStr( i )

			ksDatas[ i ] = KSData.make( strings[ i ] )

			if ozBinaryType == 'original' then

				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithSha256(
					localAssetId( i ), ksDatas[ i ], blobSha256( i ) )

			elseif ozBinaryType == 'xmp_develop' then

					scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushXmpDevelopBlob(
						localAssetId( i ), ksDatas[ i ] )

			else

				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushBlob(
					localAssetId( i ), ksDatas[ i ], ozBinaryType, ozBinarySubtype )

			end

			paths[ i ] = makeUniqueSavePath( i )

			rxTest:log( "Making sure no saved negative exists at " .. tostring( paths[ i ] ) )

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

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

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

		end

		rxTest:log( "Scheduling a test expecting saveFailed" )

		SchedulerTestAPI.runScheduledRequests( scheduler )

		for i = 1, #fakeSaveResultsStreams do

			fakeSaveResultsStreams[ i ]:onError( string.format( "fale wail #%s!", i ) )

		end

		watchNegativeSaves:expectData( sortTupleArray( expectedTestDataTuples ) )

		rxTest:log( "Finishing a test expecting saveFailed" )

		rxTest:expectEqual( 
			{},
			sortTupleArray( 
				scheduler:_mapAllRequestsWithState( 
							PixelPathProviderStateMachine.SendDataStates.HaveRenditionPath,
							takeFirstN( 2 ) ) ) )

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

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

			local renderSize = 100 + i

			rxTest:expectEqual( nil,
				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
					localAssetId( i ), 'rendition', renderSize, digest( i ) ) )

			rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( ksDatas[ i ] ) )

		end

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

	end

end

--------------------------------------------------------------------------------
-- rxTestQuitPendingSaveThread
--
-- 	test quitting the scheduler main loop before the save thread finished (save
-- 	thread is mocked)
--------------------------------------------------------------------------------

local function rxTestQuitPendingSaveThread( rxTest )

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

	local catalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )

	local scheduler = PixelPathProviderScheduler( catalog )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )

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

	local fakeSaveResultsStreams = { Rx.Subject() }

	scheduler._testData.runLoopTaskStates.PixelProviderSaveDownloadedTask.saveToDiskThread =
		makeFakeSaveThread( 
			rxTest, "rxTestQuitPendingSaveThread", fakeSaveResultsStreams )

	local strings = {}

	local ksDatas = {}

	strings[ 1 ] = makeSaveDataStr( 1 )

	ksDatas[ 1 ] = KSData.make( strings[ 1 ] )

	scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithSha256(
		localAssetId( 1 ), ksDatas[ 1 ], blobSha256( 1 ) )

	SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
								PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveOriginal )

	rxTest:expectEqual( true,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 1 ), 'original', 'master' ) ~= nil )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	scheduler:close()

	-- check to make sure all the render blobs are cleaned up
	rxTest:expectEqual( nil,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 1 ), 'original', 'master' ) )

	fakeSaveResultsStreams[ 1 ]:onCompleted()

	rxTest:expectEqual( false, WFCloseSupport.hasBeenOrWillBeClosed( ksDatas[ 1 ] ) )

end

--------------------------------------------------------------------------------
-- rxTestPurgeAbandoned
--
-- test that abandoned blobs are purged
--------------------------------------------------------------------------------

local function rxTestPurgeAbandoned( rxTest )

	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	local strings = {}

	local ksDatas = {}

	strings[ 1 ] = makeSaveDataStr( 1 )

	ksDatas[ 1 ] = KSData.make( strings[ 1 ] )

	scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithSha256(
		localAssetId( 1 ), ksDatas[ 1 ], blobSha256( 1 ) )

	strings[ 2 ] = makeSaveDataStr( 2 )

	ksDatas[ 2 ] = KSData.make( strings[ 2 ] )

	scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushBlob(
		localAssetId( 2 ), ksDatas[ 2 ], 'proxy', '2560' )

	rxTest:expectEqual( true,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 1 ), 'original', 'master' ) ~= nil )

	rxTest:expectEqual( true,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 2 ), 'proxy', '2560' ) ~= nil )

	SchedulerTestAPI.runScheduledRequests( scheduler )

	-- check to make sure all the render blobs are cleaned up

	rxTest:expectEqual( nil,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 1 ), 'original', 'master' ) )

	rxTest:expectEqual( nil,
		scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
			localAssetId( 2 ), 'proxy', '2560' ) )

	rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( ksDatas[ 1 ] ) )

	rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( ksDatas[ 2 ] ) )

end

--------------------------------------------------------------------------------
-- rxTestJoinActiveSave
-- 
-- Test that all appropriate requests join an active save (save thread is
-- mocked)
--------------------------------------------------------------------------------

local function isOzBinaryTypeState( ozBinaryType, state )

	return string.find( string.lower( state ), 
						string.lower( ozBinaryType ) ) ~= nil

end

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

local function rxTestJoinActiveSave( rxTest )

	local scheduler, catalog  = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )

	-- run the runLoop once to bootstrap test data
	SchedulerTestAPI.runScheduledRequests( scheduler )

	addRecordToDocStore( rxTest, catalog, 1 )

	for _, ozBinaryType in ipairs( { 'original', 'proxy' } ) do

		local ozBinarySubtype = ozBinaryType == 'proxy' and '2650' or 'master'

		local needToSaveBinaryStates =
				ozBinaryType == 'proxy' and 
					PixelProviderSharedBlobCache.needToSaveProxyStates or
					PixelProviderSharedBlobCache.needToSaveOriginalStates

		for _, needToSaveBinaryState in ipairs( needToSaveBinaryStates ) do

			local expectedJoinedSaveTuples = {}

			local expectedSameStateTuples = {}

			-- set up a pending save request

			local fakeSaveResultsStreams = { Rx.Subject() }

			scheduler._testData.runLoopTaskStates.PixelProviderSaveDownloadedTask.saveToDiskThread =
				makeFakeSaveThread( 
					rxTest, "rxTestJoinActiveSave", fakeSaveResultsStreams )

			-- suspend runLoop to make sure it doesn't advance our reqeust
			-- state until we want it to do so

			SchedulerTestAPI.suspendRunLoop( scheduler )

			-- save task relies on baseRenderInfoCache for saving
			addBaseRenderInfoCacheEntry( 
				scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache, 1 )

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

			local ksData = KSData.make( makeSaveDataStr( 1 ) )

			if ozBinaryType == 'original' then

				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushOriginalBlobWithSha256(
					localAssetId( 1 ), ksData, blobSha256( 1 ) )

			else

				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushBlob(
					localAssetId( 1 ), ksData, ozBinaryType, ozBinarySubtype )

			end

			SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
										needToSaveBinaryState )
					
			rxTest:logf( "expecting that %s.%s.%s will join save", 
					 				pixStreamId( 1 ), localAssetId( 1 ), needToSaveBinaryState )

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

			-- resume runLoop long enough to set up the save
			
			SchedulerTestAPI.resumeRunLoop( scheduler )
			
			SchedulerTestAPI.runScheduledRequests( scheduler )

			-- suspend runLoop as we add the rest of these requests
			SchedulerTestAPI.suspendRunLoop( scheduler )

			-- add a bunch requests that should join this one
			for i, state in ipairs( PixelProviderSharedBlobCache.fulfilledBySuccessfulNegativeSaveStates ) do

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

				SchedulerTestAPI.setState( scheduler, pixStreamId( i + 1 ), localAssetId( 1 ), state ) 

				if isOzBinaryTypeState( ozBinaryType, state ) then

					-- these are "eventually needs saved negative states" that
					-- we expect to join this one.
					
					rxTest:logf( "expecting that %s.%s.%s will join save", 
					 				pixStreamId( i + 1 ), localAssetId( 1 ), state )

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

				else

					-- these are "eventually needs saved negative states" that
					-- are a different negative type, so we don't expect the state
					-- to change

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

				end

			end

			local watchNegativeSaves = rxTest:observe( 
											getNFromTestInfoStream(
													rxTest,
													"watching for didPixelCacheSave",
													scheduler._testData.testInfoStream,
													#expectedJoinedSaveTuples,
													isTestMessage( "didPixelCacheSave" ),
													stripTestMessageAndMakeTuple )
												:map( sortTupleArray ) )


			fakeSaveResultsStreams[ 1 ]:onCompleted()

			rxTest:log( "Waiting to process save for " .. needToSaveBinaryState )

			watchNegativeSaves:expectData( sortTupleArray( expectedJoinedSaveTuples ) )

			rxTest:log( "Finished process save for " .. needToSaveBinaryState )

			rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( ksData ) )

			rxTest:expectEqual( 
				{},
				sortTupleArray( 
					scheduler:_mapAllRequestsWithState( 
								PixelPathProviderStateMachine.SendDataStates.SendPermanentError,
								takeFirstN( 1 ) ) ) )

			rxTest:expectEqual( 
				sortTupleArray( expectedJoinedSaveTuples ),
				sortTupleArray( 
					scheduler:_mapAllRequestsWithStates( 
								{ PixelPathProviderStateMachine.SendDataStates.HaveLocalNegativePath,
									PixelPathProviderStateMachine.RenderStates.NeedsRender,
									ozBinaryType ~= 'proxy' and
										PixelPathProviderStateMachine.SendDataStates.HaveOriginalForBestPossibleLocalNegativePath or
										PixelPathProviderStateMachine.SendDataStates.HaveProxyForBestPossibleLocalNegativePath
									},
								takeFirstN( 2 ) ) ) )

			rxTest:expectEqual( 
				sortTupleArray( expectedSameStateTuples ),
				sortTupleArray( 
					scheduler:_mapAllRequestsWithStates( 
								PixelProviderSharedBlobCache.fulfilledBySuccessfulNegativeSaveStates,
								extractPixStreamIdLocalAssetIdState ) ) )

			scheduler:abandonAllLocalAssetIdRequests( localAssetId( 1 ) )
		
			scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache:destroyAll()

			-- resume runLoop, this test is done, and suspend/resume nest
			SchedulerTestAPI.resumeRunLoop( scheduler )

		end

	end

end

--------------------------------------------------------------------------------
-- rxTestSaveDownloadedXmp 
-- 
-- 		test a real save
--
--------------------------------------------------------------------------------

local function rxTestSaveDownloadedXmp( rxTest )

	rxTest:setTimeLimit( 5 )
	
	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.runScheduledRequests( scheduler )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )
	
	local mockDocStore, docValuesPerId = makeMockDocstoreForSaveTask()

	SchedulerTestAPI.setTaskState( 
		scheduler, "PixelProviderSaveDownloadedTask",
		"docStore", mockDocStore )

	local fakeXmpStr = makeSaveDataStr( 1 )

	local sha256 = WFCrypto.SHA256.digest( fakeXmpStr )

	local expectedHappyPathEndState = {

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.LocalBinaryPathCheckStates.NeedCheckIfExternalDevelopAuxFilesAreLocal,

			[ PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.LocalBinaryPathCheckStates.RenditionStrategyNeedCheckIfExternalDevelopAuxFilesAreLocal,

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpSidecar ]=
				PixelPathProviderStateMachine.SendDataStates.HaveLocalXmpSidecarPath,

		}

	for _, needSaveXmpState in ipairs( 
			{
				PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop,
				PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop,
				PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpSidecar,
			} ) do


		rxTest:log( "add fake doc value for asset 1" )

		docValuesPerId[ 1 ] = 
			{ 
				_id = id( 1 ),
				develop = {
					xmpCameraRaw = {
						sha256 = sha256
					}
				},
			}

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

		-- save task relies on baseRenderInfoCache for saving
		addBaseRenderInfoCacheEntry( 
			scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache, 1 )

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

			docValuesPerId[ 1 ]._localOnly =
				testWithPrevLocalOnly and
					{
						files = {
							original = {
								xmp = {
									fileSize = 123,
									path = "prev",
								}
							},
							xmp_develop = {
								fileSize = 123,
								path = "prev",
								sha256 = "nope",
							},
						}
					} or
					nil

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

				local expectedTestDataTuples = {}

				local expectedErrorStateTuples = {}

				local expectedEndSaveRequestTuples = {}

				local xmpBlob

				local expectedTestMessage = testWithXmpBlob and 
												"didPixelCacheSave" or "skipSaveNoBlob"

				local watchNegativeSaves = rxTest:observe( 
												getNFromTestInfoStream(
														rxTest, 
														"watching for " .. expectedTestMessage,
														scheduler._testData.testInfoStream,
														1,
														isTestMessage( expectedTestMessage ),
														stripTestMessageAndMakeTuple )
													:map( sortTupleArray ) )

				local ozBinaryType = needSaveXmpState ~=
										PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpSidecar and
												'xmp_develop' or 'xmp_sidecar'

				if testWithXmpBlob then

					xmpBlob = KSData.make( fakeXmpStr )


					if ozBinaryType == 'xmp_develop' then

							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushXmpDevelopBlob(
								localAssetId( 1 ), xmpBlob )

					else

						scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushBlob(
							localAssetId( 1 ), xmpBlob, ozBinaryType, ozBinaryType )

					end

				end

				SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
											needSaveXmpState )

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

				expectedErrorStateTuples[ #expectedErrorStateTuples + 1 ] = 
					not( testWithXmpBlob ) and
						{ pixStreamId( 1 ), localAssetId( 1 ) } or nil

				expectedEndSaveRequestTuples[ #expectedEndSaveRequestTuples + 1 ] = 
					testWithXmpBlob and 
							{ pixStreamId( 1 ), localAssetId( 1 ) } or nil

				rxTest:logf( "Scheduling a %s test expecting %s", needSaveXmpState, expectedTestMessage )

				SchedulerTestAPI.runScheduledRequests( scheduler )

				watchNegativeSaves:expectData( sortTupleArray( expectedTestDataTuples ) )

				rxTest:logf( "Finishing a %s test expecting %s", needSaveXmpState, expectedTestMessage )

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

				rxTest:expectEqual( 
					sortTupleArray( expectedEndSaveRequestTuples ),
					sortTupleArray( 
						scheduler:_mapAllRequestsWithState( 
									expectedHappyPathEndState[ needSaveXmpState ],
									takeFirstN( 2 ) ) ) )

				if testWithXmpBlob then

					-- check docValue is right
					rxTest:logf( "Checking docValue._localOnly.files %s", 
									WFTableArchiving.debugTableToString(
										KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files" ) ) )

					if needSaveXmpState ~=
								PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpSidecar then

						rxTest:expectEqual( 
							"number",
							type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "fileSize" ) ) )

						rxTest:expectEqual( 
							"string",
							type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "path" ) ) )

						rxTest:logf( "checking for matching sha %s for %s", sha256, needSaveXmpState )

						rxTest:expectEqual( 
							sha256,
							KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "sha256" ) )

					else

						rxTest:expectEqual( 
							"number",
							type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", "original", "xmp", "fileSize" ) ) )

						rxTest:expectEqual( 
							"string",
							type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", "original", "xmp", "path" ) ) )

					end

					rxTest:logf( "Check that there is no docValue._localOnly.%s.%s", ozBinaryType, ozBinaryType )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, ozBinaryType ) )

					-- check to make sure all the render blobs are cleaned up
					--
											
					if ozBinaryType == 'xmp_develop' then

						rxTest:expectEqual( nil,
							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekXmpDevelopBlob(
								localAssetId( 1 ) ) )

					else

						rxTest:expectEqual( nil,
							scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
								localAssetId( 1 ), ozBinaryType, ozBinaryType ) )

					end

					local path = unpack(
						scheduler._testData.runLoopCaches.sharedCaches.localBinaryPathCache:popCacheItem( 
											pixStreamId( 1 ), localAssetId( 1 ) ) or {} )

					rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( xmpBlob ) )

					rxTest:log( "Making sure saved xmp exists at " .. path )

					rxTest:expectEqual( "file", WFFileUtils.exists( path ) )

					local f = io.open( path, 'r' )

					local contents = f:read( "*all" )

					f:close()

					WFFileUtils.delete( path )

					rxTest:expectEqual( fakeXmpStr, contents )

				end
				
				if not testWithXmpBlob then
				end

			end

		end

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

end

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

--[[
-- use logRequest to debug tests
--
-- 				scheduler
-- 					:_mapAllRequestsWithPixStreamId( 
-- 						pixStreamId( 1 ), 
-- 							logRequest, 
-- 								rxTest, 
-- 								string.format( "after runScheduledRequests with state %s", 
-- 												needSaveXmpState ) )

local function logRequest( request, rxTest, desc )

	local requestPlaceHolders = "%s" .. string.rep( ", %s", #request )

	rxTest:logf( "%s: { " .. requestPlaceHolders .. " } ",
					desc, unpack( request ) )

end
--]]

--------------------------------------------------------------------------------
-- rxTestSaveDownloadedXmp 
-- 
-- 		test a real save with and without sha256 mismatch
--
--------------------------------------------------------------------------------
local function rxTestXmpDevelopSha256Match( rxTest )

	rxTest:setTimeLimit( 5 )
	
	local scheduler = makeSchedulerForTest( rxTest )

	local SchedulerTestAPI = scheduler:_expose_test_API()

	SchedulerTestAPI.runScheduledRequests( scheduler )

	SchedulerTestAPI.addRunLoopTaskToAllowlist( scheduler, 
												"PixelProviderSaveDownloadedTask" )
	
	local mockDocStore, docValuesPerId = makeMockDocstoreForSaveTask()

	SchedulerTestAPI.setTaskState( 
		scheduler, "PixelProviderSaveDownloadedTask",
		"docStore", mockDocStore )

	local fakeXmpStr = makeSaveDataStr( 1 )

	local sha256 = WFCrypto.SHA256.digest( fakeXmpStr )

	local bad_sha256 = sha256[ 1 ] ~= 'f' and 'f' or 'e' string.sub( sha256, 2 )

	local expectedMatchingSha256EndState = {

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.LocalBinaryPathCheckStates.NeedCheckIfExternalDevelopAuxFilesAreLocal,

			[ PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.LocalBinaryPathCheckStates.RenditionStrategyNeedCheckIfExternalDevelopAuxFilesAreLocal,

		}

	local expectedTempMismatchSha256EndState = {

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.DocstoreInfoStates.NeedXmpDevelopInfo,

			[ PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.LocalBinaryPathCheckStates.RenditionStrategyNeedCheckIfExternalXmpDevelopIsLocal,

		}

	local expectedFatalMismatchSha256EndState = {

			[ PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.SendDataStates.SendPermanentError,

			[ PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop ]=
				PixelPathProviderStateMachine.SendDataStates.SendPermanentError,

		}

	local mismatchCount = 0

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

		for _, needSaveXmpState in ipairs( 
				{
					PixelPathProviderStateMachine.SaveDownloadStates.NeedToSaveXmpDevelop,
					PixelPathProviderStateMachine.SaveDownloadStates.RenderNeedToSaveXmpDevelop,
				} ) do

			rxTest:log( "add fake doc value for asset 1" )

			docValuesPerId[ 1 ] = 
				{ 
					_id = id( 1 ),
					develop = {
						xmpCameraRaw = {
							sha256 = matchingSha256 and sha256 or bad_sha256
						}
					},
				}

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

			-- save task relies on baseRenderInfoCache for saving
			addBaseRenderInfoCacheEntry( 
				scheduler._testData.runLoopCaches.sharedCaches.baseRenderInfoCache, 1 )

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

				rxTest:logf( "========>Running test with matchingSha256 %s, needSaveXmpState %s, testWithPrevLocalOnly %s", 
								matchingSha256, needSaveXmpState, testWithPrevLocalOnly )

				local prevFileSize = 123

				local prevPath = "prevPath"

				local prevSha256 = "prevSha256"

				docValuesPerId[ 1 ]._localOnly =
					testWithPrevLocalOnly and
						{
							files = {
								xmp_develop = {
									fileSize = prevFileSize,
									path = prevPath,
									sha256 = prevSha256,
								},
							}
						} or
						nil

				local expectedTestDataTuples = {}

				local expectedEndSaveRequestTuples = {}

				local xmpBlob = KSData.make( fakeXmpStr )

				local ozBinaryType = 'xmp_develop' or 'xmp_sidecar'

				scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:pushXmpDevelopBlob(
					localAssetId( 1 ), xmpBlob )

				local expectingSaveFailure = 
						not matchingSha256 and
							( PixelProviderSaveDownloadedTask._privateTestExport.TREAT_PERSISTENT_XMP_DEVELOP_SHA256_MISMATCH_AS_FATAL or
								mismatchCount < 1 )

				local expectedTestMessage = 
						not expectingSaveFailure and
							"didPixelCacheSave" or "saveFailed"

				local watchNegativeSaves = rxTest:observe( 
												getNFromTestInfoStream(
														rxTest, 
														"watching for " .. expectedTestMessage,
														scheduler._testData.testInfoStream,
														1,
														isTestMessage( expectedTestMessage ),
														stripTestMessageAndMakeTuple )
													:map( sortTupleArray ) )

				SchedulerTestAPI.setState( scheduler, pixStreamId( 1 ), localAssetId( 1 ), 
											needSaveXmpState )

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

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

				rxTest:logf( "Scheduling a %s test expecting %s", needSaveXmpState, expectedTestMessage )

				SchedulerTestAPI.runScheduledRequests( scheduler )

				watchNegativeSaves:expectData( sortTupleArray( expectedTestDataTuples ) )

				rxTest:logf( "Finishing a %s test expecting %s", needSaveXmpState, expectedTestMessage )

				if not matchingSha256 then

					mismatchCount = mismatchCount + 1

				end

				local expectedSaveEndState = 
						( expectingSaveFailure and mismatchCount >= 2 and
							expectedFatalMismatchSha256EndState[ needSaveXmpState ] ) or
						( expectingSaveFailure and mismatchCount < 2 and
							expectedTempMismatchSha256EndState[ needSaveXmpState ] ) or
						expectedMatchingSha256EndState[ needSaveXmpState ]

				rxTest:logf( "Checking the %s test request tuples with %s are correct mismatchCount %s", 
								needSaveXmpState, expectedSaveEndState, mismatchCount )

				rxTest:expectEqual( 
					sortTupleArray( expectedEndSaveRequestTuples ),
					sortTupleArray( 
						scheduler:_mapAllRequestsWithState( 
									expectedSaveEndState,
									takeFirstN( 2 ) ) ) )

				-- check docValue is right
				rxTest:logf( "Checking docValue._localOnly.files %s when sha256 match is %s and has prev local only %s", 
								matchingSha256, testWithPrevLocalOnly,
								WFTableArchiving.debugTableToString(
									KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files" ) or {} ) )

				if not expectingSaveFailure then

					rxTest:expectEqual( 
						"number",
						type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "fileSize" ) ) )

					rxTest:expectEqual( 
						"string",
						type( KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "path" ) ) )

					rxTest:logf( "checking for matching sha %s for %s", sha256, needSaveXmpState )

					rxTest:expectEqual( 
						sha256,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "sha256" ) )

					rxTest:logf( "Check that there is no docValue._localOnly.%s.%s", ozBinaryType, ozBinaryType )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, ozBinaryType ) )

				elseif testWithPrevLocalOnly then

					rxTest:expectEqual( 
						prevFileSize,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "fileSize" ) )

					rxTest:expectEqual( 
						prevPath,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "path" ) )

					rxTest:logf( "checking for matching sha %s for %s", sha256, needSaveXmpState )

					rxTest:expectEqual( 
						prevSha256,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "sha256" ) )

					rxTest:logf( "Check that there is no docValue._localOnly.%s.%s", ozBinaryType, ozBinaryType )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, ozBinaryType ) )

				else

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "fileSize" ) )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "path" ) )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, "sha256" ) )

					rxTest:logf( "Check that there is no docValue._localOnly.%s.%s", ozBinaryType, ozBinaryType )

					rxTest:expectEqual( 
						nil,
						KSTableUtils.derefOpt( docValuesPerId[ 1 ], "_localOnly", "files", ozBinaryType, ozBinaryType ) )
				end

				-- check to make sure all the render blobs are cleaned up
				--
										
				if ozBinaryType == 'xmp_develop' then

					rxTest:expectEqual( nil,
						scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekXmpDevelopBlob(
							localAssetId( 1 ) ) )

				else

					rxTest:expectEqual( nil,
						scheduler._testData.runLoopCaches.sharedCaches.pendingSaveBlobCache:peekBlob(
							localAssetId( 1 ), ozBinaryType, ozBinaryType ) )

				end

				local path = unpack(
					scheduler._testData.runLoopCaches.sharedCaches.localBinaryPathCache:popCacheItem( 
										pixStreamId( 1 ), localAssetId( 1 ) ) or {} )

				rxTest:expectEqual( true, WFCloseSupport.hasBeenOrWillBeClosed( xmpBlob ) )

				if matchingSha256 then

					rxTest:logf( "Making sure saved xmp exists at %s", path )

					rxTest:expectEqual( "file", WFFileUtils.exists( path ) )

					local f = io.open( path, 'r' )

					local contents = f:read( "*all" )

					f:close()

					WFFileUtils.delete( path )

					rxTest:expectEqual( fakeXmpStr, contents )

				end

			end

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

end

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

local function rxTestGetXmpDevelopSavePath( rxTest )

	local fakeDocStore = {}

	local fakeFileSpecObj = 
		{

			pathForSettingsXmp = function( _, xmpSha256 ) return xmpSha256 end,

		}

	local sha256_1 = WFCrypto.SHA256.digest( "sha256_1" )

	local sha256_2 = WFCrypto.SHA256.digest( "sha256_2" )

	for _, xmpBlobSha256 in ipairs( { sha256_1, sha256_2, false } ) do

		for _, docValSha256 in ipairs( { sha256_1, sha256_2, false } ) do

			local docValue = { _id = id( 1 ), _localId = 1 }

			if docValSha256 then

				KSTableUtils.setNested( 
					docValue, docValSha256, "develop", "xmpCameraRaw", "sha256" )

			end

			rxTest:logf( "---> testing with xmpBlobSha256 %s and docValSha256 %s", 
							xmpBlobSha256, docValSha256 )

			-- call it the first time, we expect to succeed only if we have two
			-- sha256s and they match
			local ok1, actualXmpPath1 = 
					PixelProviderSaveDownloadedTask._privateTestExport.getXmpDevelopSavePath( 
							fakeDocStore, fakeFileSpecObj, docValue._localId,
							docValue, xmpBlobSha256 or nil )

			local expectedOk1 = xmpBlobSha256 and docValSha256 and
									xmpBlobSha256 == docValSha256

			rxTest:logf( "checking if we got the path ok the first time: %s, actual %s", expectedOk1, ok1 )

			rxTest:expectEqual( expectedOk1, ok1 )

			if ok1 then

				rxTest:logf( "checking if we got the right path the first time: %s, actual %s", docValSha256, actualXmpPath1 )

				rxTest:expectEqual( docValSha256, actualXmpPath1 )

			end

			-- call it the second time, we expect it to succeed if we have two
			-- sha256s even if they mismatch
			local ok2, actualXmpPath2 = 
					PixelProviderSaveDownloadedTask._privateTestExport.getXmpDevelopSavePath( 
							fakeDocStore, fakeFileSpecObj, docValue._localId,
							docValue, xmpBlobSha256 or nil )

			local expectedOk2 = xmpBlobSha256 and docValSha256 and true or false

			rxTest:logf( "checking if we got the path ok the second time: %s, actual %s", expectedOk2, ok2 )

			rxTest:expectEqual( expectedOk2, ok2 )

			if ok2 then

				rxTest:logf( "checking if we got the right path the second time: %s, actual %s", docValSha256, actualXmpPath2 )

				rxTest:expectEqual( docValSha256, actualXmpPath2 )

			end

		end

	end

end

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

return WFRxTestCase.wrapSuite( 'PixelProviderSaveDownloadedTaskTest', {

	rxTestSaveDownloadedNegative = rxTestSaveDownloadedNegative,

	rxTestSaveDownloadedNegativeFail = rxTestSaveDownloadedNegativeFail,

	rxTestQuitPendingSaveThread = rxTestQuitPendingSaveThread,

	rxTestPurgeAbandoned = rxTestPurgeAbandoned,

	rxTestJoinActiveSave = rxTestJoinActiveSave,

	rxTestSaveDownloadedXmp = rxTestSaveDownloadedXmp,

	rxTestXmpDevelopSha256Match = rxTestXmpDevelopSha256Match,

	rxTestGetXmpDevelopSavePath = rxTestGetXmpDevelopSavePath,

} )
