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

WFCatTrashDesktopTest.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2019 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

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

local WFPlatformInfo = require "WFPlatformInfo"
if not WFPlatformInfo.isDesktop then
	return {}
end

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

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

	-- WF.core.self_test
local WFRxTestCase = require 'WFRxTestCase'
local WFTestFileUtils = require 'WFTestFileUtils'

	-- WF.core.os
local KSDate = require 'KSDate'
local KSPathUtils = require 'KSPathUtils'
local WFFileUtils = require 'WFFileUtils'

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local KSTableUtils = require 'KSTableUtils'

	-- WF.oz_client.managed_catalog
local NmbLocalRenditionDigest = require 'NmbLocalRenditionDigest'
local WFCatCopies = require 'WFCatCopies'
local WFCatDelete = require 'WFCatDelete'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatIdUtils = require 'WFCatIdUtils'
local WFCatIdleDownloader = require 'WFCatIdleDownloader'
local WFCatTrash = require 'WFCatTrash'
local WFCatTrashTestUtils = require 'WFCatTrashTestUtils'
local WFCatTrashUtils = require 'WFCatTrashUtils'
local WFCatStacks = require 'WFCatStacks'
local WFCatStorage = require 'WFCatStorage'

	-- WF.web.http_client
local WFMockHttpRequest = require 'WFMockHttpRequest'

	-- WF.web.json
local KSJsonDecoder = require 'KSJsonDecoder'
local KSJsonEncoder = require 'KSJsonEncoder'

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

local coveragePath

local child = KSPathUtils.child
local parent = KSPathUtils.parent
local derefOpt = KSTableUtils.derefOpt

local NmbLocalRenditionDigest_maybeComputeFromDocValue = assert( NmbLocalRenditionDigest.maybeComputeFromDocValue )

local addDocumentWithLocalMaster = assert( WFCatTrashTestUtils.addDocumentWithLocalMaster )
local addDocumentWithLocalMasterAndProxy = assert( WFCatTrashTestUtils.addDocumentWithLocalMasterAndProxy )
local addXmpDevelopToDocument = assert( WFCatTrashTestUtils.addXmpDevelopToDocument )
local addXmpSidecarToDocument = assert( WFCatTrashTestUtils.addXmpSidecarToDocument )
local albumAssetUpdateResponse = assert( WFCatTrashTestUtils.albumAssetUpdateResponse )
local albumUpdateResponse = assert( WFCatTrashTestUtils.albumUpdateResponse )
local assetDeletedResponse = assert( WFCatTrashTestUtils.assetDeletedResponse )
local assetUpdateResponse = assert( WFCatTrashTestUtils.assetUpdateResponse )
local canDownloadRequiredBinaryFiles = assert( WFCatTrashTestUtils.canDownloadRequiredBinaryFiles )
local catalogPrefetchReqResp = assert( WFCatTrashTestUtils.catalogPrefetchReqResp )
local createMasterReqResp = assert( WFCatTrashTestUtils.createMasterReqResp )
local createProxyReqResp = assert( WFCatTrashTestUtils.createProxyReqResp )
local emptyPullReqResp = assert( WFCatTrashTestUtils.emptyPullReqResp )
local futureUpdatesResponse = assert( WFCatTrashTestUtils.futureUpdatesResponse )
local getLatestRevisionId = assert( WFCatTrashTestUtils.getLatestRevisionId )
local getNewlyCreatedAsset = assert( WFCatTrashTestUtils.getNewlyCreatedAsset )
local getOriginalFilePath = assert( WFCatTrashTestUtils.getOriginalFilePath )
local getProxyFilePath = assert( WFCatTrashTestUtils.getProxyFilePath )
local getTrashAssetForDeletedAsset = assert( WFCatTrashTestUtils.getTrashAssetForDeletedAsset )
local httpGetAssetAsDecodedJson = assert( WFCatTrashTestUtils.httpGetAssetAsDecodedJson )
local makeImportSource = assert( WFCatTrashTestUtils.makeImportSource )
local pollForUpdateWith = assert( WFCatTrashTestUtils.pollForUpdateWith )
local pollUntilHasRequiredLinks = assert( WFCatTrashTestUtils.pollUntilHasRequiredLinks )
local pollUploadCount = assert( WFCatTrashTestUtils.pollUploadCount )
local prefetchReqResp = assert( WFCatTrashTestUtils.prefetchReqResp )
local pushCreateAlbumAssetReqResp = assert( WFCatTrashTestUtils.pushCreateAlbumAssetReqResp )
local pushCreateAlbumReqResp = assert( WFCatTrashTestUtils.pushCreateAlbumReqResp )
local pushCreateAssetReqResp = assert( WFCatTrashTestUtils.pushCreateAssetReqResp )
local pushCreateMultipleAssetsReqResp = assert( WFCatTrashTestUtils.pushCreateMultipleAssetsReqResp )
local pushDeleteAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteAssetReqResp )
local pushDeleteRestoreAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteRestoreAssetReqResp )
local pushDeleteTrashAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteTrashAssetReqResp )
local pushDeleteTrashReqResp = assert( WFCatTrashTestUtils.pushDeleteTrashReqResp )
local pushRestoreAssetReqResp = assert( WFCatTrashTestUtils.pushRestoreAssetReqResp )
local pushUpdateAssetReqResp = assert( WFCatTrashTestUtils.pushUpdateAssetReqResp )
local pushUpdateRestoredReqResp = assert( WFCatTrashTestUtils.pushUpdateRestoredReqResp )
local restoredAssetUpdateResponse = assert( WFCatTrashTestUtils.restoredAssetUpdateResponse )
local setupCloudTrashTestSession = assert( WFCatTrashTestUtils.setupCloudTrashTestSession )
local simulateOtherClientAssetDelete = assert( WFCatTrashTestUtils.simulateOtherClientAssetDelete )
local stripBothUpdateFields = assert( WFCatTrashTestUtils.stripBothUpdateFields )
local stripForAssetUpdate = assert( WFCatTrashTestUtils.stripForAssetUpdate )
local stripMissingProxyInTrashAssetUpdate = assert( WFCatTrashTestUtils.stripMissingProxyInTrashAssetUpdate )
local syncStatusQuery = assert( WFCatTrashTestUtils.syncStatusQuery )
local transformOrderAndStripAssetUpdate = assert( WFCatTrashTestUtils.transformOrderAndStripAssetUpdate )
local trashAssetUpdateResponse = assert( WFCatTrashTestUtils.trashAssetUpdateResponse )
local uploadXmpDevelopReqResp = assert( WFCatTrashTestUtils.uploadXmpDevelopReqResp )
local uploadXmpReqResp = assert( WFCatTrashTestUtils.uploadXmpReqResp )

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5
local stripped = '<<stripped>>'

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

local WFCatTrashDesktopTest = {}

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

local function deleteBinariesFromDocValue( catalog, docValue )

	local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
	local proxyPath = WFCatFileSpec.buildProxyPathFromDocValue( docValue )
	local xmpSidecarPath = WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )
	local xmpDevelopPath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )

	if originalPath then
		WFFileUtils.delete( originalPath )
	end

	if proxyPath then
		WFFileUtils.delete( proxyPath )
	end

	if xmpSidecarPath then
		WFFileUtils.delete( xmpSidecarPath )
	end

	if xmpDevelopPath then
		WFFileUtils.delete( xmpDevelopPath )
	end

	local overlay = {
		_id = docValue._id,
		_localOnly = {
			files = {
				original = { _delete = true }, -- master and xmp sidecar
				proxy = { _delete = true }, -- proxies
				xmp_develop = { _delete = true }, -- external develop settings
			}
		}
	}

	return catalog:docStore():applyOverlay( overlay )

end

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

local function createCopyXmpReqResp( catalogId, assetId, revisionId, sourceXmpLink, errorCode, newId )
	
	local url = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp'

	errorCode = errorCode or '201'

	local response = {
		[ 'Content-Length' ] = 0,
		Location = "",
	}

	local fail
	
	if errorCode == '404' then
		fail = {
			{
				code = 1000,
				description = "Resource not found",
				errors = {
					revision = {
						"does not exist",
					},
				},
				new_id = newId,
				subtype = "ResourceNotFoundError",
			},
			{
				status_code = errorCode,
				[ 'X-Result-Code' ] = '1000',
			}
		}
	end
	
	local requestHeaders =  {
				{
					header = "Accept",
					value = "application/json",
				},
				{
					header = "Content-Type",
					value = "application/json",
				}
			}
	
	return {
		url = url,
		data = '{ "file_href": "' .. sourceXmpLink .. '" }',
		options = {
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 1,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		response = response,
		fail = fail,

		headers = {
			status_code = errorCode,
		},
	}
	

end

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

local function createCopyMasterReqResp( catalogId, assetId, revisionId, sourceAssetLink, errorCode, new_id )

	errorCode = errorCode or '202'

	local data = '{ "file_href": "' .. sourceAssetLink .. '" }'

	local requestHeaders = {
		{
			header = "X-Generate-Renditions",
			value = "all",
		},
		{ 
			header = "X-Read-Meta",
			value = "none"
		},
		{
			header = "Content-Type",
			value = "application/json",
		},
		{
			header = "Accept",
			value = "application/json",
		},
		{
			header = "X-Generate-Proxies",
			value = "all",
		},
		{
			header = "X-Async-Copy",
			value = "true",
		},
	}

	local response = ""
	local responseHeaders = nil
	if errorCode == '404' then

		response = KSJsonEncoder.encodeOzUploadJson( {
			code = 1000,
			description = "Resource not found",
			errors = {
				revision = { "does not exist" }
			},
			new_id = new_id,
			subtype = "ResourceNotFoundError",
		} )

		responseHeaders = {
			[ 'X-Result-Code' ] = '1000'
		}

	end

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/master' )
		:withRequestOptions( {
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:withResponseHeaders( responseHeaders )
		:expectRequestDataAfterTransformation( data )
		:respond( errorCode, response )

end

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

local function createCopyUpdate( assetId, revId, subtype, original, copyId, purgeDate, userCreated, userUpdated )

	return {
		action = "create",
		id = assetId,
		revision_id = revId,
		subtype = subtype,
		type = "asset",
		original = original,
		payload = {
			captureDate = "2018-01-01T00:00:00Z",
			copy = {
				copy_id = copyId,
			},
			importSource = {
				contentType = "image/jpeg",
				fileName = "IMG_7763.JPG",
				fileSize = 2072173,
				importTimestamp = "2018-04-15T19:19:19Z",
				importedBy = "87f526b44f9e41eca6cb5a550f84ceb6",
				originalHeight = 2448,
				originalWidth = 3264,
			},
			location = {
				altitude = 18,
				latitude = 47.611714,
				longitude = -122.403008,
			},
			purgeDate = purgeDate,
			userCreated = userCreated,
			userUpdated = userUpdated,
		},
	}

end

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

local function transformJsonToTableAndStripUpdate( stripIndex, actualData, expectedData )

	actualData = KSJsonDecoder.decodeWebSafeJson( actualData )
	expectedData = KSJsonDecoder.decodeWebSafeJson( expectedData )

	stripBothUpdateFields( nil, expectedData, actualData, stripIndex, 'payload', 'location', 'country' )
	stripBothUpdateFields( nil, expectedData, actualData, stripIndex, 'payload', 'location', 'isoCountryCode' )
	stripBothUpdateFields( nil, expectedData, actualData, stripIndex, 'payload', 'location', 'state' )
	stripBothUpdateFields( stripped, expectedData, actualData, stripIndex, 'payload', 'purgeDate' )
	stripBothUpdateFields( stripped, expectedData, actualData, stripIndex, 'payload', 'userCreated' )
	stripBothUpdateFields( stripped, expectedData, actualData, stripIndex, 'payload', 'userUpdated' )
	stripBothUpdateFields( nil, expectedData, actualData, stripIndex, 'payload', 'aesthetics' )
	stripBothUpdateFields( nil, expectedData, actualData, stripIndex, 'payload', 'autoTags' )
	stripBothUpdateFields( stripped, expectedData, actualData, stripIndex, 'payload', 'importSource', 'importedBy' )

	return actualData, expectedData

end

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

local function pushUpdateOriginalAndCreateCopyReqResp( catalogId, originalAssetId, originalOzRevId, originalNewRevId, trashOrRestoredAssetId, trashOrRestoredRevId, trashOrRestoredSubtype, copiedAssetId, copyId )

	-- Fields that we are going to strip
	local userCreated = '2018-04-15T18:18:18Z'
	local trashPurgeDate = '2018-04-15T22:22:22Z'

	local original
	local purgeDate
	if trashOrRestoredSubtype == 'deleted_image' then
		original = {
			id = copiedAssetId,
			userCreated = userCreated,
		}

		purgeDate = trashPurgeDate
	end

	-- Push:
	-- 1. An update to the copied asset to add the copy_id on it.
	-- 2. A create request for the trash or restored asset since we copied it
	-- which is essentially the same as importing a new asset and then deleting and restoring it.
	local expectedUpdates = {
		{
			action = "update",
			id = originalAssetId,
			revision_id = originalNewRevId,
			type = "asset",
			if_match = originalOzRevId,
			payload = {
				copy = {
					copy_id = copyId,
				},
			},
		},
		createCopyUpdate( trashOrRestoredAssetId, trashOrRestoredRevId, trashOrRestoredSubtype, original, copyId, purgeDate, userCreated, userCreated ),
	}

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( expectedUpdates )
		:requestDataTransformation( function( actualData, expectedData )
			actualData, expectedData = transformJsonToTableAndStripUpdate( 2, actualData, expectedData )
			return actualData, expectedData
		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushCreateCopyAndDeleteAssetReqResp( catalogId, copiedAssetId, copiedRevId, copyId, assetId, revId, subtype, userCreated, trashAssetId, trashRevId, trashAssetSubtype, trashPurgeDate )

	local expectedUpdates = {
		createCopyUpdate( copiedAssetId, copiedRevId, subtype, nil, copyId, nil, userCreated, userCreated ),
		{
			action = "delete",
			type = "asset",
			deleted_asset = {
				id = trashAssetId,
				payload = {
					userCreated = userCreated,
					purgeDate = trashPurgeDate,
				},
				revision_id = trashRevId,
			},
			id = assetId,
			revision_id = revId,
		},
	}

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( expectedUpdates )
		:requestDataTransformation( function( actualData, expectedData )
			actualData, expectedData = transformJsonToTableAndStripUpdate( 1, actualData, expectedData )
			stripBothUpdateFields( stripped, expectedData, actualData, 2, 'deleted_asset', 'payload', 'purgeDate' )
			stripBothUpdateFields( stripped, expectedData, actualData, 2, 'deleted_asset', 'payload', 'userCreated' )
			return actualData, expectedData
		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function assetUpdateOfCopyResponse( copyId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, links, developSettings, alwaysHaveLocationData )

	local assetUpdate = assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, links, developSettings, alwaysHaveLocationData )

	assetUpdate.payload.copy = {
		copy_id = copyId,
	}

	return assetUpdate

end

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

local function restoredAssetUpdateOfCopyResponse( copyId, copiedAssetId, restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, links, developSettings, alwaysHaveLocationData )

	local assetUpdate = assetUpdateOfCopyResponse( copyId, restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, links, developSettings, alwaysHaveLocationData )

	assetUpdate.original = {
		id = copiedAssetId,
	}

	return assetUpdate

end

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

local function trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, subtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, originalAssetId, hasMasterLink, hasRenditions, 
												hasXmpDevelopLink, hasXmpSidecarLink )

	local copiedTrashAssetUpdate = trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, subtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, originalAssetId, hasMasterLink, hasRenditions, hasXmpDevelopLink, hasXmpSidecarLink )
	copiedTrashAssetUpdate.payload.copy = {
		copy_id = copyId,
	}

	-- Copied photo gets location data even though it doesn't have a master.
	copiedTrashAssetUpdate.payload.location = {
		altitude = 18,
		latitude = 47.611714,
		longitude = -122.403008,
	}

	return copiedTrashAssetUpdate

end

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

local function pollHttpRequestCount( catalogSpec )
	return Observable.timer( .1, .1, WFRxScheduler.sharedScheduler() )
		:map( function()
			return catalogSpec:requestCount()
		end )
end

--==============================================================================
-- @mark Tests that involve the Copy Photo operation
--==============================================================================

local function testAddSyncCopyDelete( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add one photo and sync to Oz.
--		2) Pause sync
--		3) Copy the photo
--		4) Delete the copied photo
--		5) (optional) restore the photo
--		6) Sync

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )
	
	if not options.mock then
		rxTest:setRetryLimit( 3, 30 )
	end

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	local originalOzRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			originalOzRevId = docValue._revId
			return type( originalOzRevId )
		end )
	, 'string' )

	-- Optionally delete the local Master
	if options.dropLocalMaster then

		rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
			_id = assetId,
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			}
		} ), ':ignore:' )

	end

	-- Copy the photo
	local copiedAssetId
	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end )
	, 1 )

	-- Delete the copied photo
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { copiedAssetId }, pc ) )

	-- Make sure the copied asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, copiedAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	local newRevisionId
	local copyId
	-- Get the original asset's new revision id as we added a copy_id to it
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			newRevisionId = docValue._revId
			copyId = derefOpt( docValue, 'copy', 'copy_id' )
			return newRevisionId ~= revisionId
		end )
	, true )

	if options.restore then

		-- Restore the trash asset before syncing it.
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, copiedAssetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )
		rxTest:expectEqual( 'string', type( restoredRevId ) )
		local restoredCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( restoredDocValue )

		-- Make sure the trash asset was deleted
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_deleted' ), true )

		catalogSpec:expectHttpRequest( pushUpdateOriginalAndCreateCopyReqResp( catalogId, assetId, originalOzRevId, newRevisionId, restoredAssetId, restoredRevId, subtype, copiedAssetId, copyId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

		-- Get the update for the copied asset, and the update to the restored asset.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			assetUpdateOfCopyResponse( copyId, restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = options.xmpDevelop }, nil, true ),
		}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

		-- get the source link for the copy
		local sourceAssetLink
		local sourceXmpLink
		rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
			:map( function( docValue )
				sourceAssetLink = derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )
				sourceXmpLink = derefOpt( docValue, '_localOnly', 'sourceXmpLink' )
				restoredRevId = docValue._revId
				return type( sourceAssetLink )
			end ),
		'string' )
		
		-- upload the copy xmp
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, restoredAssetId, restoredRevId, sourceXmpLink ) )
		
		-- Wait for the uploader to issue the copy master request
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = true } ),
		}), 'success' )
	
		restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
	
		-- Upload the copy master.
		catalogSpec:expectHttpRequest( createCopyMasterReqResp( catalogId, restoredAssetId, restoredRevId, sourceAssetLink ) )

		-- Wait for the uploader to issue the copy master request.
		rxTest:observe( pollUploadCount( uploader, 'master-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		-- Wait for oz to generate the renditions before requesting next update
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredCreateWithRevId, subtype ), true )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = true, xmpDevelop = options.xmpDevelop }),
		}), 'success' )

	else

		catalogSpec:expectHttpRequest( pushUpdateOriginalAndCreateCopyReqResp( catalogId, assetId, originalOzRevId, newRevisionId, trashAssetId, trashRevId, trashAssetSubtype, copiedAssetId, copyId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

		-- Response from adding the copy_id.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, copiedAssetId, false, false ),
		}), 'success' )

		-- get the source link for the copy
		local sourceAssetLink
		local sourceXmpLink
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				sourceAssetLink = derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )
				sourceXmpLink = derefOpt( docValue, '_localOnly', 'sourceXmpLink' )
				trashRevId = docValue._revId
				return type( sourceAssetLink )
			end ),
		'string' )
		
		-- upload the copy xmp
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, trashAssetId, trashRevId, sourceXmpLink ) )
		
		-- Wait for the uploader to issue the copy master request
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, copiedAssetId, false, false, false, true ),
		}), 'success' )
			
		trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
		
		-- Upload the copy master
		catalogSpec:expectHttpRequest( createCopyMasterReqResp( catalogId, trashAssetId, trashRevId, sourceAssetLink ) )

		-- Wait for the uploader to issue the copy master request
		rxTest:observe( pollUploadCount( uploader, 'master-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		-- Wait for oz to generate the renditions before requesting next update
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, subtype ), true )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, copiedAssetId, true, true ),
		}), 'success' )

	end

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDelete( rxTest )
	-- NMB-9385
	testAddSyncCopyDelete( rxTest, { mock = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDelete_testNightly( rxTest )
	-- NMB-9385
	testAddSyncCopyDelete( rxTest )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteRestore( rxTest )
	-- NMB-9385
	testAddSyncCopyDelete( rxTest, { mock = true, restore = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteRestore_testNightly( rxTest )
	-- NMB-9385
	testAddSyncCopyDelete( rxTest, { restore = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDelete_NoLocalMaster( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDelete( rxTest, { mock = true, dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDelete_NoLocalMaster_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDelete( rxTest, { dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteRestore_NoLocalMaster( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDelete( rxTest, { mock = true, restore = true, dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteRestore_NoLocalMaster_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDelete( rxTest, { restore = true, dropLocalMaster = true } )
end

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

local function testAddSyncCopySyncDelete( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add one photo and sync to Oz
--		2) Pause binary Uploader using debug tool
--		3) Copy the photo
-- 		4) Delete the copied photo
--		5) (optional) restore the photo
--		6) Turn on the uploader / sync

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )

	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	local originalOzRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			originalOzRevId = docValue._revId
			return type( originalOzRevId )
		end )
	, 'string' )

	-- Optionally delete the local Master
	if options.dropLocalMaster then

		rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
			_id = assetId,
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			}
		} ), ':ignore:' )

	end

	-- Copy the photo
	local copiedAssetId
	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end )
	, 1 )

	-- get the revId and source link for the copy
	local sourceAssetLink, sourceXmpLink
	local copiedRevId
	local copyId
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			sourceAssetLink = derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )
			sourceXmpLink = derefOpt( docValue, '_localOnly', 'sourceXmpLink' )
			copiedRevId = docValue._revId
			copyId = derefOpt( docValue, 'copy', 'copy_id' )
			return type( sourceAssetLink )
		end ),
	'string' )

	local newRevisionId
	-- Get the original asset's new revision id as we added a copy_id to it
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			newRevisionId = docValue._revId
			return newRevisionId ~= revisionId
		end )
	, true )

	-- Send the metadata for the original asset to have a copy_id, and a update and create for the copy
	catalogSpec:expectHttpRequest( pushUpdateOriginalAndCreateCopyReqResp( catalogId, assetId, originalOzRevId, newRevisionId, copiedAssetId, copiedRevId, subtype, copiedAssetId, copyId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop }, nil, true )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	
	-- start the uploader
	uploader:startContinuousUploads()
		
	-- Add the expected uploader http requests.
	catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink ) )
	
	-- Wait for the uploader to issue the upload xmp request
	rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()
		
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
			{ proxy = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = true } ),
		}), 'success' )

	-- Upload the copy master.
	catalogSpec:expectHttpRequest( createCopyMasterReqResp( catalogId, copiedAssetId, copiedRevId, sourceAssetLink ) )

	-- Wait for the uploader to issue the copy master request.
	rxTest:observe( pollUploadCount( uploader, 'master-copy' ))
	:expectDataWithinTime( 2 * waitMultiplier, 1 )
	:dispose()

	-- Delete the copied photo immediately after to verify oz doesn't have an issue
	-- with the copy operation is asynchronous.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { copiedAssetId }, pc ) )

	-- Make sure the copied asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			copiedRevId = docValue._revId
			return docValue._deleted
		end )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, copiedAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	if options.restore then

		-- Restore the trash asset before syncing it.
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, copiedAssetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )
		rxTest:expectEqual( 'string', type( restoredRevId ) )
		local restoredCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( restoredDocValue )

		-- Make sure the trash asset was deleted
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_deleted' ), true )

		catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, copiedAssetId, copiedRevId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredCreateWithRevId, subtype ), true )

		-- Get the update for the copied asset, and the update to the restored asset.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateOfCopyResponse( copyId, copiedAssetId, restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = true, xmpDevelop = options.xmpDevelop }),
			assetDeletedResponse( copiedAssetId, trashAssetId, userId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	else

		-- Issue the delete without waiting for the async copy on oz to complete (don't check for links on the copy).
		catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, copiedAssetId, copiedRevId, trashAssetId, trashRevId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Wait for oz to generate the renditions before requesting next update
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

		-- Response from deleting the copied asset.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, copiedAssetId, true, true ),
			assetDeletedResponse( copiedAssetId, trashAssetId, userId ),
		}, stripMissingProxyInTrashAssetUpdate ), 'success' )

	end

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopySyncDelete( rxTest )
	-- NMB-9385
	testAddSyncCopySyncDelete( rxTest, { mock = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopySyncDelete_testNightly( rxTest )
	-- NMB-9385
	testAddSyncCopySyncDelete( rxTest )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopySyncDeleteRestore( rxTest )
	-- NMB-9385
	testAddSyncCopySyncDelete( rxTest, { mock = true, restore = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopySyncDeleteRestore_testNightly( rxTest )
	-- NMB-9385
	testAddSyncCopySyncDelete( rxTest, { restore = true } )
end

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

local function testAddSyncCopyDeleteSource( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add one photo and sync to Oz.
--		2) Pause sync
--		3) Copy the photo
--		4) Delete the photo originally added in step 1
--		5) (optional) permanently delete the original photo that was moved to trash by step 4
--		6) Sync

-- Expected that binary will get copied from the new_id that oz will provide.

	options = options or {}

	local waitMultiplier = options.mock and 3 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"
	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )
	local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, originalFilePath, proxyFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	local originalOzRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			originalOzRevId = docValue._revId
			return type( originalOzRevId )
		end )
	, 'string' )

	-- Optionally delete the local Master
	if options.dropLocalMaster then

		rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
			_id = assetId,
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			}
		} ), ':ignore:' )

	end

	if options.dropLocalProxy then

		rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
			_id = assetId,
			_localOnly = {
				files = {
					proxy = { _delete = true }
				}
			}
		} ), ':ignore:' )

	end

	-- Copy the photo
	local copiedAssetId
	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end )
	, 1 )

	-- Delete the original asset from step 1 that we copied so that we can cause
	-- trouble for the source not being found.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )
	local trashCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( trashDocValue )

	-- Get the original asset's new revision id as we deleted it
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue._deleted
		end )
	, true )

	-- Get the copied asset's copy_id.
	local copyId
	local copiedRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			copyId = derefOpt( docValue, 'copy', 'copy_id' )
			copiedRevId = docValue._revId
			return not docValue._deleted
		end )
	, true )

	catalogSpec:expectHttpRequest( pushCreateCopyAndDeleteAssetReqResp( catalogId, copiedAssetId, copiedRevId, copyId, assetId, revisionId, subtype, userCreated, trashAssetId, trashCreateWithRevId, trashAssetSubtype, trashPurgeDate ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Response from adding the copy_id, note that since the source asset
	-- was moved to trash and then copied before it was synced up it will
	-- not have the copy_id on it until a subsequent update is applied after
	-- going through conflict resolution.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = false, renditions = false, xmpDevelop = options.xmpDevelop }, nil, true ),
		trashAssetUpdateResponse( trashAssetId, trashCreateWithRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}), 'success' )

	-- Wait for the conflict to be resolved
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
		:pluck( '_conflict' ) )
	:expectDataWithinTime( 5 * waitMultiplier, nil )
	:dispose()

	-- Get the latest revisionId for trash asset
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( trashDocValue )
			trashRevId = trashDocValue._revId
			return trashDocValue
		end )
	, ':ignore:' )

	-- push the update for adding the copy_id
	catalogSpec:expectHttpRequest( pushUpdateAssetReqResp( catalogId, trashAssetId, trashRevId, subtype, options.xmpDevelop ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateOfCopyResponse( copyId, trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
			trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

	-- Get the source link for the copy.
	local sourceAssetLink, sourceXmpLink
	local copiedOriginalFilePath
	local copiedProxyFilePath
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			sourceAssetLink = derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )
			sourceXmpLink = derefOpt( docValue, '_localOnly', 'sourceXmpLink' )
			copiedRevId = docValue._revId
			copiedOriginalFilePath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
			copiedProxyFilePath = WFCatFileSpec.buildProxyPathFromDocValue( docValue )
			return type( sourceAssetLink )
		end ),
	'string' )

	local newSourceAssetId = trashAssetId
	if options.noNewIdOnCreateCopyError404 then
		newSourceAssetId = nil
	end

	if options.delTrashSource then

		-- Delete the trash asset before the uploader has a chance to use the trash asset source links.
		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

		-- Get the latest revisionId for trash asset
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( trashDocValue )
				trashRevId = trashDocValue._revId
				return trashDocValue
			end )
			:pluck( '_deleted' )
		, true )
					
		-- Make sure we send purge trash to Oz
		catalogSpec:expectHttpRequest( pushDeleteTrashReqResp( catalogId, trashAssetId, trashRevId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the trash delete response from Oz
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, nil, userId ),
		} ), 'success' )
		
		-- Add the expected uploader http requests - xmp copy will fail and remove the binary links leaving the master by itself.
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink, '404', newSourceAssetId ) )
			
		if options.dropLocalMaster and options.dropLocalProxy then
			
			-- start the uploader
			uploader:startContinuousUploads()
			
			-- Wait for the update to get applied to the assets docValue
			rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
				:pluck( '_localOnly', 'localOriginalError' ))
			:expectDataWithinTime( 5 * waitMultiplier, 'CopyFailedSourceMissing' )
			:dispose()

		elseif options.dropLocalMaster then
			
			-- start the uploader
			uploader:startContinuousUploads()
			
			-- Expect a proxy style binary will be uploaded.
			local proxyId = KSIdGenerator.uuidInLowercaseHex()
			catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, copiedAssetId, copiedRevId, proxyId, copiedProxyFilePath ) )

			-- Wait for the uploader to issue the upload master request
			rxTest:observe( pollUploadCount( uploader, 'proxy' ))
			:expectDataWithinTime( 5 * waitMultiplier, 1 )
			:dispose()

			-- Wait for oz to generate the renditions before requesting next update
			rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, copiedAssetId, copiedRevId,
				subtype, { proxyOnlyAsset = true } )
			, true )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
					{ proxy = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			}), 'success' )

		else -- have local master
			
			-- Expect the master will be uploaded from the local binary.
			catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, copiedAssetId, copiedRevId, copiedOriginalFilePath ) )

			-- start the uploader
			uploader:startContinuousUploads()
			
			-- Wait for the uploader to issue the upload master request
			rxTest:observe( pollUploadCount( uploader, 'master' ))
			:expectDataWithinTime( 5 * waitMultiplier, 2 )
			:dispose()

			-- Wait for oz to generate the renditions before requesting next update
			rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, copiedAssetId, copiedRevId, subtype ), true )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
					{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			}), 'success' )

		end

	else
		
		uploader:startContinuousUploads()
		
		-- Add the expected uploader http requests.
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink, '404', newSourceAssetId ) )
		
		-- Get sourceLink from the trash asset for the next attempt.

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashRevId = docValue._revId
			sourceAssetLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
			sourceXmpLink = WFCatFileSpec.getXmpLinkFromDocValue( docValue )
			return type( sourceAssetLink )
		end ),
		'string' )
		
		-- Issue the updated copy xmp call
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink ) )
				
		-- Wait for the uploader to issue the upload xmp request
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar-copy' ))
			:expectDataWithinTime( 5 * waitMultiplier, 1 )
			:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = true } ), 
			} ), 'success' )
		
		-- issue updated copy master call
		catalogSpec:expectHttpRequest( createCopyMasterReqResp( catalogId, copiedAssetId, copiedRevId, sourceAssetLink ) )

		-- Wait for the uploader to issue the copy master request
		rxTest:observe( pollUploadCount( uploader, 'master-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		-- Wait for oz to generate the renditions before requesting next update
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, copiedAssetId, copiedRevId,
			subtype, { } )
		, true )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
		} ), 'success' )

	end

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSource_Master( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDeleteSource( rxTest, { mock = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSource_Master_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDeleteSource( rxTest, {} )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSource_NoMaster( rxTest )

	-- NMB-9385 / NMB-11692
	-- This should behave the same as having the master local as we update
	-- the source and try again with the trash asset id.
	testAddSyncCopyDeleteSource( rxTest, { mock = true, dropLocalMaster = true } )

end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSource_NoMaster_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	-- This should behave the same as having the master local as we update
	-- the source and try again with the trash asset id.
	testAddSyncCopyDeleteSource( rxTest, { dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSource_NoNewId( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDeleteSource( rxTest, { mock = true, noNewIdOnCreateCopyError404 = true } )
end

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

--function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSource_NoNewId_testNightly( rxTest )
--	-- NMB-9385 / NMB-11692
-- Oz provides new_id on error 404 so just use mocked version
--	testAddSyncCopyDeleteSource( rxTest, { expectNewId = true } )
--end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSourceDelTrash_Master( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDeleteSource( rxTest, { mock = true, delTrashSource = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSourceDelTrash_Master_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	testAddSyncCopyDeleteSource( rxTest, { delTrashSource = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSourceDelTrash_NoMaster( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should show up as a Lightrom Classic style proxy only asset
	testAddSyncCopyDeleteSource( rxTest, { mock = true, delTrashSource = true, dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSourceDelTrash_NoMaster_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should show up as a Lightroom Classic style asset proxy only asset
	testAddSyncCopyDeleteSource( rxTest, { delTrashSource = true, dropLocalMaster = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopyDeleteSourceDelTrash_NoMasterOrProxy( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should be marked as failed and show up in sync status as such.
	testAddSyncCopyDeleteSource( rxTest, { mock = true, delTrashSource = true, dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopyDeleteSourceDelTrash_NoMasterOrProxy_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should be marked as failed and show up in sync status as such.
	testAddSyncCopyDeleteSource( rxTest, { delTrashSource = true, dropLocalMaster = true, dropLocalProxy = true } )
end

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


local function testAddSyncCopyOtherDeleteSource( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add one photo and sync to Oz.
--		2) Pause sync
--		3) Copy the photo
--		4) Other client deletes the photo originally added in step 1
--		5) Sync

-- Expected that binary will get copied from the new_id that oz will provide.

	options = options or {}

	local waitMultiplier = options.mock and 3 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )
	assert( pc )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"
	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )
	local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, originalFilePath, proxyFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	local originalOzRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			originalOzRevId = docValue._revId
			return type( originalOzRevId )
		end )
	, 'string' )

	-- Copy the photo
	local copiedAssetId
	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end )
	, 1 )

	-- Get the original asset's new revision id as we added a copy_id to it
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
	, ':ignore:' )

	-- Get the copied asset's copy_id.
	local copyId
	local copiedRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			copyId = derefOpt( docValue, 'copy', 'copy_id' )
			copiedRevId = docValue._revId
			return not docValue._deleted
		end )
	, true )

	catalogSpec:expectHttpRequest( pushUpdateOriginalAndCreateCopyReqResp( catalogId, assetId, originalOzRevId, revisionId, copiedAssetId, copiedRevId, subtype, copiedAssetId, copyId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop }, nil, true )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Get the source link and revId for the copy.
	local sourceAssetLink, sourceXmpLink
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			sourceAssetLink = derefOpt( docValue, '_localOnly', 'sourceOriginalLink' )
			sourceXmpLink = derefOpt( docValue, '_localOnly', 'sourceXmpLink' )
			copiedRevId = docValue._revId
			return type( sourceAssetLink )
		end ),
	'string' )

	-- Other Client ------------------------------------------------------------

	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashRevId,
		userCreated = userCreated,
		trashPurgeDate = trashPurgeDate,
		options = options,
	}

	-- Back to Original Client -------------------------------------------------

	-- Expect an error on trying to copy the original that is no longer there
	-- because the other client deleted it.
	
	local startHttpRequestCount = catalogSpec:requestCount()

	uploader:startContinuousUploads()
	
	catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink, '404', otherClientTrashAssetId ) )
	
	-- Wait for the uploader to issue the request, it will fail, and do nothing.
	rxTest:observe( pollHttpRequestCount( catalogSpec )
		:map( function( count )
			return count - startHttpRequestCount
		end ) )
	:expectDataWithinTime( 5 * waitMultiplier, 1 )
	:dispose()

	uploader:stopContinuousUploads()

	-- Prove that we don't have otherClientTrashId's metadata locally
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	, {
		_id = otherClientTrashAssetId,
		_empty = true,
	} )

	if options.handlesMissingLinks then

		-- This is only run via a mock as we can't control getting an update without the master
		-- link from oz.  I believe it is possible to though.

		-- Get the metadata for otherClientTrashAssetId that includes the links for uploading binaries.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, otherClientTrashAssetId, otherClientTrashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, assetId, false, false ),
			assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		}), 'success' )

		-- Expect the same error again trying to copy the original that is no longer there
		-- and we didn't have an asset for new_id = otherClientTrashAssetId so the code will just
		-- delay a bit and try it again in hopes we get the new_id in the metadata stream.
		-- This one will also fail as we don't have links for the source asset.
		catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink, '404', otherClientTrashAssetId ) )

		startHttpRequestCount = catalogSpec:requestCount()

		uploader:startContinuousUploads()

		-- Wait for the uploader to issue the request, it will fail, and do nothing.
		rxTest:observe( pollHttpRequestCount( catalogSpec )
			:map( function( count )
				return count - startHttpRequestCount
			end ) )
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		uploader:stopContinuousUploads()

		-- Get an update for the trash asset that has links.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, otherClientTrashAssetId, otherClientTrashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, assetId, true, true ),
		}), 'success' )

	else

		-- Make sure oz has the master link.
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, otherClientTrashAssetId, otherClientTrashRevId,
			subtype, { master = true } )
		, true )

		-- Get the metadata for otherClientTrashAssetId that includes the links for uploading binaries.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateOfCopyResponse( copyId, otherClientTrashAssetId, otherClientTrashRevId, fileName, trashAssetSubtype, trashAssetCreated,
				trashPurgeDate, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		}), 'success' )

	end
	
	uploader:startContinuousUploads()
	
	catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, sourceXmpLink, '404', otherClientTrashAssetId ) )
	
	-- Get sourceLink from the trash asset made by the other client for the next attempt.
	local newSourceAssetLink, newSourceXmpLink
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( docValue )
		otherClientTrashRevId = docValue._revId
		newSourceAssetLink = WFCatFileSpec.getOriginalLinkFromDocValue( docValue )
		newSourceXmpLink = WFCatFileSpec.getXmpLinkFromDocValue( docValue )
		return type( sourceAssetLink )
	end )
	, 'string' )
	
	catalogSpec:expectHttpRequest( createCopyXmpReqResp( catalogId, copiedAssetId, copiedRevId, newSourceXmpLink ) )
	
	-- Wait for sidecar to complete
	rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar-copy' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
		       { master = false, renditions = false, xmpDevelop = false, xmpSidecar = true } ), 
		} ), 'success' )

	-- Issue updated copyMaster call.
	catalogSpec:expectHttpRequest( createCopyMasterReqResp( catalogId, copiedAssetId, copiedRevId, newSourceAssetLink ) )

	-- Wait for the uploader to issue the copy master request
	rxTest:observe( pollUploadCount( uploader, 'master-copy' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1 )
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, copiedAssetId, copiedRevId,
		subtype, { } )
	, true )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateOfCopyResponse( copyId, copiedAssetId, copiedRevId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = true, renditions = true, xmpDevelop = false, xmpSidecar = true } ),
	}), 'success' )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopy_OtherDeleteSourceMasterLink( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should be marked as failed and show up in sync status as such.
	testAddSyncCopyOtherDeleteSource( rxTest, { mock = true } )
end

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

function WFCatTrashDesktopTest.rxTest_CT_OzCopy_OtherDeleteSource_testNightly( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should be marked as failed and show up in sync status as such.
	testAddSyncCopyOtherDeleteSource( rxTest )
end

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

function WFCatTrashDesktopTest.rxTest_CT_MockCopy_OtherDeleteSource_MissingLinks( rxTest )
	-- NMB-9385 / NMB-11692
	-- Asset should be marked as failed and show up in sync status as such.
	testAddSyncCopyOtherDeleteSource( rxTest, { mock = true, handlesMissingLinks = true } )
end

--==============================================================================
-- @mark Tests that involve restore failing because of storage or license expiry issues
--==============================================================================

local function testAddSyncDeleteSyncExpiredOrOverQuota( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 20 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local storageMonitor = WFCatStorage( catalog, { calmSeconds = .01, sampleSeconds = .01, dontFetchAccountDoc = true } )
	assert( storageMonitor )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local captureDate = "2018-01-01T00:00:00Z"
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	if options.noUserCreated then
		userCreated = nil
	end
	if options.noImportTimestamp then
		importTimestamp = nil
	end

	local expectedUserCreated = userCreated
	if not userCreated then
		expectedUserCreated = importTimestamp
		if importTimestamp == nil then
			expectedUserCreated = assetCreated
		end
	end

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )

	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded before the master is uploaded
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
		
		uploader:startContinuousUploads()
		
		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		-- Get the updated metadata with the xmp link.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
									{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = true }, expectedDevelopSettings )
		}, stripForAssetUpdate( false ) ), 'success' )

	end
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath, nil, options.xmpSidecar ) )
		
	uploader:startContinuousUploads()
	
	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end
	
	-- Wait for the upload master to complete, (it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )
	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, xmpSidecar = true }, expectedDevelopSettings )
	}, stripForAssetUpdate( waitForRenditions ) ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

	-- Wait for the update to get applied to the assets docValue for xmpDevelop
	if options.xmpDevelop then
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()
	end

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Get the revId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )

	-- Make sure renditions oz generated renditions for the trash asset, as we may not
	-- have waited for them to be generated before we first deleted the asset.  There
	-- was a bug on oz where it didn't know to generate them if they weren't there when
	-- copying an asset to a trash asset.
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

	-- Make sure the trash asset still has the _localOnly.file for the master (could
	-- have done this check right after the moveAssetsToTrash but checking here after
	-- the metadata puller gets updates is good as well).
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )

			trashDocValue = docValue

			local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'
			return haveOriginal
		end ),
	true )


	-- Check to see if oz updated the renditions because we may have deleted before they were fully generated.
	do

		local hasNewRenditionLinks = false

		rxTest:expectSingleValue( httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

				for k, v in pairs( assetAsJson.links ) do
					v.href = string.format( "/v2/catalogs/%s/%s", catalogId, v.href )
				end

				hasNewRenditionLinks = not KSTableUtils.deepEquals( trashDocValue._serverProvidedMetadata.links, assetAsJson.links )
				return assetAsJson

			end )
		, ':ignore:' )

		if hasNewRenditionLinks then
			-- Get the trashed asset back in the sync feed with the links to upload the original
			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
			}, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )
		end

	end

	-- check for binary links
	local expectedBinaryLinks = options.expectedBinaryLinks
	if expectedBinaryLinks then

		-- Verify the expected binaries all have links, if not check with oz for updates
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				return docValue
			end )
		, ':ignore:' )

		canDownloadRequiredBinaryFiles( rxTest, catalogSpec, trashDocValue, expectedBinaryLinks, options.expectedBinarySizes )

	end

	if options.restore then

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				trashDocValue = docValue
				return docValue
			end )
		, ':ignore:' )

		if options.restoreExpired then
			pusher:setUserIsExpired( true )
			catalogSpec:setSubscriptionExpired( true ) -- this is a special mocked catspec function
			rxTest:expectSingleValue( deleteBinariesFromDocValue( catalog, trashDocValue ), ':ignore:' )
		end

		if options.restoreOverQuota then
			pusher:setUserIsOverQuota( true )
			rxTest:expectSingleValue( deleteBinariesFromDocValue( catalog, trashDocValue ), ':ignore:' )
		end

		-- Restore the asset after syncing up so the deleted asset exists on oz.
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( trashAssetId, derefOpt( restoredDocValue, '_localOnly', 'universal', 'trash', 'createFrom', 'id' ) )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

		-- Make sure the trash asset was deleted
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:pluck( '_deleted' ),
		true )

		local kApplyTintXmp = [[
			<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='XMP toolkit 3.0-28, framework 1.6'>
			<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:iX='http://ns.adobe.com/iX/1.0/'>
			 <rdf:Description rdf:about=''
			  xmlns:crs='http://ns.adobe.com/camera-raw-settings/1.0/'
				crs:Version='11.1'
				crs:ProcessVersion='11.0'
				crs:Temperature='50000'
				crs:Tint='+8'
				crs:Saturation='0'
			 </rdf:Description>
			</rdf:RDF>
			</x:xmpmeta>
			]]

		if options.modifyAfterRestore then
			
			-- Change xmpCameraRaw before we delete.  (oz won't know about this and this will
			-- have to get re-applied/merged in during conflict resolution and sent as a subsequent update)
			rxTest:expectSingleValue( catalog:docStore():applyOverlay {
					_id = restoredAssetId,
					develop = {
						processingModel = "lightroom",
						xmpCameraRaw = kApplyTintXmp
					}
				}, ':ignore:' )
			
		end

		local function canDownloadUrls( docValue )

			local missingUrls = {}

			if not WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) then
				missingUrls[ #missingUrls + 1 ] = "missing master link"
			end

			if not WFCatFileSpec.getProxyLinkFromDocValue( docValue ) then
				missingUrls[ #missingUrls + 1 ] = "missing proxy link"
			end

			if not WFCatFileSpec.getXmpDevelopLinkFromDocValue( docValue ) then
				missingUrls[ #missingUrls + 1 ] = "missing xmp_develop link"
			end

			if not WFCatFileSpec.getXmpLinkFromDocValue( docValue ) then
				missingUrls[ #missingUrls + 1 ] = "missing xmp sidecar link"
			end

			return #missingUrls == 0 and 'success' or missingUrls

		end

		if options.restoreExpired or options.restoreOverQuota then

			local expectedErrorSubtype = options.restoreExpired and 'TrialOrSubscriptionExpiredError'
			expectedErrorSubtype = expectedErrorSubtype or ( options.restoreOverQuota and 'StorageExceededError' )

			-- Give the pusher a chance to mark the asset as out of storage or expired
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

			-- Verify document is marked as account expired, and get localId

			local localDocId
			rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
				:map( function( docValue )
					localDocId = docValue._localId
					local ozErrorSubtype = derefOpt( docValue, '_localOnly', 'ozError', 'subtype' )
					return ozErrorSubtype, canDownloadUrls( docValue )
				end )
			, expectedErrorSubtype, 'success' )

			-- Verify document shows up as needing to be downloaded.

			rxTest:observe( syncStatusQuery( catalog ) )
			:expectDataWithinTime( 2 * waitMultiplier, {
				{
					localDocId,
					1, -- needsOriginalDownload
					0, -- hasLocalOriginal
					1, -- needsProxyDownload
					0, -- hasLocalProxy
					"2560", -- proxySize
					0, -- isMaybeDenied
					0, -- isImportant
					0, -- isMarkedForOffline
					importTimestamp,
					captureDate,
					0, -- waitingForProxyOnOz
					1, -- needsDevelopXmpDownload
					0, -- ozHasNonThumbnailRendition
				},
			} )
			:dispose()

			-- Make sure the pusher isn't sending anything yet
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

			pusher:setUserIsExpired( false )
			pusher:setUserIsOverQuota( false )
			catalogSpec:updateStorageUsed( 1 ) -- should cause an update to have the ozError removed
			catalogSpec:setSubscriptionExpired( false ) -- should cause expired ozError to be removed

			-- Wait for the ozError to be cleared
			rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId ):pluck(  '_localOnly', 'ozError' ) )
			:expectDataWithinTime( 5 * waitMultiplier, nil )
			:dispose()

		end

		-- Verify we send up the restore.

		catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, trashAssetId, trashRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Make sure renditions are generated for the restored asset
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		-- Get the newly created image asset from oz.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop )
		} ), 'success' )

		if options.modifyAfterRestore then

			-- make sure document is marked with a conflict
			rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
				:map( function( docValue )
					if docValue._conflict then
						return true
					end
					return docValue
				end ),
			true )

			-- Wait for the conflict to be resolved
			rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId ):pluck( '_conflict' ) )
			:expectDataWithinTime( 5 * waitMultiplier, nil )
			:dispose()

			-- Verify we send up a restored asset update to include the modifications that were made after the restore
			catalogSpec:expectHttpRequest( pushUpdateRestoredReqResp( catalogId, restoredAssetId, restoredRevId, subtype, false, kApplyTintXmp ) )
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		end

		-- Since we don't actually download files in this test, don't make this check when
		-- performing the out of storage or expired tests as they are removing the binary
		-- to make sure we indicate that it needs to be downloaded.
		if not options.restoreExpired and not options.restoreOverQuota then

			rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
				:map( function( docValue )
					local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
					local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'
					return haveOriginal
				end ),
			true )

		end

	end

	if options.deleteTrash then

		-- Delete the asset after syncing up so the deleted asset also gets deleted on oz.
		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				-- Get the last revision of the original asset (revision when applyOverlay with _deleted = true).
				trashRevId = docValue._revId
				return docValue
			end ), ':ignore:' )

		catalogSpec:expectHttpRequest( pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the trash delete success from Oz
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, nil, userId ),
		} ), 'success' )

	end

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockRestore_AccountExpired( rxTest )

	-- NMB-10345

	-- User deletes an asset
	-- User account goes expired
	-- user restores asset

	-- 1 Delete photo
	-- 2 Sync
	-- 3 Pause sync
	-- 4 Restore the photo (while account out of storage)
	-- 6 Resume sync

	testAddSyncDeleteSyncExpiredOrOverQuota( rxTest, {
		mock = true,
		restore = true,
		restoreExpired = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockRestoreModify_AccountExpired( rxTest )

	-- NMB-10345

	-- User deletes an asset
	-- User account goes expired
	-- user restores asset

	-- 1 Delete photo
	-- 2 Sync
	-- 3 Pause sync
	-- 4 Restore the photo (while account out of storage)
	-- 5 Modify the photo
	-- 6 Resume sync

	testAddSyncDeleteSyncExpiredOrOverQuota( rxTest, {
		mock = true,
		restore = true,
		modifyAfterRestore = true,
		restoreExpired = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockRestore_OverQuota( rxTest )

	-- NMB-10393 Scenario:

	-- User deletes an asset, syncs, then goes offline.
	-- User uploads stuff on another client and uses up all storage.
	-- While offline user restores asset then modifies it then goes online.

	-- asset should still exist on client with modifications and show as not synced

	-- reduction of storage below limit should properly restore asset and show as synced

	-- 1 Delete photo
	-- 2 Sync
	-- 3 Pause sync
	-- 4 Restore the photo (while account out of storage)
	-- 5 Modify the photo
	-- 6 Resume sync

	testAddSyncDeleteSyncExpiredOrOverQuota( rxTest, {
		mock = true,
		restore = true,
		restoreOverQuota = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockRestoreModify_OverQuota( rxTest )

	-- NMB-10393 Scenario:

	-- User deletes an asset, syncs, then goes offline.
	-- User uploads stuff on another client and uses up all storage.
	-- While offline user restores asset then modifies it then goes online.

	-- asset should still exist on client with modifications and show as not synced

	-- reduction of storage below limit should properly restore asset and show as synced

	-- 1 Delete photo
	-- 2 Sync
	-- 3 Pause sync
	-- 4 Restore the photo (while account out of storage)
	-- 5 Modify the photo
	-- 6 Resume sync

	testAddSyncDeleteSyncExpiredOrOverQuota( rxTest, {
		mock = true,
		restore = true,
		modifyAfterRestore = true,
		restoreOverQuota = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

--==============================================================================
-- @mark tests that relate to deleting assets in stacks
--==============================================================================

-- NMB-10360
--1 Pause Sync
--2 Add 3 assets
--3 Make them into a stack
--4 delete the stack
--5 notice stack is gone
--6 Open Trash view

local function testAddDeleteSyncStack( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, _, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local importTimestamp = '2018-04-15T19:19:19Z'

	local catalogId = catalog:ozCatalogId()

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )


	local subtype = 'image'
	
	local files = {
		{
			fileName = "IMG_7763.JPG",
			userCreated = "2018-04-15T18:18:18Z",
		},
		{
			fileName = "IMG_7766.jpg",
			userCreated = "2018-04-15T18:18:19Z",
		},
		{
			fileName = "IMG_9876.CR2",
			userCreated = "2018-04-15T18:18:20Z",
		},
	}
	
	local assetIds = {}

	for _, fileItem in ipairs( files ) do

		local fileName = fileItem.fileName
		local userCreated = fileItem.userCreated

		local assetId = KSIdGenerator.uuidInLowercaseHex()
		table.insert( assetIds, assetId )

		local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
		local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

		local importSource = makeImportSource( catalog, fileName, importTimestamp )
		
		-- Add an image asset
		rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource ),
			':ignore:' )
		
	end
	
	-- create a valid stack
	local theStackId
	rxTest:expectSingleValue( WFCatStacks.createStack( catalog, assetIds )
	:map( function( stackId )
		theStackId = stackId
		return stackId
	end ), ":ignore:" )
	
	
	-- Verify stack document.
	
	rxTest:expectSingleValue( catalog:docStore():docValue( theStackId )
	:map( function( stackDocValue )
		if stackDocValue then
			if stackDocValue._id == theStackId and stackDocValue.subtype ==  "stack" then
				return true
			end
			return false
		end
	end ), true )
	
	-- Delete the stack

	-- When image deletion is triggered in Lr CC via a stack, the assets in the
	-- stack are identified and explicitly deleted. The stack asset itself is not
	-- explicitly deleted. Its deletion is handled in 'stackRelatedOverlays' in
	-- WFCatDelete.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, assetIds, pc, WFCatDelete.stackRelatedOverlays ) )
	
	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	-- Verify stack is gone
	
	rxTest:expectSingleValue( catalog:docStore():docValue( theStackId ):pluck( '_deleted'), true )
	
	-- Verify assets are gone
	
	rxTest:expectSingleValue( catalog:docStore():docValue( assetIds[ 1 ] ):pluck( '_deleted'), true )
	rxTest:expectSingleValue( catalog:docStore():docValue( assetIds[ 2 ] ):pluck( '_deleted'), true )
	rxTest:expectSingleValue( catalog:docStore():docValue( assetIds[ 3 ] ):pluck( '_deleted'), true )
	
	-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	
	-- Verify assets are in trash
	
	local trashAssetId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetIds[ 1 ] )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( 'subtype'), 'deleted_image' )

	trashAssetId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetIds[ 2 ] )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( 'subtype'), 'deleted_image' )

	trashAssetId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetIds[ 3 ] )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( 'subtype'), 'deleted_image' )
	
	-- 'Turn on' sync and validate that 'the right thing' happens, which should
	-- be pushing of the 3 trash assets.
	catalogSpec:expectHttpRequest( catalogSpec:expectHttpRequest( pushCreateMultipleAssetsReqResp( catalogId,
			files, "deleted_image" ) ))
			
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockAddDelete_stack( rxTest )

	testAddDeleteSyncStack( rxTest, { mock = true } )

end

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

function WFCatTrashDesktopTest.rxTest_CT_OzAddDelete_stack_testNightly( rxTest )

	testAddDeleteSyncStack( rxTest )

end

--==============================================================================
-- @mark tests that relate to storing trash originals locally
--==============================================================================

-- Since these tests use a mocked up version of WFCatIdleDownloader the tests
-- we only run the mock version.  Also WFCatIdleDownloader is a Desktop specific
-- API so don't run these on mobile.  The album tests also rely on
-- NmbOfflineAssets and NmbOfflineAssetsTask.

local downloadStream = Rx.ReplaySubject( 100 )

local ids = {}
local subscriptions = {}

local function disposeOfDownloadSubscriptions()

	for key, disposable in pairs( subscriptions ) do
		disposable:dispose()
		subscriptions[ key ] = nil
	end

	subscriptions = {}
end

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

local function createDownloader( docStore, localOnlyFiles )

	disposeOfDownloadSubscriptions()
	ids = {}

	return function( localId, binaryType )

		local key = localId .. ':' .. binaryType

		local downloadingAlreadyStarted = ids[ key ]

		ids[ key ] = localId

		local delay = 0.001

		subscriptions[ key ] = Observable.timer( delay, WFRxScheduler.sharedScheduler() )

			:take( 1 )

			:flatMapLatest( function()

				if downloadingAlreadyStarted then
					return Observable.throw( "Error Downloading already started for " .. downloadingAlreadyStarted )
				end

				local localOnly = { files = {} }

				if binaryType == 'original' then
					localOnly.files.original = {
						master = localOnlyFiles.original.master
					}
				end

				if binaryType == 'proxy' then
					-- Never generated a proxy just fake it with master path.
					KSTableUtils.setNested( localOnly, localOnlyFiles.original.master, 'files', 'proxy', '2560' )
				end

				return docStore:applyOverlay {
					_id = localId,
					_localOnly = localOnly,
				}

			end )

			:subscribe( function()
				downloadStream:onNext( true, localId, true, binaryType )
				subscriptions[ key ] = nil
			end )

	end

end

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

local function setupDownloader( wfCatalog, keepAllOriginals, keepAllProxies, optArgs )

	optArgs = optArgs or {}

	-- Reset the subject to a new one in case prior test completed or disposed it.
	downloadStream = Rx.ReplaySubject( 100 )

	local downloader = {}

	local localOnlyFiles = optArgs.localOnlyFiles

	downloader.func = createDownloader( wfCatalog:docStore(), localOnlyFiles )

	downloader.cancelFunc = function( binaryType, localIds )

		for _, localId in ipairs( localIds ) do

			local key = localId .. ':' .. binaryType

			local maybeSubscription = subscriptions[ key ]
			subscriptions[ key ] = nil

			if maybeSubscription then
				maybeSubscription:dispose()
				maybeSubscription = nil
			end

		end

	end

	keepAllOriginals = not not keepAllOriginals
	keepAllProxies = not not keepAllProxies

	optArgs.debugQueryLimit = 1

	local wfCatIdleDownloader = WFCatIdleDownloader( wfCatalog, downloader.func, downloadStream, downloader.cancelFunc, keepAllOriginals, keepAllProxies, optArgs )

	return wfCatIdleDownloader, downloader

end

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

local function testAddSyncDeleteStoreLocal( rxTest, options )

	WFCatIdUtils.debug_clearCaches()

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local storageMonitor = WFCatStorage( catalog, { calmSeconds = .01, sampleSeconds = .01 } )
	assert( storageMonitor )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	if options.noUserCreated then
		userCreated = nil
	end
	if options.noImportTimestamp then
		importTimestamp = nil
	end

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false }, options.xmpDevelop )
	}, stripForAssetUpdate( false ) ), 'success' )

	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	end

	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete, (it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ) )
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = true }, options.xmpDevelop )
	}, stripForAssetUpdate( true ) ), 'success' )

	if options.xmpSidecar then
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ) )
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )

	local localOnlyFiles = KSTableUtils.deepCopy( trashDocValue._localOnly.files )

	-- Delete the local binaries so we have to download them if requested.
	-- We will leave the binaries on disk and simply replace the file path
	-- entry when WFCatIdleDownloader asks us to download the file.
	rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
		_id = trashDocValue._id,
		_localOnly = {
			files = {
				original = { _delete = true }, -- master and xmp sidecar
				proxy = { _delete = true }, -- proxies
				xmp_develop = { _delete = true }, -- external develop settings
				xmp_sidecar = { _delete = true },
			}
		}
	} ), ':ignore:' )

	-- Get the revId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

	local localDocId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			localDocId = docValue._localId
			return type( localDocId )
		end )
	, 'number' )

	-- Make sure the pusher isn't sending anything.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Verify document shows up as needing to be downloaded.

	local captureDate = "2018-01-01T00:00:00Z"
	rxTest:observe( syncStatusQuery( catalog ) )
	:expectDataWithinTime( 2 * waitMultiplier, {
		{
			localDocId,
			1, -- needsOriginalDownload
			0, -- hasLocalOriginal
			1, -- needsProxyDownload
			0, -- hasLocalProxy
			"2560", -- proxySize
			0, -- isMaybeDenied
			0, -- isImportant
			0, -- isMarkedForOffline
			importTimestamp,
			captureDate,
			0, -- waitingForProxyOnOz
			0, -- needsDevelopXmpDownload
			1, -- ozHasNonThumbnailRendition
		},
	} )
	:dispose()

	local wfCatIdleDownloader, downloader = setupDownloader( catalog, options.keepOriginalsLocal, options.keepProxiesLocal, { localOnlyFiles = localOnlyFiles } )

	rxTest:expectNotEqual( nil, downloader )

	-- WFCatIdleDownloader won't do anything until this is called
	wfCatIdleDownloader:reportStatusOfVolumes( {
		offlineVolumes = {},
		volumesOverIdleSpaceThreshold = {},
		volumesOutOfSpace = {},
		remainingSpace = {}
	} )

	wfCatIdleDownloader:start()

	rxTest:finally( function()
		wfCatIdleDownloader:stop()
		wfCatIdleDownloader:close()
	end )

	local function paramsAsArray( ... )
		return { ... }
	end

	if options.keepOriginalsLocal and options.keepProxiesLocal then

		rxTest:expectSingleValue( downloadStream:map( paramsAsArray ):take( 2 ):toArray(), {
			{ true, localDocId, true, 'original' },
			{ true, localDocId, true, 'proxy' }
		} )

	elseif options.keepOriginalsLocal then

		rxTest:expectSingleValue( downloadStream:map( paramsAsArray ):take( 1 ),
			{ true, localDocId, true, 'original' }
		)

	elseif options.keepProxiesLocal then

		rxTest:expectSingleValue( downloadStream:map( paramsAsArray ):take( 1 ),
			{ true, localDocId, true, 'proxy' }
		)

	end

	-- Verify document has the correct binaries in the files section.
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )

			local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath ~= nil and WFFileUtils.exists( originalPath ) == 'file'

			local proxyPath = WFCatFileSpec.buildProxyPathFromDocValue( docValue )
			local haveProxy = proxyPath ~= nil and WFFileUtils.exists( proxyPath ) == 'file'

			return haveOriginal, haveProxy

		end )
	, not not options.keepOriginalsLocal, not not options.keepProxiesLocal )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockStoreAllLocally( rxTest )

	-- Scenario:  (NMB-10264)
	-- Test If keep all assets offline then it will remain local.
	-- Test if turning it on keep all assets online it will cause it to download.
	-- Test that Local Storage "Required Space" and "Photo Cache" values are affected by items in the trash.

	testAddSyncDeleteStoreLocal( rxTest, { mock = true, keepOriginalsLocal = true, keepProxiesLocal = true } )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockStoreProxiesLocally( rxTest )

	-- Scenario:  (NMB-10264)
	-- Test If keep all assets offline then it will remain local.
	-- Test if turning it on keep all assets online it will cause it to download.
	-- Test that Local Storage "Required Space" and "Photo Cache" values are affected by items in the trash.

	testAddSyncDeleteStoreLocal( rxTest, { mock = true, keepProxiesLocal = true } )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockStoreNothingLocally( rxTest )

	-- Scenario:  (NMB-10264)
	-- Test If keep all assets offline then it will remain local.
	-- Test if turning it on keep all assets online it will cause it to download.
	-- Test that Local Storage "Required Space" and "Photo Cache" values are affected by items in the trash.

	testAddSyncDeleteStoreLocal( rxTest, { mock = true } )

end

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

local function testAddSyncDeleteStoreAlbumLocal( rxTest, options )

	WFCatIdUtils.debug_clearCaches()

	options = options or {}

	local keepAlbumLocal = not not options.keepAlbumLocal

	-- Need catalog:getDeletionBacklog() to be defined NmbOfflineAssets to
	-- function correctly so create an actual NmbCatalog.
	options.catalogClassType = 'NmbCatalog'

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local storageMonitor = WFCatStorage( catalog, { calmSeconds = .01, sampleSeconds = .01 } )
	assert( storageMonitor )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local captureDate = "2018-01-01T00:00:00Z"
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	if options.noUserCreated then
		userCreated = nil
	end
	if options.noImportTimestamp then
		importTimestamp = nil
	end

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false }, options.xmpDevelop )
	}, stripForAssetUpdate( false ) ), 'success' )

	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	end

	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete, (it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ) )
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = true }, options.xmpDevelop )
	}, stripForAssetUpdate( true ) ), 'success' )

	if options.xmpSidecar then
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ) )
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

	-- Add an album and the asset to the album
	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetRevId
	local albumName = "test album"
	local albumRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )

	-- Sync up the album
	catalogSpec:expectHttpRequest( pushCreateAlbumReqResp( catalogId, albumId, albumName ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	} ), 'success' )


	-- Add the album asset
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId,
			type = 'album_asset',
			album = { id = albumId },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( pushCreateAlbumAssetReqResp( catalogId, albumAssetId, albumId, assetId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
	} ), 'success' )


	-- make a copy of the _localOnly.files section as our downlaoder uses
	-- it to complete the downloads.
	local localOnlyFiles
	local localDocId
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			localDocId = docValue._localId
			localOnlyFiles = KSTableUtils.deepCopy( docValue._localOnly.files )
			return type( localOnlyFiles )
		end )
	, 'table' )

	-- Make it look like the local binaries were purged from disk.
	-- Delete the local binaries so we have to download them if requested.
	-- We will leave the binaries on disk and simply replace the file path
	-- entry when WFCatIdleDownloader asks us to download the file.
	rxTest:expectSingleValue( catalog:docStore():applyOverlay( {
		_id = assetId,
		_localOnly = {
			files = {
				original = { _delete = true }, -- master and xmp sidecar
				proxy = { _delete = true }, -- proxies
				xmp_develop = { _delete = true }, -- external develop settings
				xmp_sidecar = { _delete = true },
			}
		}
	} ), ':ignore:' )


	local NmbOfflineAssets = require 'NmbOfflineAssets'
	local NmbOfflineAssetsTask = require 'NmbOfflineAssetsTask'

	if keepAlbumLocal then
		rxTest:expectOnlyCompleted( NmbOfflineAssets.enableLocalAlbumStorage( catalog, { albumId } ) )
	else
		rxTest:expectOnlyCompleted( NmbOfflineAssets.disableLocalAlbumStorage( catalog, { albumId } ) )
	end

	-- Make sure the asset gets marked for download when the album is.
	local nmbOfflineAssetsTask = NmbOfflineAssetsTask( catalog, { debugMode = true } )
	assert( nmbOfflineAssetsTask )

	local keepLocalAsNumber = keepAlbumLocal and 1 or 0

	-- Verify Sync Status
	rxTest:observe( syncStatusQuery( catalog ) )
	:expectDataWithinTime( 5, {
		{
			localDocId,
			1, -- needsOriginalDownload
			0, -- hasLocalOriginal
			1, -- needsProxyDownload
			0, -- hasLocalProxy
			"2560", -- proxySize
			0, -- isMaybeDenied
			keepLocalAsNumber, -- isImportant
			keepLocalAsNumber, -- isMarkedForOffline
			importTimestamp,
			captureDate,
			0, -- waitingForProxyOnOz
			0, -- needsDevelopXmpDownload
			1, -- ozHasNonThumbnailRendition
		},
	} )
	:dispose()

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	local trashAssetId, trashRevId, _ = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )

	-- Get the revId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

	local trashLocalId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashLocalId = docValue._localId
			return type( trashLocalId )
		end )
	, 'number' )

	-- Make sure the pusher isn't sending anything.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Verify Sync Status
	rxTest:observe( syncStatusQuery( catalog ) )
	:expectDataWithinTime( 5, {
		{
			trashLocalId,
			1, -- needsOriginalDownload
			0, -- hasLocalOriginal
			1, -- needsProxyDownload
			0, -- hasLocalProxy
			"2560", -- proxySize
			0, -- isMaybeDenied
			0, -- isImportant
			0, -- isMarkedForOffline
			importTimestamp,
			captureDate,
			0, -- waitingForProxyOnOz
			0, -- needsDevelopXmpDownload
			1, -- ozHasNonThumbnailRendition
		},
	} )
	:dispose()

	local wfCatIdleDownloader, downloader = setupDownloader( catalog, false, false, { localOnlyFiles = localOnlyFiles } )

	rxTest:expectNotEqual( nil, downloader )

	-- WFCatIdleDownloader won't do anything until this is called
	wfCatIdleDownloader:reportStatusOfVolumes( {
		offlineVolumes = {},
		volumesOverIdleSpaceThreshold = {},
		volumesOutOfSpace = {},
		remainingSpace = {}
	} )

	wfCatIdleDownloader:start()

	rxTest:finally( function()
		wfCatIdleDownloader:stop()
		wfCatIdleDownloader:close()
	end )

	local function paramsAsArray( ... )
		return { ... }
	end

	local expectedDownloads = { { true, trashLocalId, true, 'proxy' } }

	-- Verify we don't download the master for the trash asset via WFCatIdleDownloader.
	rxTest:expectSingleValue( downloadStream
		:map( paramsAsArray )
		:bufferWithTime( 1, WFRxScheduler.sharedScheduler() )
		:take( 1 )
	, expectedDownloads )

	-- Restore the asset so we can verify it will get downloaded
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )
	local restoredLocalId = restoredDocValue._localId

	-- Make sure we only sync up the newly restored asset (shouldn't issue a restore in this case)
	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, trashAssetId, trashRevId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the restored asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( true ) ), 'success' )

	-- Verify Sync Status

	if keepAlbumLocal then

		rxTest:observe( syncStatusQuery( catalog ) )
		:expectDataWithinTime( 5, {
			{
				restoredLocalId,
				0, -- needsOriginalDownload
				1, -- hasLocalOriginal
				0, -- needsProxyDownload
				1, -- hasLocalProxy
				"2560", -- proxySize
				0, -- isMaybeDenied
				1, -- isImportant
				1, -- isMarkedForOffline
				importTimestamp,
				captureDate,
				0, -- waitingForProxyOnOz
				0, -- needsDevelopXmpDownload
				1, -- ozHasNonThumbnailRendition
			},
		} )
		:dispose()

	else

		rxTest:observe( syncStatusQuery( catalog ) )
		:expectDataWithinTime( 5, {
			{
				restoredLocalId,
				1, -- needsOriginalDownload
				0, -- hasLocalOriginal
				0, -- needsProxyDownload
				1, -- hasLocalProxy
				"2560", -- proxySize
				0, -- isMaybeDenied
				0, -- isImportant
				0, -- isMarkedForOffline
				importTimestamp,
				captureDate,
				0, -- waitingForProxyOnOz
				0, -- needsDevelopXmpDownload
				1, -- ozHasNonThumbnailRendition
			},
		} )
		:dispose()


	end

	-- Verify we don't download the master for the trash asset via WFCatIdleDownloader.
	-- Note: The downloadStream in CloudTrash tests is a replaySubject so we should include
	-- everything that happened by the download on each check
	if keepAlbumLocal then
		table.insert( expectedDownloads, { true, restoredLocalId, true, 'original' } )
	end

	rxTest:expectSingleValue( downloadStream
		:map( paramsAsArray )
		:bufferWithTime( 1, WFRxScheduler.sharedScheduler() )
		:take( 1 )
	, expectedDownloads )

	-- Verify document has the correct binaries in the files section.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )

			local originalPath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath ~= nil and WFFileUtils.exists( originalPath ) == 'file'

			local proxyPath = WFCatFileSpec.buildProxyPathFromDocValue( docValue )
			local haveProxy = proxyPath ~= nil and WFFileUtils.exists( proxyPath ) == 'file'

			local isMarkedForOfflineUse = not not derefOpt( docValue,  '_localOnly', 'isMarkedForOfflineUse' )

			return haveOriginal, haveProxy, isMarkedForOfflineUse

		end )
	, keepAlbumLocal, true, keepAlbumLocal )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockStoreAlbumLocallyTrue( rxTest )

	-- Scenario:  NMB-10264
	-- Test if an asset came from an album which is marked "Store Album Locally" the master will be stored on disk locally.
	-- Test if the album changes "Store Album Locally" it won't affect the item in the trash.

	testAddSyncDeleteStoreAlbumLocal( rxTest, { mock = true, keepAlbumLocal = true } )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockStoreAlbumLocallyFalse( rxTest )

	-- Scenario:  NMB-10264
	-- Test if an asset came from an album which is marked "Store Album Locally" the master will be stored on disk locally.
	-- Test if the album changes "Store Album Locally" it won't affect the item in the trash.

	testAddSyncDeleteStoreAlbumLocal( rxTest, { mock = true, keepAlbumLocal = false } )

end

--==============================================================================
-- @mark Tests that verify renditions are preserved when deleting and restoring
--==============================================================================

local function testAddDeleteRestore_PreservesBinaries( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype

	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )
	local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, originalFilePath, proxyFilePath, subtype, userCreated, importSource )
	, ':ignore:' )
	
	local expectedDevelopSettings = WFCatTrashTestUtils.getXmpDevelopSettings()
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )

	-- Add the renditions by copying the proxy to the rendition paths. This must
	-- be done after messing with the xmpDevelop as it will affect the path.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )

			local xmpCameraRawMd5 = NmbLocalRenditionDigest_maybeComputeFromDocValue( docValue )
			for _, longEdge in ipairs( { 320, 640, 1280, 2048 } ) do

				local renditionPath = catalog:fileSpec():pathForPreviewDigestAndLongEdge( docValue._id, xmpCameraRawMd5, longEdge, false )
				WFFileUtils.mkdir( parent( renditionPath ) )
				WFFileUtils.copy( proxyFilePath, renditionPath )
				if WFFileUtils.exists( renditionPath ) ~= "file" then
					return false
				end

			end

			return true

		end )
	, true )

	-- Delete the asset before syncing up anything related to it.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc, nil, WFCatDelete.gatherBinaryFilesToMove ) )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	-- Restore the asset before syncing up anything related to it.
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc, nil, nil, WFCatDelete.gatherBinaryFilesToMove ) )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure we only sync up the newly restored asset (shouldn't issue a restore in this case)
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the restored asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )

	uploader:startContinuousUploads()

	-- /rels/xmp/develop gets uploaded before master
	restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
	catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredRevId, xmpDevelopFilePath ) )
	
	-- /rels/xmp gets uploaded before master is uploaded
	catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, restoredAssetId, restoredRevId, xmpSidecarFilePath ) )

	-- Wait for sidecar to complete if there is one.
	rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()
	
	-- get the updated metadata with the xmp link
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, xmpSidecar = true }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )
	
		-- Wait for the upload of xmp_develop.
	rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()
	
	-- Get the updated metadata with the xmp_develop link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = true, xmpSidecar = true }, expectedDevelopSettings )
	}, stripForAssetUpdate( true ) ), 'success' )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, restoredAssetId, restoredRevId, originalFilePath, nil, true ) )
	
	-- Wait for the upload master to complete, it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true, xmpDevelop = true, xmpSidecar = true }, expectedDevelopSettings )
	}, stripForAssetUpdate( true ) ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

	-- Make sure the original and trash assets were deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId ):pluck( '_deleted' ), true )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_deleted' ), true )

	-- Make sure the restored asset has local files
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )

			local missingBinaries = {}

			local function addToArrayIfMissing( fileDescription, filePath )
				local fileExists = filePath and WFFileUtils.exists( filePath ) == 'file'
				if not fileExists then
					table.insert( missingBinaries, string.format( 'missing %s: %s', fileDescription, filePath ) )
				end
			end

			addToArrayIfMissing( 'original', WFCatFileSpec.buildOriginalPathFromDocValue( docValue ) )
			addToArrayIfMissing( 'proxy', WFCatFileSpec.buildProxyPathFromDocValue( docValue ) )

			local xmpDevelopInfo = WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ) or {}
			local xmpFilePath = xmpDevelopInfo.path
			addToArrayIfMissing( 'xmpDevelop', xmpFilePath )

			addToArrayIfMissing( 'xmpSidecar', WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue ) )

			local xmpCameraRawMd5 = NmbLocalRenditionDigest_maybeComputeFromDocValue( docValue )
			for _, longEdge in ipairs( { 320, 640, 1280, 2048 } ) do
				local renditionPath = catalog:fileSpec():pathForPreviewDigestAndLongEdge( docValue._id, xmpCameraRawMd5, longEdge, false )
				addToArrayIfMissing( 'rendition' .. tostring( longEdge), renditionPath )
			end

			return missingBinaries

		end )
	, {} )

end

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

function WFCatTrashDesktopTest.rxTest_CT_MockAddDeleteRestore_PreservesBinaries( rxTest )

	-- Verify delete and restore operations preserves all binaries including
	-- renditions.

	-- No need to verify this on oz as this test is verifying local binary
	-- files related, it is desktop specific because LrDesktop stores renditions
	-- externally from the docValue by using a filename based on the hash of the
	-- asset id and on certain contents of the docValue itself.

	testAddDeleteRestore_PreservesBinaries( rxTest, { mock = true } )

end

--==============================================================================
-- @mark setup
--==============================================================================

function WFCatTrashDesktopTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( WFCatTrashDesktopTest )
