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

WFCatTrashTest.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2018 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and 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 Systems Incorporated.

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


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

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

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

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

	-- WF.oz_client.managed_catalog
local WFCatIdUtils = require 'WFCatIdUtils'
local WFCatTrash = require 'WFCatTrash'
local WFCatTrashTestUtils = require 'WFCatTrashTestUtils'
local WFCatTrashUtils = require 'WFCatTrashUtils'
local WFCatStorage = require 'WFCatStorage'

	-- WF.oz_client.managed_catalog.sync
local WFCatBinaryUploaderTestUtils = require 'WFCatBinaryUploaderTestUtils'

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

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

local coveragePath

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

local IMG_7763fileSize = 2072173
local IMG_7772fileSize = 211332

local failStubbedTests = false

local assetTableName = 'coreInfo'
if WFPlatformInfo.isDesktop then
	assetTableName = 'assets'
end

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5

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

local WFCatTrashTest = {}

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>
	]]

local function getApplyVideoDevelopTint( now )
	return {
		version = 1,
		processVersion = 1,
		adjustments = {
			userUpdated = now,
			incrementalTint = 8,
		},
		croppedWidth = 100,
		croppedHeight = 100,
	}
end

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

local function setTimeoutAndRetryLimit( rxTest, options, timeoutValue )

	rxTest:setTimeLimit( timeoutValue )

	if not options.mock then
		rxTest:setRetryLimit( 3, 30 )
	end

end

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

function WFCatTrashTest.reportAllChanges( scanState, docValue )
	return scanState, docValue
end


--==============================================================================
-- @mark Tests that involve deleting synced assets
--==============================================================================

local function testAddSyncDeleteSync( rxTest, options )

	options = options or {}

	options.aux = options.xmpDevelop

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 20 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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'
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	fileName = options.fileName or fileName
	sha256 = options.sha256 or sha256
	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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

	if options.mock then
		-- TODO [kcarlson 2018-07-09]:  Not sure why this doesn't get issued when using oz
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.catalogPrefetchReqResp( catalogId ) )
	end

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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' )
	local developUserUpdated, _

	if options.xmpDevelop then
		revisionId, _, developUserUpdated = WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
		expectedDevelopSettings.userUpdated = developUserUpdated
	end

	local auxMockHttpRequests, auxInfo

	if options.aux then

		revisionId, auxMockHttpRequests, auxInfo = WFCatTrashTestUtils.addAuxToDocument( rxTest, catalog, assetId, options )

		local auxId = auxInfo and next( auxInfo )

		expectedDevelopSettings.masks = {
			[ auxId ] = {
				digest = auxInfo[ auxId ].digest
			}
		}

	end

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

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false },
								expectedDevelopSettings, nil, auxInfo )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

	uploader:startContinuousUploads()

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

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = false, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )
	end

	if options.aux then

		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )

		for _, request in ipairs( auxMockHttpRequests ) do
			catalogSpec:expectHttpRequest( request )
		end

		-- Wait for the upload of Aux

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'aux-direct' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	if options.xmpDevelop then
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
		
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )
	end

	-- Upload the master
	revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createMasterReqResp( catalogId, assetId, revisionId, originalFilePath, nil, options.xmpSidecar ) )

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

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

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
	}, WFCatTrashTestUtils.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

	-- Wait for the update to get applied to the assets docValue for aux
	if options.aux then

		local auxId = auxInfo and next( auxInfo )

		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/aux/' .. auxId, '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 = WFCatTrashTestUtils.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( WFCatTrashTestUtils.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 )

	local theTrashAssetUpdateResponse = WFCatTrashTestUtils.trashAssetUpdateResponse(
			trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
			trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions,
			options.xmpDevelop, options.xmpSidecar, expectedDevelopSettings, options.aux,
			auxInfo )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		theTrashAssetUpdateResponse,
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
    }, WFCatTrashTestUtils.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( WFCatTrashTestUtils.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 = WFCatAssetUtils.getFullOriginalPathFromDocValue( 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:waitFor( WFCatTrashTestUtils.httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

				for k, v in pairs( assetAsJson.links ) do
					if string.sub(v.href, 1, 5) ~= 'https' then
						v.href = string.format( "/v2/catalogs/%s/%s", catalogId, v.href )
					end
				end

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

			end ) )

		if hasNewRenditionLinks then
		
			theTrashAssetUpdateResponse = WFCatTrashTestUtils.trashAssetUpdateResponse(
					trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
					trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions,
					options.xmpDevelop, options.xmpSidecar, expectedDevelopSettings, options.aux,
					auxInfo )
		
			-- Get the trashed asset back in the sync feed with the links to upload the original
			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller,
					{ theTrashAssetUpdateResponse },
					WFCatTrashTestUtils.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:waitFor( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				return docValue
			end ) )

		WFCatTrashTestUtils.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:' )

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

		local restoredAssetId, restoredRevId, restoredDocValue = WFCatTrashTestUtils.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 )

		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

		-- Verify we send up the restore.

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

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

		if options.restoreExpired or options.storageExceeded then

			-- Get the error back from oz.
			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.storageExceededUpdateResponse( 'create', restoredAssetId, restoredRevId, 'asset' ),
			} ), 'success' )

			-- Verify we properly apply an error state to a photo if oz reports
			-- back kStorageExceededError or kTrialOrSubscriptionExpiredError.

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

			-- Verify document is marked as account expired or out of storage.
			rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
				:pluck( '_localOnly', 'ozError', 'subtype' ) )
			:expectDataWithinTime( 5 * waitMultiplier, expectedErrorSubtype )
			:dispose()

			-- Make sure we don't try to send anything back up for the asset.
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

		else

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

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

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

			rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
				:map( function( docValue )
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( 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( WFCatTrashTestUtils.pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId ) )

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

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

	end

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_image( rxTest )
	testAddSyncDeleteSync( rxTest, {
		mock = true,
		expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForImage(),
		expectedBinarySizes = { [ '/rels/master' ] = 4 } -- mocks use 'mock' for binary data
	} )
end

--------------------------------------------------------------------------------
if not WFPlatformInfo.isAndroid then

function WFCatTrashTest.rxTest_CT_MockDelete_video( rxTest )

		testAddSyncDeleteSync( rxTest, {
			mock = true,
			fileName = "IMG_7772.mp4",
			sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
			subtype = "video",
			expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForVideo(),
			expectedBinarySizes = { [ '/rels/master' ] = 4 } -- mocks use 'mock' for binary data
		} )

end

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_usesImportTimestamp( rxTest )
	testAddSyncDeleteSync( rxTest, { mock = true, noUserCreated = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_usesAssetCreated( rxTest )
	testAddSyncDeleteSync( rxTest, { mock = true, noUserCreated = true, noImportTimestamp = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_image_xmp( rxTest )

	local expectedBinaryLinksForImageWithXmp = WFCatTrashTestUtils.expectedBinaryLinksForImage()

	table.insert( expectedBinaryLinksForImageWithXmp, '/rels/xmp/develop' )

	local xmpDevelopSize = #WFCatTrashTestUtils.getCameraRaw( true )
	local xmpSidecarSize = #WFCatTrashTestUtils.getXmpSidecar()

	testAddSyncDeleteSync( rxTest,
	{
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
		expectedBinaryLinks = expectedBinaryLinksForImageWithXmp,
		expectedBinarySizes = {
			[ '/rels/master' ] = 4, -- mocks use 'mock' for binary data
			[ '/rels/xmp' ] = 4, -- mocks use 'mock' for binary data
			[ '/rels/xmp/develop' ] = 4, -- mocks use 'mock' for binary data
		}
	})

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteSyncRestore( rxTest )

	-- Restore after the delete has been uploaded to oz so the metadata pusher will
	-- send the create restored asset command.

	testAddSyncDeleteSync( rxTest, { mock = true, restore = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteSyncRestore_xmp( rxTest )

	-- Verify /rels/xmp (sidecar) and /rels/xmp_develop binaries are properly restored.
	testAddSyncDeleteSync( rxTest, { mock = true, restore = true, xmpSidecar = true, xmpDevelop = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDelete_image_testNightly( rxTest )
	testAddSyncDeleteSync( rxTest,
	{
		expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForImage(),
		expectedBinarySizes = { [ '/rels/master' ] = IMG_7763fileSize }
	})
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_image_xmp_testNightly( rxTest )

	local expectedBinaryLinksForImageWithXmp = WFCatTrashTestUtils.expectedBinaryLinksForImage()

	table.insert(expectedBinaryLinksForImageWithXmp, '/rels/xmp/develop' )

	local xmpDevelopSize = #WFCatTrashTestUtils.getCameraRaw( true )
	local xmpSidecarSize = #WFCatTrashTestUtils.getXmpSidecar()

	testAddSyncDeleteSync( rxTest,
	{
		xmpDevelop = true,
		xmpSidecar = true,
		expectedBinaryLinks = expectedBinaryLinksForImageWithXmp,
		expectedBinarySizes = {
			[ '/rels/master' ] = IMG_7763fileSize,
			[ '/rels/xmp' ] = xmpSidecarSize,
			[ '/rels/xmp/develop' ] = xmpDevelopSize,
		}
	})

end

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

if not WFPlatformInfo.isAndroid then

function WFCatTrashTest.rxTest_CT_OzDelete_video_testNightly( rxTest )
	testAddSyncDeleteSync( rxTest, {
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
		expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForVideo(),
		expectedBinarySizes = { [ '/rels/master' ] = IMG_7772fileSize }
	} )
end

end

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

function WFCatTrashTest.rxTest_CT_OzDelete_image_noRenditions_testNightly( rxTest )

	-- NMB-10550
	--Scenario: When Master for Asset A has been uploaded but renditions not generated yet when Asset A is deleted

	testAddSyncDeleteSync( rxTest, {
		noWaitForRenditions = true,
		expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForImage(),
		expectedBinarySizes = { [ '/rels/master' ] = IMG_7763fileSize }
	})

end

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

if not WFPlatformInfo.isAndroid then

function WFCatTrashTest.rxTest_CT_OzDelete_video_noRenditions_testNightly( rxTest )

	-- NMB-10550
	--Scenario: When Master for Asset A has been uploaded but renditions not generated yet when Asset A is deleted

	testAddSyncDeleteSync( rxTest, {
		noWaitForRenditions = true,
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
		expectedBinaryLinks = WFCatTrashTestUtils.expectedBinaryLinksForVideo(),
		expectedBinarySizes = { [ '/rels/master' ] = IMG_7772fileSize }
	} )

end

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteSyncRestore_testNightly( rxTest )

	testAddSyncDeleteSync( rxTest, { restore = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteSyncRestore_xmp_testNightly( rxTest )

	-- Verify /rels/xmp (sidecar) and /rels/xmp_develop binaries are properly restored.
	testAddSyncDeleteSync( rxTest, { restore = true, xmpSidecar = true, xmpDevelop = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockRestoreModify( rxTest )

	-- NMB-9372

	-- Restore and modify offline after the delete has been uploaded to oz so
	-- the metadata pusher will restore with the offline modifications intact.

	testAddSyncDeleteSync( rxTest, { mock = true, restore = true, modifyAfterRestore = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzRestoreModify_testNightly( rxTest )

	-- NMB-9372

	-- Restore and modify offline after the delete has been uploaded to oz so
	-- the metadata pusher will restore with the offline modifications intact.

	testAddSyncDeleteSync( rxTest, { restore = true, modifyAfterRestore = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockRestore_StorageExceededError( 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 but no assets
	--   flagged with out of storage error yet.
	-- 5 Modify the photo
	-- 6 Resume sync

	-- Expect to see asset marked as out of storage.

	-- Note: There are additional tests for 10393 in WFCatTrashDesktopTest
	-- that related to the account already having been marked as out of storage.

	-- This is a mocked only test as we are generating the kStorageExceededError.
	testAddSyncDeleteSync( rxTest, { mock = true, restore = true, storageExceeded = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockRestoreModify_StorageExceededError( 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 but no assets
	--   flagged with out of storage error yet.
	-- 5 Modify the photo
	-- 6 Resume sync

	-- Expect to see asset marked as out of storage.

	-- Note: There are additional tests for 10393 in WFCatTrashDesktopTest
	-- that related to the account already having been marked as out of storage.

	-- This is a mocked only test as we are generating the kStorageExceededError.
	testAddSyncDeleteSync( rxTest, { mock = true, restore = true, storageExceeded = true, modifyAfterRestore = true } )

end

--==============================================================================
-- @mark Tests that involve adding and deleting assets before syncing
--==============================================================================

local function testAddDeleteSync( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

	local expectedUserCreated = userCreated

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

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

	-- Delete the asset before syncing up anything related to it.
	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 = WFCatTrashTestUtils.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' )  )

	-- Sync up the deleted asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )

	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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	-- Update the trashRevId with the latest revision id before issuing the WFCatTrashTestUtils.createMasterReqResp
	trashRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, trashAssetId )

	-- Upload the master
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createMasterReqResp( catalogId, trashAssetId, trashRevId, originalFilePath ) )
	uploader:startContinuousUploads()

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

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

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true )
	}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )

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

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return haveOriginal
		end ),
	true )

end

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

function WFCatTrashTest.rxTest_CT_OzAddDelete_image_testNightly( rxTest )

	testAddDeleteSync( rxTest )

end

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

if not WFPlatformInfo.isAndroid then

function WFCatTrashTest.rxTest_CT_OzAddDelete_video_testNightly( rxTest )

	testAddDeleteSync( rxTest, { fileName = "IMG_7772.mp4", sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963", subtype = "video" } )

end

end
--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddDelete_xmp_testNightly( rxTest )
	-- Also verify we can upload to /xmp and /xmp/develop
	rxTest:expectEqual( false, failStubbedTests )
end

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

function WFCatTrashTest.rxTest_CT_MockAddDelete_image( rxTest )
	testAddDeleteSync( rxTest, { mock = true } )
end

--------------------------------------------------------------------------------
if not WFPlatformInfo.isAndroid then

function WFCatTrashTest.rxTest_CT_MockAddDelete_video( rxTest )
	testAddDeleteSync( rxTest, { mock = true, fileName = "IMG_7772.mp4", sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963", subtype = "video" } )
end

end
--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddDelete_xmp( rxTest )
	-- Also verify we can upload to /xmp and /xmp/develop
	rxTest:expectEqual( false, failStubbedTests )
end

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

--[[

	\brief Test workflow where asset is created, completes sync to Oz, then is
		modified locally and deleted before that local modification syncs

	\param rxTest (WFRxTestCaseRunner)
	\param options (table) Optional modifiers:

		mock (boolean) - mock tests if true, end to end tests with Oz stage if false

		subtype (string) - document subtype. Defaults to image.

		fileName (string) - importSource fileName. Defaults to IMG_7763.JPG

		xmpSidecar (boolean) - if true then include xmp sidecar binary

		xmpDevelop (boolean) - if true then include external xmp develop settings.
			Also test will include external xmpDevelop on asset synced to Oz and
			change the external xmpDevelop on the modification.

		aux (boolean) - if true then include aux binary. It is not supported to
			include both aux and xmpDevelop for atomicDevelopSettings tests since
			the order of upload of these binaries when ADS is enabled is
			indeterministic. Aux binaries are applied as part of the modified
			revision in this test.

		atomicDevelopSettings (boolean) - if true enable atomicDevelopSettings
			feature for test

		undo (boolean) - if true then a second modification is applied to the
			document setting the xmpDevelop setting back to the original value.
			This is only an interesting workflow with ADS enabled.

]]
local function testAddSyncModifyDeleteSync( rxTest, options )

	-- NMB-10288
	-- Scenario: User adds an image, lets it sync, pauses sync,
	-- the user then modifies the asset, and then deletes it,
	-- turn on sync.
	-- User then looks on another client to see if the modifications were sent
	-- to oz as it's appearance should be up to date in the trash.

	-- Details:
	--	LrCC has un-synced edits on A (say a_revision_2)
	--
	--	1. user deletes A on LrCC.
	--	2. LrCC creates B locally (with b_revision_1 that contains the un-synced edits from A)
	--	3. LrCC sends a Delete A request to Oz with B id and b_revision_1_id.
	--		(user could make several edits resulting in several revisions)
	--		(implies LrCC needs to specify the oz label-ed revision id)
	--	4. Oz creates B (with B id and b_revision_1_id) based on A (with a_revision_1) and creates an extra empty revision on B, and Oz sends B in sync feed (b_revision_id_1 and b_revision_id_2 ).
	--	5. LrCC receives B in sync feed and merges what it has locally (in b_revision_1) with what comes from Oz.
	--	6. LrCC sends an update on B (b_revision_3) to Oz so that Oz can get the latest edits as well.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 = options.subtype or 'image'
	local fileName = options.fileName or 'IMG_7763.JPG'
	local sha256 = options.sha256 or "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	revisionId = rxTest:getFirstValue( WFCatTrashTestUtils.addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource ) )

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

	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	local xmpDevelopSha256, developUserUpdated

	if options.xmpDevelop then
		revisionId, xmpDevelopSha256, developUserUpdated =
				WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
		expectedDevelopSettings.userUpdated = developUserUpdated
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = false, aux = false, xmpSidecar = false, },
				expectedDevelopSettings )
	}, WFCatTrashTestUtils.stripUpdatesAndLinksIgnoringRenditions() ), 'success' )

	if options.xmpSidecar then
		-- /rels/xmp gets uploaded first
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
	end

	uploader:startContinuousUploads()

	if options.xmpSidecar then

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

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = false, aux = false, xmpSidecar = options.xmpSidecar },
				expectedDevelopSettings )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	local assetDoc
	local xmpDevelopBucket

	if options.xmpDevelop then

		if options.atomicDevelopSettings then

			xmpDevelopBucket = 'xmp_develop-direct'

			rxTest:waitFor( catalog:docStore():docValue( assetId )
				:map( function( docValue )
					assetDoc = docValue
					return docValue
				end )
			)

			local xmpDevelopMocks = {}
			WFCatBinaryUploaderTestUtils.getXmpDevelopDirectUploadMocksForAsset( catalog, assetDoc,
					{
						developSettingsSha256 = xmpDevelopSha256,
						xmpDevelopSettingsFilePath = xmpDevelopFilePath,
						xmpDevelopSettingsFileSize = WFFileUtils.calculatePathSize( xmpDevelopFilePath ),
					}, xmpDevelopMocks, { atomicDevelopSettings = true, forTestNightly = not options.mock, } )
			for _, mock in ipairs( xmpDevelopMocks ) do
				catalogSpec:expectHttpRequest( mock )
			end

		else

			xmpDevelopBucket = 'xmp_develop'

			revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
			catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )

		end

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, xmpDevelopBucket ))
			:expectDataWithinTime( 5 * waitMultiplier, 1 )
			:dispose()

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, aux = false, xmpSidecar = options.xmpSidecar },
				expectedDevelopSettings )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	-- Upload the master
	local errorCode = nil
	local skipXReadMeta = options.xmpSidecar
	revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createMasterReqResp( catalogId, assetId, revisionId, originalFilePath,
			errorCode, skipXReadMeta ) )

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

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

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
				{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, aux = false },
				expectedDevelopSettings )
	}, WFCatTrashTestUtils.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()

	-- to prevent immediate attempt to upload of binaries with ADS enabled disable binary upload briefly

	if options.atomicDevelopSettings then
		uploader:stopContinuousUploads()
	end

	-- Modify the asset after it has been synced but before it is deleted

	-- 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)

	local prevXmpDevelopFilePath = xmpDevelopFilePath
	local prevXmpDevelopSha256 = xmpDevelopSha256
	local prevExpectedDevelopSettings = expectedDevelopSettings

	if options.xmpDevelop then

		local smallXmp = true
		expectedDevelopSettings = WFCatTrashTestUtils.getXmpDevelopSettings( smallXmp )
		xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop2.xmp' )
		revisionId, xmpDevelopSha256, developUserUpdated =
			WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, nil, smallXmp )
		expectedDevelopSettings.userUpdated = developUserUpdated

	else

		developUserUpdated = KSDate.timeInIso8601Format()

		if subtype == 'video' then

			expectedDevelopSettings = getApplyVideoDevelopTint( developUserUpdated )
			revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay {
					_id = assetId,
					develop = expectedDevelopSettings,
					userUpdated = developUserUpdated,
				} )

		else

			expectedDevelopSettings = {
				processingModel = "lightroom",
				xmpCameraRaw = kApplyTintXmp
			}
			revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay {
					_id = assetId,
					develop = expectedDevelopSettings,
					userUpdated = developUserUpdated,
				} )

		end

	end

	local auxMockHttpRequests, auxInfo

	if options.aux then

		revisionId, auxMockHttpRequests, auxInfo = WFCatTrashTestUtils.addAuxToDocument( rxTest, catalog, assetId, options )

		local auxId = auxInfo and next( auxInfo )

		expectedDevelopSettings.masks = {
			[ auxId ] = {
				digest = auxInfo[ auxId ].digest
			}
		}

	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 = WFCatTrashTestUtils.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' ) )

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

	-- Get the the first revision of the trash asset as that is what we should be sending up
	-- as we want to make sure a conflict is generated if it has been modified.
	rxTest:waitFor( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashRevId = WFCatTrashUtils.getCreateWithRevisionId( docValue )
			return docValue
		end )
	)

	-- 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( WFCatTrashTestUtils.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 )

	-- Make sure the createWithRevisionId is different than the current _revId,
	-- a newer revId indicates the modifications were applied.
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			return docValue._revId ~= WFCatTrashUtils.getCreateWithRevisionId( docValue )
		end )
	, true )

	local theTrashAssetUpdateResponse = WFCatTrashTestUtils.trashAssetUpdateResponse(
			trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated,
			trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop,
			true, prevExpectedDevelopSettings )

	-- Get the trashed asset back in the sync feed with links
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		theTrashAssetUpdateResponse,
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )

	-- make sure document is marked with a conflict
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		: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( trashAssetId )
		:map( function( docValue )
			trashRevId = docValue._revId
			return docValue
		end )
		:pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()

	-- Verify we send up a trash asset update to include the modifications that were made before the delete
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateTrashReqResp( catalogId, trashAssetId, trashRevId,
			trashAssetSubtype, options.xmpDevelop, expectedDevelopSettings, auxInfo ) )

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

	local xmpDevelopInvalid = true

	if options.atomicDevelopSettings then

		uploader:startContinuousUploads()

	else

		-- sync metadata back down to allow upload of binaries

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype,
					trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId,
					true, true, options.xmpDevelop, true, expectedDevelopSettings, false, auxInfo,
					xmpDevelopInvalid, prevXmpDevelopSha256 ),
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	if options.aux then

		rxTest:expectEqual( true, not options.xmpDevelop or not options.atomicDevelopSettings )
			-- atomicDevelopSettings tests will fail if both xmpDevelop and aux are in options

		for _, request in ipairs( auxMockHttpRequests ) do
			WFCatTrashTestUtils.updateAssetIdInAuxMockHttpRequest( request, assetId, trashAssetId )
			catalogSpec:expectHttpRequest( request )
		end

		-- Wait for the upload of Aux

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'aux-direct' ))
			:expectDataWithinTime( 5 * waitMultiplier, 1)
			:dispose()

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype,
					trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId,
					true, true, options.xmpDevelop, true, expectedDevelopSettings, options.aux, auxInfo,
					xmpDevelopInvalid, prevXmpDevelopSha256 ),
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	if options.xmpDevelop then

		if options.atomicDevelopSettings then

			local xmpDevelopMocks = {}
			WFCatBinaryUploaderTestUtils.getXmpDevelopDirectUploadMocksForAsset( catalog, trashDocValue,
					{
						developSettingsSha256 = xmpDevelopSha256,
						xmpDevelopSettingsFilePath = xmpDevelopFilePath,
						xmpDevelopSettingsFileSize = WFFileUtils.calculatePathSize( xmpDevelopFilePath ),
					}, xmpDevelopMocks, { atomicDevelopSettings = true, forTestNightly = not options.mock, } )
			for _, mock in ipairs( xmpDevelopMocks ) do
				catalogSpec:expectHttpRequest( mock )
			end

		else

			trashRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, trashAssetId )
			catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, trashAssetId, trashRevId, xmpDevelopFilePath ) )

		end

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, xmpDevelopBucket ))
			:expectDataWithinTime( 5 * waitMultiplier, 2 )
			:dispose()

		xmpDevelopInvalid = false
		local xmpDevelopFileSize = 6504
			-- file size of the small xmp

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype,
					trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId,
					true, true, options.xmpDevelop, true, expectedDevelopSettings, options.aux, auxInfo,
					xmpDevelopInvalid, xmpDevelopSha256, xmpDevelopFileSize ),
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	-- not sure if this is important
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'
			return haveOriginal
		end ),
	true )

	if options.undo then

		rxTest:expectEqual( true, options.xmpDevelop and options.atomicDevelopSettings )
			-- the undo aspect of this test only makes sense with xmpDevelop and atomicDevelopSettings options both enabled.

		-- modify the asset again reverting the external develop to one with the original
		-- sha256 when the asset was restored. This xmp_develop binary should sync again.

		expectedDevelopSettings = prevExpectedDevelopSettings
		xmpDevelopFilePath = prevXmpDevelopFilePath

		revisionId, xmpDevelopSha256, developUserUpdated =
			WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
		expectedDevelopSettings.userUpdated = developUserUpdated

		-- refresh trashDocValue so it has the correct develop.userUpdated
		rxTest:waitFor( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				trashDocValue = docValue
				return docValue
			end )
		)

		local xmpDevelopMocks = {}
		WFCatBinaryUploaderTestUtils.getXmpDevelopDirectUploadMocksForAsset( catalog, trashDocValue,
				{
					developSettingsSha256 = xmpDevelopSha256,
					xmpDevelopSettingsFilePath = xmpDevelopFilePath,
					xmpDevelopSettingsFileSize = WFFileUtils.calculatePathSize( xmpDevelopFilePath ),
				}, xmpDevelopMocks, { atomicDevelopSettings = true, forTestNightly = not options.mock, } )
		for _, mock in ipairs( xmpDevelopMocks ) do
			catalogSpec:expectHttpRequest( mock )
		end

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, xmpDevelopBucket ))
			:expectDataWithinTime( 5 * waitMultiplier, 3 )
			:dispose()

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateTrashReqResp( catalogId, trashAssetId, revisionId,
				trashAssetSubtype, true, expectedDevelopSettings ) )

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

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, revisionId, fileName, trashAssetSubtype,
					trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId,
					true, true, true, true, expectedDevelopSettings )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

end

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

function WFCatTrashTest.rxTest_CT_MockModifyDelete_image( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_image_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyDelete_video( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, {
		mock = true,
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video"
	} )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_video_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, {
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
	} )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyDelete_xmpDevelop( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { mock = true, xmpSidecar = true, xmpDevelop = true, } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_xmpDevelop_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { xmpSidecar = true, xmpDevelop = true, } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyDelete_xmpDevelop_ADS( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { mock = true, xmpSidecar = true, xmpDevelop = true,
			atomicDevelopSettings = true, undo = true, } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_xmpDevelop_ADS_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { xmpSidecar = true, xmpDevelop = true,
			atomicDevelopSettings = true, undo = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyDelete_aux( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { mock = true, xmpDevelop = true, aux = true, } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_aux_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest, { xmpDevelop = true, aux = true } )
end

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

local function testAddSyncModifyDeleteRestoreSync( rxTest, options )

	-- NMB-11106 / NMB-11058
	-- Scenario:
	-- 1 User adds an image, lets it sync, pauses sync,
	-- 2 modifies the asset by adding a tint
	-- 3 delete the asset
	-- 4 restore the asset
	-- 5 turn on sync.
	-- 6 Wait 1 minute
	-- Expected result:
	-- restored asset retains it's new tint

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

	local expectedUserCreated = userCreated

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )
	if options.sha256 then
		importSource.sha256 = options.sha256
	end

	-- Add an image asset
	revisionId = rxTest:getFirstValue( WFCatTrashTestUtils.addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource ) )

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

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripUpdatesAndLinksIgnoringRenditions() ), 'success' )

	-- Upload the master
	revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

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


	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.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()

	if options.xmpSidecar then
		-- Using the xmp_create link template means we'll be uploading to the latest revision
		local latestRevisionId
		rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
			:pluck( '_serverProvidedMetadata', 'revision_ids' )
			:map( function( revisionIds )
				latestRevisionId = revisionIds[ #revisionIds ]
				return latestRevisionId
			end ), ':ignore:' )

		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpReqResp( catalogId, assetId, latestRevisionId, xmpSidecarFilePath ) )

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

	-- Modify the asset after it has been synced but before it is deleted

	-- 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)
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )

	local appliedDevelop

	if options.xmpDevelop then
		revisionId = WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	else

		local now = KSDate.timeInIso8601Format()

		if subtype == 'video' then

			appliedDevelop = getApplyVideoDevelopTint( now )
			revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay {
					_id = assetId,
					develop = appliedDevelop,
					userUpdated = now,
				} )

		else

			appliedDevelop = {
				processingModel = "lightroom",
				xmpCameraRaw = kApplyTintXmp
			}
			revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay {
					_id = assetId,
					develop = appliedDevelop,
					userUpdated = now,
				} )

		end

	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 = WFCatTrashTestUtils.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' )  )

	-- Make sure the createWithRevisionId is different than the current _revId,
	-- a newer revId indicates the modifications were applied.
	local trashCreateWithRevisionId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashCreateWithRevisionId = WFCatTrashUtils.getCreateWithRevisionId( docValue )
			trashRevId = docValue._revId
			return trashRevId ~= trashCreateWithRevisionId
		end )
	, true )

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

	-- now restore the asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

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

	-- Make sure the createWithRevisionId is different than the current _revId,
	-- a different revId indicates the modifications were applied.
	local restoredCreateWithRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			restoredCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( docValue )
			restoredRevId = docValue._revId
			return restoredRevId ~= restoredCreateWithRevId
		end )
	, true )

	-- 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 send up the delete, and restore
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredCreateWithRevId, subtype ) )

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

	-- Wait for oz to generate the renditions on the restored asset before requesting next update.
	-- Doing so will also ensure the trash asset has been deleted so we don't get an intermediate
	-- update while oz is processing the delete restore request.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredCreateWithRevId, subtype ), true )

	-- Get the restored asset, and the tombstone for the original asset

	local userCreatedAndUpdate = {
		userCreated = userCreated,
		userUpdated = KSDate.timeInIso8601Format() -- The restored asset from Oz has an updated userUpdated field.
	}

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreatedAndUpdate, importSource, assetId, true, true, options.xmpDevelop ),
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )

	-- make sure document is marked with a conflict
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):pluck( '_conflict' ), 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 an asset update to include the modifications that were made before the delete
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateAssetReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetSubtype, options.xmpDevelop, appliedDevelop ) )

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

	if options.xmpDevelop then
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredRevId, xmpDevelopFilePath ) )
	end

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

end

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

function WFCatTrashTest.rxTest_CT_MockModifyDeleteRestore( rxTest )
	-- NMB-11106 / NMB-11058
	-- see testAddSyncModifyDeleteRestoreSync above
	testAddSyncModifyDeleteRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDeleteRestore_testNightly( rxTest )
	-- NMB-11106 / NMB-11058
	-- see testAddSyncModifyDeleteRestoreSync above
	testAddSyncModifyDeleteRestoreSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyDeleteRestore_video( rxTest )
	-- see testAddSyncModifyDeleteRestoreSync above
	testAddSyncModifyDeleteRestoreSync( rxTest, {
		mock = true,
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
	} )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyDeleteRestore_video_testNightly( rxTest )
	-- see testAddSyncModifyDeleteRestoreSync above
	testAddSyncModifyDeleteRestoreSync( rxTest, {
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
	} )
end

--==============================================================================
-- @mark Tests that involve adding, deleting and restoring assets before syncing
--==============================================================================

local function testAddDeleteRestoreSync( rxTest, options )

	options = options or {}

	options.aux = options.xmpDevelop

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

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

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

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

	local auxMockHttpRequests, auxInfo

	if options.aux then
		revisionId, auxMockHttpRequests, auxInfo = WFCatTrashTestUtils.addAuxToDocument( rxTest, catalog, assetId, options )
	end

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

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = WFCatTrashTestUtils.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 ) )

	local restoredAssetId, restoredRevId, restoredDocValue = WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushCreateAssetReqResp( catalogId, fileName, subtype ) )

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

	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil
	if expectedDevelopSettings then
		expectedDevelopSettings.userUpdated = developUserUpdated
	end

	if options.aux then

		local auxId = auxInfo and next( auxInfo )

		expectedDevelopSettings.masks = {
			[ auxId ] = {
				digest = auxInfo[ auxId ].digest
			}
		}

	end

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

	uploader:startContinuousUploads()

	-- /rels/xmp - if present - gets uploaded before master
	if options.xmpSidecar then

		-- Using the xmp_create link template means we'll be uploading to the latest revision
		restoredRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, restoredAssetId )

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpReqResp( catalogId, restoredAssetId, restoredRevId, xmpSidecarFilePath ) )

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

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = false, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )

	end

	if options.aux then
	
		restoredRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, restoredAssetId )

		for _, request in ipairs( auxMockHttpRequests ) do
			WFCatTrashTestUtils.updateAssetIdInAuxMockHttpRequest( request, assetId, restoredAssetId )
			catalogSpec:expectHttpRequest( request )
		end

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'aux-direct' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		-- Get the updated metadata with the master link.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )
		
	end

	-- /rels/xmpDevelop gets uploaded before master
	if options.xmpDevelop then

		restoredRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, restoredAssetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredRevId, xmpDevelopFilePath ) )

		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		-- Get the updated metadata with the master link.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )
		
	end

	-- Upload the master
	restoredRevId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, restoredAssetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createMasterReqResp( catalogId, restoredAssetId, restoredRevId, originalFilePath, nil, options.xmpSidecar ) )

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

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
	}, WFCatTrashTestUtils.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()

	-- Wait for the update to get applied to the assets docValue for aux
	if options.aux then

		local auxId = auxInfo and next( auxInfo )

		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/aux/' .. auxId, 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()

	end

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

	-- Make sure the trash asset was deleted
	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 originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return haveOriginal
		end ),
	true )

	-- Didn't bother checking renditions here as this should be the same as uploading a new asset.

end

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

function WFCatTrashTest.rxTest_CT_OzAddDeleteRestore_testNightly( rxTest )

	-- Restore before the asset create and delete have been uploaded to oz so the
	-- metadata pusher will not need to send the restore command.

	testAddDeleteRestoreSync( rxTest )

end

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

function WFCatTrashTest.rxTest_CT_MockAddDeleteRestore( rxTest )

	-- Restore before the asset create and delete have been uploaded to oz so the
	-- metadata pusher will not need to send the restore command.

	testAddDeleteRestoreSync( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzAddDeleteRestore_xmp_testNightly( rxTest )

	-- Restore before the asset create and delete have been uploaded to oz so the
	-- metadata pusher will not need to send the restore command.

	-- This version verifies LrCC will properly preserve xmp_develop and xmp sidecar
	-- files as well.

	testAddDeleteRestoreSync( rxTest, { xmpDevelop = true, xmpSidecar = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockAddDeleteRestore_xmp( rxTest )

	-- Restore before the asset create and delete have been uploaded to oz so the
	-- metadata pusher will not need to send the restore command.

	-- This version verifies LrCC will properly preserve xmp_develop and xmp sidecar
	-- files as well.

	testAddDeleteRestoreSync( rxTest, { mock = true, xmpDevelop = true, xmpSidecar = true } )

end

--==============================================================================
-- @mark Tests that involve deleting and restoring asset before syncing
--==============================================================================

local function testAddSyncDeleteRestoreSync( rxTest, options )

-- NMB-9370
-- Scenario:
-- Steps:
--		A) Add one asset and sync to Oz. And user pauses sync
--		B) deletes an existing asset that was on oz. And restore the asset. Sync again
-- After 2 steps the test verify:
--		A) Delete asset A and create asset C from trash B are sent up

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- Step 1:

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
							{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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


-- Step 2:

	-- Delete the asset which is on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
		, true )

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

	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
			return docValue
		end )
	, ':ignore:' )

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

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

	if options.deleteRestore2x then

		-- Bug NMB-10949 deleting and restoring an asset twice causes it to lose it's master
		-- (actually it says doing it with a Lightroom Classic asset ends up with a stuck sync)

		-- Delete the restored asset which is not on Oz
		rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

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

		-- Make sure a deleted_image was created
		local trashAssetId2, trashRevId2, trashDocValue2 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest, catalog, waitMultiplier,
			{ assetId, trashAssetId, restoredAssetId } )
		rxTest:expectEqual( 'string', type( trashAssetId2 ) )
		rxTest:expectEqual( 'string', type( trashRevId2 ) )
		rxTest:expectEqual( trashAssetSubtype, trashDocValue2.subtype )

		local trashUserCreated2 = trashDocValue2.userCreated
		local trashPurgeDate2 = trashDocValue2.purgeDate

		-- Restore the trash asset that was not on oz before syncing it.
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId2 }, pc ) )

		local restoredAssetId2, restoredRevId2, restoredDocValue2 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest,
			catalog, waitMultiplier, { assetId, trashAssetId, restoredAssetId, trashAssetId2 } )

		rxTest:expectEqual( 'asset', restoredDocValue2.type )
		rxTest:expectEqual( subtype, restoredDocValue2.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue2, 'original' ) )

		-- update the expected trash and restored asset ids as we won't be
		-- sending the intermediate delete and restore.
		trashAssetId = trashAssetId2
		trashRevId = trashRevId2
		trashPurgeDate = trashPurgeDate2
		trashUserCreated = trashUserCreated2
		restoredAssetId = restoredAssetId2
		restoredRevId = restoredRevId2

	end

	-- Make sure the last trash userCreated and purgeDate data is correctly stored in restored asset createFrom field
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( docValue )
			return createFromInfo.lastTrashUserCreated == trashUserCreated and createFromInfo.lastTrashPurgeDate == trashPurgeDate
		end )
	, true )

	-- Send create restored asset from original asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, subtype ) )

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

	if options.splitResponse then
		-- As seen in sporadic unit test failures sometimes the MetadataPuller will get
		-- the request after oz has completed the create trash asset but before
		-- oz has created the restored asset. When this happens we will get a
		-- delete for the original asset and an update for the trash asset.
		-- This test replicates that as it exposes a case where the docstore
		-- improperly overwrites a deleted document with prior contents.

		-- Get trash asset and tombstone for original asset.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
		} ), 'success' )

		-- Make sure the trash asset is still deleted.  A prior version of the DocStore
		-- would incorrectly overwrite the deleted document with the prior trash
		-- asset contents and this would fail.
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:pluck( '_deleted' ),
		true )

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

		-- Get restored asset and tombstone for the trash asset.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop )
		} ), 'success' )

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

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

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0 ) )

	else

		-- Wait for oz to generate the renditions on the restored asset before requesting next update.
		-- Doing so will also ensure the trash asset has been deleted so we don't get an intermediate
		-- update while oz is processing the delete restore request.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

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

		-- Get the restored asset (with the links that point to the existing master)
		-- back in the sync feed.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
			WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop )
		} ), 'success' )

	end

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

	-- Make sure the trash asset was deleted
	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 originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return haveOriginal
		end ),
	true )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_testNightly( rxTest )

	-- NMB-9370

	-- Restore before the asset delete has been uploaded to oz, the
	-- metadata pusher will send delete original asset and create restored asset from trash asset

	testAddSyncDeleteRestoreSync( rxTest )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore( rxTest )

	-- NMB-9370

	-- Restore before the asset delete has been uploaded to oz, the
	-- metadata pusher will send delete original asset and create restored asset from trash asset

	testAddSyncDeleteRestoreSync( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_SplitResponse( rxTest )

	-- NMB-11489

	-- Restore before the asset delete has been uploaded to oz, the
	-- metadata pusher will send delete original asset and create restored
	-- asset from trash asset.

	-- Depending on where oz with the processing of the two operations
	-- the MetadataPuller may end up getting an update where the trash asset
	-- has been created but the restore hasn't been processed yet, this
	-- results in a split response that originally resulted in the DocStore
	-- overwriting a deleted trash asset with it's original contents.  This
	-- test verifies that will not happen.

	testAddSyncDeleteRestoreSync( rxTest, { mock = true, splitResponse = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore2X_testNightly( rxTest )

	-- Restore before the asset delete has been uploaded to oz, the
	-- metadata pusher will send delete original asset and create restored asset from trash asset

	testAddSyncDeleteRestoreSync( rxTest, { deleteRestore2x = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore2X( rxTest )

	-- Restore before the asset delete has been uploaded to oz, the
	-- metadata pusher will send delete original asset and create restored asset from trash asset

	testAddSyncDeleteRestoreSync( rxTest, { mock = true, deleteRestore2x = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteTrash( rxTest )

	-- NMB-11212
	testAddSyncDeleteSync( rxTest, { mock = true, deleteTrash = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteTrash_testNightly( rxTest )

	-- NMB-11212
	testAddSyncDeleteSync( rxTest, { deleteTrash = true } )

end

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

local function testAddSyncDeleteRestoreDeleteSync( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add one asset and sync to Oz.
--		2) Pause sync
--		2) Deletes the existing asset that was on oz.
-- 		3) Restore the asset.
--		4) Delete the asset again
--		5) Sync
--
-- Expected:
-- 		A) should send one delete with the newest trash asset id
--		B) does not send update the first trash asset or it's restore

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- Step 1:

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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


-- Step 2:

	-- Delete the asset which is on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

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

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

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

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

	-- Delete the restored asset which is not on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

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

	-- Make sure a deleted_image was created
	local trashAssetId2, trashRevId2, trashDocValue2 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId, restoredAssetId } )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )
	rxTest:expectEqual( assetId, trashDocValue2.original.id )

	-- Send delete to Oz (note the delete of the original asset will refer to the second trash asset).
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId2, trashRevId2 ) )

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

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId2, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

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

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

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

	-- Make sure the second trash asset has local files
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 )
		:map( function( docValue )
			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return haveOriginal
		end ),
	true )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestoreDelete( rxTest )
	testAddSyncDeleteRestoreDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestoreDelete_testNightly( rxTest )
	testAddSyncDeleteRestoreDeleteSync( rxTest )
end

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

local function testAddDeleteSyncRestoreDeleteSync( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo.
--		5) Delete the restored photo.
--		6) Sync
--
-- Expected:
-- 		A) Step 6 should send a create with restoreFrom and a delete with deleted_asset info.
--		B) Step 6 should not try to upload the binary again if it pulls an update without
--         the master link.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- Step 1: Add photo

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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

-- Step 2:  Delete the photo.

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

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

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

-- Step 3: Sync the Delete

	-- Send delete to Oz (note the delete of the original asset will refer to the second trash asset).
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

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

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

-- Step 4: Restore the trash asset that is on oz.

	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId } ) )

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

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

-- Step 5: Delete the restored asset

	-- Delete the restored asset which is not on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId } ) )

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

	-- Make sure a deleted_image was created
	local trashAssetId2, trashRevId2, trashDocValue2 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId, restoredAssetId } )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )
	rxTest:expectEqual( trashAssetId, trashDocValue2.original.id )

-- Step 6: Sync the restore and delete to oz

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )

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

	if options.storageExceeded then

		-- Get the error back from oz.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.storageExceededUpdateResponse( 'create', restoredAssetId, restoredRevId, 'asset' ),
		} ), 'success' )

		-- Verify document is marked with StorageExceededError.
		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
			:pluck( '_localOnly', 'ozError', 'subtype' ) )
		:expectDataWithinTime( 5 * waitMultiplier, 'StorageExceededError' )
		:dispose()

		-- Make sure we don't try to send anything back up for the asset.
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

		return -- end of test for StorageExceededError

	end

	-- Wait for oz to generate the renditions on the restored asset before requesting next update.
	-- Doing so will also ensure the trash asset has been deleted so we don't get an intermediate
	-- update while oz is processing the restore delete request.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId2, trashRevId2, trashAssetSubtype ), true )

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, restoredAssetId, true, true ),
		WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( restoredAssetId, trashAssetId2, userId ),
	}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )

	if options.updateWithoutMasterLink then

		-- Make it look like oz didn't provide a master, xmp, or xmp/develop links.
		-- Sometimes oz will send an update without them and when it does we want to make sure
		-- the BinaryUploader does not re-upload a master unneccessarily.
		rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay( {
				label = 'oz',
				{
					_id = trashAssetId2,
					_serverProvidedMetadata = {
						links = {
							[ '/rels/master' ] = { _delete = true },
							[ '/rels/xmp' ] = { _delete = true },
							[ '/rels/xmp/develop' ] = { _delete = true },
						},
					},
				}
			} )
		, ':ignore:' )

		rxTest:sleep( .5 ) -- Make sure the index store has a chance to tell the binary uploader about the updated links.

		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId, localFilePath, uploadUrl, binaryType, contentType,
				isDenied, sha256, fileSize, sha256Error, sourceLinkForCopy,
				localVolumeGuid, localRelativePath
			FROM needsBinaryUpload
			ORDER BY fileMaybeOnOfflineVolume ASC, isDenied ASC, priorityForUpload DESC, importDate ASC, captureDate ASC
			LIMIT 100]],
			quiesceUntilUpToDate = true,
		})
		:expectData { }
		:dispose()

	end

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

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

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

	-- Make sure the second trash asset has local files
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 )
		:map( function( docValue )
			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return haveOriginal
		end ),
	true )

	-- Try pulling an empty update to generate some http traffic to make sure the
	-- uploader didn't try to send a binary.  false means there were no updates.

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pullUpdatesReqResp( catalogId, {} ) )
	rxTest:expectSingleValue( puller:getLatestUpdates( 'onceWithStatus'), false )

	-- Make sure the uploader only uploaded one asset.
	rxTest:expectEqual( 1, uploader:DEBUG_uploadCount( 'master' ) )

end

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

function WFCatTrashTest.rxTest_CT_MockRestoreDelete( rxTest )
	-- NMB-11107
	testAddDeleteSyncRestoreDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_testNightly( rxTest )
	-- NMB-11107
	testAddDeleteSyncRestoreDeleteSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_StorageExceededError( rxTest )

	-- This is a mocked only test as we are generating the kStorageExceededError.

	-- NMB-11108
	testAddDeleteSyncRestoreDeleteSync( rxTest, { mock = true, storageExceeded = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_updateWithoutLinks( rxTest )
	-- NMB-11609
	testAddDeleteSyncRestoreDeleteSync( rxTest, { mock = true, updateWithoutMasterLink = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_updateWithoutLinks_testNightly( rxTest )
	-- NMB-11609
	testAddDeleteSyncRestoreDeleteSync( rxTest, { updateWithoutMasterLink = true } )
end

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

local function testAddDeleteSyncRestoreDeleteDelTrashSync( rxTest, options )

-- Scenario:
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo.
--		5) Delete the restored photo.
--		6) Permanently delete the trashed photo
--		7) Sync
--
-- Expected:
-- 		Should only send a single delete request deleting the original trash photo

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- Step 1: Add photo

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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

-- Step 2:  Delete the photo.

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

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

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

-- Step 3: Sync the Delete

	-- Send delete to Oz (note the delete of the original asset will refer to the second trash asset).
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

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

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

	-- get the updated trashRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map(function( docValue )
			trashRevId = docValue._revId
			return type( trashRevId )
		end )
	, 'string' )

-- Step 4: Restore the trash asset that is on oz.

	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId } ) )

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

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

-- Step 5: Delete the restored asset

	-- Delete the restored asset which is not on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId } ) )

	-- Make sure a deleted_image was created
	local trashAssetId2, trashRevId2, trashDocValue2 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId, restoredAssetId } )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )
	rxTest:expectEqual( trashAssetId, trashDocValue2.original.id )

-- Step 6: delete the newly created trash

	-- Delete the restored asset which is not on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId2 } ) )

-- Step 7: Sync should send a single delete to oz to delete the original trash asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteTrashReqResp( catalogId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trash asset tombstone back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetDeletedResponse( trashAssetId, userId ),
	}, WFCatTrashTestUtils.stripForAssetUpdate( true ) ), 'success' )


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

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

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

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

end

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

function WFCatTrashTest.rxTest_CT_MockRestoreDeleteDelTrash( rxTest )
	-- NMB-14039
	testAddDeleteSyncRestoreDeleteDelTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestoreDeleteDelTrash_testNightly( rxTest )
	-- NMB-14039
	testAddDeleteSyncRestoreDeleteDelTrashSync( rxTest )
end


--==============================================================================
-- @mark Tests that involve adding, directly deleting
--==============================================================================

local function testAddSyncDirectlyDeleteSync( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 20 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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'
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	fileName = options.fileName or fileName
	subtype = options.subtype or 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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

	if options.mock then
		-- TODO [kcarlson 2018-07-09]:  Not sure why this doesn't get issued when using oz
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.catalogPrefetchReqResp( catalogId ) )
	end

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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 = WFCatTrashTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

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

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

	uploader:startContinuousUploads()

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

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

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.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

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

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

	-- Directly delete asset
	rxTest:expectOnlyCompleted( WFCatTrash.directlyDeleteAssets( catalog, { assetId }, pc ) )

	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			-- get the last revision of the trash asset (revision when applyOverlay with _deleted = true).
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAndDeleteTrashAssetWithTrashInfoStripReqResp( catalogId, assetId, revisionId ) )

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

	-- Wait until the asset and trash asset show up in the sync feed.
	local expectedUpdates = {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, 'trashAssetId', userId ),
		WFCatTrashTestUtils.trashAssetDeletedResponse( 'trashAssetId', userId ),
	}

	local expectedResponse = {
		-- This is actually abbreviated as it doesn't contain base or links but that shouldn't matter.
		updates = expectedUpdates
	}

	local function removeNonAssetUpdates( updates )

		for x = #updates, 1, -1 do
			if updates[ x ].type ~= 'asset' then
				table.remove( updates, x )
			end
		end

		return updates

	end

	rxTest:observe( WFCatTrashTestUtils.pollHttpGetMetadataUpdates( catalogSpec, catalog:docStore(), '200', expectedResponse )
		:map( function( statusCode, response )
			local assetUpdates = removeNonAssetUpdates( response.updates )
			return statusCode, #assetUpdates
		end ) )
	:expectDataWithinTime( 5 * waitMultiplier, '200', 2 )
	:dispose()

	-- Verify the trash asset is deleted
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, 'trashAssetId', userId ),
		WFCatTrashTestUtils.trashAssetDeletedResponse( 'trashAssetId', userId ),
	}, function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

		-- Remove album_asset, comment, and favorite deletes as sometimes they are delivered and sometimes not.
		ozDataAsTable.updates = removeNonAssetUpdates( ozUpdateTable )

		mockDataAsTable, ozDataAsTable = WFCatTrashTestUtils.stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		return mockDataAsTable, ozDataAsTable

	end ), 'success' )

end

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

function WFCatTrashTest.rxTest_CT_MockAddSyncDirectlyDeleteSync( rxTest )
	-- This shouldn't send anything to oz at all
	testAddSyncDirectlyDeleteSync( rxTest, { mock = true } )
end

--==============================================================================
-- @mark tests that involve oz removing expired assets
--==============================================================================

function WFCatTrashTest.rxTest_CT_ExpiredTrashDeletedByOz( rxTest )
-- verify that oz deletes expired assets via a delete on the deleted_image and deleted_video assets

-- Upload an asset with timeOfDelete with 30 seconds left, wait 2 minutes and make
-- sure we receive a delete on the assets.

-- Should be able to check that the asset images no lonber exists on disk as well.
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashTest.rxTest_CT_Restore_ExpiredTrashDeletedByOz( rxTest )
--Scenario: When B is restored in LrCC while user is offline and Auto purge happens on Oz based on 60 days
	rxTest:expectEqual( false, failStubbedTests )
end

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

local function testHideExpiredAssets( rxTest )

	local allTrashAssetRequiredFields = "purgeDate"

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

	local getMostImmediatePurgeDateFunction = function( rows, ascDesc )

		local mostImmediatePurgeDate

		if rows[ 1 ] then
			local mostImmediatePurgeDateRowIndex = ascDesc == 'ASC' and 1 or #rows
			local mostImmediatePurgeDateRow = rows[ mostImmediatePurgeDateRowIndex ]
			mostImmediatePurgeDate = mostImmediatePurgeDateRow[ 1 ]
		end

		return mostImmediatePurgeDate

	end

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

	local function masterHref( baseUrl, assetId )
		return string.format( "%s/%s/revisions/%s/master", baseUrl, assetId, KSIdGenerator.uuidInLowercaseHex() )
	end

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

	local waitMultiplier = 1

	rxTest:setTimeLimit( 15 * waitMultiplier )

	local testSession, catalog, catalogSpec = WFCatTrashTestUtils.setupCloudTrashTestSession( rxTest, { mock = true } )
	assert( testSession )

	local baseUrl = catalogSpec:catalogHref()

	local now = KSDate.timeInIso8601Format()

	local userCreated = now
	local importTimestamp = now

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	local catalogId = catalog:ozCatalogId()
	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local assetId2 = KSIdGenerator.uuidInLowercaseHex()

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.addDocumentWithLocalMaster( catalog, catalogId, assetId1, originalFilePath, subtype, userCreated, importSource )
	, ':ignore:' )

	fileName = 'IMG_9876.CR2'

	-- Add another image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.addDocumentWithLocalMaster( catalog, catalogId, assetId2, originalFilePath, subtype, userCreated, importSource )
	, ':ignore:' )

	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			{
				_id = assetId1,
				_serverProvidedMetadata = {
					links = {
						[ '/rels/master' ] = {
							href = masterHref( baseUrl, assetId1 )
						}
					},
				},
			},
			{
				_id = assetId2,
				_serverProvidedMetadata = {
					links = {
						[ '/rels/master' ] = {
							href = masterHref( baseUrl, assetId2 )
						}
					},
				},
			}

		} )
	, ':ignore:' )

	-- Delete images

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId1 } ) )
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId2 } ) )

	-- Assets should now appear in the trash asset list.

	rxTest:expectSingleValue( WFCatTrash.trashAssetList( catalog, allTrashAssetRequiredFields, getMostImmediatePurgeDateFunction )
		:filter( function( list )
			return #list == 2
		end )
		:take( 1 )
		:map( function( val )
			return #val
		end ),
		2 )

	local localTrashAssetId = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId1 )

	-- Change purge date for the first image to be 2 seconds from now.
	local currentTime = KSDate.timeInIso8601Format()
	local twoSecsLater = KSDate.shiftIso8601DateTime( currentTime, 2 )

	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			{
				_id = localTrashAssetId,
				purgeDate = twoSecsLater,
			}
		} )
	, ':ignore:' )

	-- Verify purgeDate for trash asset 1.
	rxTest:expectSingleValue( catalog:docStore():docValue( localTrashAssetId )
		:pluck( 'purgeDate' )
	, twoSecsLater )

	-- Should still get a list of 2 for a moment or two.
	rxTest:observe( WFCatTrash.trashAssetList( catalog, allTrashAssetRequiredFields, getMostImmediatePurgeDateFunction )
		:map( function( list )
			return #list
		end ) )
	:expectDataWithinTime( 2 * waitMultiplier, 2 )
	:dispose()

	-- And now we should (very) soon be getting 1 item in the trash asset list.
	rxTest:observe( WFCatTrash.trashAssetList( catalog, allTrashAssetRequiredFields, getMostImmediatePurgeDateFunction )
		:map( function( list )
			return #list
		end ) )
	:expectDataWithinTime( 3 * waitMultiplier, 1 )
	:dispose()

end

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

function WFCatTrashTest.rxTest_CT_Mock_hide_expiredTrash( rxTest )

	-- NMB-9383
	-- Assets with a purge date which is in the past should not appear in the
	-- trash asset list.

	testHideExpiredAssets( rxTest )
end

--==============================================================================
-- @mark tests that involve double delete or restore
--==============================================================================

local function testDoubleDeleteOrRestore( rxTest, options )

	-- NMB-12753/NMB-12848 - double delete/double restore

	-- We've seen evidence that sync clients can sometimes issue a delete/restore for
	-- the same asset twice, which may lead to resource not found or resource already
	-- exists errors received from Oz. This test makes sure restored assets stay
	-- restored, and deleted assets don't get permanently deleted in those cases.

	options = options or {}

	-- This test needs X-Quicker-Retries added to the header to be able to
	-- run in a reasonable amount of time.
	options.injectQuickerRetries = not options.mock

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local now = KSDate.timeInIso8601Format()

	local userCreated = now
	local importTimestamp = now
	local assetCreated = now
	local trashPurgeWindow

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeWindow = purgeAssetsAfter
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

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

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )

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

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

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete.
	rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'master' ) )
	:expectDataWithinTime( 5 * waitMultiplier, 1 )
	:dispose()

	uploader:stopContinuousUploads()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then

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

	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = waitForRenditions } )
	}, WFCatTrashTestUtils.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()

	-- Image asset is now complete and fully synced.

	-- Delete image

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

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

	local trashAssetId, trashAssetRevId, trashDoc = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )

	local trashCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( trashDoc )

	local trashPurgeDate = KSTableUtils.derefOpt( trashDoc, 'purgeDate' )
	local trashAssetCreated  = KSTableUtils.derefOpt( trashDoc, 'userCreated' )

	-- Send the delete before pulling updates
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, emptyRevId, trashAssetId, trashCreateWithRevId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )

	if options.doubleDelete then

		-- Apply a _localOnlyChange to the trash asset to provoke a 2nd create POST
		-- for the trash asset. In real life, this could be a camera profile related
		-- change.

		rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay( {
				{
					_id = trashAssetId,
					_localOnly = {
						bogus = {
							bogusBogus = 'bogusbogus',
						},
					},
				}
			} )
		, ':ignore:' )

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, emptyRevId, trashAssetId, trashCreateWithRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )

		-- Pull updates from Oz
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", assetId, emptyRevId, "asset", assetId, "asset", trashAssetId, userId ),
		} ), 'success' )

		-- Wait for the error to be processed.
		rxTest:observe( WFCatTrashTestUtils.pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1, false )
		:dispose()

		-- Make sure the trash asset was not deleted

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' )
		, nil )

	else

		-- Pull updates from Oz
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
		} ), 'success' )

	end

	if options.doubleRestore then

		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:pluck( '_deleted' ),
		true )

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

		if not options.mock then
			puller:stopContinuousUpdates()
		end

		-- Send create restored asset from original asset
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashAssetRevId, restoredDocValue.userUpdated ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		if not options.mock then
			-- need to give Oz time to restore the asset (sometimes it takes a bit - especially stage)
			rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilAssetExistsWithLinks( 3* waitMultiplier, catalogSpec, catalogId, restoredAssetId, WFCatTrashTestUtils.expectedBinaryLinksForImage(), options ), true )
		end

		-- Apply a _localOnlyChange to the trash asset to provoke a 2nd create POST
		-- for the restored asset.

		rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay( {
				{
					_id = restoredAssetId,
					_localOnly = {
						bogus = {
							bogusBogus = 'bogusbogus',
						},
					},
				}
			} )
		, ':ignore:' )

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashAssetRevId, restoredDocValue.userUpdated ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Wait for ResourceNotFoundError error.

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", restoredAssetId, userId ),
		} ), 'success' )

		-- Make sure the restored asset didn't get deleted again, which, if it
		-- did happen, would happen as part of the error handling. So, first
		-- we make sure that the error handling is complete, then that the
		-- restored asset has not been deleted.

		-- Wait for the error to be processed.
		rxTest:observe( WFCatTrashTestUtils.pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1, false )
		:dispose()

		rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:pluck( '_deleted' )
		, nil )

		-- Check to see how the ResourceNotFoundError was resolved
		rxTest:expectEqual( {
			ResourceNotFoundError = {
				count = 1,
				resolution = {
					IgnoredOperationAlreadyHappened = {
						count = 1,
						sourceDocIds = {
							restoredAssetId,
						},
					},
				},
			},
		}, errorHandler:DEBUG_errorsResolved() )

	elseif options.doubleDelete then

		-- Check to see how the ResourceNotFoundError was resolved
		rxTest:expectEqual( {
			ResourceNotFoundError = {
				count = 1,
				resolution = {
					IgnoredOperationAlreadyHappened = {
						count = 1,
						sourceDocIds = {
							assetId,
						},
					},
				},
			},
		}, errorHandler:DEBUG_errorsResolved() )

	else
		rxTest:expectEqual( {}, errorHandler:DEBUG_errorsResolved() )
	end

end

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

function WFCatTrashTest.rxTest_CT_MockDoubleDelete( rxTest )

	testDoubleDeleteOrRestore( rxTest, { mock = true, doubleDelete = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDoubleRestore( rxTest )

	testDoubleDeleteOrRestore( rxTest, { mock = true, doubleRestore = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDoubleDelete_testNightly( rxTest )

	testDoubleDeleteOrRestore( rxTest, { doubleDelete = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDoubleRestore_testNightly( rxTest )

	testDoubleDeleteOrRestore( rxTest, { doubleRestore = true } )

end

--==============================================================================
-- @mark Deleting, restoring or permanently deleting a large amount of photos
--==============================================================================

local function testAddSyncDeleteSyncLarge( rxTest, options )

	WFCatIdUtils.debug_clearCaches()

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	waitMultiplier =  waitMultiplier * ( options.numPhotos / 1000 )

	setTimeoutAndRetryLimit( rxTest, options, 70 * waitMultiplier )

	local testSession, catalog, _, _, _, _, pc = WFCatTrashTestUtils.setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )

	local originalFileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize

	local files_xmp_develop

	if options.xmpDevelop then

		local xmpDevelopString = WFCatTrashTestUtils.getCameraRaw( true )
		local xmpDevelopData = KSData.make( xmpDevelopString )
		local xmpDevelopFileSize = #xmpDevelopString
		KSData.saveToFile( xmpDevelopData, xmpDevelopFilePath )
		rxTest:expectEqual( 'file', WFFileUtils.exists( xmpDevelopFilePath ) )

		rxTest:finally( function()
			WFFileUtils.delete( xmpDevelopFilePath )
		end )

		files_xmp_develop = {
			path = xmpDevelopFilePath,
			fileSize = xmpDevelopFileSize,
			sha256 = WFCatTrashTestUtils.xmpDevelopSha256,
		}

	end

	local files_original_xmp
	local xmpSidecarExists = false

	if options.xmpSidecar then

		local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
		local xmpSidecarString = WFCatTrashTestUtils.getXmpSidecar()
		local xmpSidecarData = KSData.make( xmpSidecarString )
		local xmpSidecarFileSize = #xmpSidecarString
		KSData.saveToFile( xmpSidecarData, xmpSidecarFilePath )
		rxTest:expectEqual( 'file', WFFileUtils.exists( xmpSidecarFilePath ) )

		rxTest:finally( function()
			WFFileUtils.delete( xmpSidecarFilePath )
		end )

		files_original_xmp = {
			relativePath = WFFileUtils.removeVolumeFromPath( xmpSidecarFilePath ),
			volumeGuid = WFFileUtils.getVolumeGuidForPath( xmpSidecarFilePath ),
			fileSize = xmpSidecarFileSize,
		}

		xmpSidecarExists = true

	end

	local assetIds = {}
	do
		local overlays = {}

		local relativePath = WFFileUtils.removeVolumeFromPath( originalFilePath )
		local volumeGuid = WFFileUtils.getVolumeGuidForPath( originalFilePath )

		local spm_links = {
			[ '/rels/master' ] = { href = '/rels/master' },
			[ '/rels/xmp' ] = { href = '/rels/xmp' },
			[ '/rels/xmp/develop' ] = { href = '/rels/xmp/develop' },
		}

		for i = 1, options.numPhotos do

			local assetId = KSIdGenerator.uuidInLowercaseHex()
			local newRevId = KSIdGenerator.uuidInLowercaseHex()

			assetIds[ i ] = assetId

			overlays[ i ] = {
				_id = assetId,
				_newRevId = newRevId,
				type = 'asset',
				subtype = subtype,
				userCreated = userCreated,
				userUpdated = userCreated,
				captureDate = "2018-01-01T00:00:00Z",

				-- make it look like we pulled these documents down from oz
				_serverProvidedMetadata = {
					created = userCreated,
					links = spm_links,
					revision_ids = {
						newRevId,
					},
				},

				_localOnly = {
					files = {
						original = {
							master = {
								relativePath = relativePath,
								volumeGuid = volumeGuid,
								size = originalFileSize,
							},
							xmp = files_original_xmp,
						},
						xmp_develop = files_xmp_develop,
					},
					xmpSidecarExists = xmpSidecarExists,
				},

				importSource = importSource,
			}
		end

		rxTest:expectSingleValue( WFRxBatchedBulkApplyOverlay( "create documents", catalog:docStore(), overlays ), ':ignore:' )

	end

	local queryNumberOfAssets = Observable.defer( function()
		return catalog:indexStore():query {
			sql = [[SELECT COUNT(*) FROM ]] .. assetTableName,
			quiesceUntilUpToDate = true,
		}
		:pluck( 1, 1 )
	end )

	rxTest:observe( queryNumberOfAssets )
	:expectDataWithinTime( 10 * waitMultiplier, options.numPhotos )
	:dispose()


	if options.modifyBeforeDelete then

		do
			local overlays = {}

			for i = 1, options.numPhotos do

				overlays[ i ] = {
					_id = assetIds[ i ],
					develop = {
						processingModel = "lightroom",
						xmpCameraRaw = kApplyTintXmp
					},
				}

			end

			rxTest:expectSingleValue( WFRxBatchedBulkApplyOverlay( "modify documents", catalog:docStore(), overlays ), ':ignore:' )

		end

	end

	collectgarbage()
	collectgarbage()
	collectgarbage()

	local changeCounterBeforeMoveToTrash
	rxTest:expectSingleValue( catalog:docStore():changeCounterOnce()
		:map( function( changeCounter )
			changeCounterBeforeMoveToTrash = changeCounter
			return type( changeCounter )
		end )
	, 'number' )


	local startTime
	startTime = KSDate.timeNowForAnimation()

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, assetIds, pc ) )

	local moveAssetsToTrashDuration = KSDate.timeNowForAnimation() - startTime
	rxTest:logf( "WFCatTrash.moveAssetsToTrash with %d items took %.1f seconds.", options.numPhotos, moveAssetsToTrashDuration )

	local queryNumberOfTrashAssets = Observable.defer( function()
		return catalog:indexStore():query {
			sql = [[SELECT COUNT(*) FROM trashAssets]],
			quiesceUntilUpToDate = true,
		}
		:pluck( 1, 1 )
	end )

	rxTest:observe( queryNumberOfTrashAssets )
	:expectDataWithinTime( 10 * waitMultiplier, options.numPhotos )
	:dispose()

	-- Get trash asset Ids
	local trashAssetIds = {}

	local getTrashAssetIds = Observable.defer( function()
		return catalog:indexStore():query {
			sql = [[SELECT docId FROM trashAssets]],
			quiesceUntilUpToDate = true,
		}
		:where( function( rows )
			return #rows == options.numPhotos
		end )
		:map( function( rows )
			for i = 1, #rows do
				rows[ i ] = rows[ i ][ 1 ]
			end
			trashAssetIds = rows
			return #trashAssetIds
		end )
		:take( 1 )
	end )

	rxTest:observe( getTrashAssetIds )
	:expectDataWithinTime( 10 * waitMultiplier, options.numPhotos )
	:dispose()

	collectgarbage()
	collectgarbage()
	collectgarbage()



	if options.restore then

		startTime = KSDate.timeNowForAnimation()

		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, trashAssetIds, pc ) )

		local restoreDuration = KSDate.timeNowForAnimation() - startTime
		rxTest:logf( "WFCatTrash.restoreTrashAssets with %d items took %.1f seconds.", options.numPhotos, restoreDuration )

		rxTest:observe( queryNumberOfAssets )
		:expectDataWithinTime( 10 * waitMultiplier, options.numPhotos )
		:dispose()

		rxTest:observe( queryNumberOfTrashAssets )
		:expectDataWithinTime( 10 * waitMultiplier, 0 )
		:dispose()

	elseif options.delTrash then

		startTime = KSDate.timeNowForAnimation()

		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, trashAssetIds, pc ) )

		local delTrashDuration = KSDate.timeNowForAnimation() - startTime
		rxTest:logf( "WFCatTrash.deleteTrashAssets with %d items took %.1f seconds.", options.numPhotos, delTrashDuration )

		rxTest:observe( queryNumberOfAssets )
		:expectDataWithinTime( 10 * waitMultiplier, 0 )
		:dispose()

		rxTest:observe( queryNumberOfTrashAssets )
		:expectDataWithinTime( 10 * waitMultiplier, 0 )
		:dispose()

	end

end

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

function WFCatTrashTest.rxTest_CT_Mock_Large_DeleteRestore_testNightly( rxTest )

	testAddSyncDeleteSyncLarge( rxTest, { mock = true, restore = true, numPhotos = 2999 } )

end

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

function WFCatTrashTest.rxTest_CT_Mock_Large_DeleteDelTrash_testNightly( rxTest )

	testAddSyncDeleteSyncLarge( rxTest, { mock = true, delTrash = true, numPhotos = 3000 } )

end

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

function WFCatTrashTest.rxTest_CT_Mock_Large_ModifyBeforeDelete_testNightly( rxTest )

	testAddSyncDeleteSyncLarge( rxTest, { mock = true, modifyBeforeDelete = true, numPhotos = 3001 } )

end

--==============================================================================
-- @mark tests that relate to timer checks
--==============================================================================

local function testTrashTimerModel( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 10 * waitMultiplier )

	local testSession, catalog = WFCatTrashTestUtils.setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local trashAssetCreated = '2018-04-15T21:21:21Z'

	local subtype = 'image'
	local trashAssetSubtype = 'deleted_' .. subtype

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local currentTime = KSDate.timeInIso8601Format()
	local purgeDate = KSDate.shiftIso8601DateTime( currentTime, 1 )

	-- Create an trash asset without local binary

	local notOnOz = options.autoPurge and options.onlyIfNotOnOz and options.notOnOz

	revisionId = rxTest:getFirstValue( catalog:docStore():setDocValue(
		{
			_id = trashAssetId,
			type = 'asset',
			subtype = trashAssetSubtype,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = purgeDate,
			_serverProvidedMetadata = not notOnOz and {
				links = {
					['/rels/master'] = {
						href = '/asset/revision/master'
					}
				}
			} or nil,
			_localOnly = {
				files = {
					master = WFPlatformInfo.isiOS and {
						path = 'originals/file'
					} or nil,
					original = WFPlatformInfo.isDesktop and {
						master = {
							volumeGuid = 'volumeGuid',
							relativePath = 'originals/file'
						}
					} or nil
				},
				originalFilePath = WFPlatformInfo.isAndroid and 'originals/file' or nil
			}
		} ) )


	if options.trashAssetList then

		local fieldsToReturn = "purgeDate"
		local getMostImmediatePurgeDateFunction = function( rows )
			local mostImmediatePurgeDate
			if rows[ 1 ] then
				mostImmediatePurgeDate = rows[ 1 ][ 1 ]
			end
			return mostImmediatePurgeDate
		end

		-- trashAssetList should return trash asset immediately, and update afterwards since the trash asset hits purgeDate
		local trashAssetListObs = rxTest:observe( WFCatTrash.trashAssetList( catalog, fieldsToReturn, getMostImmediatePurgeDateFunction ) )
			:expectData { { purgeDate } }

		trashAssetListObs
			:expectDataWithinTime( 3 * waitMultiplier, {} )
			:dispose()

	elseif options.countTrashAssets then

		-- countTrashAssets should return count immediately, and update afterwards since the trash asset hits purgeDate
		local countTrashAssetsObs = rxTest:observe( WFCatTrash.countTrashAssets( catalog ) )
			:expectData { { 1, purgeDate } }

		countTrashAssetsObs
			:expectDataWithinTime( 3 * waitMultiplier, { { 0 } } )
			:dispose()

	elseif options.autoPurge then

		-- trash asset should be purged once it hits purgeDate
		local autoPurgeObs = rxTest:observe( WFCatTrash.autoPurge( catalog, options.onlyIfNotOnOz ) )
			:expectData { purgeDate }

			autoPurgeObs
			:expectDataWithinTime( 3 * waitMultiplier, {} )
			:dispose()

		-- make sure the trash asset is purged if onlyIfNotOnOz is false, 
		-- or trash asset is not on Oz

		local trashAssetShouldBePurged = not options.onlyIfNotOnOz or options.notOnOz
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:pluck( '_deleted' ),
		trashAssetShouldBePurged and true or nil )

	end

end

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

function WFCatTrashTest.rxTest_CT_MockTimerModel_TrashAssetList( rxTest )

	testTrashTimerModel( rxTest, { mock = true, trashAssetList = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockTimerModel_CountTrashAssets( rxTest )

	testTrashTimerModel( rxTest, { mock = true, countTrashAssets = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockTimerModel_AutoPurge( rxTest )

	testTrashTimerModel( rxTest, { mock = true, autoPurge = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockTimerModel_AutoPurgeOnlyIfNotOnOz_isOnOz( rxTest )

	testTrashTimerModel( rxTest, { mock = true, autoPurge = true, onlyIfNotOnOz = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockTimerModel_AutoPurgeOnlyIfNotOnOz_notOnOz( rxTest )

	testTrashTimerModel( rxTest, { mock = true, autoPurge = true, onlyIfNotOnOz = true, notOnOz = true } )

end


--==============================================================================
-- @mark tests that related to restore assets with local binary
--==============================================================================

local function testRestoreOnlyAssetsWithLocalOriginal( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 5 * waitMultiplier )

	local testSession, catalog = WFCatTrashTestUtils.setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local trashAssetCreated = '2018-04-15T21:21:21Z'

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

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

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetId2 = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetId3 = KSIdGenerator.uuidInLowercaseHex()
	local revisionId, revisionId2, revisionId3

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

	local originalFileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize

	-- Create an trash asset with local original
	local fileInfo, originalFilePathInfo
	if WFPlatformInfo.isDesktop then
		fileInfo = {
			original = {
				master = {
					relativePath = WFFileUtils.removeVolumeFromPath( originalFilePath ),
					volumeGuid = WFFileUtils.getVolumeGuidForPath( originalFilePath ),
					size = originalFileSize
				}
			}
		}

	elseif WFPlatformInfo.isiOS then
		fileInfo = {
			master = {
				path = originalFilePath
			}
		}
	elseif WFPlatformInfo.isAndroid then
		originalFilePathInfo = originalFilePath
	end

	rxTest:expectSingleValue( catalog:docStore():setDocValue(
		{
			_id = trashAssetId,
			type = 'asset',
			subtype = trashAssetSubtype,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = "2019-01-01T00:00:00Z",
			_localOnly = {
				files = fileInfo,
				originalFilePath = originalFilePathInfo,
				xmpSidecarExists = false,
			},
		} )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )


	-- Create an trash asset with local proxy
	local fileInfoProxy, proxyFilePathInfo
	if WFPlatformInfo.isDesktop then
		fileInfoProxy = {
			proxy = {
				['2560'] = {
					relativePath = WFFileUtils.removeVolumeFromPath( originalFilePath ),
					volumeGuid = WFFileUtils.getVolumeGuidForPath( originalFilePath ),
					size = originalFileSize
				}
			}
		}

	elseif WFPlatformInfo.isiOS then
		fileInfoProxy = {
			proxy = {
				path = originalFilePath
			}
		}
	elseif WFPlatformInfo.isAndroid then
		proxyFilePathInfo = {
			filePath = originalFilePath
		}
	end

	rxTest:expectSingleValue( catalog:docStore():setDocValue(
		{
			_id = trashAssetId2,
			type = 'asset',
			subtype = trashAssetSubtype,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = "2019-02-01T00:00:00Z",
			_localOnly = {
				files = fileInfoProxy,
				proxy = proxyFilePathInfo,
				xmpSidecarExists = false,
			},
		} )
		:map( function( revId )
			revisionId2 = revId
			return revId
		end )
	, ':ignore:' )

	-- Create an trash asset without local binary
	rxTest:expectSingleValue( catalog:docStore():setDocValue(
		{
			_id = trashAssetId3,
			type = 'asset',
			subtype = trashAssetSubtype,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = "2019-02-01T00:00:00Z",
			_serverProvidedMetadata =
			{
				links = {
					['/rels/master'] = {
						href = '/asset/revision/master'
					}
				}
			}
		} )
		:map( function( revId )
			revisionId3 = revId
			return revId
		end )
	, ':ignore:' )

	rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT COUNT(*) FROM trashAssets]],
			quiesceUntilUpToDate = true,
		})
		:expectData { { 3 } }
		:dispose()

	-- Restore trash assets using restoreOnlyAssetsWithLocalOriginal flag
	local restoreOnlyAssetsWithLocalOriginal = true
	local restoreCountObservable = Rx.ReplaySubject( 1 )

	local obs = rxTest:observe( restoreCountObservable )

	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId, trashAssetId2, trashAssetId3 }, nil, restoreOnlyAssetsWithLocalOriginal, restoreCountObservable ) )

	obs:expectData( 1 )
		:dispose()

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

	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )

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

	-- Make sure the trash asset with local binary is restored and the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT COUNT(*) FROM trashAssets]],
			quiesceUntilUpToDate = true,
		})
		:expectData { { 2 } }
		:dispose()

end

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

function WFCatTrashTest.rxTest_CT_Mock_RestoreOnlyAssetsWithLocalOriginal( rxTest )

	-- LRMI-4190968 Restore in Residual Mode on mobile
	-- Restore only assets with local binary (master or proxy)

	testRestoreOnlyAssetsWithLocalOriginal( rxTest, { mock = true } )

end

--==============================================================================
-- @mark tests related to trash not treated like other assets
--==============================================================================

function WFCatTrashTest.rxTest_CT_MockRestoreTrashAssetOfDuplicate( rxTest )

	-- Scenario:  (NMB-10286)
	-- 1 Import an asset
	-- 2 Delete it
	-- 3 Import same asset from step 1
	-- 4 Restore trash asset

	-- Expected: step 4 succeeds and you have to assets that are the same
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashTest.rxTest_CT_OzRestoreTrashAssetOfDuplicate_testNightly( rxTest )

	-- Scenario:  (NMB-10286)
	-- 1 Import an asset
	-- 2 Delete it
	-- 3 Import same asset from step 1
	-- 4 Restore trash asset

	-- Expected: step 4 succeeds and you have to assets that are the same
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashTest.rxTest_CT_OzTrashAssetNotCountedInCloudStorage_testNightly( rxTest )
	-- NMB-10287
	-- Moving items to the trash should result in a reduction of cloud storage
	rxTest:expectEqual( false, failStubbedTests )

end

--==============================================================================
-- @mark Test WFCatTrashTest's internal function testPollUntilHasRequiredLinks
--==============================================================================

local function testPollUntilHasRequiredLinks( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

	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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

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

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


	local function debugPollUntilHasRequiredLinks( timeoutSeconds, catalogSpec, catalogId, assetId, revisionId, subtype, isMock )

		local fileName = 'IMG_7763.JPG' -- just need to have one to satisfy WFCatTrashTestUtils.getAssetReqResp we won't use the value as we are just looking for links
		local assetCreated
		local userCreated
		local importSource

		local httpRequest = WFCatTrashTestUtils.getAssetReqResp( catalogId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = true }, isMock )

		httpRequest:respond( '404', '{"code":1000,"description":"Resource not found","errors":{"asset":["does not exist"]},"subtype":"ResourceNotFoundError"}' )

		local callCount = 0

		local function debugHasRequiredLinks( docValue, expectedLinks )

			if callCount == 0 then

				callCount = callCount + 1

				-- Update to the actual response we want, as we overrode the first call with an error
				-- this will result in the next call expecting everything.
				httpRequest:respond( '200', KSJsonEncoder.encodeOzUploadJson(
					WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = true } )
				) )

				-- Since we can't force oz to generate the 404, just remove the links so we try
				-- again as we want to make the next check, where one asset is missing.
				KSTableUtils.setNested( docValue, nil, '_serverProvidedMetadata', 'links' )

			elseif docValue._serverProvidedMetadata then

				callCount = callCount + 1

				if callCount == 2 then
					-- Remove one of the links, this should result in this call failing and having the
					-- the polling routine trying again.
					docValue._serverProvidedMetadata.links[ '/rels/rendition_type/640' ] = nil
				end

				if docValue.subtype == 'deleted_image' then

					-- Oz doesn't guarantee the proxy will be there for trash assets.
					-- The edge case where this comes close to being important is if you
					-- restored an asset on an over quota or expired account we would attempt
					-- to get the proxy from trash, we will fall back to getting the master
					-- in this case so it is believed not to be a problem.
					docValue._serverProvidedMetadata.links[ '/rels/proxy_type/2560' ] = nil

				end

			end

			return WFCatTrashTestUtils.hasRequiredLinks( docValue, expectedLinks )

		end

		return WFCatTrashTestUtils.pollHttpGetAsset( timeoutSeconds, debugHasRequiredLinks, catalogSpec, catalogId, assetId, httpRequest )

	end

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

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

	-- Make sure we sync up the newly created asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false, xmpDevelop = false } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	-- Upload the master
	revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- poll on an asset with a missing rendition to verify we wait for all renditions
	rxTest:expectSingleValue( debugPollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	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, _ = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )

	rxTest:expectSingleValue( catalog:docStore():docValue( assetId ):map( function( docValue )
		revisionId = docValue._revId
		return true
	end ), true )

	-- 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( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

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

	-- poll on a deleted asset with a missing rendition but has one rendition to verify we consider it valid.
	rxTest:expectSingleValue( debugPollUntilHasRequiredLinks( 7 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )


end

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

function WFCatTrashTest.rxTest_CT_Mock_PollUntilHasRequiredLinks( rxTest )
	testPollUntilHasRequiredLinks( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_Oz_PollUntilHasRequiredLinks_testNightly( rxTest )
	testPollUntilHasRequiredLinks( rxTest )
end

--==============================================================================
-- @mark Test client RestoreAssetError (502) handling
--==============================================================================

local function testRestoreRestoreAssetError( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- Step 1: Add photo

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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

-- Step 2:  Delete the photo.

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

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

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

-- Step 3: Sync the Delete

	-- Send delete to Oz
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

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

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

-- Step 4: Restore the trash asset that is on oz.
	local restoredAssetId, restoredRevId, userUpdated
	if not options.otherClient then
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId } ) )

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

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

	-- Step 4.5: Sync the restore

		-- Send the restore to Oz
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, userUpdated  ) )

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

	else

		-- The failed restore (that caused Oz to send a RestoreAssetError) happened on another client but, currently, in this case
		-- all clients will recieve both a partially restored asset doc and the RestoreAssetError. Because the partially restored doc
		-- won't exist on Oz and just needs to be cleaned up enough that it won't show in clients we don't really care about the
		-- restored asset/rev ids we get in the partially restored asset doc we're pretending Oz is sending back to us so
		-- we will just use random id/revId
		restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
		restoredRevId = KSIdGenerator.uuidInLowercaseHex()

	end

-- Step 5: Receive partial asset and RestoreAssetError from Oz

	local restoredDocHasSPM = rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId ):map( function( restoredDocValue ) return not not derefOpt( restoredDocValue, '_serverProvidedMetadata' ) end ) )

	if options.deleteRestoredAsset then

		local userCreatedOrTable = { userCreated = userCreated, userUpdated = userUpdated }

		-- Get a restored asset partial response (no links), to simulate a situation we
		-- saw on oz we will get the restoreAssetError later after the user has
		-- deleted the restored asset.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

		rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId } ) )
		local trashAssetId2, trashRevId2 = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
		rxTest:expectEqual( 'string', type( trashAssetId2 ) )
		rxTest:expectEqual( 'string', type( trashRevId2 ) )

		-- Get the RestoreAssetError
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

		rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			restoredRevId = docValue._revId
			return docValue
		end ), ':ignore:' )

		-- Now send up the delete request
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )

		-- Verify WFCatErrorHandler will remove the createFrom trash info
		-- so the photo will be created via a regular create operation.
		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
		:map( function( docValue )
			return WFCatTrashUtils.createFromInfo( docValue )
		end ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()

		-- The error handler should process the RNFE and turn the delete into a create trash asset
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	elseif options.partialRestore then

		-- Get the restored asset partial response (no links) and a RestoreAssetError
		local userCreatedOrTable = { userCreated = userCreated, userUpdated = userUpdated }
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
			WFCatTrashTestUtils.restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

		restoredDocHasSPM:expectDataWithinTime( 2 * kOzWaitMultiplier, true )

	else

		-- Just get a RestoreAssetError to test
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

	end

	-- Make sure we have nothing to send (This also flushes out the restoreAssetError update from oz)
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

-- Step 6: Handle RestoreAssetError from Oz
	-- (happens asynchronously)

	if options.deleteRestoredAsset then

		-- already handled above

	elseif options.otherClient then

		-- make sure the restoredAsset doc that we got from Oz doesn't have any _serverProvidedMetadata
		restoredDocHasSPM:expectDataWithinTime( 2 * kOzWaitMultiplier, false )

		-- we're the other client and shouldn't respond to the RestoreAssetError with an update to Oz
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	else

-- Step 7: Retry Restore the Photo if this is the client that initially tried the restore

		-- Send the restore to Oz
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, userUpdated  ) )

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

	end

	-- There shouldn't be anything else waiting to be sent at this point
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashTest.rxTest_CT_Mock_RestoreAssetErrorMainClientPartial( rxTest )
-- NMB-13281/LRMI-4193007/LRMA-4197671
-- A client may attempt to restore a document and that fails on Oz for some reason. In many cases Oz will send a ResourceNotFoundError,
-- but there are some cases where Oz sends a 502/RestoreAssetErrror which should cause the client that initiated the restore to retry
-- that restoration attempt. At that time, Oz should hopefully succeed or fail, and if it fails, it should fail with a more useful error
-- (usually the ResourceNotFoundError). This mock test validates that when the client receives the RestoreAssetError, the retry will
-- happen. Not handling this can result in local duplicates of the restored asset.
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo
--		5) Receive partial asset and RestoreAssetError from Oz
-- 		6) Handle RestoreAssetError from Oz
--		7) Retry Restore the Photo

	testRestoreRestoreAssetError( rxTest, { mock = true, partialRestore = true } )

end

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

function WFCatTrashTest.rxTest_CT_Mock_RestoreAssetErrorMainClientPartial_Delete( rxTest )
-- NMB-13896 #0 (original bug fix)
-- This is a variant of RestoreAssetErrorMainClientPartial but the user had also deleted the restored asset before receiving the error.

-- A client may attempt to restore a document and that fails on Oz for some reason. In many cases Oz will send a ResourceNotFoundError,
-- but there are some cases where Oz sends a 502/RestoreAssetErrror which should cause the client that initiated the restore to retry
-- that restoration attempt. At that time, Oz should hopefully succeed or fail, and if it fails, it should fail with a more useful error
-- (usually the ResourceNotFoundError). This mock test validates that when the client receives the RestoreAssetError, the retry will
-- happen. Not handling this can result in local duplicates of the restored asset.
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo
--		5) Receive partial asset and RestoreAssetError from Oz
--		6) Delete the photo
-- 		7) Handle RestoreAssetError from Oz (ignored as restored asset docValue._empty is true)
--		8) Issue delete against restored asset creating trash asset
--		9) Get ResourceNotFoundError for restored asset
--		10) Create trash from scratch

	testRestoreRestoreAssetError( rxTest, { mock = true, partialRestore = true, deleteRestoredAsset = true } )

end

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

function WFCatTrashTest.rxTest_CT_Mock_RestoreAssetErrorMainClientNoPartial( rxTest )
-- NMB-13281/LRMI-4193007/LRMA-4197671
-- A client may attempt to restore a document and that fails on Oz for some reason. In many cases Oz will send a ResourceNotFoundError,
-- but there are some cases where Oz sends a 502/RestoreAssetErrror which should cause the client that initiated the restore to retry
-- that restoration attempt. At that time, Oz should hopefully succeed or fail, and if it fails, it should fail with a more useful error
-- (usually the ResourceNotFoundError). This mock test validates that when the client receives the RestoreAssetError, the retry will
-- happen. Not handling this can result in local duplicates of the restored asset.
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo
--		5) Receive partial asset and RestoreAssetError from Oz
-- 		6) Handle RestoreAssetError from Oz
--		7) Retry Restore the Photo

	testRestoreRestoreAssetError( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_Mock_RestoreAssetErrorOtherClient( rxTest )
-- NMB-13281/LRMI-4193007/LRMA-4197671
-- This test covers the behavior of the client when a different client issues a restore that fails on Oz with a RestoreAssetError
-- In these cases, all clients will receive a partially restored asset doc (which may contain some binary links) but that does not
-- exist on Oz and then a RestoreAssetError. Clients that didn't initiate the restore shouldn't respond to Oz with anything regarding
-- this error but they do have to ensure that, since the restored asset doc doesn't really exist on Oz, that they remove any binary
-- links which point to Oz and which might otherwise cause the client to think that the asset really does exist on Oz and can be shown
-- to users.
	-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync.
-- 		4) Restore the photo ( happens on another client )
--		5) Receive partial asset and RestoreAssetError from Oz
-- 		6) Ignore RestoreAssetError from Oz

	testRestoreRestoreAssetError( rxTest, { mock = true, otherClient = true } )

end

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

local function testRestoreDeleteRestoreAssetError( rxTest, options )

-- NMB-14814 / NMB-13896 #0 scenario B
-- A client may attempt to restore and delete a document and that restore fails
-- on Oz for some reason (supposedly because master is still uploading).
--
-- In this case Oz sends a 502/RestoreAssetErrror which should cause the client
-- that initiated the restore to retry that restore/delete attempt.
-- At that time, Oz should hopefully succeed or fail with a different error.
--
-- This mock test validates that when the client receives the RestoreAssetError,
-- the retry will happen.
--
-- Not handling this may result in local duplicates of the restored asset.
--
-- Steps:
--		1) Pause Sync
--		2) Add a large photo (to force it to upload the file in chunks).
--		3) Delete the photo
--		4) Using the Debug panel turn on the metadata pusher and puller allowing the deleted item to sync without a master
--		5) Using the Debug panel turn on the binary uploader, after a few seconds of uploading pause the binary uploader (before it completes, if using Charles wait for at lease one chunk to finish)
--		6) Using the Debug panel turn off the metadata pusher and puller
--		7) Restore the photo
--		8) Delete the photo
--		9) Using the Debug panel turn on the metadata pusher
--		10) After a couple seconds turn on the metadata puller
--		11) After a couple seconds turn on the binary uploader
--		12) Once everything is synced run a diagnostic log and look at the 'Sync Error Handler' section to make sure it encountered a 'RestoreAssetError'
--
-- Expected
-- 		Sync should Receive partial asset update and RestoreAssetError from Oz (after step 10)
--		Sync should process the RestoreAssetError from Oz and resend the restore/delete request
--		Asset should be in the trash locally, and when viewed on LrWeb, binaries should
--		successfully upload making the asset complete.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

	local importSource = WFCatTrashTestUtils.makeImportSource( catalog, fileName, importTimestamp, sha256 )

-- 1) Pause Sync
-- 2) Add a large photo (to force it to upload the file in chunks).

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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

-- 3) Delete the photo

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

	-- Make sure the original asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

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

-- 4) Using the Debug panel turn on the metadata pusher and puller allowing the deleted item to sync without a master

	-- Send delete to Oz
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

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

	-- Get the trash asset (with the links that point to the existing master)
	-- back in the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
	}), 'success' )

-- 5) Using the Debug panel turn on the binary uploader, after a few seconds of uploading pause the binary uploader (before it completes, if using Charles wait for at lease one chunk to finish)
-- 6) Using the Debug panel turn off the metadata pusher and puller
-- 7) Restore the photo

	local restoredAssetId, restoredRevId, userUpdated
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

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

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

-- 8) Delete the photo

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

	-- Make sure the restored asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			return docValue
		end )
		:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId2, trashRevId2 = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )

-- 9) Using the Debug panel turn on the metadata pusher
-- 10) After a couple seconds turn on the metadata puller
-- 11) After a couple seconds turn on the binary uploader

	-- Send the restore / delete pair to Oz
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Receive partial asset and RestoreAssetError from Oz

	if options.partialRestore then

		-- Get the restored asset partial response (no links) and a RestoreAssetError
		local userCreatedOrTable = { userCreated = userCreated, userUpdated = userUpdated }
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
			WFCatTrashTestUtils.restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

	else

		-- Get a RestoreAssetError without a partial response
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false ) ), 'success' )

	end

	-- Make sure we have nothing to send (This also flushes out the restoreAssetError update from oz)
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Retry sending the Restore / Delete
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the tombstone and trash asset from oz
	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( restoredAssetId, trashAssetId2, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse2( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetId, trashPurgeDate, userCreated, importSource, restoredAssetId, { master = true, renditions = true } ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- There shouldn't be anything else waiting to be sent at this point
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_RestoreAssetError_MainClientPartial( rxTest )

-- NMB-14814 / NMB-13896 #0 scenario B
-- A client may attempt to restore and delete a document and that restore fails
-- on Oz for some reason (supposedly because master is still uploading).

	testRestoreDeleteRestoreAssetError( rxTest, { mock = true, partialRestore = true } )

end

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


function WFCatTrashTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( 'WFCatTrashTest', WFCatTrashTest )
