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

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.rxlua
local WFRxScheduler = require 'WFRxScheduler'

	-- 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 WFArguments = require 'WFArguments'
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'
local WFRxBatchedBulkApplyOverlay = require 'WFRxBatchedBulkApplyOverlay'

	-- WF.oz_client.managed_catalog
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatIdUtils = require 'WFCatIdUtils'
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFCatTrash = require 'WFCatTrash'
local WFCatTrashTestUtils = require 'WFCatTrashTestUtils'
local WFCatTrashUtils = require 'WFCatTrashUtils'
local WFCatStorage = require 'WFCatStorage'
local WFCatVersions = require 'WFCatVersions'
local WFCatOzTestSession = require 'WFCatOzTestSession'

	-- WF.oz_client.shared.utils
local LrAssetUtils = require 'LrAssetUtils'

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

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

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

local coveragePath

local check = WFArguments.check

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

local addDocumentWithLocalMaster = assert( WFCatTrashTestUtils.addDocumentWithLocalMaster )
local addDocumentWithLocalMasterAndProxy = assert( WFCatTrashTestUtils.addDocumentWithLocalMasterAndProxy )
local addXmpDevelopToDocument = assert( WFCatTrashTestUtils.addXmpDevelopToDocument )
local addXmpSidecarToDocument = assert( WFCatTrashTestUtils.addXmpSidecarToDocument )
local albumAssetUpdateResponse = assert( WFCatTrashTestUtils.albumAssetUpdateResponse )
local albumUpdateResponse = assert( WFCatTrashTestUtils.albumUpdateResponse )
local assetDeletedResponse = assert( WFCatTrashTestUtils.assetDeletedResponse )
local assetUpdateResponse = assert( WFCatTrashTestUtils.assetUpdateResponse )
local canDownloadRequiredBinaryFiles = assert( WFCatTrashTestUtils.canDownloadRequiredBinaryFiles )
local catalogPrefetchReqResp = assert( WFCatTrashTestUtils.catalogPrefetchReqResp )
local createMasterReqResp = assert( WFCatTrashTestUtils.createMasterReqResp )
local createProxyReqResp = assert( WFCatTrashTestUtils.createProxyReqResp )
local emptyPullReqResp = assert( WFCatTrashTestUtils.emptyPullReqResp )
local expectedBinaryLinksForImage = assert( WFCatTrashTestUtils.expectedBinaryLinksForImage )
local expectedBinaryLinksForVideo = assert( WFCatTrashTestUtils.expectedBinaryLinksForVideo )
local futureUpdatesResponse = assert( WFCatTrashTestUtils.futureUpdatesResponse )
local getAssetReqResp = assert( WFCatTrashTestUtils.getAssetReqResp )
local getCameraRaw = assert( WFCatTrashTestUtils.getCameraRaw )
local getLatestRevisionId = assert( WFCatTrashTestUtils.getLatestRevisionId )
local getNewlyCreatedAsset = assert( WFCatTrashTestUtils.getNewlyCreatedAsset )
local getOriginalFilePath = assert( WFCatTrashTestUtils.getOriginalFilePath )
local getProxyFilePath = assert( WFCatTrashTestUtils.getProxyFilePath )
local getTrashAssetForDeletedAsset = assert( WFCatTrashTestUtils.getTrashAssetForDeletedAsset )
local hasCorrectAlbumAssetUpdate = assert( WFCatTrashTestUtils.hasCorrectAlbumAssetUpdate )
local hasCorrectRestoreAssetUpdate = assert( WFCatTrashTestUtils.hasCorrectRestoreAssetUpdate )
local hasRequiredLinks = assert( WFCatTrashTestUtils.hasRequiredLinks )
local httpGetAssetAsDecodedJson = assert( WFCatTrashTestUtils.httpGetAssetAsDecodedJson )
local makeImportSource = assert( WFCatTrashTestUtils.makeImportSource )
local pollForUpdateWith = assert( WFCatTrashTestUtils.pollForUpdateWith )
local pollHttpGetAsset = assert( WFCatTrashTestUtils.pollHttpGetAsset )
local pollUntilHasRequiredLinks = assert( WFCatTrashTestUtils.pollUntilHasRequiredLinks )
local pollUploadCount = assert( WFCatTrashTestUtils.pollUploadCount )
local prefetchReqResp = assert( WFCatTrashTestUtils.prefetchReqResp )
local pullUpdatesReqResp = assert( WFCatTrashTestUtils.pullUpdatesReqResp )
local pushCreateAlbumReqResp = assert( WFCatTrashTestUtils.pushCreateAlbumReqResp )
local pushCreateAlbumAssetReqResp = assert( WFCatTrashTestUtils.pushCreateAlbumAssetReqResp )
local pushCreateAssetReqResp = assert( WFCatTrashTestUtils.pushCreateAssetReqResp )
local pushDeleteAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteAssetReqResp )
local pushDeleteRestoreAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteRestoreAssetReqResp )
local pushDeleteTrashAssetReqResp = assert( WFCatTrashTestUtils.pushDeleteTrashAssetReqResp )
local pushDeleteTrashReqResp = assert( WFCatTrashTestUtils.pushDeleteTrashReqResp )
local pushRestoreAssetReqResp = assert( WFCatTrashTestUtils.pushRestoreAssetReqResp )
local pushUpdateAssetReqResp = assert( WFCatTrashTestUtils.pushUpdateAssetReqResp )
local pushUpdateRestoredReqResp = assert( WFCatTrashTestUtils.pushUpdateRestoredReqResp )
local restoredAssetUpdateResponse = assert( WFCatTrashTestUtils.restoredAssetUpdateResponse )
local setupCloudTrashTestSession = assert( WFCatTrashTestUtils.setupCloudTrashTestSession )
local simulateOtherClientAssetDelete = assert( WFCatTrashTestUtils.simulateOtherClientAssetDelete )
local stripBothUpdateFields = assert( WFCatTrashTestUtils.stripBothUpdateFields )
local stripForAssetUpdate = assert( WFCatTrashTestUtils.stripForAssetUpdate )
local stripUpdatesAndLinks = assert( WFCatTrashTestUtils.stripUpdatesAndLinks )
local stripUpdatesAndLinksIgnoringRenditions = assert( WFCatTrashTestUtils.stripUpdatesAndLinksIgnoringRenditions )
local transformJsonToTable = assert( WFCatTrashTestUtils.transformJsonToTable )
local transformOrderAndStripAssetUpdate = assert( WFCatTrashTestUtils.transformOrderAndStripAssetUpdate )
local trashAssetUpdateResponse = assert( WFCatTrashTestUtils.trashAssetUpdateResponse )
local uploadXmpDevelopReqResp = assert( WFCatTrashTestUtils.uploadXmpDevelopReqResp )
local uploadXmpReqResp = assert( WFCatTrashTestUtils.uploadXmpReqResp )

local IMG_7763fileSize = 2072173
local IMG_7772fileSize = 211332

local failStubbedTests = false

local stripped = '<<stripped>>'

local assetTableName = 'coreInfo'
local albumAssetTable = 'album_asset'
if WFPlatformInfo.isDesktop then
	assetTableName = 'assets'
	albumAssetTable = 'album_asset_v2'
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 setTimeoutAndRetryLimit( rxTest, options, timeoutValue )

	rxTest:setTimeLimit( timeoutValue )

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

end

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

local function addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
	
	importSource.willNotUploadMaster = true
	
	
	local docValue = {
		_id = assetId,
		type = 'asset',
		subtype = subtype,
		userCreated = userCreated,
		userUpdated = userCreated,
		captureDate = "2018-01-01T00:00:00Z",

		importSource = importSource,
	}
	
	if importSource.fileName == 'IMG_7763.JPG' then
		docValue.location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
	end
	
	docValue = WFCatAssetTestUtils.addProxyPathToDocument( docValue, proxyFilePath )
	
	return catalog:docStore():setDocValue( docValue )

end

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

local function putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions )

	return WFMockHttpRequest()
		:put( createSpaceUrl )
		:withRequestOptions( createSpaceOptions )
		:expectRequestData( createSpaceData )
		:requestDataTransformation( transformJsonToTable )
		:respond( '201', [[]] )

end

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

local function spaceUpdateResponse( catalogId, spaceId )

	return {
		country_blocked = false,
		created = "2018-11-28T18:20:06.011987Z",
		createdOnClient = "wf_mac_self_tests-1.0.0.Dev-OSX",
		id = spaceId,
		links = {
			["/rels/space"] = {
				href = "/v2/spaces/" .. spaceId,
			},
			["/rels/space_resources"] = {
				href = "/v2/spaces/" .. spaceId .. "/resources",
			},
			["/rels/web_space"] = {
				href = "http://bit.ly/2QmmDRw",
			},
			["self"] = {
				href = "spaces/" .. spaceId,
			},
		},
		payload = {
			download = false,
			location = false,
			metadata = false,
			presentationFilters = true,
			private = false,
		},
		revision_ids = {
			"8ba90ef31293932b1554eb5d4193e175",
			"2c43598d85e0ec0a8edd314924b0f9c6",
		},
		subtype = "shared",
		type = "space",
		updated = "2018-11-28T18:20:06.643910Z",
	}

end

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

local function spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId )

	return {
		created = "2018-08-23T20:25:31.964992Z",
		id = spaceAlbumId,
		album = {
			id = albumId,
			links = {
				self = {
					href = "albums/" .. albumId
				}
			}
		},
		space = {
			id = spaceId,
			links = {
				self = {
					href = "spaces/" .. spaceId
				}
			}
		},
		revision_ids = {
			spaceAlbumRevId,
		},
		type = "space_album",
		updated = "2018-08-23T20:25:31.964992Z",
	}

end

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

local function feedbackUserUpdate()

	return {
		avatar = {
			placeholder = true,
		},
		created = "2018-11-28T19:57:54.620379Z",
		first_name = "Test",
		full_name = "Test User",
		id = "847aef1622e010bc34270a34b6c3bca1",
		last_name = "User",
		links = {
			["/rels/avatars/large"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/large",
				size = 138,
			},
			["/rels/avatars/large2x"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/large2x",
				size = 276,
			},
			["/rels/avatars/medium"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/medium",
				size = 115,
			},
			["/rels/avatars/medium2x"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/medium2x",
				size = 230,
			},
			["/rels/avatars/small"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/small",
				size = 50,
			},
			["/rels/avatars/small2x"] = {
				href = "/v2/avatars/847aef1622e010bc34270a34b6c3bca1/20181128195821/small2x",
				size = 100,
			},
		},
		type = "feedbackuser",
		updated = "2018-11-28T19:58:21.035090Z",
	}

end

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

local function hasCorrectDeleteAssetUpdate( assetUpdate, assetId, trashAssetId, trashRevisionId )

	local hasCorrectUpdate = assetUpdate
						and assetUpdate.action == 'delete'
						and assetUpdate.type == 'asset'
						and assetUpdate.id == assetId
						and type( assetUpdate.revision_id ) == 'string'
						and KSDate.isKSDate( derefOpt( assetUpdate, 'deleted_asset', 'payload', 'userCreated' ) )
						and KSDate.isKSDate( derefOpt( assetUpdate, 'deleted_asset', 'payload', 'purgeDate' ) )
						and type( derefOpt( assetUpdate, 'deleted_asset', 'id' ) ) == 'string'
						and type( derefOpt( assetUpdate, 'deleted_asset', 'revision_id' ) ) == 'string'

	if trashAssetId then
		hasCorrectUpdate = hasCorrectUpdate and derefOpt( assetUpdate, 'deleted_asset', 'id' ) == trashAssetId
	end

	if trashRevisionId then
		hasCorrectUpdate = hasCorrectUpdate and derefOpt( assetUpdate, 'deleted_asset', 'revision_id' ) == trashRevisionId
	end

	return hasCorrectUpdate

end

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

local function pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype )
	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumUpdate, assetUpdate, albumAssetUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' then
					assetUpdate = update
				elseif update.type == 'album' then
					albumUpdate = update
				elseif update.type == 'album_asset' then
					albumAssetUpdate = update
				end
			end

			local correctAlbumUpdate = albumUpdate
				and albumUpdate.action == 'create'
				and albumUpdate.type == 'album'
				and albumUpdate.subtype == 'collection'
				and albumUpdate.id == albumId
				and type( albumUpdate.revision_id ) == 'string'
				and albumUpdate.payload.name == albumName

			local correctAssetUpdate = assetUpdate
				and assetUpdate.action == 'create'
				and assetUpdate.type == 'asset'
				and assetUpdate.subtype == subtype
				and assetUpdate.id == assetId
				and type( assetUpdate.revision_id ) == 'string'
				and derefOpt( assetUpdate, 'payload', 'importSource', 'fileName' ) == fileName
-- TODO [kcarlson 2018-08-22]: check more of the fields? especially userCreated

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

			if correctAssetUpdate and correctAlbumUpdate and correctAlbumAssetUpdate  and #updatesBeingPushed == 3 then
				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

local function pushAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype )
	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumAssetUpdate = updatesBeingPushed[ 1 ]

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

			if correctAlbumAssetUpdate  and #updatesBeingPushed == 1 then
				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

local function pushCreateAssetAndAssocDocsReqResp( catalogId, assetId, fileName, subtype, albumId, spaceId, commentId, commentBody, favoriteId )
	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate, albumAssetUpdate, commentUpdate, favoriteUpdate
			
			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' then
					assetUpdate = update
				elseif update.type == 'album_asset' then
					albumAssetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				elseif update.type == 'favorite' then
					favoriteUpdate = update
				end
			end
			
			local correctAssetUpdate = assetUpdate
				and assetUpdate.action == 'create'
				and assetUpdate.type == 'asset'
				and assetUpdate.subtype == subtype
				and assetUpdate.id == assetId
				and type( assetUpdate.revision_id ) == 'string'
				and derefOpt( assetUpdate, 'payload', 'importSource', 'fileName' ) == fileName
			
			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )
			local hasCommentCorrectUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, assetId, commentBody )
			local hasFavoriteCorrectUpdate = WFCatTrashTestUtils.hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, assetId )

			if correctAssetUpdate and correctAlbumAssetUpdate and hasCommentCorrectUpdate and hasFavoriteCorrectUpdate and #updatesBeingPushed == 4 then
				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

local function pushRestoreAssetAndCommentFavoriteDocsReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId,
										spaceId, commentId, commentBody, favoriteId )
	
	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate, commentUpdate, favoriteUpdate
			
			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' then
					assetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				elseif update.type == 'favorite' then
					favoriteUpdate = update
				end
			end
			
			local correctRestoreAssetUpdate = hasCorrectRestoreAssetUpdate( assetUpdate, restoredAssetId, trashAssetId, trashRevId )
			
			local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, restoredAssetId, commentBody )
			local correctFavoriteUpdate = WFCatTrashTestUtils.hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, restoredAssetId )

			if correctRestoreAssetUpdate and correctCommentUpdate and correctFavoriteUpdate and #updatesBeingPushed == 3 then
				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
		

end

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

local function pushDeleteAndDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId, assetId, revId, userCreated, purgeDate )

	local function transformJsonToTableAndStripUserCreated( actualData, expectedData )
		actualData = KSJsonDecoder.decodeWebSafeJson( actualData )
		expectedData = KSJsonDecoder.decodeWebSafeJson( expectedData )

		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'deleted_asset', 'payload', 'userCreated' )

		return actualData, expectedData
	end

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( {
			{
				type = 'asset',
				action = 'delete',
				id = assetId,
				revision_id = revId,
				deleted_asset = {
					id = trashAssetId,
					revision_id = trashRevId,
					payload = {
						userCreated = userCreated,
						purgeDate = purgeDate,
					}
				},
			},
			{
				type = 'asset',
				action = 'delete',
				id = trashAssetId,
				revision_id = trashRevId,
			},
		} )
		:requestDataTransformation( transformJsonToTableAndStripUserCreated )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushDeleteAndDeleteTrashAssetWithTrashInfoStripReqResp( catalogId, assetId, revId )

	local function transformJsonToTableAndStripTrashInfo( actualData, expectedData )
		actualData = KSJsonDecoder.decodeWebSafeJson( actualData )
		expectedData = KSJsonDecoder.decodeWebSafeJson( expectedData )

		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'deleted_asset', 'id' )
		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'deleted_asset', 'revision_id' )
		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'deleted_asset', 'payload', 'userCreated' )
		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'deleted_asset', 'payload', 'purgeDate' )
		
		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'id' )
		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'revision_id' )

		return actualData, expectedData
	end

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( {
			{
				type = 'asset',
				action = 'delete',
				id = assetId,
				revision_id = revId,
				deleted_asset = {
					id = 'trashAssetId',
					revision_id = 'trashRevId',
					payload = {
						userCreated = 'userCreated',
						purgeDate = 'purgeDate',
					}
				},
			},
			{
				type = 'asset',
				action = 'delete',
				id = 'trashAssetId',
				revision_id = 'trashRevId',
			},
		} )
		:requestDataTransformation( transformJsonToTableAndStripTrashInfo )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevisionId, userUpdated  )

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( {
			{
				action = "create",
				id = restoredAssetId,
				payload = {
					userUpdated = userUpdated,
				},
				restore_from = {
					id = trashAssetId,
				},
				revision_id = restoredRevId,
				subtype = "image",
				type = "asset",
			}
		} )
		:requestDataTransformation( transformJsonToTable )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

local function pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 )

	local expectedUpdates = {
		{
			action = "create",
			id = restoredAssetId,
			payload = {
				userUpdated = "2019-01-13T14:43:34.838Z",
			},
			restore_from = {
				id = trashAssetId,
			},
			revision_id = restoredRevId,
			subtype = "image",
			type = "asset",
		},
		{
			action = "delete",
			deleted_asset = {
				id = trashAssetId2,
				payload = {
					userCreated = "2018-04-15T18:18:18Z",
					purgeDate = "2018-06-14T18:18:18Z",
				},
				revision_id = trashRevId2,
			},
			id = restoredAssetId,
			revision_id = restoredRevId,
			type = "asset",
		},
	}

	local function transformJsonToTableAndStripUserUpdated( actualData, expectedData )
		actualData = KSJsonDecoder.decodeWebSafeJson( actualData )
		expectedData = KSJsonDecoder.decodeWebSafeJson( expectedData )

		stripBothUpdateFields( stripped, expectedData, actualData, 1, 'payload', 'userUpdated' )
		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'deleted_asset', 'payload', 'userCreated' )
		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'deleted_asset', 'payload', 'purgeDate' )

		return actualData, expectedData
	end

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( expectedUpdates )
		:requestDataTransformation( transformJsonToTableAndStripUserUpdated )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushDeleteRestoreAssetAndCreateAssocDocsReqResp( catalogId, originalAssetId, restoredAssetId, trashAssetId, trashRevId, albumId, spaceId, commentId, commentBody )

	-- The update array should include: delete asset (a) with deleted_asset (b) and create restore asset (c) from trash asset (b)
	-- and the add album asset, add favorite, add comment

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, assetCreateUpdate, albumAssetUpdate, commentUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' and update.action == 'delete' then
					assetDeleteUpdate = update
				elseif update.type == 'asset' and update.action == 'create' then
					assetCreateUpdate = update
				elseif update.type == 'album_asset' then
					albumAssetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				end
			end

			local correctDeleteAssetUpdate = hasCorrectDeleteAssetUpdate( assetDeleteUpdate, originalAssetId, trashAssetId, trashRevId )
			local correctRestoreAssetUpdate = hasCorrectRestoreAssetUpdate( assetCreateUpdate, restoredAssetId, trashAssetId, trashRevId )
			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, restoredAssetId, albumId )
			local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, restoredAssetId, commentBody )

			if correctDeleteAssetUpdate and correctRestoreAssetUpdate and correctAlbumAssetUpdate and correctCommentUpdate
				and #updatesBeingPushed == 4 then

				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushDeleteAssetAndCreateAssocsReqResp( catalogId, originalAssetId, trashAssetId, trashRevId, spaceId, albumId, commentId, commentBody, favoriteId )

	-- The update array should include: delete asset (a) with deleted_asset (b)
	-- and the update album_asset, update favorite, update comment

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, albumAssetUpdate, commentUpdate, favoriteUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' and update.action == 'delete' then
					assetDeleteUpdate = update
				elseif update.type == 'album_asset' then
					albumAssetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				elseif update.type == 'favorite' then
					favoriteUpdate = update
				end
			end

			local correctAlbumAssetUpdate = WFCatTrashTestUtils.hasCorrectAlbumAssetUpdate( albumAssetUpdate, trashAssetId, albumId )
			local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, trashAssetId, commentBody )
			local correctFavoriteUpdate = WFCatTrashTestUtils.hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, trashAssetId )
			
			local correctDeleteAssetUpdate = hasCorrectDeleteAssetUpdate( assetDeleteUpdate, originalAssetId, trashAssetId, trashRevId )
			
			if correctDeleteAssetUpdate and correctAlbumAssetUpdate and correctCommentUpdate and correctFavoriteUpdate
				and #updatesBeingPushed == 4 then

				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, useExternalDevelopSettings, developSettings )


	local status = '202'

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate = updatesBeingPushed[ 1 ]

			local correctAssetUpdate = assetUpdate.action == 'update'
				and assetUpdate.id == trashAssetId
				and not derefOpt( assetUpdate, 'payload', 'purgeDate' )
				and not derefOpt( assetUpdate, 'payload', 'userCreated' )

			if correctAssetUpdate then
				if useExternalDevelopSettings then
					correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw', 'sha256' ) ~= nil
				else
					correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw' ) == developSettings
				end
			end

			correctAssetUpdate = correctAssetUpdate and derefOpt( assetUpdate, 'payload', 'original' ) == nil

			if correctAssetUpdate and #updatesBeingPushed == 1 then
				return 'success'
			end

			return str

		end )
		:respond( status, futureUpdatesResponse( catalogId ) )

end

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

local function assetGoneErrorUpdateResponse( assetId, revId, assetType, newId, errorTable )

	return {
		errors = errorTable or {
			asset = {
				"no longer exists",
				},
			},
		id = KSIdGenerator.uuidInLowercaseHex(),
		new_id = newId,
		source = {
			action = "create",
			id = assetId,
			revision_id = revId,
			type = assetType,
		},
		status = 410,
		subtype = "AssetGoneError",
		type = "error",
		updated = KSDate.timeInIso8601Format(),
	}

end

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

local function restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId )
	
	return {
           id = KSIdGenerator.uuidInLowercaseHex(),
           type = "error",
           status = 502,
           subtype = "RestoreAssetError",
           errors = {
               asset = {
                   "restore failed"
               },
           },
           source = {
               id = restoredAssetId,
               action = "create",
               type = "asset",
               revision_id = restoredRevId,
               restore_from = {
                   id = trashAssetId
               }
           }
       } 

end

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

local function albumAssetDeletedResponse( docId, userId, newAlbumAssetId )

	local now = KSDate.timeInIso8601Format()

	return {
		id = docId,
		deleted = true,
		deleted_by = userId,
		type = 'album_asset',
		updated = now,
		new_id = newAlbumAssetId,
	}
end

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

local function commentDeletedResponse( commentId, userId, newCommentId )

	local now = KSDate.timeInIso8601Format()

	return {
		id = commentId,
		deleted = true,
		deleted_by = userId,
		type = 'comment',
		updated = now,
		new_id = newCommentId,
	}
end

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

local function trashAssetDeletedResponse( trashAssetId, userId, maybeNewId )

	local now = KSDate.timeInIso8601Format()

	return {
		id = trashAssetId,
		deleted = true,
		deleted_by = userId,
		new_id = maybeNewId,
		type = 'asset',
		updated = now,
	}
end

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

local function favoriteDeletedResponse( favoriteId, userId, assetId, newFavoriteId )

	local now = KSDate.timeInIso8601Format()

	return {
		asset = {
			id = assetId,
		},
		id = favoriteId,
		deleted = true,
		deleted_by = userId,
		type = 'favorite',
		updated = now,
		new_id = newFavoriteId,
	}
end

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

local function trashAssetUpdateResponse2( trashAssetId, trashRevisionId, fileName, subtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, originalAssetId, linksExpected, developSettings )

	check( 'string', 'string', 'string', 'string', 'string', 'string', '?string', 'table', 'string', '?table', '?table' )

	local response = assetUpdateResponse( trashAssetId, trashRevisionId, fileName, subtype, trashAssetCreated, trashAssetCreated, importSource, 
				linksExpected, developSettings )

	-- Trash assets always have an extra revision added onto them in case an asset was modified in order to create
	-- a conflict with what is locally on the trash asset which will cause a subsequent pushed update to
	-- get the modifications

	table.insert( response.revision_ids, KSIdGenerator.uuidInLowercaseHex() )

	-- Trash assets have purgeDate
	response.payload.purgeDate = trashPurgeDate

	-- Trash assets also have an original table so add that to the asset update response.
	response.original = {
		id = originalAssetId,
		userCreated = userCreated, -- original asset's userCreated value is preserved here
	}

	return response

end

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

local function joinIdAlreadyInUseErrorUpdateResponse( action, docId, docRevId, docType )

	local response = {
		id = KSIdGenerator.uuidInLowercaseHex(),
		updated = "2019-02-28T21:16:46.426394Z",
		type = "error",
		status = 400,
		subtype = "JoinIdAlreadyInUseError",
		errors = {},
		source = {
			id = docId,
			action = action,
			type = docType,
			revision_id = docRevId or KSIdGenerator.uuidInLowercaseHex(),
		}
	}
	
	return response

end

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

local function resourceExistsErrorUpdateResponse( action, docId, docRevId, docType, catalogId )

	local response = {
		id = KSIdGenerator.uuidInLowercaseHex(),
		updated = "2019-02-28T21:16:46.426394Z",
		type = "error",
		status = 403,
		subtype = "ResourceExistsError",
		errors = {
			[ docType ] = { "already exists" }
		},
		source = {
			id = docId,
			action = action,
			type = docType,
			revision_id = docRevId or KSIdGenerator.uuidInLowercaseHex(),
			links = {
				self = {
					href = '/v2/catalogs/' .. catalogId .. '/updates/' .. docId,
				},
			},
		}
	}
	
	return response

end

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

local function resourceNotFoundErrorUpdateResponse( action, docId, docRevId, docType, missingDocId, missingDocType, maybeNewId, maybeUserId )

	local response = {
		id = KSIdGenerator.uuidInLowercaseHex(),
		missing_resource_id = missingDocId,
		updated = "2019-02-28T21:16:46.426394Z",
		type = "error",
		status = 404,
		subtype = "ResourceNotFoundError",
		errors = {
			[ missingDocType ] = { "does not exist" }
		},
		source = {
			id = docId,
			action = action,
			type = docType,
			revision_id = docRevId or KSIdGenerator.uuidInLowercaseHex(),
		},
		new_id = maybeNewId,
		deleted_by = maybeUserId

	}
	
	return response

end

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

local function storageExceededUpdateResponse( action, docId, docRevId, docType )

	local response = {
		id = KSIdGenerator.uuidInLowercaseHex(),
		updated = "2019-03-28T23:13:43.326394Z",
		type = "error",
		status = 403,
		subtype = "StorageExceededError",
		errors = {
			[ docType ] = { "quota exceeded" }
		},
		source = {
			id = docId,
			action = action,
			type = docType,
			revision_id = docRevId or KSIdGenerator.uuidInLowercaseHex(),
		},
	}

	return response

end

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

local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

	-- TODO [kcarlson 2019-09-30]:  According to TODO [matthewj 2019-04-02]: Oz
	-- has a bug with not setting location coords when doing proxy uploads.
	-- Instead of not running the corresponding tests, run them but ignore the
	-- fact that the location coords are not there.
	--
	-- Calls to this function should be replaced with stripUpdatesAndLinks
	-- when Oz fixes their bug.

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

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

	stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'payload', 'location' )

	return mockDataAsTable, ozDataAsTable

end

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

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

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

local function pollHttpUrl( catalogSpec, docUrl, expectedStatusCode, expectedResponse )

	local httpRequest = WFMockHttpRequest()
		:get( WFCatOzTestSession.stripUpdatedSinceTime( docUrl ) )
		:respond( expectedStatusCode, KSJsonEncoder.encodeOzUploadJson( expectedResponse ) )

	return Observable.timer( .1, 2, WFRxScheduler.sharedScheduler() )
		:flatMap( function()

			catalogSpec:expectHttpRequest( httpRequest )
			return catalogSpec:get( docUrl, { dontCompareToMockResponse = true } )
			:map( function( data, resultHeaders )
				return resultHeaders.status_code, KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )
			end )
			:catch( function( json, resultHeaders, message )
				return Observable.just( resultHeaders.status_code, KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( json ) )
			end )

		end )

end

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

local function pollHttpGetMetadataUpdates( catalogSpec, docStore, expectedStatusCode, expectedResponse )
	return docStore:localDocValue( 'Oz-nextUpdateFeedLink' )
		:flatMap( function( docValue )
			local updatesUrl = KSTableUtils.derefOpt( docValue, 'link' )
			return pollHttpUrl( catalogSpec, updatesUrl, expectedStatusCode, expectedResponse )
		end )
end

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

local function pollUntilDeleted( timeoutSeconds, catalogSpec, catalogId, assetId, revisionId, options )
	
	local isAssetDeleted = function( docValue )
		return docValue._deleted == true
	end
	
	local requestOptions = {
		returnHttpErrorsAsData = true,
		dontCompareToMockResponse = true,
	}
	
	if options.injectQuickerRetries then
		requestOptions.requestHeaders = {
			{
				header = "X-Quicker-Retries",
				value = "true",
			}
		}
	end
	
	local httpRequest = WFMockHttpRequest()
		:get( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId )
		:withRequestOptions( requestOptions )
		:respond( '404', KSJsonEncoder.encodeOzUploadJson(
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset" )
		 ) )
	
	return pollHttpGetAsset( timeoutSeconds, isAssetDeleted, catalogSpec, catalogId, assetId, httpRequest )

end

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

local function pollUntilAssetExistsWithLinks( timeoutSeconds, catalogSpec, catalogId, assetId, expectedLinks, options )
	
	local doesAssetExistWithLinks = function( docValue )
		if docValue then
			return docValue.testResponse 
				-- test response from mock test so let it pass
				or
				( not docValue._deleted and hasRequiredLinks( docValue, expectedLinks or {} ) )
		end
		return false
	end
	
	local requestOptions = {
		returnHttpErrorsAsData = true,
		dontCompareToMockResponse = true,
	}
	
	if options and options.injectQuickerRetries then
		requestOptions.requestHeaders = {
			{
				header = "X-Quicker-Retries",
				value = "true",
			}
		}
	end
	
	local httpRequest = WFMockHttpRequest()
		:get( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId )
		:withRequestOptions( requestOptions )
		:respond( '200', KSJsonEncoder.encodeOzUploadJson( { testResponse = true } ) )
	
	return pollHttpGetAsset( timeoutSeconds, doesAssetExistWithLinks, catalogSpec, catalogId, assetId, httpRequest )
end

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

local function pollErrorHandlerErrorsProcessed( errorHandler, errorType )
	return Observable.timer( .5, .5, WFRxScheduler.sharedScheduler() )
		:map( function()
			local errorSummary, errorInProgress = errorHandler:DEBUG_errorsProcessed()
			return derefOpt( errorSummary, errorType, 'count' ), errorInProgress
		end )
end

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

local function removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

	for x = #ozUpdateTable, 1, -1 do
		if ozUpdateTable[ x ].type == 'space'
		or ozUpdateTable[ x ].type == 'feedbackuser' then
			table.remove( ozUpdateTable, x )
		end
	end

end

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

local function removeSporadicUpdatesWithTypes( typesToRemove )

	if type( typesToRemove ) == 'string' then
		typesToRemove = { typesToRemove }
	end

	-- Convert typesToRemove into a lookup table.
	typesToRemove = KSTableUtils.invert( typesToRemove )

	return function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		-- Remove the sporadic feedback user / space update before running through
		-- stripUpdatesAndLinks to make sure things line up properly.
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

		for x = #ozUpdateTable, 1, -1 do
			if typesToRemove[ ozUpdateTable[ x ].type ] then
				table.remove( ozUpdateTable, x )
			end
		end

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

		return mockDataAsTable, ozDataAsTable

	end

end

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

local removeSporadicSpaceFeedbackAndAssetUpdates = removeSporadicUpdatesWithTypes( { 'space', 'feedbackuser', 'asset' } )

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

local removeSporadicSpaceAndAssetUpdates = removeSporadicUpdatesWithTypes( { 'space', 'asset' } )

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

local removeSporadicAssetUpdates = removeSporadicUpdatesWithTypes( { 'asset' } )

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

local function testAddSyncDeleteSync( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

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

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
	end
	
	uploader:startContinuousUploads()
	
	if options.xmpSidecar then
		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = options.xmpSidecar } )
		}, stripForAssetUpdate( false ) ), 'success' )
	end
	
	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath, nil, options.xmpSidecar ) )

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

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

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

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

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

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

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

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

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

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

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

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

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

			trashDocValue = docValue

			local originalPath = 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:expectSingleValue( httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

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

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

			end )
		, ':ignore:' )

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

	end

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

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

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

	end

	if options.restore then

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

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

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

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

		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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

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

			-- 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( pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId ) )

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

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

	end

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_image( rxTest )
	testAddSyncDeleteSync( rxTest, {
		mock = true,
		expectedBinaryLinks = 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",
			subtype = "video",
			expectedBinaryLinks = 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 = expectedBinaryLinksForImage()

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

	local xmpDevelopSize = #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 = expectedBinaryLinksForImage(),
		expectedBinarySizes = { [ '/rels/master' ] = IMG_7763fileSize }
	})
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_image_xmp_testNightly( rxTest )

	local expectedBinaryLinksForImageWithXmp = expectedBinaryLinksForImage()

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

	local xmpDevelopSize = #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",
		subtype = "video",
		expectedBinaryLinks = 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 = 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",
		subtype = "video",
		expectedBinaryLinks = 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 = 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"

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

	-- Upload the master
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the proxy and renditions before requesting next update
	rxTest:expectSingleValue( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true )
	}, 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", 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", 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

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

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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )
	
	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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


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

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

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

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

	uploader:startContinuousUploads()

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

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


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

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( 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' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	else

		local now = KSDate.timeInIso8601Format()

		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = assetId,
				develop = {
					processingModel = "lightroom",
					xmpCameraRaw = kApplyTintXmp
				},
				userUpdated = now,
			}
			:map( function( revId )
				revisionId = revId
				return revId
			end )
		, ':ignore:' )

	end

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

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

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

	-- 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:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashRevId = WFCatTrashUtils.getCreateWithRevisionId( docValue )
			return docValue
		end ), ':ignore:' )

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

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

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

	-- Get the trashed asset back in the sync feed with links
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop  ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, 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 ):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( pushUpdateTrashReqResp( catalogId, trashAssetId, trashRevId, trashAssetSubtype, options.xmpDevelop, kApplyTintXmp ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.xmpDevelop then
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, trashAssetId, trashRevId, xmpDevelopFilePath ) )
	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 )

end

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

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

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

function WFCatTrashTest.rxTest_CT_OzModifyDelete_testNightly( rxTest )
	-- For description see testAddSyncModifyDeleteSync above
	testAddSyncModifyDeleteSync( rxTest )
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 = 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'

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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


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

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

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

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

	uploader:startContinuousUploads()

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

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


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

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( 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' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	else
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = assetId,
				develop = {
					processingModel = "lightroom",
					xmpCameraRaw = kApplyTintXmp
				}
			}
			:map( function( revId )
				revisionId = revId
				return revId
			end )
		, ':ignore:' )
	end

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

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

	-- 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 = 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( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredCreateWithRevId ) )
	
	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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( restoredAssetId, restoredCreateWithRevId, fileName, subtype, assetCreated, userCreatedAndUpdate, importSource, assetId, true, true, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
		assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
	}, 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( pushUpdateAssetReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetSubtype, options.xmpDevelop, kApplyTintXmp ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.xmpDevelop then
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( 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

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

local function testAddDeleteRestoreSync( 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 = 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"

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

	-- 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 = 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 = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure we only sync up the newly restored asset (shouldn't issue a restore in this case)
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil 
	
	-- Get the restored asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings)
	}, stripForAssetUpdate( false ) ), 'success' )

	uploader:startContinuousUploads()

	-- /rels/xmpDevelop gets uploaded before master
	if options.xmpDevelop then
		restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredRevId, xmpDevelopFilePath ) )
	end
	
	-- /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 = getLatestRevisionId( rxTest, catalog, restoredAssetId )
		
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, restoredAssetId, restoredRevId, xmpSidecarFilePath ) )

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

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

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

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

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

	-- Make sure the original 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 = 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"

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

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

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

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

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

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

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

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

	-- 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 = 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 = 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 = 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 = 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( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
	
	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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ),
			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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

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

		-- 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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		-- Make sure renditions are generated for the restored asset
		rxTest:expectSingleValue( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

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

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

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

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

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

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

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

	-- 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 = 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 = 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 = 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId2, userId ),
		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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add photo

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

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

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

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

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

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

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

	-- 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 = 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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 = 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 = 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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			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( 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( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, restoredAssetId, true, true ),
		assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		assetDeletedResponse( restoredAssetId, trashAssetId2, userId ),
	}, 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( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add photo

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

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

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

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

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

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

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

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

	-- 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 = 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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 = 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 = 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( pushDeleteTrashReqResp( catalogId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trash asset tombstone back in the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetDeletedResponse( trashAssetId, userId ),
	}, 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, deleting and restoring assets w/ albums before syncing
--==============================================================================

local function testAddDeleteRestoreSyncAssocDocs( 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 = 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"

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

	-- 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 = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	-- Make sure the trash asset knows about the album asset
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ?]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData { { albumId } }
	:dispose()

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

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

	-- Make sure the restored asset has an album asset
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ?]], restoredAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData { { albumId } }
	:dispose()

	-- Make sure we sync up the newly restored asset, album, and album_asset (shouldn't issue a restore in this case)
	catalogSpec:expectHttpRequest( pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, restoredAssetId, albumId, albumName, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )
		
	-- Get the restored asset, album and album_asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = false } ),
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName ),
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, restoredAssetId )
	}), 'success' )

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

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

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

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

	-- Make sure the original 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 )

	-- Make sure no album assets are laying around for the original or trash asset.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? or assetId == ? ]], assetId, trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData { }
	:dispose()

end

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

function WFCatTrashTest.rxTest_CT_MockAddDeleteRestore_AssocDocs( rxTest )

	-- NMB-10279
	-- This test addresses the scenario where a user adds deletes and restores
	-- an asset that belongs to an album, all while offline and then goes
	-- online to sync it.  In this case the test should show only the creation
	-- of the restored asset, and it's associated documents (the album and
	-- album_asset).  Favorites and comments are not covered in this case as
	-- LrCC has no way to add them.

	testAddDeleteRestoreSyncAssocDocs( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzAddDeleteRestore_AssocDocs_testNightly( rxTest )

	-- NMB-10279
	-- This test addresses the scenario where a user adds deletes and restores
	-- an asset that belongs to an album, all while offline and then goes
	-- online to sync it.  In this case the test should show only the creation
	-- of the restored asset, and it's associated documents (the album and
	-- album_asset).  Favorites and comments are not covered in this case as
	-- LrCC has no way to add them.

	testAddDeleteRestoreSyncAssocDocs( rxTest )

end

--==============================================================================
-- @mark Tests that involve adding, deleting and restoring assets w/ albums, comments and favorite
--==============================================================================

local function testAddAssocDocsSyncAddAssocDocsDeleteRestoreSync( rxTest, options )

-- NMB-9375
-- Scenario:
--
-- Step 1: User pauses sync adds a photo, album1, and adds the photo to the album1, and then syncs.
-- Step 2: Asset is shared by some other client, a viewer adds a comment and a favorite. Sync pulls down the newly added comment and favorite docs.
-- Step 3: User pauses sync, adds album2, and then syncs.
-- Step 4: User pauses sync, adds the photo to album2, add comment2 and favorite 2, deletes the photo, restores the photo, and then syncs
--
-- After Step 4 the test verify:
--		A) Delete asset A and create asset C from trash B are sent up
--		B) That the photo is in both albums
--		C) Only the newly added album_assetid, favorite and favorite is sent up
--		D) Both comments and both favorites exist on the restored asset.

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

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

	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

	-- Sync down the asset, album and album_asset.  The asset will have the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false } ),
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )

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

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

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

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

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

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentBody1 = "Comment1 Mumble, mumble, mumble"
	local commentBody2 = "Comment2 Mumble, mumble, mumble"
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()

	-- This first part of this step is contrived as we aren't the other client. But
	-- we want to add the space_album, and comment documents here so when we run
	-- this test against oz it will have the documents as well and move them around
	-- during the delete and restore process.
	local now = KSDate.timeInIso8601Format()

	local spaceAlbumRevId
	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			label = 'oz',
			{
				_id = spaceId,
				userCreated = now,
				userUpdated = now,
				type = 'space',
				subtype = 'shared',
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			{
				_id = spaceAlbumId,
				type = 'space_album',
				space = { id = spaceId },
				album = { id = albumId },
			}
		} ) :map( function( overlayResults )
				spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
				return overlayResults
			end )
	, ':ignore:' )

	-- Now create the space via a direct put as MetadataPusher doesn't handles 'space' docs.
	local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	local createSpaceOptions = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
		payload = {
			private = false,
			download = false,
			metadata = false,
			location = false,
			presentationFilters = true,
		},
		subtype = 'shared',
		resources = {
			{
				id = albumId,
				type = 'album',
				space_album_id = spaceAlbumId,
			}
		}
	} )

	catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		spaceUpdateResponse( catalogId, spaceId )
	}), 'success' )

	local commentRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId,
			body = commentBody1,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			commentRevId = revId
			return revId
		end )
	, ':ignore:' )
	
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody1 ),
		feedbackUserUpdate()
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )

	local favoriteRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'favorite',
			subtype = 'user',
			_id = favoriteId,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			favoriteRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Sync down the favorite update.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

	-- Make sure the comment and favorite are correctly stored
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM spacefavorite where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM comment where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

-- Step 3:

	-- Add asset to another album before we issue the delete so we can
	-- verify it will only restore the one album_asset
	local albumId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumName2 = 'AlbumName2'
	local albumRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId2,
			type = 'album',
			subtype = 'collection',
			name = albumName2,
		}
		:map( function( revId )
			albumRevId2 = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync down the album
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId2, albumRevId2, albumName2 ),
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

-- Step 4:

	-- Add the asset to another album asset before we delete it
	-- Add another favorite and comment to asset before we delete it
	-- these won't be synced until after the asset has been deleted and restored
	local albumAssetRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId2,
			type = 'album_asset',
			album = { id = albumId2 },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	local commentId2 = KSIdGenerator.uuidInLowercaseHex()
	local commentRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId2,
			body = commentBody2,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = {
				created = socialCreated,
			}
		}
		:map( function( revId )
			commentRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	-- Delete the asset that exists on oz.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

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

	local function sortByFirstElement( expectedQueryResult )
		table.sort( expectedQueryResult, function( a, b ) return a[ 1 ] < b[ 1 ] end )
		return expectedQueryResult
	end

	-- Make sure the trash asset knows about both album assets
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

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

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

	-- Make sure the restored asset belongs to both albums
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	-- Make sure the restored asset still has comments and favorites attached
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { favoriteId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { commentId }, { commentId2 } } ) )
	:dispose()

	-- Make sure we push the restore asset and the newly added album_asset to oz.
	-- Because we restored the asset before the trash asset was synced delete will refer to the original
	-- asset as the deleted_asset id and not the trash asset .
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetAndCreateAssocDocsReqResp( catalogId, assetId, restoredAssetId, trashAssetId, trashRevId, albumId2, spaceId, commentId2, commentBody2 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 4, kTimeoutForSendingUpdates ) )

	-- Get the restored asset (with the links that point to the existing master),
	-- the restored asset's album_assets, and tombstones of the trash album_assets
	-- back in the sync feed.
	local restoredAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	local restoredCommentId = KSIdGenerator.uuidInLowercaseHex()
	local restoredCommentRevId = KSIdGenerator.uuidInLowercaseHex()
	local restoredFavoriteId = KSIdGenerator.uuidInLowercaseHex()
	local restoredFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()
	local trashAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashCommentId = KSIdGenerator.uuidInLowercaseHex()
	local trashFavoriteId = KSIdGenerator.uuidInLowercaseHex()

	local function transformOrderAndStrip_DeleteRestore_AssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

		if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
			return {}, {} -- don't compare partial updates
		end

		local function stripUpdateField( i, ... ) return stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, ... ) end

		-- Sort order of updates for delete and restore should be:
			-- 1 album_asset1
			-- 2 album_asset2
			-- 3 asset
			-- 4 comment1
			-- 5 comment2
			-- 6 favorite1
			-- 7 deleted album_asset1 (may be swapped with 9)
			-- 8 deleted trash album_asset1 (may be swapped with 8)
			-- 9 deleted asset (original)
			-- 10 deleted asset (trash)
			-- 11 deleted comment1
			-- 12 deleted trash comment1
			-- 13 deleted favorite
			-- 14 deleted trash_favorite

		-- Strip the id's off the updates as they will all be new.
		for i = 1, 14 do
			local id = derefOpt( mockUpdateTable, i, 'id' )
			if id ~= restoredAssetId and id ~= assetId then
				stripUpdateField( i, 'id' )
			end
		end

		-- Strip the links in the comments and favorites updates as they have an new id in them.
		for i = 4, 6 do
			stripUpdateField( i, 'links', 'self', 'href' )
		end

		-- Also strip the new_id off the deletes as they will all be new and unknown.
		for i = 7, 14 do
			stripUpdateField( i, 'new_id' )
		end

		return mockDataAsTable, ozDataAsTable

	end

	local userId = catalog:ozUserId()

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

	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			albumAssetUpdateResponse( restoredAlbumAssetId, restoredAlbumAssetRevId, albumId, restoredAssetId ), -- new unknown id and revId will be created by oz
			albumAssetUpdateResponse( albumAssetId2, albumAssetRevId2, albumId2, restoredAssetId ), -- new unknown id and revId will be created by oz
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, restoredCommentId, restoredCommentRevId, spaceId, restoredAssetId, commentBody1 ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId2, commentRevId2, spaceId, restoredAssetId, commentBody2 ),
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, restoredFavoriteId, restoredFavoriteRevId, spaceId, restoredAssetId ),
			albumAssetDeletedResponse( albumAssetId, userId, trashAlbumAssetId ),
			albumAssetDeletedResponse( trashAlbumAssetId, userId, restoredAlbumAssetId ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			commentDeletedResponse( commentId, userId, trashCommentId ),
			commentDeletedResponse( trashCommentId, userId, restoredCommentId ),
			favoriteDeletedResponse( favoriteId, userId, assetId, trashFavoriteId ),
			favoriteDeletedResponse( trashFavoriteId, userId, trashAssetId, restoredFavoriteId ),
		}
		, transformOrderAndStrip_DeleteRestore_AssocDocs )
	, 'success' )

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

	-- Make sure albumAssetId2 exists as we never deleted it as it was first uploaded against the restored asset
	rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId2 )
		:pluck( '_deleted' )
	, nil )

	-- Make sure the original album_assets were in fact deleted.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable ..[[ where assetId == ?]], assetId,
		quiesceUntilUpToDate = true,
	})
	:expectData { }
	:dispose()

	-- Make sure the restored asset still belongs to both albums after oz
	-- deletes the updated album_assets and adds new ones.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable ..[[ where assetId == ? ORDER BY albumId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	local function arraySize( queryResults )
		return #queryResults
	end

	-- Make sure the restored asset has new comments and favorites attached as oz will delete
	-- the old ones and create new ones for compatibility with older clients.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ) )
	:expectData( 1 )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ) )
	:expectData( 2 )
	:dispose()


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

	-- Make sure no album assets are laying around for the original or trash asset.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? or assetId == ? ]], assetId, trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData { }
	:dispose()

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_AssocDocs( rxTest )

	-- NMB-9375
	testAddAssocDocsSyncAddAssocDocsDeleteRestoreSync( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_AssocDocs_testNightly( rxTest )

	-- NMB-9375
	testAddAssocDocsSyncAddAssocDocsDeleteRestoreSync( rxTest )

end

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

local function pushDeleteAssetAndCreateAssocDocsReqResp( catalogId, originalAssetId, trashAssetId, trashRevId, albumId, spaceId, commentId, commentBody )

	-- The update array being pushed should include the delete asset, a create
	-- album asset, and a create comment, as they were added after the original
	-- sync but before the delete.

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, albumAssetUpdate, commentUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' and update.action == 'delete' then
					assetDeleteUpdate = update
				elseif update.type == 'album_asset' then
					albumAssetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				end
			end

			local correctDeleteAssetUpdate = hasCorrectDeleteAssetUpdate( assetDeleteUpdate, originalAssetId, trashAssetId, trashRevId )
			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, trashAssetId, albumId )
			local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, trashAssetId, commentBody )

			if correctDeleteAssetUpdate and correctAlbumAssetUpdate and correctCommentUpdate
				and #updatesBeingPushed == 3 then

				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest, options )

-- NMB-10089
-- Scenario:
--
-- Step 1: User pauses sync adds a photo, album1, and adds the photo to the album1, and then syncs.
-- Step 2: Asset is shared by some other client, a viewer adds a comment and a favorite. Sync pulls down the newly added comment and favorite docs.
-- Step 3: User pauses sync, adds album2, and then syncs.
-- Step 4: User pauses sync, adds the photo to album2, adds comment2, deletes the photo, and then syncs.
--
-- After Step 4 the test verifies:
--		A) Delete asset A and create asset C from trash B are sent up
--		B) That the photo is in both albums
--		C) Only the newly added album_assetid and comment is sent up
--		D) Both comments and and the favorite exist on the deleted asset. (user can't verify this)

-- optional Step 5: Once sync is complete user restores the photo, and then syncs

-- After Step 5 the test verifies:
--		E) Both comments and the favorite exist on the restored asset.

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local socialCreated = '2018-04-15T21:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

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

	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

	-- Sync down the asset, album and album_asset.  The asset will have the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false } ),
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )

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

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

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

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

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

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentBody1 = "Comment1 Mumble, mumble, mumble"
	local commentBody2 = "Comment2 Mumble, mumble, mumble"
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()

	-- This first part of this step is contrived as we aren't the other client. But
	-- we want to add the space_album, and comment documents here so when we run
	-- this test against oz it will have the documents as well and move them around
	-- during the delete and restore process.
	local now = KSDate.timeInIso8601Format()

	local spaceAlbumRevId
	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			label = 'oz',
			{
				_id = spaceId,
				userCreated = now,
				userUpdated = now,
				type = 'space',
				subtype = 'shared',
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			{
				_id = spaceAlbumId,
				type = 'space_album',
				space = { id = spaceId },
				album = { id = albumId },
			}
		} ) :map( function( overlayResults )
				spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
				return overlayResults
			end )
	, ':ignore:' )

	-- Now create the space via a direct put as MetadataPusher doesn't handles 'space' docs.
	local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	local createSpaceOptions = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
		payload = {
			private = false,
			download = false,
			metadata = false,
			location = false,
			presentationFilters = true,
		},
		subtype = 'shared',
		resources = {
			{
				id = albumId,
				type = 'album',
				space_album_id = spaceAlbumId,
			}
		}
	} )

	catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		spaceUpdateResponse( catalogId, spaceId )
	}), 'success' )

	local commentRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId,
			body = commentBody1,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			commentRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody1 ),
		feedbackUserUpdate()
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )

	local favoriteRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'favorite',
			subtype = 'user',
			_id = favoriteId,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			favoriteRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Sync down the favorite update.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

	-- Make sure the comment and favorite are correctly stored
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM spacefavorite where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM comment where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

-- Step 3:

	-- Add asset to another album before we issue the delete so we can
	-- verify it will only restore the one album_asset
	local albumId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumName2 = 'AlbumName2'
	local albumRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId2,
			type = 'album',
			subtype = 'collection',
			name = albumName2,
		}
		:map( function( revId )
			albumRevId2 = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync down the album
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId2, albumRevId2, albumName2 )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

-- Step 4:

	-- Add the asset to another album asset before we delete it
	-- Add another comment to asset before we delete it
	-- these won't be synced until after the asset has been deleted and restored
	local albumAssetRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId2,
			type = 'album_asset',
			album = { id = albumId2 },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	local commentId2 = KSIdGenerator.uuidInLowercaseHex()
	local commentRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId2,
			body = commentBody2,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = {
				created = socialCreated,
			}
		}
		:map( function( revId )
			commentRevId2 = revId
			return revId
		end )
	, ':ignore:' )


	-- Delete the asset that exists on oz.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

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

	local function sortByFirstElement( expectedQueryResult )
		table.sort( expectedQueryResult, function( a, b ) return a[ 1 ] < b[ 1 ] end )
		return expectedQueryResult
	end

	-- Make sure the trash asset has both album assets
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	-- Make sure the trash asset still has the favorite.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { favoriteId } } )
	:dispose()

	-- Make sure the trash asset still has both comments.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { commentId }, { commentId2 } } ) )
	:dispose()

	catalogSpec:expectHttpRequest( pushDeleteAssetAndCreateAssocDocsReqResp( catalogId, assetId, trashAssetId, trashRevId, albumId2, spaceId, commentId2, commentBody2 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

	local function transformOrderAndStrip_DeleteAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		-- Remove the sporadic feedback user / space update before running through
		-- stripUpdatesAndLinks to make sure things line up properly.
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
		removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

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

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

		if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
			return {}, {} -- don't compare partial updates
		end

		local function stripUpdateField( i, ... ) return stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, ... ) end

		-- Sort order of updates for delete should be:
			-- 1 album_asset1
			-- 2 album_asset2
			-- 3 asset
			-- 4 comment1
			-- 5 comment2
			-- 6 favorite1
			-- 7 deleted album_asset1
			-- 8 deleted asset
			-- 9 deleted comment1
			-- 10 deleted favorite1

		-- oz will create doc ids for these so strip them:

		-- don't strip id for album_asset2 as we know it should be albumAssetId2
		-- order of album_assets may be flipped as we sort on albumid
		if derefOpt( mockUpdateTable, 1, 'id' ) == albumAssetId2 then
			stripUpdateField( 2, "id" ) --  album_asset1
		else
			stripUpdateField( 1, "id" ) --  album_asset1
		end

		stripUpdateField( 3, "id" ) -- asset
		stripUpdateField( 4, "id" ) -- comment1
		stripUpdateField( 4, 'links', 'self', 'href' ) -- comment1
		stripUpdateField( 5, "id" ) -- comment2
		stripUpdateField( 5, 'links', 'self', 'href' ) -- comment2
		stripUpdateField( 6, "id" ) -- favorite
		stripUpdateField( 6, 'links', 'self', 'href' ) -- favorite
		stripUpdateField( 7, "new_id" ) -- album_asset1
		stripUpdateField( 8, "new_id" )
		stripUpdateField( 9, "new_id" )
		stripUpdateField( 10, "new_id" )

		return mockDataAsTable, ozDataAsTable

	end

	-- The _revId was changed when the album_asset was updated to refer to the trash asset,
	-- get the latest version so we don't cause a conflict which would result in sending
	-- another update for it.
	rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId2 )
		:map( function( docValue )
			albumAssetRevId2 = docValue._revId
			return docValue
		end )
	, ':ignore:' )

	local userId = catalog:ozUserId()
	local trashAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	local trashCommentId = KSIdGenerator.uuidInLowercaseHex()
	local trashCommentRevId = KSIdGenerator.uuidInLowercaseHex()
	local trashFavoriteId = KSIdGenerator.uuidInLowercaseHex()
	local trashFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			albumAssetUpdateResponse( trashAlbumAssetId, trashAlbumAssetRevId, albumId, trashAssetId ), -- new unknown id and revId will be created by oz
			albumAssetUpdateResponse( albumAssetId2, albumAssetRevId2, albumId2, trashAssetId ), -- This was uploaded as a trash album_asset so no delete will be issued for it
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, trashCommentId, trashCommentRevId, spaceId, trashAssetId, commentBody1 ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId2, commentRevId2, spaceId, trashAssetId, commentBody2 ), -- This was uploaded as a trash comment so no delete will be issued for it
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, trashFavoriteId, trashFavoriteRevId, spaceId, trashAssetId ),
			albumAssetDeletedResponse( albumAssetId, userId, trashAlbumAssetId ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			commentDeletedResponse( commentId, userId, trashCommentId ),
			favoriteDeletedResponse( favoriteId, userId, assetId, trashFavoriteId ),
		},
		transformOrderAndStrip_DeleteAssocDocs )
	, 'success' )

	-- Make sure the trash asset belongs to both albums
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectDataWithinTime( 5, sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	local function arraySize( queryResults )
		return #queryResults
	end

	-- Make sure the trash asset still has the favorite attached
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectData( 1 )
	:dispose()

	-- Make sure the restored asset still has the comments attached
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectData( 2 )
	:dispose()

	if options.restore then

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

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

		-- Make sure the restored asset belongs to both albums
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
		:dispose()

		-- Make sure the restored asset still has the favorite attached
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 1 )
		:dispose()

		-- Make sure the restored asset still has the comments attached
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 2 )
		:dispose()

		catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, trashAssetId, trashRevId ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the restored asset (with the links that point to the existing master),
		-- the restored asset's album_assets, and tombstones of the trash album_assets
		-- back in the sync feed.
		local restoredAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetRevId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentId = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentRevId = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentRevId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredFavoriteId = KSIdGenerator.uuidInLowercaseHex()
		local restoredFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()


		local function transformOrderAndStrip_RestoreAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

			if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
				return {}, {} -- don't compare partial updates
			end

			local function stripUpdateField( i, ... ) return stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, ... ) end

			-- Sort order of updates for delete should be:
				-- 1 album_asset1
				-- 2 album_asset2
				-- 3 asset
				-- 4 comment1
				-- 5 comment2
				-- 6 favorite1
				-- 7 deleted album_asset1
				-- 8 deleted album_asset2
				-- 9 deleted asset (trash)
				-- 10 deleted comment1
				-- 11 deleted comment2
				-- 12 deleted favorite1

			-- Strip the id's off the updates as they will all be new.
			for i = 1, 6 do
				stripUpdateField( i, "id" )
			end

			-- Strip the links in the comments and favorites as they have an new id in them.
			for i = 4, 6 do
				stripUpdateField( i, 'links', 'self', 'href' )
			end

			-- Strip the id's off the deletes as we didn't bother to retrieve them above.
			-- Also strip the new_id as they will all be new and unknown.
			for i = 7, 12 do
				stripUpdateField( i, 'id' )
				stripUpdateField( i, 'new_id' )
			end

			return mockDataAsTable, ozDataAsTable

		end

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

		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
				albumAssetUpdateResponse( restoredAlbumAssetId, restoredAlbumAssetRevId, albumId, restoredAssetId ), -- new unknown id and revId will be created by oz
				albumAssetUpdateResponse( restoredAlbumAssetId2, restoredAlbumAssetRevId2, albumId2, restoredAssetId ), -- new unknown id and revId will be created by oz
				restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
				WFCatTrashTestUtils.commentUpdateResponse( catalogId, restoredCommentId, restoredCommentRevId, spaceId, restoredAssetId, commentBody1 ),
				WFCatTrashTestUtils.commentUpdateResponse( catalogId, restoredCommentId2, restoredCommentRevId2, spaceId, restoredAssetId, commentBody2 ),
				WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, restoredFavoriteId, restoredFavoriteRevId, spaceId, restoredAssetId ),
				albumAssetDeletedResponse( trashAlbumAssetId, userId, restoredAlbumAssetId ),
				albumAssetDeletedResponse( albumAssetId2, userId, restoredAlbumAssetId2 ),
				assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
				commentDeletedResponse( trashCommentId, userId, restoredCommentId ),
				commentDeletedResponse( commentId2, userId, restoredCommentId2 ),
				favoriteDeletedResponse( trashFavoriteId, userId, trashAssetId, restoredFavoriteId ),
			}
			, transformOrderAndStrip_RestoreAssocDocs )
		, 'success' )

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

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

		-- Make sure the original album_assets were in fact deleted.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ?]], assetId,
			quiesceUntilUpToDate = true,
		})
		:expectData { }
		:dispose()

		-- Make sure the restored asset still belongs to both albums after oz
		-- deletes the updated album_assets and adds new ones.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
		:dispose()


		-- Make sure the restored asset has new comments and favorites attached as oz will delete
		-- the old ones and create new ones for compatibility with older clients.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 1 )
		:dispose()

		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 2 )
		:dispose()

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

		-- Make sure no album assets are laying around for the original or trash asset.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? or assetId == ? ]], assetId, trashAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData { }
		:dispose()

	end

end

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

function WFCatTrashTest.rxTest_CT_OzDelete_AssocDocs_testNightly( rxTest )

	-- NMB-10089
	testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest )

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_AssocDocs( rxTest )

	-- NMB-10089
	testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteSyncRestore_AssocDocs_testNightly( rxTest )

	-- NMB-10089
	testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest, { restore = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteSyncRestore_AssocDocs( rxTest )

	-- NMB-10089
	testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest, { mock = true, restore = true } )

end

--==============================================================================
-- @mark tests that involve deleting existing assets and removing the trash assets before syncing
--==============================================================================

local function testAddSyncDeleteDelTrashSyncAssocDocs( rxTest, options )

	-- NMB-10294
	-- Scenario:
	-- 1 Add photo
	-- 2 Wait for it to sync
	-- 3 Pause sync
	-- 4 Delete photo
	-- 5 go to trash view and delete the newly created trash asset
	-- 6 turn on sync

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier
	
	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )
	
	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

	-- Sync down the asset, album and album_asset.  The asset will have the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false } ),
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )
	
	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

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

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

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

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentBody1 = "Comment1 Mumble, mumble, mumble"
	local commentBody2 = "Comment2 Mumble, mumble, mumble"
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()

	-- This first part of this step is contrived as we aren't the other client. But
	-- we want to add the space_album, and comment documents here so when we run
	-- this test against oz it will have the documents as well and move them around
	-- during the delete and restore process.
	local now = KSDate.timeInIso8601Format()

	local spaceAlbumRevId
	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			label = 'oz',
			{
				_id = spaceId,
				userCreated = now,
				userUpdated = now,
				type = 'space',
				subtype = 'shared',
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			{
				_id = spaceAlbumId,
				type = 'space_album',
				space = { id = spaceId },
				album = { id = albumId },
			}
		} ) :map( function( overlayResults )
				spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
				return overlayResults
			end )
	, ':ignore:' )

	-- Now create the space via a direct put as MetadataPusher doesn't handles 'space' docs.
	local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	local createSpaceOptions = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
		payload = {
			private = false,
			download = false,
			metadata = false,
			location = false,
			presentationFilters = true,
		},
		subtype = 'shared',
		resources = {
			{
				id = albumId,
				type = 'album',
				space_album_id = spaceAlbumId,
			}
		}
	} )

	catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		spaceUpdateResponse( catalogId, spaceId )
	}), 'success' )

	local commentRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId,
			body = commentBody1,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			commentRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody1 ),
		feedbackUserUpdate()
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )


	local favoriteRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'favorite',
			subtype = 'user',
			_id = favoriteId,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			favoriteRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Sync down the favorite update.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

	-- Make sure the comment and favorite are correctly stored
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM spacefavorite where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM comment where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

	-- Add asset to another album before we issue the delete so we can
	-- verify it will only restore the one album_asset
	local albumId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumName2 = 'AlbumName2'
	local albumRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId2,
			type = 'album',
			subtype = 'collection',
			name = albumName2,
		}
		:map( function( revId )
			albumRevId2 = revId
			return revId
		end )
	, ':ignore:' )

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

	-- Sync down the album
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId2, albumRevId2, albumName2 )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

	-- Add the asset to another album asset before we delete it
	-- Add another comment to asset before we delete it
	-- these won't be synced up as the asset will be removed from trash.
	local albumAssetRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId2,
			type = 'album_asset',
			album = { id = albumId2 },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	local commentId2 = KSIdGenerator.uuidInLowercaseHex()
	local commentRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId2,
			body = commentBody2,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = {
				created = socialCreated,
			}
		}
		:map( function( revId )
			commentRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	-- Delete the asset that exists on oz.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

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

	local function sortByFirstElement( expectedQueryResult )
		table.sort( expectedQueryResult, function( a, b ) return a[ 1 ] < b[ 1 ] end )
		return expectedQueryResult
	end

	-- Make sure the trash asset has both album assets
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	-- Make sure the trash asset still has the favorite.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { favoriteId } } )
	:dispose()

	-- Make sure the trash asset still has both comments.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { commentId }, { commentId2 } } ) )
	:dispose()

	-- Now delete the trash asset (Oz refers to this as purging it)
	rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, 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:' )

	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( pushDeleteAndDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId, assetId, revisionId, userCreated, purgeDate ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	local userId = catalog:ozUserId()

	-- Wait until the asset and trash asset show up in the sync feed.
	local expectedUpdates = {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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
	
	local assetUpdateCount = 0
	rxTest:observe( pollHttpGetMetadataUpdates( catalogSpec, catalog:docStore(), '200', expectedResponse )
		:map( function( statusCode, response )
			local assetUpdates = removeNonAssetUpdates( response.updates )
			-- we might get the asset updates split across metadata pulls so keep track of the total so far
			assetUpdateCount = assetUpdateCount + #assetUpdates
			return statusCode, assetUpdateCount
		end ) )
	:expectDataWithinTime( 5 * waitMultiplier, '200', 2 )
	:dispose()

	-- Verify the trash asset is deleted
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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 = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		return mockDataAsTable, ozDataAsTable

	end ), 'success' )

	local function arraySize( queryResults )
		return #queryResults
	end

	-- Make sure the trash asset favorite is gone
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectDataWithinTime( 5 * waitMultiplier, 0 )
	:dispose()

	-- Make sure the trash asset doesn't have comments
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectData( 0 )
	:dispose()

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteDelTrash( rxTest )

	-- NMB-10294

	-- Scenario:
	-- 1 Add photo
	-- 2 Wait for it to sync
	-- 3 Pause sync
	-- 4 Delete photo
	-- 5 go to trash view and delete the newly created trash asset
	-- 6 turn on sync

	-- Make sure all the assoicated docs have been deleted from the doc store
	-- as well.

	testAddSyncDeleteDelTrashSyncAssocDocs( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDeleteDelTrash_testNightly( rxTest )

	-- NMB-10294
	-- Scenario:
	-- 1 Add photo
	-- 2 Wait for it to sync
	-- 3 Pause sync
	-- 4 Delete photo
	-- 5 go to trash view and delete the newly created trash asset
	-- 6 turn on sync

	-- Make sure all the assoicated docs have been deleted from the doc store
	-- as well.

	testAddSyncDeleteDelTrashSyncAssocDocs( rxTest )

end

--==============================================================================
-- @mark tests that involve removing existing trash assets after syncing
--==============================================================================

local function testAddDeleteSyncDelTrashSync( rxTest, options )
	rxTest:expectEqual( false, failStubbedTests )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteSyncDelTrash( rxTest )
	testAddDeleteSyncDelTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteSyncDelTrash_testNightly( rxTest )
	testAddDeleteSyncDelTrashSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteSyncDelTrash_AssocDocs( rxTest )
	testAddDeleteSyncDelTrashSync( rxTest, { mock = true, assoc = true } )
end

--==============================================================================
-- @mark Tests that involve adding, deleting, and deleting from trash a new asset before syncing
--==============================================================================

local function testAddDeleteDelTrashSync( rxTest, options )
	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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


	-- Validate asset? Or is that covered by other tests?

	-- Delete asset
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )
	
	local trashAssetId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	
	-- Validate trash asset, or leave that to other tests?
	
	-- Remove the asset from the trash.
	rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )
	
	
	-- 'Turn on' sync and validate that 'the right thing' happens, which should
	-- be nothing?
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashTest.rxTest_CT_MockAddDeleteDelTrash( rxTest )
	-- This shouldn't send anything to oz at all
	testAddDeleteDelTrashSync( rxTest, { mock = true, assoc = true } )
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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

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

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

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

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

	uploader:startContinuousUploads()

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

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

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

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

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

	-- 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( 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 = {
		assetDeletedResponse( assetId, 'trashAssetId', userId ),
		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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, 'trashAssetId', userId ),
		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 = 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 = 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 catalogId = catalog:ozCatalogId()
	local assetId1 = KSIdGenerator.uuidInLowercaseHex()
	local assetId2 = KSIdGenerator.uuidInLowercaseHex()

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

	-- Add another image asset
	rxTest:expectSingleValue( 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 = 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 other client actions
--==============================================================================

local function simulateOtherClientAssetCreate( args )

	local rxTest = args.rxTest
	local ozCatalog = args.catalog
	local catalogSpec = args.catalogSpec

	local catalogId = ozCatalog:ozCatalogId()

	local sendUpdateOptions = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	local createAssetUpdate = {{
		type = 'asset',
		action = 'create',
		subtype = args.subtype,
		revision_id = args.revisionId,
		id = args.assetId,
		original = {
			id = args.originalId,
			userCreated = args.originalUserCreated,
		},
		payload = {
			purgeDate = args.purgeDate,
			userCreated = args.userCreated,
			userUpdated = args.userUpdated,
			captureDate = args.captureDate,
			importSource = args.importSource,
			develop = args.develop,
			location = args.location,
		},
	}}

	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, args.fileName, args.subtype ) )

	local sendUpdateUrl = "/v2/catalogs/" .. catalogId .."/updates"

	rxTest:expectSingleValue(
		catalogSpec:put( sendUpdateUrl, KSJsonEncoder.encodeOzUploadJson( createAssetUpdate ), sendUpdateOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '202' )

end

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

local function simulateOtherClientAssetRestore( args )

	local rxTest = args.rxTest
	local ozCatalog = args.catalog
	local catalogSpec = args.catalogSpec
	local assetId = args.assetId
	local revisionId = args.revisionId
	local trashAssetId = args.trashAssetId
	local trashAssetRevId = args.trashAssetRevId
	local userUpdated = args.userUpdated

	local catalogId = ozCatalog:ozCatalogId()

	local sendUpdateOptions = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	local restoreAssetUpdate = {{
		type = 'asset',
		action = 'create',
		revision_id = revisionId,
		id = assetId,
		payload = {
			userUpdated = userUpdated,
		},
		restore_from = {
			id = trashAssetId,
			revision_id = trashAssetRevId
		}
	}}

	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, assetId, trashAssetId, trashAssetRevId ) )

	local sendUpdateUrl = "/v2/catalogs/" .. catalogId .."/updates"

	rxTest:expectSingleValue(
		catalogSpec:put( sendUpdateUrl, KSJsonEncoder.encodeOzUploadJson( restoreAssetUpdate ), sendUpdateOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '202' )

	if not args.options.mock then
		-- Oz team has requested that we add a delay of 1 second between the two restore
		-- operations as they don't want to fix a bug where if two clients restore the same
		-- asset at 'exactly' the same time.  As oz will fail to recognize it is already restoring one
		-- and accidentally create two trash assets.
		rxTest:sleep( 1 )
	end

end

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

local function simulateOtherClientDeleteTrash( args )

	local rxTest = args.rxTest
	local catalog = args.catalog
	local catalogSpec = args.catalogSpec
	local trashAssetId = args.trashAssetId
	local trashAssetRevId = args.trashAssetRevId

	local catalogId = catalog:ozCatalogId()

	-- Send an update with a delete trash asset without our docstore knowing about it
	-- so it appears as if a different client issued the permanent delete.
	catalogSpec:expectHttpRequest( pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashAssetRevId ) )

	local sendUpdateUrl = "/v2/catalogs/" .. catalogId .."/updates"

	local deleteTrashAssetUpdate = {
		{
			action = "delete",
			id = trashAssetId,
			revision_id = trashAssetRevId,
			type = "asset",
		}
	}

	local sendUpdateOptions = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectSingleValue(
		catalogSpec:put( sendUpdateUrl, KSJsonEncoder.encodeOzUploadJson( deleteTrashAssetUpdate ), sendUpdateOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '202' )

end

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

local function testOtherDeleteSyncRestore( rxTest, options )

	-- NMB-13862
	-- This test was written to try and flush out a bug reported by Oz
	-- that says we improperly issued a delete/restore request identifying
	-- the trash asset in the create's id as well as the restore_from.

	-- Scenario:
	-- 1 Pause Sync
	-- 2 Import asset on other client running 2.3 (pre-trash client)
	-- 3 Delete asset on other client
	-- 4 Turn on Sync
	-- 5 (optional) wait a minute and then restore
	-- Verify that the asset is restored

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local userUpdated = '2018-04-15T18:18:19Z'
	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'

	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 revId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )
	local location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
	-- Default to not expecting any conflicts
	local expectedConflicts = {}

	if options.otherCreateTrash then

		-- Simulate trash asset creation by other client
		simulateOtherClientAssetCreate {
			rxTest = rxTest,
			catalog = catalog,
			catalogSpec = catalogSpec,
			assetId = trashAssetId,
			subtype = trashAssetSubtype,
			revisionId = revId,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = trashPurgeDate,
			importSource = importSource,
			fileName = fileName,
			location = location,
			options = options,
			originalId = assetId,
			originalUserCreated = userCreated,
		}

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		}, stripForAssetUpdate( false ) ), 'success' )

	elseif options.otherCreate then

		-- Simulate asset creation by other client
		simulateOtherClientAssetCreate {
			rxTest = rxTest,
			catalog = catalog,
			catalogSpec = catalogSpec,
			assetId = assetId,
			subtype = subtype,
			revisionId = revId,
			userCreated = userCreated,
			userUpdated = userCreated,
			captureDate = "2018-01-01T00:00:00Z",
			importSource = importSource,
			fileName = fileName,
			location = location,
			options = options,
		}

		-- Pull the asset create back down now
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( assetId, revId, fileName, subtype, userCreated, userCreated, importSource, { master = false, renditions = false } )
		}, stripForAssetUpdate( false ) ), 'success' )

		if options.modifyAssetBeforeDelete 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 = assetId,
					userUpdated = userUpdated,
					develop = {
						processingModel = "lightroom",
						xmpCameraRaw = kApplyTintXmp
					}
				}, ':ignore:' )

		end

		if options.downloadProxyBeforeDelete then

			-- Since we didn't upload anything we are just faking like we downloaded
			-- a proxy by setting the _localOnly files and spm link for it.

			local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

			local proxyFileName = "IMG_7763.DNG"
			local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )
			local proxyFileSize = WFFileUtils.fileAttributes( proxyFilePath ).fileSize

			local localOnly
			if WFPlatformInfo.isDesktop then
				localOnly = {
					files = {
						proxy = {
							['2560'] = {
								relativePath = WFFileUtils.removeVolumeFromPath( proxyFilePath ),
								volumeGuid = WFFileUtils.getVolumeGuidForPath( proxyFilePath ),
								size = proxyFileSize,
							},
						},
					},
				}
			elseif WFPlatformInfo.isiOS then
				localOnly = {
					files = {
						proxy = {
							path = proxyFilePath,
						},
					},
				}
			elseif WFPlatformInfo.isAndroid then
				localOnly = {
					proxy = {
						filePath = proxyFilePath,
					},
				}
			end

			rxTest:expectSingleValue( catalog:docStore():applyOverlay {
					_id = assetId,
					_serverProvidedMetadata = {
						links = {
							[ '/rels/proxy_type/2560' ] = {
								href = 'url',
							},
							[ '/rels/xmp' ] = {
								href = 'url',
							},
						}
					},
					_localOnly = localOnly,
				}, ':ignore:' )

		end

		-- Simulate delete asset by other client
		simulateOtherClientAssetDelete {
			rxTest = rxTest,
			catalog = catalog,
			catalogSpec = catalogSpec,
			assetId = assetId,
			revisionId = revId,
			trashAssetId = trashAssetId,
			trashAssetRevId = trashAssetRevId,
			userCreated = userCreated,
			trashPurgeDate = trashPurgeDate,
			options = options,
		}

		if options.splitResponse1 then

			-- Split Response, Batch one: Tombstone A, Batch 2: Trash Asset B

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )


		elseif options.splitResponse1dupe then

			-- Split Response, Batch one: Tombstone A, Tombstone A, Batch 2: Trash Asset B

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId ),
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

		elseif options.splitResponse2 then

			-- Split Response, Batch one: Trash Asset B, Batch 2: Tombstone A

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

		elseif options.splitResponse3dupe then

			-- Split Response, Batch one: Tombstone A  Batch2: Tombstone A  Batch 3: Trash Asset B
			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )


		else

			-- Receive Tombstone and trash asset in same batch

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
				assetDeletedResponse( assetId, trashAssetId, userId )
			}, stripForAssetUpdate( false ) ), 'success' )

		end

		if options.splitResponse1 or options.splitResponse1dupe or options.splitResponse3dupe then

			if options.modifyAssetBeforeDelete then

				-- Expect to resolve conflict via localRevWithOzUserUpdated because the puller copied
				-- the asset modifications, however when doing so it does not set the userUpdated field.
				-- The conflict resolver had been modified to identify this case and preserve the userUpdated
				-- field set from the other client restore instead when this happens (but keeps the local mods).
				expectedConflicts = { localRevWithOzUserUpdated = { trashAssetId } }

			elseif options.downloadProxyBeforeDelete then

				expectedConflicts = { remoteRevByChangeCounter = { trashAssetId } }

			end

		end

		-- Wait for conflicts to be solved by conflictResolver (if any)
		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
		:map( function( trashDocValue )
			return trashDocValue._conflict, conflictResolver:DEBUG_conflictsResolved()
		end ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil, expectedConflicts )
		:dispose()

		if options.modifyAssetBeforeDelete then
			catalogSpec:expectHttpRequest( pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, options.xmpDevelop, kApplyTintXmp ) )
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
		end

	else

		-- Simulate asset creation by other client
		simulateOtherClientAssetCreate {
			rxTest = rxTest,
			catalog = catalog,
			catalogSpec = catalogSpec,
			assetId = assetId,
			subtype = subtype,
			revisionId = revId,
			userCreated = userCreated,
			userUpdated = userCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = trashPurgeDate,
			importSource = importSource,
			location = location,
			fileName = fileName,
			options = options,
		}

		-- Simulate delete asset by other client
		simulateOtherClientAssetDelete {
			rxTest = rxTest,
			catalog = catalog,
			catalogSpec = catalogSpec,
			assetId = assetId,
			revisionId = revId,
			trashAssetId = trashAssetId,
			trashAssetRevId = trashAssetRevId,
			userCreated = userCreated,
			trashPurgeDate = trashPurgeDate,
			options = options,
		}

		if options.splitResponse1 then

			-- Split Response, Batch one: Tombstone A, Batch 2: Trash Asset B

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

		elseif options.splitResponse1dupe then

			-- Split Response, Batch one: Tombstone A, Tombstone A, Batch 2: Trash Asset B

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId ),
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

		elseif options.splitResponse2 then

			-- Split Response, Batch one: Trash Asset B, Batch 2: Tombstone A

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

		elseif options.splitResponse3dupe then

			-- Split Response, Batch one: Tombstone A  Batch2: Tombstone A  Batch 3: Trash Asset B
			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
			}, stripForAssetUpdate( false ) ), 'success' )

		else

			-- Receive Tombstone and trash asset in same batch

			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
				assetDeletedResponse( assetId, trashAssetId, userId )
			}, stripForAssetUpdate( false ) ), 'success' )

		end

	end

	if options.restore then

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

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

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

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

	end

	-- Shouldn't have anything to push.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Make sure there are no errors reported by the error handler.
	rxTest:expectEqual( {}, errorHandler:DEBUG_errorsResolved() )

	-- Make sure there are no new unexpected conflicts.
	rxTest:expectEqual( expectedConflicts, conflictResolver:DEBUG_conflictsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherDelete_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherDeleteSyncRestore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDelete_noTombstone( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreateTrash = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherDeleteSyncRestore_noTombstone( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, restore = true, otherCreateTrash = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherDelete_NoTombstone_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreateTrash = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherDeleteSyncRestore_NoTombstone_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { restore = true, otherCreateTrash = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_OtherDelete_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_Restore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_OtherDelete_Restore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_Restore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_Restore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_Restore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_OtherDelete_Restore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, restore = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_Modify_OtherDelete_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, modifyAssetBeforeDelete = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_Modify_OtherDelete_Restore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, modifyAssetBeforeDelete = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_GetProxy_OtherDelete_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, downloadProxyBeforeDelete = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse3dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzOtherCreate_GetProxy_OtherDelete_Restore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, downloadProxyBeforeDelete = true, restore = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse3dupe = true } )
end

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

local function testAddDeleteSyncOtherRestore( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

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

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

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

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

	local expectedUserCreated = userCreated

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

	-- Update the trashRevId with the latest revision id before issuing the createMasterReqResp
	trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the proxy and renditions before requesting next update
	rxTest:expectSingleValue( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true )
	}, stripForAssetUpdate( true ) ), 'success' )

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

	-- Restore asset on other client
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevision = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = restoredAssetId,
		revisionId = restoredAssetRevision,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}

	if options.splitResponseTombstoneFirst then

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		} ), 'success' )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
		} ), 'success' )

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

	elseif options.splitResponseUpdateFirst then

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
		} ), 'success' )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		} ), 'success' )

	else

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		} ), 'success' )

	end

	-- Check that trash asset is now deleted.
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_deleted' ), true )

	-- Check that there is no conflict.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):pluck( '_conflict' ), nil )

	-- Make sure the restored asset has a local master
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):map( LrAssetUtils.hasMasterLocally ), true )

	-- check that we think the current revision exists on oz to ensure we don't have a stuck sync
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):map( LrAssetUtils.isLatestMetadataUploaded ), true )

	-- check that there is nothing to push
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashTest.rxTest_CT_MockOtherRestore( rxTest )

-- Test that it properly syncs restored trash assets that were restored by
-- another client and that it can download thumbnails.

	testAddDeleteSyncOtherRestore( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockOtherRestore_splitResponse_tombstone( rxTest )

-- Test that it properly syncs restored trash assets that were restored by
-- another client and that it can download thumbnails.

	testAddDeleteSyncOtherRestore( rxTest, { mock = true, splitResponseTombstoneFirst = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockOtherRestore_splitResponse_update( rxTest )

-- Test that it properly syncs restored trash assets that were restored by
-- another client and that it can download thumbnails.

	testAddDeleteSyncOtherRestore( rxTest, { mock = true, splitResponseUpdateFirst = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzOtherRestore_testNightly( rxTest )

-- Test that it properly syncs restored trash assets that were restored by
-- another client and that it can download thumbnails.

	testAddDeleteSyncOtherRestore( rxTest )

end

--------------------------------------------------------------------------------
	
local function transformResponseWithResourceNotFoundError( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

	if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
		return {}, {}
	end

	local mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
	
	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'id' )
	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'new_id' )
	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'source', 'revision_id' )
	stripBothUpdateFields( nil, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'deleted_by' )
	
	return mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks

end

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

local function transformResponseWithUnknownNewIds( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

	-- Remove the sporadic feedback user / space update before running through
	-- stripUpdatesAndLinks to make sure things line up properly.
	removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

	if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
		return {}, {}
	end

	local mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
	
	for i, mockUpdate in ipairs( mockDataMinusLinksAndUpdates.updates ) do
		local ozUpdate = ozDataMinusUpdatesAndLinks.updates[ i ]
		
		if mockUpdate.type == 'album_asset' and mockUpdate.deleted == true then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'new_id' )
		elseif mockUpdate.type == 'album_asset' then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'asset', 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'asset', 'links', 'self', 'href' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'links', 'self', 'href' )
		elseif mockUpdate.type == 'error' then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'new_id' )
			stripBothUpdateFields( nil, mockUpdate, ozUpdate, 'deleted_by' )
		end
	end
	return mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks

end

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

local function testDeleteOtherDeleteSync( rxTest, options )

-- NMB-9390

-- Verify deleted by this client while off line and deleted by other online
-- client, and then go on line.
-- Checks if we receive delete from other before we push our delete.
-- Checks if we receive delete from other after we push our delete.

	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, _, errorHandler = 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'

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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


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

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

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete.
	rxTest:observe( 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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { injectQuickerRetries = options.injectQuickerRetries } ), true )

	end

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

	-- Pause sync
	
	-- Apply an edit, for the purpose of having something to merge  into the
	-- trash asset generated by the other client in the push-first-upon-unpause case.
	
	local editedRevisionId
	
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = assetId,
			develop = {
				processingModel = "lightroom",
				xmpCameraRaw = kApplyTintXmp
			}
		}
		:map( function( revId )
			editedRevisionId = revId
			return revId
		end )
	, ':ignore:' )

	local ratedRevision
	local ratingsUuid = KSIdGenerator.uuidInLowercaseHex()
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = assetId,
			ratings = {
				[ ratingsUuid ] = {
					rating = 3
				}
			}
		}
		:map( function( revId )
			ratedRevision = revId
			return revId
		end )
	, ':ignore:' )
	
	-- 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 localTrashAssetId, _, trashDoc = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	
	local trashCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( trashDoc )
	
	local trashPurgeDate = KSTableUtils.derefOpt( trashDoc, 'purgeDate' )
	local trashAssetCreated  = KSTableUtils.derefOpt( trashDoc, 'userCreated' )

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

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	
	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = trashPurgeDate,
		options = options,
	}
	
	-- Back to Original Client -------------------------------------------------

	-- Unpause sync
	
	if options.pushFirstAfterUnpause then
		-- Send the delete before pulling updates
		catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, emptyRevId, localTrashAssetId, trashCreateWithRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )
		
		local expectedUpdate = {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", trashAssetId, userId ),
		}
		
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, expectedUpdate,
			transformResponseWithResourceNotFoundError )
		, 'success' )
	else
	
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			} )
		, 'success' )

		-- Send the delete after pulling updates
		catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, emptyRevId, localTrashAssetId, trashCreateWithRevId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )
	end
	
	-- Wait for trash asset from the 'other client delete' to appear in the docstore.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( function( docValue )
		return true
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )

	-- Shouldn't have anything to push at this point.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, 5 * waitMultiplier ) )
	
	if options.pushFirstAfterUnpause then
		-- The trash asset which originated on this client should get deleted as
		-- a result of the error handlng of the 404 received in response to the push.
		rxTest:observe( catalog:docStore():ongoingDocValue( localTrashAssetId )
			--:pluck( '_empty' )
			:map( function( value )
				return value._empty == true
			end )
		 )
		:expectDataWithinTime( 5 * waitMultiplier, true )
		:dispose()
		
		-- Check that the rating was applied to the Oz trash doc. (Assuming that
		-- the develop change was too if the rating was.)
		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
			:pluck( 'ratings', ratingsUuid, 'rating' )
			:map( function( value )
				return value and value == 3
			end )
		 )
		:expectDataWithinTime( 5 * waitMultiplier, true )
		:dispose()

		-- Send up the rating and develop change.
		catalogSpec:expectHttpRequest( pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, options.xmpDevelop, kApplyTintXmp ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )

	end

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	if options.pushFirstAfterUnpause then

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

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

end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDelete_pushFirst_testNightly( rxTest )

	testDeleteOtherDeleteSync( rxTest, { pushFirstAfterUnpause = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDelete_pullFirst_testNightly( rxTest )

	testDeleteOtherDeleteSync( rxTest, {} )

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDelete_pushFirst( rxTest )

	testDeleteOtherDeleteSync( rxTest, { mock = true, pushFirstAfterUnpause = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDelete_pullFirst( rxTest )

	testDeleteOtherDeleteSync( rxTest, { mock = true } )

end

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

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 = 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'

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete.
	rxTest:observe( 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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { injectQuickerRetries = options.injectQuickerRetries } ), true )

	end

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

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

		-- Wait for the error to be processed.
		rxTest:observe( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			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 = 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( 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( pollUntilAssetExistsWithLinks( 3* waitMultiplier, catalogSpec, catalogId, restoredAssetId, 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( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashAssetRevId, restoredDocValue.userUpdated ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
		
		-- Wait for ResourceNotFoundError error.

		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
			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( 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

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

local function testAddToAlbumOtherDeleteSync( rxTest, options )

	-- NMB-11698
	--
	-- Scenario:
	--
	-- Client 1 adds asset to an album while sync is paused.
	-- Client 2 deletes the asset while syncing.
	-- Client 1 unpauses sync.
	-- Client 2 restores the asset.
	--
	-- The objective of this test is to verify that the asset shows up in the
	-- album after the restore. Push first and pull first after unpause are
	-- variations of the test.
	
	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	if not options.mock then
		-- Having a WFCatSyncAutoDeleter instance allows us to have the test fail
		-- for the same reason that things would go off the rails in the real Lr CC app
		-- when the WFCatSyncAutoDeleter instance deletes the album asset as a result
		-- of the other-client-delete of the asset. That should no longer happen, after
		-- appropriate cloud trash motivated changes have been made to WFCatSyncAutoDeleter.
		-- See 'remapOzDocumentData' - self._deletedDocumentIdStream should not be
		-- triggered unless the deleted document does *not* have a 'new_id'.
		autoDeleter = require 'WFCatSyncAutoDeleter'( catalog, { delay = 0.1 } )
		assert( autoDeleter, "failed to instantiate autoDeleter" )
	end
	
	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'

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

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId
	
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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


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

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

	uploader:startContinuousUploads()

	-- Wait for the upload master to complete.
	rxTest:observe( 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( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, options ), true )

	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = waitForRenditions } )
	}, 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()
	
	-- Create album
	
	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )
	
	-- Sync up the album
	catalogSpec:expectHttpRequest( pushCreateAlbumReqResp( catalogId, albumId, albumName ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset and album
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Image asset is now complete and fully synced.

	-- Pause sync
	
	-- Add asset to album
	
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetRevId
	local knownAlbumAssetIds = { albumAssetId }
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId,
			type = 'album_asset',
			album = { id = albumId },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId = revId
			return revId
		end )
	, ':ignore:' )

	-- Delete asset on other client

	local trashPurgeDate = KSDate.shiftIso8601DateTime( KSDate.timeInIso8601Format(), 5184000 )
	local trashAssetCreated = trashPurgeDate
	local ozTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local ozTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	
	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = trashPurgeDate,
		options = options,
	}

	-- Resume sync
	
	if options.pushFirstAfterUnpause then

		-- Send the album asset before pulling updates
		catalogSpec:expectHttpRequest( pushAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )

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

		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( ozTrashAssetId, ozTrashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( assetId, ozTrashAssetId, userId ), -- This will resuilt in the trashAsset being updated
			assetGoneErrorUpdateResponse( albumAssetId, albumAssetRevId, 'album_asset', ozTrashAssetId, {} ),
		}, transformResponseWithResourceNotFoundError )
		, 'success' )

		-- Sleep long enough that autoDeleter does its thing, or not, preferrably.
		if not options.mock then
			-- It should suffice to validate that WFCatSyncAutoDeleter doesn't
			-- interfere in an undesirable manner in the nightly tests.
			rxTest:sleep( 1 )
		end
	else

		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( ozTrashAssetId, ozTrashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			assetDeletedResponse( assetId, ozTrashAssetId, userId ), -- This will resuilt in the trashAsset being updated
			} )
		, 'success' )
		
		-- Sleep long enough that autoDeleter does its thing, or not, preferrably.
		if not options.mock then
			-- It should suffice to validate that WFCatSyncAutoDeleter doesn't
			-- interfere in an undesirable manner in the nightly tests.
			rxTest:sleep( 1 )
		end

		-- Push album asset update.
		catalogSpec:expectHttpRequest( pushAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 ) )
		
		-- Wait for asset gone response

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetGoneErrorUpdateResponse( albumAssetId, albumAssetRevId, 'album_asset', ozTrashAssetId, {} ),
		}, transformResponseWithResourceNotFoundError )
		, 'success' )		
	end

	local newAssetIdForAlbumAsset
	
	-- Wait for album asset to be updated according to new_id received fom Oz.
	rxTest:observe( catalog:docStore():ongoingDocValue( albumAssetId )
		:pluck( 'asset', 'id' )
		:map( function( assetIdForAlbumAsset )
			if assetIdForAlbumAsset ~= assetId then
				newAssetIdForAlbumAsset = assetIdForAlbumAsset
				return true
			end
			
			return false
		end ))
	:expectDataWithinTime( 5, true )
	:dispose()
	
	-- Push error resolution results. (Probably need test permutations for various timings of this;
	-- before or after other client restore.)
	catalogSpec:expectHttpRequest( pushAlbumAssetReqResp( catalogId, newAssetIdForAlbumAsset, albumId, albumName, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 * waitMultiplier ) )
	
	-- Make sure that was the only update.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, 5 * waitMultiplier ) )

	-- Restore asset on other client
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevision = KSIdGenerator.uuidInLowercaseHex()
	
	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = restoredAssetId,
		revisionId = restoredAssetRevision,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
		userUpdated = userCreated,
		options = options,
	}
	
	-- Wait for update on restored asset
	
	local unknownNewAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local unknownNewAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			-- Other client restored asset.
		assetDeletedResponse( ozTrashAssetId, restoredAssetId, userId ),
			-- Oz deletes trash asset due to other client restore.
		albumAssetDeletedResponse( albumAssetId, userId, unknownNewAlbumAssetId --[[ transformer strips (new_id generated by Oz) ]] ),
			-- Oz deletes album asset,
			-- and we don't yet know the new album asset's id.
		albumAssetUpdateResponse( unknownNewAlbumAssetId --[[ transformer strips (created by Oz) ]], unknownNewAlbumAssetRevId --[[ transformer strips (created by Oz) ]], albumId, restoredAssetId )
			-- Oz creates a replacement for the deleted album asset,
			-- so we don't yet know its id or revision id.
	}, transformResponseWithUnknownNewIds )
			-- Strips the ids and timestamps we don't know at this point.
	, 'success' )
	
	-- Check that trash asset is now deleted
	rxTest:observe( catalog:docStore():ongoingDocValue( ozTrashAssetId )
		:pluck( '_empty' )
	)
	:expectDataWithinTime( 5, true )
	:dispose()
	
	local newAlbumAssetId = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, knownAlbumAssetIds, 'album_asset' )

	-- Make sure album asset is updated too.
	rxTest:observe( catalog:docStore():ongoingDocValue( newAlbumAssetId )
		:pluck( 'asset', 'id' )
		:map( function( assetIdForAlbumAsset )
			return assetIdForAlbumAsset and assetIdForAlbumAsset == restoredAssetId
		end ))
	:expectDataWithinTime( 5, true )
	:dispose()

end

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

function WFCatTrashTest.rxTest_CT_MockAddToAlbum_OtherDelete_pullFirst( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockAddToAlbum_OtherDelete_pushFirst( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { mock = true, pushFirstAfterUnpause = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzAddToAlbum_OtherDelete_pullFirst_testNightly( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, {} )

end

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

function WFCatTrashTest.rxTest_CT_OzAddToAlbum_OtherDelete_pushFirst_testNightly( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { pushFirstAfterUnpause = true } )

end

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

function WFCatTrashTest.rxTest_CT_Delete_OtherModify( rxTest )

-- Scenario: User adds an image, lets it sync, pauses sync,
-- on another online client modify the asset,
-- back on the original client that is offline delete the asset,
-- turn on sync.

-- Verifies the other client's modification are synced down and applied to items in the trash.

-- Checks if we receive modify from other before we push our delete.
-- Checks if we receive delete from other after we push our delete.

	rxTest:expectEqual( false, failStubbedTests )
end

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

function WFCatTrashTest.rxTest_CT_Delete_OtherModify_SyncRestore( rxTest )

-- Scenario

-- Verifies the other client's modification are synced down and restored by
-- this client.

-- Checks if we receive modify from other before we push our delete.
-- Checks if we receive delete from other after we push our delete.

	rxTest:expectEqual( false, failStubbedTests )
end

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

local function testAddSyncModifyOtherDeleteSync( rxTest, options )

	-- NMB-9394

	-- Verifies this client's modification are transfered from the original
	-- asset to the trash asset when a tombstone and restore is received from
	-- another client.

	-- Scenario:
	-- 1 Pauses Sync
	-- 2 Modify a photo (i.e. change tint to green)
	-- 3 On another client that is online deletes the asset.
	-- 4 Using original client turns on sync
	-- Expected results: The modifications to the asset appears correctly in the trash.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

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

	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

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

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

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

	local userUpdated = KSDate.timeInIso8601Format()

	-- 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' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, userUpdated )
	else
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = assetId,
				userUpdated = userUpdated,
				develop = {
					processingModel = "lightroom",
					xmpCameraRaw = kApplyTintXmp
				}
			}
			:map( function( revId )
				revisionId = revId
				return revId
			end )
		, ':ignore:' )
	end

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

	-- Send an update with a delete asset without our docstore knowing about it
	-- so it appears as if a different client issued the delete.  Our code
	-- doesn't currently look at deleted_by so it shouldn't be any different
	-- that this client did it instead of an other client.

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

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

	if options.sendUpdateBeforeTombstone then

		-- Send an update to original asset with the modifications that were made before the delete
		-- this update will fail with an error 400.

		-- The plan is to ignore the error 400 on the master, when the
		-- tombstone is received we will copy the localOnly file links
		-- for the asset's binaries (xmpDevelop, xmpSidecar) at that point.
		-- The uploader should still see the binaries as something that needs
		-- uploading.

		-- Note: album_asset, comment, and favorite will be handled by their own
		-- error 410 handling.

		catalogSpec:expectHttpRequest( pushUpdateAssetReqResp( catalogId, assetId, revisionId, subtype, options.xmpDevelop, kApplyTintXmp ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		local doesNotExistErrorUpdate = {
			errors = {
				asset = {
					"does not exist",
				},
			},
			id = "c7e348dad04be59f450a47a3bfeb992e",
			source = {
				action = "update",
				id = assetId,
				revision_id = revisionId,
				type = "asset",
			},
			status = 400,
			subtype = "InputValidationError",
			type = "error",
			updated = '2019-01-02T01:02:03Z',
		}

		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			doesNotExistErrorUpdate,
			assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			}, function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

				mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
				local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
				local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
				if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
					return {}, {} -- don't compare partial updates
				end

				-- Strip the _id off the error document as it will be new and unknown.
				stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, 2, 'id' )

				return mockDataAsTable, ozDataAsTable

			end )
		, 'success' )

	elseif options.splitTombstoneAndUpdate then

		-- This case will result in the conflict resolver having to resolve without
		-- doing a three way merge.  We should still end up with the modified results
		-- as the userUpdated will be newer than the otherClient's.

		-- Pull the tombstone from the other client.

		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			} )
		, 'success' )

		-- Pull the update from the other client.
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			} )
		, 'success' )

		-- Conflict is expected when asset tombstone is received first and separate from the trash asset update from Oz.
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_conflict' ), true )

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

	else

		-- Pull the update and tombstone from the other client.

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

	end

	-- WFCatMetadataPuller should be copying the modified document to the
	-- trash document when we see the tombstone.

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local trashAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, trashAssetRevId )

				-- Update trashAssetRevId the handling of assetDeletedResponse will cause an overlay to be
			-- applied to the trash asset with the modifications made to the original asset.
			trashAssetRevId = docValue._revId

			return trashAssetRevIdOnOz, localRevisionIsOnOz
		end )
	, true, false )


	-- Check to see that the trash asset has the tint modification or external develop settings that was applied earlier.
	local function haveDevelopSettings( developSettings )
		return developSettings ~= nil or 'develop settings missing'
	end

	if options.xmpDevelop then

		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
			:pluck( 'develop', 'xmpCameraRaw', 'sha256' )
			:map( haveDevelopSettings )
		 )
		:expectDataWithinTime( 1, true )
		:dispose()

	else

		rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
			:pluck( 'develop', 'xmpCameraRaw' )
			:map( haveDevelopSettings )
		)
		:expectDataWithinTime( 1, true )
		:dispose()

	end


	-- Verify we send up a trash asset update to include the modifications that were made before the delete.
	catalogSpec:expectHttpRequest( pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, options.xmpDevelop, kApplyTintXmp ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the updated trash asset back from oz, it should have the develop settings modifications we made.

	local developSettings = options.xmpDevelop and {
		processingModel = "lightroom",
		xmpCameraRaw = {
			sha256 = WFCatTrashTestUtils.xmpDevelopSha256,
		},
	} or {
		processingModel = "lightroom",
		xmpCameraRaw = kApplyTintXmp
	}

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, false, options.xmpSidecar, developSettings ),
		},
		function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

			if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
				return {}, {} -- don't compare partial updates
			end

			if options.xmpDevelop then
				-- nil out invalid flag on renditions as we haven't uploaded the xmpDevelop settings file yet.
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/1280', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/2048', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/640', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/thumbnail2x', 'invalid' )
			end

			return mockDataAsTable, ozDataAsTable

		end )
	, 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local trashAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, trashAssetRevId )

				-- Update trashAssetRevId as trashAssetUpdateResponse will have added a new revision.
			trashAssetRevId = docValue._revId

			return trashAssetRevIdOnOz and localRevisionIsOnOz
		end )
	, true )

	-- [TODO: iOS would not preserve xmp file at the moment if the delete is from other devices]
	if options.xmpDevelop and not WFPlatformInfo.isiOS then

		-- Verify we send up the modified xmp file to the trash asset.
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, trashAssetId, trashAssetRevId, xmpDevelopFilePath ) )

		-- Wait for the upload xmp file to complete, (it will upload after the xmp_develop if there is one.
		uploader:startContinuousUploads()

		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ) )
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		-- Make sure we get an update of the asset without invalid = true on the renditions.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, true, options.xmpSidecar, developSettings ),
			} )
		, 'success' )

	end

	-- make sure the binaries are there
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )

			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local originalExists = originalPath and WFFileUtils.exists( originalPath ) or 'original file missing'

			local xmpDevelopExists = 'file'
			if options.xmpDevelop and not WFPlatformInfo.isiOS then
				local xmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )
				if WFPlatformInfo.isAndroid then
					-- android stores xmp develop path as a relative path, fix that before making sure it exists
					xmpDevelopFilePath = child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), xmpDevelopFilePath )
				end
				xmpDevelopExists = xmpDevelopFilePath and WFFileUtils.exists( xmpDevelopFilePath ) or 'xmpDevelop file missing'
			end

			return originalExists, xmpDevelopExists

		end ),
	'file', 'file' )

end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDelete_testNightly( rxTest )
	-- NMB-9394
	testAddSyncModifyOtherDeleteSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDelete( rxTest )
	-- NMB-9394
	testAddSyncModifyOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDelete_splitTombstone( rxTest )
	-- LRMI-4193390
	testAddSyncModifyOtherDeleteSync( rxTest, { mock = true, splitTombstoneAndUpdate = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDelete_xmpDev_testNightly( rxTest )
	-- NMB-9394
	-- This verison uses external develop settings to make sure the file is properly preserved.
	testAddSyncModifyOtherDeleteSync( rxTest, { xmpDevelop = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDelete_xmpDev( rxTest )
	-- NMB-9394
	-- This verison uses external develop settings to make sure the file is properly preserved.
	testAddSyncModifyOtherDeleteSync( rxTest, { mock = true, xmpDevelop = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDelete_400_testNightly( rxTest )
	-- NMB-9394
	-- This verison issues the update for the modified asset before receiving
	-- the tombstone and will encounter a 400 error.
	testAddSyncModifyOtherDeleteSync( rxTest, { sendUpdateBeforeTombstone = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDelete_400( rxTest )
	-- NMB-9394
	-- This verison issues the update for the modified asset before receiving
	-- the tombstone and will encounter a 400 error.
	testAddSyncModifyOtherDeleteSync( rxTest, { sendUpdateBeforeTombstone = true, mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDelete_xmpDev_400_testNightly( rxTest )
	-- NMB-9394
	-- This verison issues the update for the modified asset before receiving
	-- the tombstone and will encounter a 400 error.
	testAddSyncModifyOtherDeleteSync( rxTest, { sendUpdateBeforeTombstone = true, xmpDevelop = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDelete_xmpDev_400( rxTest )
	-- NMB-9394
	-- This verison issues the update for the modified asset before receiving
	-- the tombstone and will encounter a 400 error.
	testAddSyncModifyOtherDeleteSync( rxTest, { sendUpdateBeforeTombstone = true, mock = true, xmpDevelop = true,  } )
end

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

local function testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, options )

	-- NMB-10868

	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- Scenario:
	-- 1 Pauses Sync
	-- 2 Modify a photo (i.e. change tint to green)
	-- 3 On another client that is online deletes the asset.
	-- 4 Still on other client restore the asset
	-- 5 Using original client turns on sync
	-- Expected results: The modifications to the asset appears correctly in the trash.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, _ = 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'

	-- set the modification dates so that the other userUpdated value is before this user.
	local initUserUpdated = KSDate.timeInIso8601Format()
	local userUpdated = KSDate.shiftIso8601DateTime( initUserUpdated, 5 )

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

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

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

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

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

	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

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

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

	-- 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' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	else
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = assetId,
				userUpdated = userUpdated,
				develop = {
					processingModel = "lightroom",
					xmpCameraRaw = kApplyTintXmp
				}
			}
			:map( function( revId )
				revisionId = revId
				return revId
			end )
		, ':ignore:' )
	end


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

	-- Send an update with a delete asset without our docstore knowing about it
	-- so it appears as if a different client issued the delete.  Our code
	-- doesn't currently look at deleted_by so it shouldn't be any different
	-- that this client did it instead of an other client.

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( userCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	-- Send create restored asset from original asset
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashAssetRevId, restoredAssetId, restoredAssetRevId ) )

	local sendUpdateUrl = "/v2/catalogs/" .. catalogId .."/updates"

	local deleteRestoreAssetUpdate = {
		{
			type = 'asset',
			action = 'delete',
			revision_id = revisionId,
			id = assetId,
			deleted_asset = {
				id = trashAssetId,
				payload = {
					userCreated = userCreated,
					purgeDate = trashPurgeDate,
				},
				revision_id = trashAssetRevId
			}
		},
		{
			action = "create",
			type = "asset",
			subtype = "image",
			restore_from = {
				id = trashAssetId
			},
			revision_id = restoredAssetRevId,
			id = restoredAssetId,
			payload = {
				userUpdated = "2019-01-08T08:08:08.008Z"
			},
		}
	}

	local sendUpdateOptions = {
		requestMethod = 'POST',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
			{ header = 'Accept', value = 'application/json' },
		},
	}

	rxTest:expectSingleValue(
		catalogSpec:put( sendUpdateUrl, KSJsonEncoder.encodeOzUploadJson( deleteRestoreAssetUpdate ), sendUpdateOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '202' )

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

	if options.sendUpdateBeforeTombstone then

		-- Send an update to original asset with the modifications that were made before the delete
		-- this update will fail with an error 410.

		-- The plan is to ignore the error 410 on the master, when the
		-- tombstone is received we will copy the localOnly file links
		-- for the asset's binaries (xmpDevelop, xmpSidecar) at that point.
		-- The uploader should still see the binaries as something that needs
		-- uploading.

		-- Note: album_asset, comment, and favorite will be handled by their own
		-- error 400 handling.

		catalogSpec:expectHttpRequest( pushUpdateAssetReqResp( catalogId, assetId, revisionId, subtype, options.xmpDevelop, kApplyTintXmp ) )
			
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	end

	if not options.splitResponse then

		-- Make sure renditions are generated for the restored asset
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredAssetRevId, subtype ), true )
	
		-- NMB-11430
		-- Wait for the delete and restore to both be processed, when this happens we will have
		-- one asset update and two asset delete's.  This is done as the metadatapuller can end
		-- up getting two asset updates  (one subtype = 'image', one subtype = 'deleted_image')
		-- and a delete.  We may have an error document in here as well if we did the push above
		-- but that shouldn't affect the delete and asset update counts.
		-- This response is actually abbreviated as it doesn't contain base or links but that shouldn't matter.
		local expectedResponse = {
			updates =  {
				restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
				assetDeletedResponse( assetId, trashAssetId, userId ),
				assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			}
		}
	
		rxTest:observe( pollHttpGetMetadataUpdates( catalogSpec, catalog:docStore(), '200', expectedResponse )
			:map( function( statusCode, response )
	
				local numDeletes = 0
				local numUpdates = 0
				local updates = response.updates or {}
				for _, update in ipairs( updates ) do
					if update.type == 'asset' then
						if update.deleted then
							numDeletes = numDeletes + 1
						else
							numUpdates = numUpdates + 1
						end
					end
				end
	
				return numDeletes, numUpdates
			end ) )
		:expectDataWithinTime( 5 * waitMultiplier, 2, 1 )
		:dispose()
	
		-- Wait for oz to generate the renditions on the restored asset before requesting next update.
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredAssetRevId, subtype ), true )

	end

	if options.sendUpdateBeforeTombstone then

		local doesNotExistErrorUpdate = {
			errors = {
				asset = {
					"does not exist",
				},
			},
			id = "c7e348dad04be59f450a47a3bfeb992e",
			source = {
				action = "update",
				id = assetId,
				revision_id = revisionId,
				type = "asset",
			},
			status = 400,
			subtype = "InputValidationError",
			type = "error",
			updated = '2019-01-02T01:02:03Z',
		}

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
				doesNotExistErrorUpdate,
				assetDeletedResponse( assetId, trashAssetId, userId ), -- see comment in block below
				assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			}, function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

				mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
				local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
				local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
				if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
					return {}, {} -- don't compare partial updates
				end

				-- Strip the _id off the error document as it will be new and unknown.
				stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, 2, 'id' )

				return mockDataAsTable, ozDataAsTable

			end )
		, 'success' )

	elseif options.splitResponse then

		-- Pretend like the updates got split across the update limit bounddry so we
		-- get the delete asset in one update, and the delete and restore in another.
		-- This is a mock only only as we can't force oz to split the updates.

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ),
			} )
		, 'success' )

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated } 
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			-- Make the second response a little bit contrived as well by placing the restoredAssetUpdate
			-- after the delete as oz doesn't guarantee order.
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, userCreateUpdate, userCreated, importSource, assetId, true, true ),
			} )
		, 'success' )

	elseif options.splitResponse2 then

		-- Pretend like the updates got split across the update limit bounddry so we
		-- get the delete asset in one update, and the delete and restore in another.
		-- This is a mock only only as we can't force oz to split the updates.

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			} )
		, 'success' )

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated } 
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreateUpdate, importSource, assetId, true, true ),
			} )
		, 'success' )

	elseif options.splitResponse3 then

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( assetId, trashAssetId, userId ),
			} )
		, 'success' )

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			} )
		, 'success' )

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated } 
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreateUpdate, importSource, assetId, true, true ),
			} )
		, 'success' )

	else

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			-- MetadataPuller will see these two tombstones and update the restored asset with the
			-- modifications that were applied to assetId. If they get split because of batch
			-- size crosssing 100 item limit and it only sees one tombstone, it will update
			-- the trash asset.  And the following update will see it needs to move the
			-- modification from the trash asset to the restored asset.
			assetDeletedResponse( assetId, trashAssetId, userId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			} )
		, 'success' )

	end

	-- Pull the tombstone from the other client.
	-- WFCatMetadataPuller should be copying the modified document to the
	-- restored document when we see the tombstone.

	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local restoredAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, restoredAssetRevId )

			-- Update restoredAssetRevId the handling of assetDeletedResponse will cause an overlay to be
			-- applied to the restored asset with the modifications made to the original asset.
			restoredAssetRevId = docValue._revId

			return restoredAssetRevIdOnOz, localRevisionIsOnOz
		end )
	, true, false )

	-- Check to see that the restored asset has the tint modification or external develop settings that was applied earlier.
	local function haveDevelopSettings( developSettings )
		return developSettings ~= nil or 'develop settings missing'
	end

	if options.xmpDevelop then

		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( 'develop', 'xmpCameraRaw', 'sha256' )
			:map( haveDevelopSettings )
		 )
		:expectDataWithinTime( 1, true )
		:dispose()

	else

		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( 'develop', 'xmpCameraRaw' )
			:map( haveDevelopSettings )
		)
		:expectDataWithinTime( 1, true )
		:dispose()

	end

	-- Verify we send up a restored asset update to include the modifications that were made before the delete.
	catalogSpec:expectHttpRequest( pushUpdateRestoredReqResp( catalogId, restoredAssetId, restoredAssetRevId, subtype, options.xmpDevelop, kApplyTintXmp ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the updated restored asset back from oz, it should have the develop settings modifications we made.

	local developSettings = options.xmpDevelop and {
		processingModel = "lightroom",
		xmpCameraRaw = {
			sha256 = WFCatTrashTestUtils.xmpDevelopSha256,
		},
	} or {
		processingModel = "lightroom",
		xmpCameraRaw = kApplyTintXmp
	}

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

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, false, true, developSettings ),
		},
		function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

			if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
				return {}, {} -- don't compare partial updates
			end

			if options.xmpDevelop then
				-- nil out invalid flag on renditions as we haven't uploaded the xmpDevelop settings file yet.
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/1280', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/2048', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/640', 'invalid' )
				KSTableUtils.setNested( ozUpdateTable, nil, 1,  'links', '/rels/rendition_type/thumbnail2x', 'invalid' )
			end

			return mockDataAsTable, ozDataAsTable

		end )
	, 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local restoredAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, restoredAssetRevId )

				-- Update restoredAssetRevId as restoredAssetUpdateResponse will have added a new revision.
			restoredAssetRevId = docValue._revId

			return restoredAssetRevIdOnOz, localRevisionIsOnOz
		end )
	, true, true )

	-- [TODO: iOS would not preserve xmp file at the moment if the delete is from other devices]
	if options.xmpDevelop and not WFPlatformInfo.isiOS then

		-- Verify we send up the modified xmp file to the restored asset.
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredAssetRevId, xmpDevelopFilePath ) )

		-- Wait for the upload xmp file to complete, (it will upload after the xmp_develop if there is one.
		uploader:startContinuousUploads()

		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ) )
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

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

		-- Make sure we get an update of the asset without invalid = true on the renditions.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, true, true, developSettings ),
			} )
		, 'success' )

	end

	-- make sure the binaries are there
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )

			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local originalExists = originalPath and WFFileUtils.exists( originalPath ) or 'original file missing'

			local xmpDevelopExists = 'file'
			if options.xmpDevelop and not WFPlatformInfo.isiOS then
				local xmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )
				
				if WFPlatformInfo.isAndroid then
					-- android stores xmp develop path as a relative path, fix that before making sure it exists
					xmpDevelopFilePath = child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), xmpDevelopFilePath )
				end
				
				xmpDevelopExists = xmpDevelopFilePath and WFFileUtils.exists( xmpDevelopFilePath ) or 'xmpDevelop file missing'
			end

			return originalExists, xmpDevelopExists

		end ),
	'file', 'file' )

end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDeleteOtherRestore_testNightly( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { } )

end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDeleteOtherRestore( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

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

end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse2( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitResponse2 = true } )

end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse3( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitResponse3 = true } )

end

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

function WFCatTrashTest.rxTest_CT_OzModifyOtherDeleteOtherRestore_xmp_testNightly( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- This version tests with external develop settings.
	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { xmpDevelop = true,  } )
end

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

function WFCatTrashTest.rxTest_CT_MockModifyOtherDeleteOtherRestore_xmp( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- This version tests with external develop settings.
	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, xmpDevelop = true,  } )
end

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

function WFCatTrashTest.rxTest_CT_MockAddSyncOtherDeleteRestoreSync_SplitResponse( rxTest )
	
	-- WF-1802 Stuck sync
	-- https://jira.corp.adobe.com/browse/WF-1802
	-- This bug is caused by split sync feed from Oz
	-- 1. Add asset and sync. Then go offline
	-- 2. Other device delete and restore the asset
	-- 3. Goes back online and get split sync feed from Oz
	--		- A deletion with new_id B
	--		- B deletion with new_id C and C docValue
	-- This would cause puller falsely create a local C with subtype deleted_image
	
	local options = { mock = true }
	
	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

	local trashAssetSubtype = 'deleted_' .. subtype

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

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

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

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

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

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

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

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

	-- 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: Receive Delete and restore action by other device from Oz sync feed
		
	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredRevId = KSIdGenerator.uuidInLowercaseHex()
	
	-- split resposne with first A deletion

	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId )
	} ), 'success' )

	-- Make sure the asset is deleted
	-- Trash doc is created without type and subtype, only _localOnly files
	
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )
	
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( tempLocalTrashDocValue )
		
			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( tempLocalTrashDocValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return tempLocalTrashDocValue.type == nil
				and tempLocalTrashDocValue.subtype == nil
				and haveOriginal
		end ),
	true )

	-- split response with second B deletion and C creation

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

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

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


--==============================================================================
-- @mark tests that involve other client actions that conflict with local actions
--==============================================================================

-- These tests are ultimately testing WFCatErrorHandler's resourceNotFoundHandler.
--
-- We always attempt to have just one asset (to avoid generating duplicates).
--
-- Expected result is to have asset of type indicated by local action on the left.
-- In some cases we have to directly create assets using whatever binaries are
-- available locally.
--
-- In addition we attempt to preserve local modifications if they exist.
--
-- Below is a table of conflicting actions showing jira story number if test exists.
--
-- XXXXXXXX indicates the other client can't do that action as both clients must
-- start with the same asset. - indicates a test that needs to be written.
--
---------------------------------Other Client-----------------------------------
--
--                          Delete------ Restore----- DelTrash---- Delete/Restore Delete/DelTrash Restore/Delete Restore/Delete/DelTrash Restore/Delete/Restore Delete/Restore/Delete Delete/Restore/Delete/DelTrash
-- T    Delete              NMB-9390     XXXXXXXX     XXXXXXXX     NMB-13896 #3   NMB-13896 #7    XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
-- H    Restore             XXXXXXXX     NMB-10313    NMB-9395     XXXXXXXX       XXXXXXXX        NMB-13896 #6   -                       -                      XXXXXXXX              XXXXXXXX
-- I    DelTrash            XXXXXXXX     -            -            XXXXXXXX       XXXXXXXX        -              -                       -                      XXXXXXXX              XXXXXXXX
-- S    Delete/Restore      NMB-13896 #4 XXXXXXXX     XXXXXXXX     NMB-13896 #5   NMB-14104       XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
--      Delete/DelTrash     -            XXXXXXXX     XXXXXXXX     -              -               XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
-- C    Restore/Delete      XXXXXXXX     NMB-13896 #1 NMB-13896 #8 XXXXXXXX       XXXXXXXX        NMB-13896 #2   -                       -                      XXXXXXXX              XXXXXXXX
-- L
-- I    Restore/Delete/DelTrash (should be same as DelTrash)
-- E    Restore/Delete/Restore (should be same as Restore)
-- N    Delete/Restore/Delete (should be same as Delete)
-- T    Delete/Restore/Delete/DelTrash (should be same as Delete/DelTrash)
--

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

local function testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, options )

-- NMB-13896 #3 / NMB-14704
-- Scenario:
-- Steps:
--		1) Add photo, Sync.
--		2) Pause Sync.
--      3) Other Client Delete, Other Client Restore.
--		4) Delete the photo.
-- 		5) Resume Sync.
--
-- Expected: Should end up with the photo in the trash.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- 1) Add photo, Sync.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

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

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

	local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

		stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'payload', 'location' )

		return mockDataAsTable, ozDataAsTable

	end

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- 2) Pause Sync. - this test issues separate sync actions so need to pause.

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

-- 3) Other Client Delete, Other Client Restore.
	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userUpdated = userCreated,
		options = options,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, options ), true )
	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- 4) Delete the photo.

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

-- 5) Resume Sync.

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		assetDeletedResponse( otherClientTrashAssetId, otherClientRestoredAssetId, userId ),
		restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
	}, stripUpdatesAndLinks ), 'success' )

	-- The error handler should update the trash asset createFrom info to refer to the
	-- otherClientTrashAsset unfortunately this will require it to refer to an intermediate
	-- restored asset id as Oz doesn't allow making one trash asset from another.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientTrashAssetId )
	:dispose()

	-- Get the intermediate asset's id and revId created by WFCatErrorHandler
	local intermediateId, intermediateRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( localTrashDocValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( localTrashDocValue )
			intermediateId = createFromInfo.lastAssetId
			intermediateRevId = createFromInfo.lastRevisionId
			return type( intermediateId ), type( intermediateRevId )
		end )
	, 'string', 'string' )

	-- Since the subtypes are the same at this point the pusher will generate a restore/delete combo
	-- and since the otherClientTrashAssetId was deleted when making otherClientRestoredAssetId this
	-- restore request will also generate a resource not found error.
	catalogSpec:expectHttpRequest( pushRestoreDeleteAssetReqResp( catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, intermediateId, intermediateRevId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )

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

	else

		-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
		}, stripUpdatesAndLinks ), 'success' )
	
		-- Wait for the errors to be processed.
		rxTest:observe( pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
		:expectDataWithinTime( 5 * waitMultiplier, 3, false )
		:dispose()

	end

	-- Make sure trashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
	:map( function( docValue )
		return docValue._deleted == nil and docValue._empty == nil
	end ), true )

	-- The error handler should update the trash asset createFrom info one more time to refer
	-- to the otherClientRestoreAsset.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientRestoredAssetId )
	:dispose()

	-- Send up the delete request this time against the other client's restored asset id.
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Success! Get the trash asset and tombstone for otherClientRestoredAssetId back in the sync feed.
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			assetDeletedResponse( otherClientRestoredAssetId, trashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
	    }, stripAssetAndLocation ), 'success' )

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

	else

		-- Success! Get the trash asset and tombstone for otherClientRestoredAssetId back in the sync feed.
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			assetDeletedResponse( otherClientRestoredAssetId, trashAssetId, userId ),
	    }, stripAssetAndLocation ), 'success' )

	end

	-- Make sure trashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
	:map( function( docValue )
		return docValue._deleted == nil and docValue._empty == nil
	end ), true )	

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredSecondPartOfTrashPair_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestoreDeleted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherRestore( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherRestore_testNightly( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherRestore_splitErrors( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitErrors = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherRestore_splitErrors_testNightly( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { splitErrors = true } )
end

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

local function testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, options )

-- NMB-13896 #6 / NMB-14708
-- Scenario:
-- Steps:
--		1) Add photo, Sync.
--		2) Delete the photo.
--		3) Sync, Pause.
--      4) Other Client Restore
--		5) Other Client Delete.
-- 		6) Restore the photo.
--		7) Resume Sync.
--
-- Expected: Photo should be restored

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- 1) Add photo, Sync.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

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

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

	local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

		stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'payload', 'location' )

		return mockDataAsTable, ozDataAsTable

	end

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- 2) Delete the photo.

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

-- 3) Sync, Pause.

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

-- 4) Other Client Restore

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}

-- 5) Other Client Delete.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = otherClientTrashPurgeDate,
		options = options,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- 6) Restore the photo.

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

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

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

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

-- 7) Resume Sync.

	-- Send up the Restore request
	catalogSpec:expectHttpRequest( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, restoredDocValue.userUpdated ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( trashAssetId, otherClientRestoredAssetId, userId ),
		assetDeletedResponse( otherClientRestoredAssetId, otherClientTrashAssetId, userId ),
		trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_newId'), nil )

	-- Get the errors with respect to what we tried to copy from as they are no longer there.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
	}, stripUpdatesAndLinks ), 'success' )

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

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( WFCatTrashUtils.getCreateFromId )
	, otherClientRestoredAssetId )

	-- Get the intermediate asset's id and revId created by WFCatErrorHandler
	local intermediateId, intermediateRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( docValue )
			intermediateId = createFromInfo.lastAssetId
			intermediateRevId = createFromInfo.lastRevisionId
			return type( intermediateId ), type( intermediateRevId ), intermediateId ~= createFromInfo.id
		end )
	, 'string', 'string', true )

	-- Verify the intermediateId has _newId updated with restoredAssetId.
	rxTest:expectSingleValue( catalog:docStore():docValue( intermediateId ):pluck( '_newId' ), restoredAssetId )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_empty'), true )

	-- Other Client Trash asset should still be there.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId ):pluck('_empty'), nil )

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, intermediateId, intermediateRevId, restoredAssetId, restoredRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- When issuing delete/restore pair oz typically sends the first resourceNotFoundError
		-- quickly and the second a minute later so make sure we handle this more common path.

		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )

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

	else

		-- Get both of the errors related to trying to delete from the otherClientRestoredAssetId
		-- which no longer exists either.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", intermediateId, "deleted_asset", nil, userId ),
		}, stripUpdatesAndLinks ), 'success' )

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

	end

	-- Verify we will now try to restore from the otherClientTrashAssetId.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( WFCatTrashUtils.getCreateFromId )
	, otherClientTrashAssetId )

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, otherClientTrashAssetId, otherClientTrashAssetRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Get the updates from the sync feed showing the photo has been restored.
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( otherClientTrashAssetId, restoredAssetId, userId ),
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, otherClientRestoredAssetId, true, true ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", intermediateId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

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

	else

		-- Get the updates from the sync feed showing the photo has been restored.
		rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( otherClientTrashAssetId, restoredAssetId, userId ),
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, otherClientRestoredAssetId, true, true ),
		}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	end

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				Ignored_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						otherClientRestoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestore = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestoreOtherDelete( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherRestoreOtherDelete_testNightly( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestoreOtherDelete_splitErrors( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherRestoreOtherDelete_splitErrors_testNightly( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest,  { splitErrors = true } )
end

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

local function testAddSyncRestoreOtherDeleteTrashSync( rxTest, options )

-- NMB-9395
-- This tests the two client scenario where one client attempts to restore an 
-- asset from the trash while another client has permanently deleted that asset 
-- from the trash. In this case, the client attempting to restore the asset 
-- should instead create the new asset from scratch instead of asking Oz to restore
-- the trash asset

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	
	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )
	
-- Step 1: Add an image asset
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ), 	
		':ignore:' )
	end
	

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }
	
	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1
		
		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()
		
		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1
		
		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()
		
		-- Wait for the uploader to issue the create master request
		rxTest:observe( pollUploadCount( uploader, 'master' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()
	
	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = true } ), true )

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()
	
-- Step 1.5: Maybe remove some of the local binaries for the asset to test different restore scenarios
	if options.dropLocalMaster then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId ) ),
			':ignore:' )
	end

	if options.dropLocalProxy then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId ) ),
			':ignore:' )
	end
	
-- Step 2: Move the asset to trash locally and send that to Oz

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )
	
	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:tap( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
		end )
	, ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	local waitForRenditions = not options.noWaitForRenditions
	
	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, stripAssetAndLocation ), 'success' )

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

-- Step 3: On a second client, remove the asset from the trash on that second client
-- so that it's totally gone on Oz

	simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- Step 4: Attempt to restore the asset 

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )
	
	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )
	
	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure 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( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, restoredDocValue.userUpdated ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )
	
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetDeletedResponse( trashAssetId, userId ),
		resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
	}, transformOrderAndStripAssetUpdate( false )), 'success' )
	
-- Step 5: The error handler should process the RNFE and turn the restore into a proper asset create
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	if options.dropLocalMaster and not options.dropLocalProxy then
		importSource.willNotUploadMaster = true
	end

	-- pull from Oz to get the asset create response
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, 
								nil --[[developSettings]], true --[[alwaysHaveLocationData]] )
		}, stripAssetAndLocation ), 'success' )
		
	-- validate the restored asset is on oz
	local isRestoredDocOnOz = rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
		:map( function( docValue )
			local receivedUpdateFromOz = docValue._serverProvidedMetadata ~= nil
			
			-- stash the latest oz revisionId so we can validate the upload
			local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
			if revisionIds then
				restoredRevId = revisionIds[ #revisionIds ]
			end
			
			return receivedUpdateFromOz
		end ) )
	isRestoredDocOnOz:expectDataWithinTime( 5 * waitMultiplier, true )
	
-- Step 6: The restored asset gets its binaries uploaded (if applicable)
	
	if not ( options.dropLocalMaster and options.dropLocalProxy ) then
		-- we should have some binaries so validate that they're uploaded
		
		-- Make sure the restored asset has the expected set of local files
		rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
			:map( function( docValue )
				local hasBinary = false
				if options.proxyOnlyAsset or options.dropLocalMaster then
					local proxyPath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
					hasBinary = proxyPath and WFFileUtils.exists( proxyPath ) == 'file'
				else
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
					hasBinary = originalPath and WFFileUtils.exists( originalPath ) == 'file'
				end
				
				return hasBinary
			end ),
		true )
		
		restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
		
		if options.proxyOnlyAsset or options.dropLocalMaster then
			expectedProxyUploadCount = expectedProxyUploadCount + 1
			
			-- Upload the proxy (and generate renditions)
			catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, restoredAssetId, restoredRevId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
			uploader:startContinuousUploads()
			
			-- wait for the uploader to issue the create proxy request
			rxTest:observe( pollUploadCount( uploader, 'proxy' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
			:dispose()
		else
			expectedOriginalUploadCount = expectedOriginalUploadCount + 1
			
			-- Upload the master
			catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, restoredAssetId, restoredRevId, filePath ) )
			uploader:startContinuousUploads()
			
			-- Wait for the uploader to issue the create master request
			rxTest:observe( pollUploadCount( uploader, 'master' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
			:dispose()
		end

		uploader:stopContinuousUploads()

		-- Get the updated metadata with the binary link.
		local expectMasterLink = not ( options.dropLocalMaster or options.proxyOnlyAsset )
		local expectProxyAndRenditionLinks = not options.dropLocalProxy

		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
									{ master = expectMasterLink, proxy = expectProxyAndRenditionLinks, renditions = expectProxyAndRenditionLinks },
									nil --[[developSettings]], true --[[alwaysHaveLocationData]] )
			}, stripAssetAndLocation ), 'success' )

	end

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

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

end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherDelTrash_Master( rxTest )
-- NMB-9395	
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherDelTrash_Master_testNightly( rxTest )
-- NMB-9395
	testAddSyncRestoreOtherDeleteTrashSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherDelTrash_NoMaster( rxTest )
-- NMB-9395
-- Expected result: The asset was a Lightroom style asset and will be restored 
-- as a Lightroom style asset

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case?
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { mock = true, proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherDelTrash_NoMaster_testNightly( rxTest )
-- NMB-9395
-- Expected result: The asset will be restored as a Lightroom style asset

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case?
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherDelTrash_NoLocalMaster( rxTest )
-- NMB-9395
-- Expected result: The asset had a master, but not locally and it's no longer on Oz 
-- so the asset will be restored as a Lightroom style asset

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case?
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { mock = true, dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherDelTrash_NoLocalMaster_testNightly( rxTest )
-- NMB-9395
-- Expected result: The asset had a master, but not locally and it's no longer on Oz 
-- so the asset will be restored as a Lightroom style asset

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case?
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherDelTrash_NoLocalBinaries( rxTest )
-- NMB-9395
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but would show up on Lr Web as an incomplete
-- asset.

-- I am thinking it should show up as a bad asset in the LrCC client that issued the 
-- restore - showing this will likely require a change as we currently don't show an asset that doesn't
-- have a master/proxy).

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case? 

-- expect to encounter and handle ResourceNotFoundError
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { mock = true, dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzRestore_OtherDelTrash_NoLocalBinaries_testNightly( rxTest )
-- NMB-9395
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but I am thinking it should show
-- up as a bad asset in the LrCC client that issued the restore - showing this
-- will likely require a change as we currently don't show an asset that doesn't
-- have a master/proxy).

-- TODO [kcarlson 2018-10-17]: Should a warning be displayed in this case? 

-- expect to encounter and handle ResourceNotFoundError
	testAddSyncRestoreOtherDeleteTrashSync( rxTest, { dropLocalMaster = true, dropLocalProxy = true } )
end

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

local function testAddPush_ModifyOtherDel_ResourceExists( rxTest, options )

-- NMB-18214 #1

-- Step 1: Add an image asset
-- Step 2. push, pause sync
-- Step 3. OtherDel
-- Step 4. modify, unpause sync
-- Step 5. push
-- Step 6. pull

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add an image asset
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end


-- Step 2. push

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

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

-- Step 3: On a second client, delete the asset.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, options ), true )

-- Original Client ------------------------------------------------------------

	local userUpdated = KSDate.timeInIso8601Format()

-- Step 4: modify the asset
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = assetId,
			userUpdated = userUpdated,
			develop = {
				processingModel = "lightroom",
				xmpCameraRaw = kApplyTintXmp
			}
		}
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

-- Step 5: push
	-- Verify we send up another create as we modified that asset and didn't pull an update for it yet.
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

-- Step 6 pull
	-- Get the trashed asset and the tombstone in the sync feed
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false, xmpDevelop = false } ),
		assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
    }, stripAssetAndLocation ), 'success' )

-- Step 7: The error handler should process the ResourceExistsError.
	if options.resourceExistsError then
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceExistsErrorUpdateResponse( "create", assetId, revisionId, "asset", catalogId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

		-- Check to see how the ResourceExistsError were resolved
		rxTest:expectEqual( {
			ResourceExistsError = {
				count = 1,
				resolution = {
					IgnoredRevisionDoesNotMatch = {
						count = 1,
						sourceDocIds = {
							assetId,
						},
					},
				},
			},
		}, errorHandler:DEBUG_errorsResolved() )

	end

	-- Since the update we performed was sent as a create and rejected we will attempt
	-- to send the update again now.
	catalogSpec:expectHttpRequest( pushUpdateTrashReqResp( catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, trashAssetSubtype, false, kApplyTintXmp ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Make sure there are no new unexpected conflicts.
	rxTest:expectEqual( {}, conflictResolver:DEBUG_conflictsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockAddPush_OtherDelModify_resourceExistsErr( rxTest )
-- NMB-18214 #1
	testAddPush_ModifyOtherDel_ResourceExists( rxTest, { mock = true, resourceExistsError = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzAddPush_OtherDelModify_resourceExistsErr_testNightly( rxTest )
-- NMB-18214 #1
	testAddPush_ModifyOtherDel_ResourceExists( rxTest, { resourceExistsError = true } )
end

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

local function testAddPushModifyPush_OtherDel_ResourceExists( rxTest, options )

-- NMB-18214 #2

-- Step 1: Add an image asset
-- Step 2. push
-- Step 3. modify the asset
-- Step 3.5. push a second create
-- Step 4. pull
-- Step 5. Other client Delete
-- Step 6. Process error doc
-- Step 7. pull delete

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	if WFPlatformInfo.isDesktop then
		options.deleteAssets = require 'WFCatDelete'.deleteAssets
	end

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add an image asset

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end


-- Step 2. push

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

	local userUpdated = KSDate.timeInIso8601Format()

-- Step 3: modify the asset

	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = assetId,
			userUpdated = userUpdated,
			develop = {
				processingModel = "lightroom",
				xmpCameraRaw = kApplyTintXmp
			}
		}
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

-- Step 3.5: push a second create

	-- Verify we send up another create as we modified that asset and didn't pull an update for it yet.
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

-- Step 4 pull

	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource ),
		resourceExistsErrorUpdateResponse( "create", assetId, revisionId, "asset", catalogId ),
	}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

-- Step 5: On a second client, delete the asset.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, options ), true )


-- Original Client ------------------------------------------------------------


-- Step 6. The error handler should process the ResourceExistsError.

	if options.resourceExistsError then

		-- The oz version doesn';t seem to check the mocks here so
		--- don't bother adding it to the expected http calls.
		if options.mock then

			-- resourceExistsError will do a get on the asset to see if it still
			-- exists.  It will come back as deleted but the docstore won't know
			-- about that yet.
			local httpRequest = WFMockHttpRequest()
				:get( '/v2/catalogs/' .. catalogId .. '/updates/' .. assetId )
				:respond( '200', KSJsonEncoder.encodeOzUploadJson( {
						base = '/v2/catalogs/' .. catalogId,
						deleted = true,
						deleted_by = '8c2ddb596616a98f1607b70b1b7a3c5c',
						id = assetId,
						new_id = otherClientTrashAssetId,
						type = 'asset',
						updated = KSDate.timeInIso8601Format(),
					}
				) )

			catalogSpec:expectHttpRequest( httpRequest )

		end

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

		-- Check to see how the ResourceExistsError were resolved
		rxTest:expectEqual( {
			ResourceExistsError = {
				count = 1,
				resolution = {
					deleted = {
						count = 1,
						sourceDocIds = {
							assetId,
						},
					},
				},
			},
		}, errorHandler:DEBUG_errorsResolved() )

	end

-- Step 7. pull delete

	-- Get the trashed asset and the tombstone in the sync feed
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false, xmpDevelop = false } ),
		assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
    }, stripAssetAndLocation ), 'success' )

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashTest.rxTest_CT_MockAddPushModifyPush_OtherDel_resourceExistsErr( rxTest )
-- NMB-18214 #2
	testAddPushModifyPush_OtherDel_ResourceExists( rxTest, { mock = true, resourceExistsError = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzAddPushModifyPush_OtherDel_resourceExistsErr_testNightly( rxTest )
-- NMB-18214 #2
	testAddPushModifyPush_OtherDel_ResourceExists( rxTest, { resourceExistsError = true } )
end

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

local function testAddSyncRestoreOtherRestoreSync( rxTest, options )

-- NMB-10313
-- This tests the two client restore scenario where two clients restore the same asset.
-- One of these will 'win' and get to Oz first and it's up to the other client to ensure 
-- that it handles the error messages from Oz appropriately, ensuring that no changes from 
-- its local restore are lost even if the actual asset restored locally is removed in favor
-- of the restored asset from the other client.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	
	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"

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

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId
	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetRevId
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentRevId
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()
	local favoriteRevId
		
	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )
	
-- Step 1: Add an image asset
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ), 	
		':ignore:' )
	end
	
	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }
	
	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	if options.includeAssociatedDocs then
		-- add any associated docs
		-- album
		local albumRevId
		local albumName = 'Album1'
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = albumId,
				type = 'album',
				subtype = 'collection',
				name = albumName,
			}
			:map( function( revId )
				albumRevId = revId
				return revId
			end )
		, ':ignore:' )
		
		-- sync up the album 
		catalogSpec:expectHttpRequest( pushCreateAlbumReqResp( catalogId, albumId, albumName ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
		
		-- album_asset
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = albumAssetId,
				type = 'album_asset',
				album = { id = albumId },
				asset = { id = assetId }
			}
			:map( function( revId )
				albumAssetRevId = revId
				return revId
			end )
		, ':ignore:' )
		
		-- Sync up the album_asset
		catalogSpec:expectHttpRequest( pushCreateAlbumAssetReqResp( catalogId, albumAssetId, albumId, assetId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
		-- Sync down the album and album_asset
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
			albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
		} ), 'success' )
	
		-- This first part of this step is contrived as we aren't the other client. But
		-- we want to add the space_album, and comment documents here so when we run
		-- this test against oz it will have the documents as well and move them around
		-- during the delete and restore process.
		local now = KSDate.timeInIso8601Format()
	
		local spaceAlbumRevId
		rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
			{
				label = 'oz',
				{
					_id = spaceId,
					userCreated = now,
					userUpdated = now,
					type = 'space',
					subtype = 'shared',
					private = false,
					download = false,
					metadata = false,
					location = false,
					presentationFilters = true,
				},
				{
					_id = spaceAlbumId,
					type = 'space_album',
					space = { id = spaceId },
					album = { id = albumId },
				}
			} ) :map( function( overlayResults )
					spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
					return overlayResults
				end )
		, ':ignore:' )
	
		-- Now create the space via a direct put as MetadataPusher doesn't handles 'space' docs.
		local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
		local createSpaceOptions = {
			requestMethod = 'PUT',
			requestHeaders = {
				{ header = 'Content-Type', value = 'application/json' },
			},
		}
		local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
			payload = {
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			subtype = 'shared',
			resources = {
				{
					id = albumId,
					type = 'album',
					space_album_id = spaceAlbumId,
				}
			}
		} )
	
		catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
		rxTest:expectSingleValue(
			catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
			:map( function( responseData, headers )
				return headers.status_code
			end )
		, '201' )
	
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
			spaceUpdateResponse( catalogId, spaceId )
		}), 'success' )
		
	end
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1
		
		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()
		
		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1
		
		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()
		
		-- Wait for the uploader to issue the create master request
		rxTest:observe( pollUploadCount( uploader, 'master' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()
	
	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = true } ), true )

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()
	
	
-- Step 2: Maybe remove some of the local binaries for the asset to test different restore scenarios
	if options.dropLocalMaster then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId ) ),
			':ignore:' )
	end

	if options.dropLocalProxy then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId ) ),
			':ignore:' )
	end
	
-- Step 3: Move the asset to trash locally and send that to Oz

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )
	
	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:tap( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
		end )
	, ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	local waitForRenditions = not options.noWaitForRenditions
	
	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	local expectedUpdates = {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}
	
	if options.includeAssociatedDocs then
		expectedUpdates[ #expectedUpdates + 1 ] = albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ) -- new unknown id and revId will be created by oz
		expectedUpdates[ #expectedUpdates + 1 ] = albumAssetDeletedResponse( albumAssetId, userId, KSIdGenerator.uuidInLowercaseHex() )
	end
	
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, expectedUpdates, transformResponseWithUnknownNewIds ), 'success' )
	
-- Other Client ------------------------------------------------------------

-- Step 4: On a second client, restore the asset from the trash on that second client
-- so that it's a new asset on Oz
	
	-- Send an update with a restore trash asset without our docstore knowing about it 
	-- so it appears as if a different client issued the restore.
	local otherRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherRestoredAssetId,
		revisionId = otherRestoredRevId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = trashUserCreated,
		options = options,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- Step 5: Attempt to restore the asset locally

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )
	
	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )
	
	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

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

-- Step 6 (optional): Add a comment/favorite to the asset to ensure these get transferred to the Oz restored asset
	local commentBody = "Comment1 Mumble, mumble, mumble"
	if options.includeAssociatedDocs then
		-- Add comment
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				type = 'comment',
				subtype = 'user',
				_id = commentId,
				body = commentBody,
				space = { id = spaceId },
				asset = { id = restoredAssetId },
				_localOnly = { created = '2018-12-15T21:21:21Z' },
			}
			:map( function( revId )
				commentRevId = revId
				return revId
			end )
		, ':ignore:' )
	
		-- Add favorite
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				type = 'favorite',
				subtype = 'user',
				_id = favoriteId,
				space = { id = spaceId },
				asset = { id = restoredAssetId },
				_localOnly = { created = '2018-12-15T21:21:21Z' },
			}
			:map( function( revId )
				favoriteRevId = revId
				return revId
			end )
		, ':ignore:' )
	
	end

-- Step 7 (optional): Modify the restored asset locally 
	
	local developSettings = {
		processingModel = "lightroom",
		xmpCameraRaw = kApplyTintXmp
	}
	
	local userUpdated = restoredDocValue.userUpdated
	
	if options.modifyAfterRestore then
		userUpdated = KSDate.timeInIso8601Format()
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = restoredAssetId,
				develop = developSettings,
				userUpdated = userUpdated,
			}
		, ':ignore:' )
	end
	
	-- Send create restored asset from original assets
	if options.includeAssociatedDocs then
	
		catalogSpec:expectHttpRequest( pushRestoreAssetAndCommentFavoriteDocsReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId,
										spaceId, commentId, commentBody, favoriteId ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )
	
	else
	
		catalogSpec:expectHttpRequest( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, userUpdated ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	end

	-- Make sure the trash asset was deleted locally
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )
	
	expectedUpdates = {
			trashAssetDeletedResponse( trashAssetId, userId, otherRestoredAssetId ),
			restoredAssetUpdateResponse( otherRestoredAssetId, otherRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, waitForRenditions ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherRestoredAssetId, userId ),
		}
		
	if options.includeAssociatedDocs then
		-- unknown id will be created by Oz and stripped when we receive the response here
		expectedUpdates[ #expectedUpdates + 1 ] = albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId )
		expectedUpdates[ #expectedUpdates + 1 ] = albumAssetDeletedResponse( albumAssetId, userId, KSIdGenerator.uuidInLowercaseHex() ) 
	end
	
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, expectedUpdates, transformResponseWithUnknownNewIds ), 'success' )
	
-- Step 8: The error handler should process the RNFE and turn the restore into a delete of the locally restored asset (which will not sync to Oz) and, if applicable, 
-- an update of the oz restored asset
	-- check that the locally restored asset is deleted
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId ):pluck( '_empty' ) )
		:expectDataWithinTime( 2 * waitMultiplier, true )

	-- any binaries that were on the locally restored asset should now be on the restored asset from Oz
	if not ( options.dropLocalMaster and options.dropLocalProxy ) then
		-- Make sure the restored asset has the expected set of local files
		rxTest:expectSingleValue( catalog:docStore():docValue( otherRestoredAssetId )
			:map( function( docValue )
				local hasBinary = 'unknown'
				if options.proxyOnlyAsset or options.dropLocalMaster then
					local proxyPath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
					hasBinary = ( proxyPath and WFFileUtils.exists( proxyPath ) == 'file' ) and 'hasProxy' or 'no proxy'
				else
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
					hasBinary = ( originalPath and WFFileUtils.exists( originalPath ) == 'file' ) and 'hasOriginal' or 'no original'
				end
				
				return hasBinary
			end ),
		(options.proxyOnlyAsset or options.dropLocalMaster) and 'hasProxy' or 'hasOriginal' )
	
	end
	
	if options.modifyAfterRestore then
		
		-- check that the restored asset has the changes from the locally restored asset that we expect
		catalogSpec:expectHttpRequest( pushUpdateRestoredReqResp( catalogId, otherRestoredAssetId, otherRestoredRevId, restoredDocValue.subtype, false, developSettings.xmpCameraRaw ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	
		-- pull from Oz to get the asset update response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				restoredAssetUpdateResponse( otherRestoredAssetId, otherRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, false, true, developSettings ),
			}, transformOrderAndStripAssetUpdate( false )), 'success' )

	elseif not options.includeAssociatedDocs then

		-- The error handler is updated the otherRestoredAssetId with info from our restored asset, so send it up in case there are changes.
		catalogSpec:expectHttpRequest( pushUpdateRestoredReqResp( catalogId, otherRestoredAssetId, otherRestoredRevId, restoredDocValue.subtype, false ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

		-- pull from Oz to get the asset update response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				restoredAssetUpdateResponse( otherRestoredAssetId, otherRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, false ),
			}, transformOrderAndStripAssetUpdate( false )), 'success' )

	end
	
	if options.includeAssociatedDocs then
		
		-- check that we send the comment and favorite for the oz restored asset id 
		local updateRequest = WFMockHttpRequest()
			:post( "/v2/catalogs/" .. catalogId .."/updates" )
			:expectRequestDataAfterTransformation( 'success', function( str )
				
				local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
				
				local commentUpdate, favoriteUpdate
				for _, update in ipairs( updatesBeingPushed ) do
					if update.type == 'comment' then
						commentUpdate = update
					elseif update.type == 'favorite' then
						favoriteUpdate = update
					end
				end
		
				local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, otherRestoredAssetId, commentBody )
				local correctFavoriteUpdate = WFCatTrashTestUtils.hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, otherRestoredAssetId )
	
				if correctCommentUpdate and correctFavoriteUpdate then
					return 'success'
				end
	
				return str
	
			end )
			:respond( '202', futureUpdatesResponse( catalogId ) )

		catalogSpec:expectHttpRequest( updateRequest )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates * 3 ) )
			
	end

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

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

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestore( rxTest )
-- NMB-10313
-- If another client beats us to a restore, we want to resolve the two 
-- restored assets such that any local changes we have are moved to the
-- restored asset that Oz accepted.
	testAddSyncRestoreOtherRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestore_NoLocalMaster( rxTest )
-- NMB-10313
-- If another client beats us to a restore, we want to resolve the two 
-- restored assets such that any local changes we have are moved to the
-- restored asset that Oz accepted.
	testAddSyncRestoreOtherRestoreSync( rxTest, { mock = true, dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestore_NoLocalBinaries( rxTest )
-- NMB-10313
-- If another client beats us to a restore, we want to resolve the two 
-- restored assets such that any local changes we have are moved to the
-- restored asset that Oz accepted.
	testAddSyncRestoreOtherRestoreSync( rxTest, { mock = true, dropLocalMaster = true, dropLocalProxy = true } )
end


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

function WFCatTrashTest.rxTest_CT_MockRestore_OtherRestore_assocDocs( rxTest )
-- NMB-10313
-- If another client beats us to a restore, we want to resolve the two 
-- restored assets such that any local changes we have are moved to the
-- restored asset that Oz accepted.
	testAddSyncRestoreOtherRestoreSync( rxTest, { mock = true, includeAssociatedDocs = true } )
end


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

function WFCatTrashTest.rxTest_CT_Restore_OtherRestore_assocDocs_testNightly( rxTest )
-- NMB-10313
-- If another client beats us to a restore, we want to resolve the two 
-- restored assets such that any local changes we have are moved to the
-- restored asset that Oz accepted.
	testAddSyncRestoreOtherRestoreSync( rxTest, { includeAssociatedDocs = true } )
end

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

function WFCatTrashTest.rxTest_CT_Restore_OtherRestore_testNightly( rxTest )
-- NMB-10313
-- If we had the original locally then it could potentially create a new asset
-- This should follow the same rules RestoreOtherDelTrash so there would be
-- May want to add the _Master, _NoMaster, _NoProxy versions as well.
	testAddSyncRestoreOtherRestoreSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockRestoreModify_OtherRestore( rxTest )
-- NMB-10313
-- If we had the original locally then it could potentially create a new asset
-- This should follow the same rules RestoreOtherDelTrash so there would be
-- May want to add the _Master, _NoMaster, _NoProxy versions as well.
	testAddSyncRestoreOtherRestoreSync( rxTest, { mock = true, modifyAfterRestore = true } )
end

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

function WFCatTrashTest.rxTest_CT_RestoreModify_OtherRestore_testNightly( rxTest )
-- NMB-10313
-- If we had the original locally then it could potentially create a new asset
-- This should follow the same rules RestoreOtherDelTrash so there would be
-- May want to add the _Master, _NoMaster, _NoProxy versions as well.
	testAddSyncRestoreOtherRestoreSync( rxTest, { modifyAfterRestore = true } )
end

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

function WFCatTrashTest.rxTest_CT_DelTrash_OtherRestore( rxTest )
-- This test should probably just fail with a message saying the asset was purged
-- If we had the original locally then it could potentially create a new asset
	rxTest:expectEqual( false, failStubbedTests )
end

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

function WFCatTrashTest.rxTest_CT_DelTrash_OtherDelTrash( rxTest )
-- This test should probably just fail with a message saying the asset was purged
-- If we had the original locally then it could potentially create a new asset
	rxTest:expectEqual( false, failStubbedTests )
end

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

local function testAddSyncDeleteRestoreOtherDeleteSync( rxTest, options )

-- NMB-13896 #4 / NMB-14705
--
-- This tests the two client scenario where one client attempts to delete a
-- photo while another client deleted and restored that asset.
--
-- Scenario:
-- Steps:
--		1) Add photo, let it sync.
--		2) Sync, Pause.
--      3) Other client delete the photo.
-- 		4) Delete the photo.
--		5) Restore the photo.
--		6) Resume Sync.
--
-- Expected: Photo is restored.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- 1) Add photo.

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath = getOriginalFilePath( tempTestFolder, fileName )
	local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

	rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
		:tap( function( revId )
			revisionId = revId
		end ),
	':ignore:' )

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

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

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

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

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

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

-- 2) Pause Sync.

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

-- 3) Other client delete the photo.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184000 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

-- Original Client ------------------------------------------------------------

-- 4) 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

-- 5) Restore the photo.

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

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

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

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

-- 6) Resume Sync.

	-- Get the other client's tombstone and trash asset from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete/Restore request
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	local expectedResourceNotFoundErrorCount
	if options.splitErrors then

		-- Typically the first error arrives in less than 10 seconds and the second error arrives after a minute.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		expectedResourceNotFoundErrorCount = 1

	else

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		expectedResourceNotFoundErrorCount = 2

	end

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

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientTrashAssetId )
	:dispose()

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, otherClientTrashAssetId, otherClientTrashAssetRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then
		-- Here is the second error, it should be ignored by WFCatErrorHandler.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
	end

	-- Get the updates from the sync feed showing the photo has been restored.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( otherClientTrashAssetId, restoredAssetId, userId ),
		restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Make sure both errors have been processed.
	rxTest:observe( pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
	:expectDataWithinTime( 5 * waitMultiplier, 2, false )
	:dispose()

	-- Check that there is nothing else to send.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 2,
			resolution = {
				IgnoredMoreThanOneOperationBehind = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDelete( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDelete_testNightly( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDelete_splitErrors( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = true } )
end

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

local function testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, options )

-- NMB-13896 #5 / NMB-14707
--
-- This tests the two client scenario where one client attempts to delete a
-- photo while another client deleted and restored that asset.
--
-- Scenario:
-- Steps:
--		1) Add photo, let it sync.
--		2) Sync, Pause.
--		3) Other client delete the photo.
--		4) Other client restore the photo.
-- 		5) Delete the photo.
--		6) Restore the photo.
--		7) Resume Sync.
--
-- Expected: Photo is restored.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- 1) Add photo.

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath = getOriginalFilePath( tempTestFolder, fileName )
	local proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

	rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
		:tap( function( revId )
			revisionId = revId
		end ),
	':ignore:' )

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

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

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

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

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

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

-- 2) Pause Sync.

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

-- 3) Other client delete the photo.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184000 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

-- 4) Other client restore the photo.

	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userUpdated = userCreated,
		options = options,
	}

-- Original Client ------------------------------------------------------------

-- 5) 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

-- 6) Restore the photo.

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

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

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

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

-- 7) Resume Sync.

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		assetDeletedResponse( otherClientTrashAssetId, otherClientRestoredAssetId, userId ),
		restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete/Restore request
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	local expectedResourceNotFoundErrorCount
	if options.splitErrors then

		-- Typically the first error arrives in less than 10 seconds and the second error arrives after a minute.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		expectedResourceNotFoundErrorCount = 1

	else

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		expectedResourceNotFoundErrorCount = 2

	end

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

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the other client's trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientTrashAssetId )
	:dispose()

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, otherClientTrashAssetId, otherClientTrashAssetRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then
		-- Here is the second error, it should be ignored by WFCatErrorHandler.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	end

	-- Verify the other client's restored asset exists. Any modifications should be
	-- moved over as well, which is tested by other tests but could be added here.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
		:map( function( otherClientRestoredDocValue )
			return otherClientRestoredDocValue._deleted, otherClientRestoredDocValue._empty
		end )
	, nil, nil )

	-- Get another resource not found this time for the otherClientTrashAssetId as it was also restored by the other client.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	-- Verify the local asset has been removed in favor of keeping
	-- the other client's restored asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( localRestoredDocValue )
			return localRestoredDocValue._deleted
		end )
	, true )

	-- Verify the other client's restored asset exists. Any modifications should be
	-- moved over as well, which is tested by other tests but could be added here.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
		:map( function( otherClientRestoredDocValue )
			return otherClientRestoredDocValue._deleted, otherClientRestoredDocValue._empty
		end )
	, nil, nil )

	-- Send an update for the local asset we merged on top of the otherClient's restored asset.
	catalogSpec:expectHttpRequest( pushUpdateAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, subtype, options.xmpDevelop ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Check that there is nothing else to send.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredMoreThanOneOperationBehind = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
				CreatedOverlaysToMergeTheRestoredAssetsDeletingLocal_LocalRestore = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherRestore( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherRestore_testNightly( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherRestore_splitErrors( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitErrors = true } )
end

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

local function testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, options )

-- NMB-14104
-- This tests the two client scenario where one client attempts to delete and
-- restore an asset while another client deleted and permanently deleted that asset
-- from the trash. In this case, the client attempting to restore the asset
-- should instead create the new asset from scratch instead of asking Oz to restore
-- the trash asset.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add an image asset
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

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

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

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 1.5: Maybe remove some of the local binaries for the asset to test different restore scenarios
	if options.dropLocalMaster then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId ) ),
			':ignore:' )
	end

	if options.dropLocalProxy then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId ) ),
			':ignore:' )
	end


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

-- Step 2: On a second client, delete the asset and then remove the asset from the trash
-- so that no longer exists on Oz

	local ozTrashAssetCreated = KSDate.timeInIso8601Format()
	local ozTrashPurgeDate = KSDate.shiftIso8601DateTime( ozTrashAssetCreated, 5184000 )
	local ozTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local ozTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = ozTrashPurgeDate,
		options = options,
	}

	simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, ozTrashAssetId, ozTrashAssetRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- Step 3: Move the asset to trash locally

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

-- Step 4: Restore the asset locally

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

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

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

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

-- Step 4.5: Turn on Sync

	-- Get the other client's tombstones from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, ozTrashAssetId, userId ),
		trashAssetDeletedResponse( ozTrashAssetId, userId ),
	}, removeSporadicAssetUpdates ), 'success' )

	-- Send up the Delete/Restore request
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Typically the first error arrives in less than 10 seconds and the second error arrives after a minute.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", ozTrashAssetId, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	else

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", ozTrashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	end

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, ozTrashAssetId )
	:dispose()

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, ozTrashAssetId, ozTrashAssetRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then
		-- typical path?
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			-- First error is leftover from our first attempt at the delete/restore.
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
			-- Second error is from the pushRestoreAssetReqResp call above.
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", ozTrashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
	else
		-- Get the error with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", ozTrashAssetId, "deleted_asset", nil, userId),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
	end

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

	-- Verify WFCatErrorHandler will remove the createFrom trash info
	-- so the photo will be created via a regular create operation.
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( function( docValue )
		return WFCatTrashUtils.createFromInfo( docValue )
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, nil )
	:dispose()

-- Step 5: The error handler should process the RNFE and turn the restore into an asset create
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	if options.dropLocalMaster and not options.dropLocalProxy then
		importSource.willNotUploadMaster = true
	end

	-- pull from Oz to get the asset create response
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false },
								nil --[[developSettings]], true --[[alwaysHaveLocationData]] )
		}, stripAssetAndLocation ), 'success' )

	-- validate the restored asset is on oz
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( function( docValue )
		local receivedUpdateFromOz = docValue._serverProvidedMetadata ~= nil

		-- stash the latest oz revisionId so we can validate the upload
		local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
		if revisionIds then
			restoredRevId = revisionIds[ #revisionIds ]
		end

		return receivedUpdateFromOz
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

-- Step 6: The restored asset gets its binaries uploaded (if applicable)

	if not ( options.dropLocalMaster and options.dropLocalProxy ) then
		-- we should have some binaries so validate that they're uploaded

		-- Make sure the restored asset has the expected set of local files
		rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
			:map( function( docValue )
				local hasBinary = false
				if options.proxyOnlyAsset or options.dropLocalMaster then
					local proxyPath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
					hasBinary = proxyPath and WFFileUtils.exists( proxyPath ) == 'file'
				else
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
					hasBinary = originalPath and WFFileUtils.exists( originalPath ) == 'file'
				end

				return hasBinary
			end ),
		true )
		
		restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
		
		if options.proxyOnlyAsset or options.dropLocalMaster then
			expectedProxyUploadCount = expectedProxyUploadCount + 1

			-- Upload the proxy (and generate renditions)
			catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, restoredAssetId, restoredRevId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
			uploader:startContinuousUploads()

			-- wait for the uploader to issue the create proxy request
			rxTest:observe( pollUploadCount( uploader, 'proxy' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
			:dispose()
		else
			expectedOriginalUploadCount = expectedOriginalUploadCount + 1

			-- Upload the master
			catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, restoredAssetId, restoredRevId, filePath ) )
			uploader:startContinuousUploads()

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

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

		-- Get the updated metadata with the binary link.
		local expectMasterLink = not ( options.dropLocalMaster or options.proxyOnlyAsset )
		local expectProxyAndRenditionLinks = not options.dropLocalProxy
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = expectMasterLink, proxy = expectProxyAndRenditionLinks, renditions = expectProxyAndRenditionLinks }, nil, true )
		}, stripAssetAndLocation ), 'success' )

	end

	-- Check that there is nothing else to send.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredMoreThanOneOperationBehind = {
					count = 1,
					sourceDocIds = {
						restoredAssetId
					},
				},
				RemovedTrashInfoToCreateAssetDirectly_docThatIncurredErrorWasTrashAsset = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherDelTrash_Master( rxTest )
-- NMB-14104
	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherDelTrash_Master_testNightly( rxTest )
-- NMB-14104
	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherDelTrash_NoMaster( rxTest )
-- NMB-14104
-- Expected result: The asset was a Lightroom style asset and will be restored
-- as a Lightroom style asset

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { mock = true, proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherDelTrash_NoMaster_testNightly( rxTest )
-- NMB-14104
-- Expected result: The asset will be restored as a Lightroom style asset

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherDelTrash_NoLocalMaster( rxTest )
-- NMB-14104
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { mock = true, dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherDelTrash_NoLocalMaster_testNightly( rxTest )
-- NMB-14104
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherDelTrash_NoLocalBinaries( rxTest )
-- NMB-14104
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but would show up on Lr Web as an incomplete
-- asset.

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { mock = true, dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherDelTrash_NoLocalBinaries_testNightly( rxTest )
-- NMB-14104
-- In this case we fail, as we only had the thumbnail.

	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherDelTrash_splitErrors( rxTest )
-- NMB-14104
	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherDelTrash_splitErrors_testNightly( rxTest )
-- NMB-14104
	testAddSyncDeleteRestoreOtherDeleteOtherDelTrashSync( rxTest )
end

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

local function testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, options )

-- NMB-13896 #7 / NMB-14709
-- This tests the two client scenario where one client attempts to delete
-- an asset while another client deleted and permanently deleted that asset
-- from the trash. In this case, the client attempting to delete the asset
-- should instead create the new asset from scratch instead of asking Oz to restore
-- the trash asset.

--Steps:
--1) Add photo, let it sync.
--2) Pause Sync.
--3) Other Client Delete the photo
--4) Other Client Permanently Delete the photo from the trash.
--5) Delete the photo.
--6) Resume Sync
--Expected: Photo in Trash with whatever Binaries are available.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

--1) Add photo, let it sync.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

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

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

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

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 1.5: Maybe remove some of the local binaries for the asset to test different restore scenarios
	if options.dropLocalMaster then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId ) ),
			':ignore:' )
	end

	if options.dropLocalProxy then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId ) ),
			':ignore:' )
	end

--2) Pause Sync.

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

--3) Other Client Delete the photo

	local ozTrashAssetCreated = KSDate.timeInIso8601Format()
	local ozTrashPurgeDate = KSDate.shiftIso8601DateTime( ozTrashAssetCreated, 5184000 )
	local ozTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local ozTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = ozTrashPurgeDate,
		options = options,
	}

--4) Other Client Permanently Delete the photo from the trash.

	simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = ozTrashAssetId,
		trashAssetRevId = ozTrashAssetRevId,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, ozTrashAssetId, ozTrashAssetRevId, options ), true )

-- Original Client ------------------------------------------------------------

--5) Delete the photo.

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

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

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

--6) Resume Sync

	-- Get the other client's tombstones from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, ozTrashAssetId, userId ),
		trashAssetDeletedResponse( ozTrashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete/Restore request
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	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( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", ozTrashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):map( WFCatTrashUtils.getCreateFromId ), ozTrashAssetId )

	local intermediateId, intermediateRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( localTrashDocValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( localTrashDocValue )
			intermediateId = createFromInfo.lastAssetId
			intermediateRevId = createFromInfo.lastRevisionId
			return type( intermediateId ), type( intermediateRevId )
		end )
	, 'string', 'string' )

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( pushRestoreDeleteAssetReqResp( catalogId, ozTrashAssetId, ozTrashAssetRevId, intermediateId, intermediateRevId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Typical path, get the error with respect to what we tried to copy from as it is no longer there.
		-- The second error will get delivered later.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", ozTrashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	else
		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", ozTrashAssetId, "deleted_asset", nil, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

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

	-- Verify WFCatErrorHandler will remove the createFrom trash info
	-- so the photo will be created via a regular create operation.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( function( docValue )
		return WFCatTrashUtils.createFromInfo( docValue )
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, nil )
	:dispose()

	-- push the create
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	if options.dropLocalMaster and not options.dropLocalProxy then
		importSource.willNotUploadMaster = true
	end

	if options.splitErrors then

		-- pull from Oz to get the asset create response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false } ),
			resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
		}, stripAssetAndLocation ), 'success' )

	else

		-- pull from Oz to get the asset create response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false } ),
		}, stripAssetAndLocation ), 'success' )

	end
	-- validate the restored asset is on oz
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( function( docValue )
		local receivedUpdateFromOz = docValue._serverProvidedMetadata ~= nil

		-- stash the latest oz revisionId so we can validate the upload
		local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
		if revisionIds then
			trashRevId = revisionIds[ #revisionIds ]
		end

		return receivedUpdateFromOz
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

-- The trash asset gets its binaries uploaded (if applicable)

	if not ( options.dropLocalMaster and options.dropLocalProxy ) then
		-- we should have some binaries so validate that they're uploaded

		-- Make sure the restored asset has the expected set of local files
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				local hasBinary = false
				if options.proxyOnlyAsset or options.dropLocalMaster then
					local proxyPath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
					hasBinary = proxyPath and WFFileUtils.exists( proxyPath ) == 'file'
				else
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
					hasBinary = originalPath and WFFileUtils.exists( originalPath ) == 'file'
				end

				return hasBinary
			end ),
		true )
		
		trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
		
		if options.proxyOnlyAsset or options.dropLocalMaster then
			expectedProxyUploadCount = expectedProxyUploadCount + 1

			-- Upload the proxy (and generate renditions)
			catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, trashAssetId, trashRevId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
			uploader:startContinuousUploads()

			-- wait for the uploader to issue the create proxy request
			rxTest:observe( pollUploadCount( uploader, 'proxy' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
			:dispose()
		else
			expectedOriginalUploadCount = expectedOriginalUploadCount + 1

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

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

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

		-- Get the updated metadata with the binary link.
		local expectMasterLink = not ( options.dropLocalMaster or options.proxyOnlyAsset )
		local expectProxyAndRenditionLinks = not options.dropLocalProxy
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = expectMasterLink, proxy = expectProxyAndRenditionLinks, renditions = expectProxyAndRenditionLinks } ),
		}, stripAssetAndLocation ), 'success' )

	end

	-- Check that there is nothing else to send.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredSecondPartOfTrashPair_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
				RemovedTrashInfoToCreateAssetDirectly_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherDelTrash_Master( rxTest )
-- NMB-13896 #7 / NMB-14709
	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { mock = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherDelTrash_Master_testNightly( rxTest )
-- NMB-13896 #7 / NMB-14709
	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherDelTrash_NoMaster( rxTest )
-- NMB-13896 #7 / NMB-14709
-- Expected result: The asset was a Lightroom style asset and will be restored
-- as a Lightroom style asset

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { mock = true, proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherDelTrash_NoMaster_testNightly( rxTest )
-- NMB-13896 #7 / NMB-14709
-- Expected result: The asset will be restored as a Lightroom style asset

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { proxyOnlyAsset = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherDelTrash_NoLocalMaster( rxTest )
-- NMB-13896 #7 / NMB-14709
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { mock = true, dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherDelTrash_NoLocalMaster_testNightly( rxTest )
-- NMB-13896 #7 / NMB-14709
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { dropLocalMaster = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherDelTrash_NoLocalBinaries( rxTest )
-- NMB-13896 #7 / NMB-14709
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but would show up on Lr Web as an incomplete
-- asset.

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { mock = true, dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashTest.rxTest_CT_OzDelete_OtherDeleteOtherDelTrash_NoLocalBinaries_testNightly( rxTest )
-- NMB-13896 #7 / NMB-14709
-- In this case we fail, as we only had the thumbnail.

	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { dropLocalMaster = true, dropLocalProxy = true } )
end

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

function WFCatTrashTest.rxTest_CT_MockDelete_OtherDeleteOtherDelTrash_splitErrors( rxTest )
-- NMB-13896 #7 / NMB-14709
	testAddSyncDeleteOtherDeleteOtherDelTrashSync( rxTest, { mock = true, splitErrors = true } )
end

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

local function testAddDeleteSyncRestoreDeleteOtherRestoreSync( rxTest, options )

-- NMB-13896 #1 / NMB-14702
-- Scenario:
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync, Pause
--      4) Other Client Restore
-- 		5) Restore and Delete the photo.
--		6) Resume Sync
--
-- Expected: Photo in Trash
--   Should get two ResourceNotFoundError, the first of which should update the
--   trash createFrom info to restore from the oz asset and the second should be
--   ignored.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add photo
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

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

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

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

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

	local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

		local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

		stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'payload', 'location' )

		return mockDataAsTable, ozDataAsTable

	end

	-- Get the updated metadata with the binary link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 2: Move the asset to trash and sync it

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:tap( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
		end )
	, ':ignore:' )

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

-- Step 3 Pause Sync - this test issues separate sync actions so need to pause.
	pusher:stopContinuousUpdates()
	
-- Other Client ------------------------------------------------------------

-- Step 4: On a second client, restore the trash asset

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}

-- Original Client ------------------------------------------------------------

-- Step 5: Restore and Delete the asset locally

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure 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 )

	-- Delete the locally restored asset.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

	-- Make sure the restored asset asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Get the new trash asset id
	local trashAssetId2, trashRevId2 = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )

-- Step 6: Turn on Sync
	-- we have to give Oz staging a bit of time to process the other restore - it's not always as fast as we would like
	-- if we don't we get different documents passed back and forth than what this test is expecting. It should still lead
	-- to the same end result, but this test is covering one specific path
	if not options.mock then
		rxTest:expectSingleValue( pollUntilAssetExistsWithLinks( 3 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, expectedBinaryLinksForImage() ), true )
	end
	
	-- Send up the Restore/Delete request
	catalogSpec:expectHttpRequest( pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( trashAssetId, otherClientRestoredAssetId, userId ),
		restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )


	if options.splitErrors then

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )

	else

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, stripUpdatesAndLinks ), 'success' )

	end

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientRestoredAssetId )
	:dispose()

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 ):pluck('_empty'), nil )
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_empty'), nil )

	-- Send up a new Delete request referring to the other client's restored asset
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )


	if options.splitErrors then
		-- Success! Get the tombstone for otherClientRestoredAssetId and the new trashAssetId2.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( otherClientRestoredAssetId, trashAssetId2, userId ),
			trashAssetUpdateResponse2( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
	else
		-- Success! Get the tombstone for otherClientRestoredAssetId and the new trashAssetId2.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( otherClientRestoredAssetId, trashAssetId2, userId ),
			trashAssetUpdateResponse2( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
	end

	rxTest:observe( catalog:docStore():ongoingDocValue( otherClientRestoredAssetId ):pluck('_empty') )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

	-- Wait for the errors to be processed if present.
	rxTest:observe( pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
	:expectDataWithinTime( 5 * waitMultiplier, 2, false )
	:dispose()

	-- Check that there is nothing else to send.
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 2,
			resolution = {
				IgnoredRevisionDoesNotMatch = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestoreDeleted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherRestore( rxTest )
-- NMB-13896 #1 / NMB-14702
	testAddDeleteSyncRestoreDeleteOtherRestoreSync( rxTest, { mock = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherRestore_testNightly( rxTest )
-- NMB-13896 #1 / NMB-14702
	testAddDeleteSyncRestoreDeleteOtherRestoreSync( rxTest )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherRestore_splitErrors( rxTest )
-- NMB-13896 #1 / NMB-14702
	testAddDeleteSyncRestoreDeleteOtherRestoreSync( rxTest, { mock = true, splitErrors = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherRestore_splitErrors_testNightly( rxTest )
-- NMB-13896 #1 / NMB-14702
	testAddDeleteSyncRestoreDeleteOtherRestoreSync( rxTest, { splitErrors = true } )
end

--------------------------------------------------------------------------------

local function testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, options )

-- NMB-13896 #2 / NMB-14703
-- Scenario:
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync, Pause.
--      4) Other Client Restore, Other Client Delete.
-- 		5) Restore and Delete the photo.
--		6) Resume Sync.
--
-- Expected: Photo in Trash
--   Should get two ResourceNotFoundError, the first of which should update the
--   trash createFrom info to restore from the oz asset and the second should be
--   ignored.
--   After a second attempt of the delete it will get one more ResourceNotFoundError
--   which will result in discarding our trash asset in favor of the other clients.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add photo
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

		-- Wait for the uploader to issue the create master request
		rxTest:observe( pollUploadCount( uploader, 'master' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = options.proxyOnlyAsset } ), true )

	local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

		stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'payload', 'location' )

		return mockDataAsTable, ozDataAsTable

	end

	-- Get the updated metadata with the binary link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 2: Move the asset to trash and sync it

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:tap( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
		end )
	, ':ignore:' )

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

-- Step 3 Pause Sync - this test issues separate sync actions so need to pause.

-- Other Client ------------------------------------------------------------

-- Step 4: On a second client, restore the trash asset

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}
	
	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )
	
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, 'image' ), true )
	
	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	
	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = otherClientTrashPurgeDate,
		options = options,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, options ), true )
	
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, 'deleted_image' ), true )
	
-- Original Client ------------------------------------------------------------

-- Step 5: Restore and Delete the asset locally

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure 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 )

	-- Delete the locally restored asset.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

	-- Make sure the restored asset asset was deleted, and get it's new revision id
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Get the new trash asset id
	local trashAssetId2, trashRevId2 = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )

-- Step 6: Turn on Sync

	-- Send up the Restore/Delete request
	catalogSpec:expectHttpRequest( pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( trashAssetId, otherClientRestoredAssetId, userId ),
		assetDeletedResponse( otherClientRestoredAssetId, otherClientTrashAssetId, userId ),
		trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
	}, transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	if options.splitErrors then

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )

	else
		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, stripUpdatesAndLinks ), 'success' )
	end

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientRestoredAssetId )
	:dispose()

	-- Verify the otherClientRestoredAssetId has _newId updated with
	-- trashAssetId2.
	rxTest:observe( catalog:docStore():ongoingDocValue( otherClientRestoredAssetId )
	:map( function( otherClientRestoredDocValue )
		return otherClientRestoredDocValue._newId
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, trashAssetId2 )
	:dispose()

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_empty'), true )
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId ):pluck('_empty'), nil )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 ):pluck('_empty'), nil )

	-- Send up a new Delete request referring to the other client's restored asset
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then
		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			-- First error is leftover from our first attempt at the delete/restore.
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
			-- Second error is from the pushDeleteAssetReqResp call above.
			resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )
	else
		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, stripUpdatesAndLinks ), 'success' )
	end


	-- Verify WFCatErrorHandler will remove the local trashAssetId and
	-- moving any mods to the otherClientTrashAssetId.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( function( trashAssetDoc2 )
		return trashAssetDoc2._empty
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

	-- Make sure otherClientTrashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				DeletedLocalAsset_LocalDelete = {
					count = 1,
					sourceDocIds = {
						otherClientRestoredAssetId,
					},
				},
				IgnoredRevisionDoesNotMatch = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestoreDeleted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherRestoreOtherDelete( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, { mock = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherRestoreOtherDelete_testNightly( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherRestoreOtherDelete_splitErrors( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = true } )
end

--------------------------------------------------------------------------------

local function testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, options )

-- NMB-14710 / NMB-13896 #8
-- This tests the two client scenario where one client attempts to restore and
-- delete an asset from the trash while another client has permanently deleted
-- that asset from the trash. In this case, the client attempting to restore
-- the asset should instead create the new trash asset from scratch.

--Steps:
--	1) Add photo.
--	2) Delete the photo.
--	3) Sync, Pause
--	4) Other Client DelTrash
--	5) Restore the photo.
--	6) Delete the photo.
--	7) Resume Sync
--
--Expected: Photo in Trash with whatever local binaries it had.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = 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 subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local proxyFileName = "IMG_7763.DNG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

--	1) Add photo.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = getOriginalFilePath( tempTestFolder, fileName )
		proxyFilePath = getProxyFilePath( tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, filePath, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
		':ignore:' )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- these are the /rels/* links we currently expect in the received document
	local linksExpectedInDoc = { master = false, proxy = false, renditions = false }

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, linksExpectedInDoc )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	
	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, filePath ) )
		uploader:startContinuousUploads()

		-- Wait for the uploader to issue the create master request
		rxTest:observe( pollUploadCount( uploader, 'master' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = true } ), true )

	-- Get the updated metadata with the binary link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 1.5: Maybe remove some of the local binaries for the asset to test different restore scenarios
	if options.dropLocalMaster then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId ) ),
			':ignore:' )
	end

	if options.dropLocalProxy then
		rxTest:expectSingleValue( catalog:docStore():applyOverlay(
			WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId ) ),
			':ignore:' )
	end

--	2) Delete the photo.

	-- 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 )
	:tap( function( docValue )
		revisionId = docValue._revId
	end )
	:pluck( '_deleted' )
	, true )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )

	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:tap( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
		end )
	, ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	local waitForRenditions = not options.noWaitForRenditions

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop } ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, stripAssetAndLocation ), 'success' )

--	3) Sync, Pause

-- Other Client ------------------------------------------------------------

--	4) Other Client DelTrash

	simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )

-- Original Client ------------------------------------------------------------

--	5) Restore the photo.

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure 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 )

--	6) Delete the photo.

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

	-- Make sure a deleted_image was created
	local trashAssetId2, trashRevId2 = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )

--	7) Resume Sync

	-- send up the restore delete
	catalogSpec:expectHttpRequest( pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	if options.splitErrors then

		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetDeletedResponse( trashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		-- Wait for the errors to be processed.
		rxTest:observe( pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1, false )
		:dispose()

	else

		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetDeletedResponse( trashAssetId, userId ),
			resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )

		-- Wait for the errors to be processed.
		rxTest:observe( pollErrorHandlerErrorsProcessed( errorHandler, 'ResourceNotFoundError' ))
		:expectDataWithinTime( 5 * waitMultiplier, 2, false )
		:dispose()

	end

	-- The error handler should process the RNFE and turn the restore into a regular trash asset create
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates * 3 ) )

	if options.dropLocalMaster and not options.dropLocalProxy then
		importSource.willNotUploadMaster = true
	end

	if options.splitErrors then

		-- pull from Oz to get the trash asset create response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, trashAssetId, false, false, options.xmpDevelop  ),
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, stripAssetAndLocation ), 'success' )

	else

		-- pull from Oz to get the trash asset create response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, trashAssetId, false, false, options.xmpDevelop  ),
		}, stripAssetAndLocation ), 'success' )

	end

	-- validate the restored asset is on oz
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( function( docValue )
		local receivedUpdateFromOz = docValue._serverProvidedMetadata ~= nil

		-- stash the latest oz revisionId so we can validate the upload
		local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
		if revisionIds then
			trashRevId2 = revisionIds[ #revisionIds ]
		end

		return receivedUpdateFromOz
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

	-- Make sure the newly re-created trash asset gets its binaries uploaded.

	if not ( options.dropLocalMaster and options.dropLocalProxy ) then
		-- we should have some binaries so validate that they're uploaded

		-- Make sure the restored asset has the expected set of local files
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 )
			:map( function( docValue )
				local hasBinary = false
				if options.proxyOnlyAsset or options.dropLocalMaster then
					local proxyPath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
					hasBinary = proxyPath and WFFileUtils.exists( proxyPath ) == 'file'
				else
					local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
					hasBinary = originalPath and WFFileUtils.exists( originalPath ) == 'file'
				end

				return hasBinary
			end ),
		true )
		
		trashRevId2 = getLatestRevisionId( rxTest, catalog, trashAssetId2 )
		
		if options.proxyOnlyAsset or options.dropLocalMaster then
			expectedProxyUploadCount = expectedProxyUploadCount + 1

			-- Upload the proxy (and generate renditions)
			catalogSpec:expectHttpRequest( createProxyReqResp( catalogId, trashAssetId2, trashRevId2, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
			uploader:startContinuousUploads()

			-- wait for the uploader to issue the create proxy request
			rxTest:observe( pollUploadCount( uploader, 'proxy' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
			:dispose()
		else
			expectedOriginalUploadCount = expectedOriginalUploadCount + 1

			-- Upload the master
			catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, trashAssetId2, trashRevId2, filePath ) )
			uploader:startContinuousUploads()

			-- Wait for the uploader to issue the create master request
			rxTest:observe( pollUploadCount( uploader, 'master' ))
			:expectDataWithinTime( 5 * waitMultiplier, expectedOriginalUploadCount )
			:dispose()
		end
		uploader:stopContinuousUploads()

		-- Get the updated metadata with the binary link.
		local expectMasterLink = not ( options.dropLocalMaster or options.proxyOnlyAsset )
		local expectProxyAndRenditionLinks = not options.dropLocalProxy
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse2( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, trashAssetId, { master = expectMasterLink, proxy = expectProxyAndRenditionLinks, renditions = expectProxyAndRenditionLinks } ),
		}, stripAssetAndLocation ), 'success' )

	end

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 2,
			resolution = {
				IgnoredRevisionDoesNotMatch = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				RemovedTrashInfoToCreateAssetDirectly_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_Master( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_Master_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_NoMaster( rxTest )
-- NMB-14710 / NMB-13896 #8
-- Expected result: The asset was a Lightroom style asset and will be restored
-- as a Lightroom style asset

	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true, proxyOnlyAsset = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_NoMaster_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
-- Expected result: The asset will be restored as a Lightroom style asset

	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { proxyOnlyAsset = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_NoLocalMaster( rxTest )
-- NMB-14710 / NMB-13896 #8
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true, dropLocalMaster = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_NoLocalMaster_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
-- Expected result: The asset had a master, but not locally and it's no longer on Oz
-- so the asset will be restored as a Lightroom style asset

	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { dropLocalMaster = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_NoLocalBinaries( rxTest )
-- NMB-14710 / NMB-13896 #8
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but would show up on Lr Web as an incomplete
-- asset.

-- This should show up as a bad asset in the LrCC client that issued the restore/Delete.

-- expect to encounter and handle ResourceNotFoundError
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true, dropLocalMaster = true, dropLocalProxy = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_NoLocalBinaries_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
-- In this case we fail, as we only had the thumbnail.
-- The asset won't show up on other clients but I am thinking it should show
-- up as a bad asset in the LrCC client that issued the restore - showing this
-- will likely require a change as we currently don't show an asset that doesn't
-- have a master/proxy).

-- expect to encounter and handle ResourceNotFoundError
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { dropLocalMaster = true, dropLocalProxy = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_Master( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true, splitErrors = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_Master_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { splitErrors = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockRestoreDelete_OtherDelTrash_splitErrors( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { mock = true, splitErrors = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzRestoreDelete_OtherDelTrash_splitErrors_testNightly( rxTest )
-- NMB-14710 / NMB-13896 #8
	testAddSyncRestoreDeleteOtherDeleteTrashSync( rxTest, { splitErrors = true } )
end

--==============================================================================
-- @mark tests that involve adding assoc docs and receiving other client delete
--==============================================================================

local function pushCreateAlbumAndCommentDocsReqResp( catalogId, assetId, assetRevisionId, albumId, spaceId, commentId, commentBody )

	-- The update array being pushed should include a create album asset,
	-- and a create comment, as they were added after the original
	-- sync but before the delete.

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumAssetUpdate, commentUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'album_asset' then
					albumAssetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				end
			end

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )
			local correctCommentUpdate = WFCatTrashTestUtils.hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, assetId, commentBody )

			if correctAlbumAssetUpdate and correctCommentUpdate
				and #updatesBeingPushed == 2 then

				return 'success'
			end

			return str

		end )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

--------------------------------------------------------------------------------

local function testAddSyncAddAlbumsOtherClientDeleteSync( rxTest, options )

-- NMB-10394
--
--Scenario:
-- 1 Using debug tool pause MetadataPusher
-- 2 Add a synced photo to 1 or more albums
-- 3 Switch to other client and delete photo
-- 4 Wait for asset to be moved to trash
-- 5 View trash on the first client and make sure the photo shows up
-- 6 Turn on MetadataPusher (shouldn't be needed but puts us back sync totally enabled state)
-- 7 Restore the photo
-- 8 Verify the photo is in the album still
--Expected Result:
--Photo is in the album.

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )
	
	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local socialCreated = '2018-04-15T21:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local assetRevId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1:

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			assetRevId = revId
			return revId
		end ),
	':ignore:' )

	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )

	local albumAssetRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId,
			type = 'album_asset',
			album = { id = albumId },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId = revId
			return revId
		end )
	, ':ignore:' )

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 3, kTimeoutForSendingUpdates ) )

	-- Sync down the asset, album and album_asset.  The asset will have the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, assetRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false } ),
		albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )

	-- Upload the master
	assetRevId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, assetRevId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, assetRevId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, assetRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = true, renditions = true } )
	}, 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:

	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentBody1 = "Comment1 Mumble, mumble, mumble"
	local commentBody2 = "Comment2 Mumble, mumble, mumble"
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()

	-- This first part of this step is contrived as we aren't the other client. But
	-- we want to add the space_album, and comment documents here so when we run
	-- this test against oz it will have the documents as well and move them around
	-- during the delete and restore process.
	local now = KSDate.timeInIso8601Format()

	local spaceAlbumRevId
	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			label = 'oz',
			{
				_id = spaceId,
				userCreated = now,
				userUpdated = now,
				type = 'space',
				subtype = 'shared',
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			{
				_id = spaceAlbumId,
				type = 'space_album',
				space = { id = spaceId },
				album = { id = albumId },
			}
		} ) :map( function( overlayResults )
				spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
				return overlayResults
			end )
	, ':ignore:' )

	-- Now create the space via a direct put as MetadataPusher doesn't handles 'space' docs.
	local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	local createSpaceOptions = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
		payload = {
			private = false,
			download = false,
			metadata = false,
			location = false,
			presentationFilters = true,
		},
		subtype = 'shared',
		resources = {
			{
				id = albumId,
				type = 'album',
				space_album_id = spaceAlbumId,
			}
		}
	} )

	catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		spaceUpdateResponse( catalogId, spaceId )
	}), 'success' )

	local commentRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId,
			body = commentBody1,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			commentRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody1 ),
		feedbackUserUpdate()
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )

	local favoriteRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'favorite',
			subtype = 'user',
			_id = favoriteId,
			space = { id = spaceId },
			asset = { id = assetId },
		}
		:map( function( revId )
			favoriteRevId = revId
			return revId
		end )
	, ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Sync down the favorite update.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

	-- Make sure the comment and favorite are correctly stored
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM spacefavorite where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM comment where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()

-- Step 3:

	-- Add asset to another album before we issue the delete so we can
	-- verify it will only restore the one album_asset
	local albumId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
	local albumName2 = 'AlbumName2'
	local albumRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId2,
			type = 'album',
			subtype = 'collection',
			name = albumName2,
		}
		:map( function( revId )
			albumRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	-- Sync up the album
	catalogSpec:expectHttpRequest( pushCreateAlbumReqResp( catalogId, albumId2, albumName2 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Sync down the album
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId2, albumRevId2, albumName2 )
	}, removeSporadicSpaceFeedbackAndAssetUpdates ), 'success' )

-- Step 4:

	-- Add the asset to another album asset before we delete it
	-- Add another comment to asset before we delete it
	-- these won't be synced until after the asset has been deleted and restored
	local albumAssetRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId2,
			type = 'album_asset',
			album = { id = albumId2 },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	local commentId2 = KSIdGenerator.uuidInLowercaseHex()
	local commentRevId2
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId2,
			body = commentBody2,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = {
				created = socialCreated,
			}
		}
		:map( function( revId )
			commentRevId2 = revId
			return revId
		end )
	, ':ignore:' )

	-- Other Client ------------------------------------------------------------

	-- Send an update with a delete asset without our docstore knowing about it
	-- so it appears as if a different client issued the delete.  Our code
	-- doesn't currently look at deleted_by so it shouldn't be any different
	-- that this client did it instead of an other client.

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = assetRevId,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = trashPurgeDate,
		options = options,
	}

	-- Back to Original Client -------------------------------------------------

	local function sortByFirstElement( expectedQueryResult )
		table.sort( expectedQueryResult, function( a, b ) return a[ 1 ] < b[ 1 ] end )
		return expectedQueryResult
	end

	-- Make sure the now deleted original asset still has both album assets.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], assetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	-- Make sure the now deleted original asset still has both favorites.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], assetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { favoriteId } } )
	:dispose()

	-- Make sure the now deleted original asset still has both comments.
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], assetId,
		quiesceUntilUpToDate = true,
	})
	:expectData( sortByFirstElement( { { commentId }, { commentId2 } } ) )
	:dispose()

	catalogSpec:expectHttpRequest( pushCreateAlbumAndCommentDocsReqResp( catalogId, assetId, assetRevId, albumId2, spaceId, commentId2, commentBody2 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- The _revId was changed when the album_asset was updated to refer to the trash asset,
	-- get the latest version so we don't cause a conflict and try and send another update
	-- for it.
	rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId2 )
		:map( function( docValue )
			albumAssetRevId2 = docValue._revId
			return docValue
		end )
	, ':ignore:' )

	local userId = catalog:ozUserId()
	local trashAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
	local trashCommentId = KSIdGenerator.uuidInLowercaseHex()
	local trashCommentRevId = KSIdGenerator.uuidInLowercaseHex()
	local trashFavoriteId = KSIdGenerator.uuidInLowercaseHex()
	local trashFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()

	local function transformOrderAndStripAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		-- Remove the sporadic feedback user / space update before running through
		-- stripUpdatesAndLinks to make sure things line up properly.
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
		removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

		mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
		ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

		if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
			return {}, {} -- don't compare partial updates
		end

		local function stripUpdateField( i, ... ) return stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, ... ) end

	-- Sort order of updates for delete should be:
		-- 1 album_asset1
		-- 2 asset (trash)
		-- 3 comment1
		-- 4 assetGoneError album_asset2
		-- 5 assetGoneError comment2
		-- 6 favorite
		-- 7 deleted album_asset1
		-- 8 deleted asset
		-- 9 deleted comment1
		-- 10 deleted favorite1

		-- Strip the id's off the updates and errors as they will all be new.
		for i = 1, 6 do
			stripUpdateField( i, "id" )
		end

		stripUpdateField( 3, 'links', 'self', 'href' ) -- comment1
		stripUpdateField( 6, 'links', 'self', 'href' ) -- favorite

		for i = 7, 10 do
			stripUpdateField( i, "new_id" ) -- strip from deletes
		end

		return mockDataAsTable, ozDataAsTable

	end

	rxTest:expectSingleValue( pollForUpdateWith( 14 * waitMultiplier, catalogSpec, catalogId, puller, {
			albumAssetUpdateResponse( trashAlbumAssetId, trashAlbumAssetRevId, albumId, trashAssetId ), -- new unknown id and revId will be created by oz
			trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, trashCommentId, trashCommentRevId, spaceId, trashAssetId, commentBody1 ),
			assetGoneErrorUpdateResponse( albumAssetId2, albumAssetRevId2, 'album_asset', trashAssetId, {} ), -- create should fail with error 410 as asset was deleted by oz.
			assetGoneErrorUpdateResponse( commentId2, commentRevId2, 'comment', trashAssetId ), -- create should fail with error 410 as asset was deleted by oz.
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, trashFavoriteId, trashFavoriteRevId, spaceId, trashAssetId ),
			albumAssetDeletedResponse( albumAssetId, userId, trashAlbumAssetId ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			commentDeletedResponse( commentId, userId, trashCommentId ),
			favoriteDeletedResponse( favoriteId, userId, assetId, trashFavoriteId ),
		},
		transformOrderAndStripAssocDocs )
	, 'success' )

	-- WFCatErrorHandler should have updated the asset ids on the newly added associated documents
	-- when it encounters error 410 which results from attempting to push them up to with
	-- the original asset id. (which was deleted by the other client).

	rxTest:sleep( 1 ) -- Make sure overlays applied by errorhandler take effect

	-- Make sure the trash asset belongs to both albums
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], trashAssetId,
		quiesceUntilUpToDate = true,
	})
	:expectDataWithinTime( 5, sortByFirstElement( { { albumId }, { albumId2 } } ) )
	:dispose()

	local function arraySize( queryResults )
		return #queryResults
	end

	-- Make sure the trash asset still has the favorite attached
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectData( 1 )
	:dispose()

	-- Make sure the trash asset still has the comments attached
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], trashAssetId,
		quiesceUntilUpToDate = true,
	}:map( arraySize ))
	:expectData( 2 )
	:dispose()

	-- Push the new album_asset and comment to the trash asset.
	catalogSpec:expectHttpRequest( pushCreateAlbumAndCommentDocsReqResp( catalogId, trashAssetId, trashAssetRevId, albumId2, spaceId, commentId2, commentBody2 ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.restore then

		-- Restore the trash asset after syncing it.
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

		-- Make sure the restored asset belongs to both albums
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
		:dispose()

		-- Make sure the restored asset still has the favorite attached
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 1 )
		:dispose()

		-- Make sure the restored asset still has the comments attached
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 2 )
		:dispose()

		catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, trashAssetId, trashAssetRevId ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the restored asset (with the links that point to the existing master),
		-- the restored asset's album_assets, and tombstones of the trash album_assets
		-- back in the sync feed.
		local restoredAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		local restoredAlbumAssetRevId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentId = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentRevId = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredCommentRevId2 = KSIdGenerator.uuidInLowercaseHex()
		local restoredFavoriteId = KSIdGenerator.uuidInLowercaseHex()
		local restoredFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()


		local function transformOrderAndStrip_RestoreAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

			-- Remove the sporadic feedback user / space update before running through
			-- stripUpdatesAndLinks to make sure things line up properly.
			local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
			removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

			mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

			local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
			ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}

			if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
				return {}, {} -- don't compare partial updates
			end

			local function stripUpdateField( i, ... ) return stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, ... ) end

			-- Sort order of updates for delete should be:
				-- 1 album_asset1
				-- 2 album_asset2
				-- 3 asset
				-- 4 comment1
				-- 5 comment2
				-- 6 favorite1
				-- 7 deleted album_asset1
				-- 8 deleted album_asset2
				-- 9 deleted asset (trash)
				-- 10 deleted comment1
				-- 11 deleted comment2
				-- 12 deleted favorite1

			-- Strip the id's off the updates as they will all be new.
			for i = 1, 6 do
				stripUpdateField( i, "id" )
			end

			-- Strip the links in the comments and favorites as they have an new id in them.
			for i = 4, 6 do
				stripUpdateField( i, 'links', 'self', 'href' )
			end

			-- Strip the id's off the deletes as we didn't bother to retrieve them above.
			-- Also strip the new_id as they will all be new and unknown.
			for i = 7, 12 do
				stripUpdateField( i, 'id' )
				stripUpdateField( i, 'new_id' )
			end

			return mockDataAsTable, ozDataAsTable

		end

		-- Make sure renditions are generated for the restored asset
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
				albumAssetUpdateResponse( restoredAlbumAssetId, restoredAlbumAssetRevId, albumId, restoredAssetId ), -- new unknown id and revId will be created by oz
				albumAssetUpdateResponse( restoredAlbumAssetId2, restoredAlbumAssetRevId2, albumId2, restoredAssetId ), -- new unknown id and revId will be created by oz
				restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
				WFCatTrashTestUtils.commentUpdateResponse( catalogId, restoredCommentId, restoredCommentRevId, spaceId, restoredAssetId, commentBody1 ),
				WFCatTrashTestUtils.commentUpdateResponse( catalogId, restoredCommentId2, restoredCommentRevId2, spaceId, restoredAssetId, commentBody2 ),
				WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, restoredFavoriteId, restoredFavoriteRevId, spaceId, restoredAssetId ),
				albumAssetDeletedResponse( trashAlbumAssetId, userId, restoredAlbumAssetId ),
				albumAssetDeletedResponse( albumAssetId2, userId, restoredAlbumAssetId2 ),
				assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
				commentDeletedResponse( trashCommentId, userId, restoredCommentId ),
				commentDeletedResponse( commentId2, userId, restoredCommentId2 ),
				favoriteDeletedResponse( trashFavoriteId, userId, trashAssetId, restoredFavoriteId ),
			}
			, transformOrderAndStrip_RestoreAssocDocs )
		, 'success' )

		-- Make sure the original albumAssetId was deleted
		rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId )
			:pluck( '_deleted' )
		, true )

		-- Make sure the original albumAssetId was deleted
		rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId2 )
			:pluck( '_deleted' )
		, true )

		-- Make sure the original album_assets were in fact deleted.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ?]], assetId,
			quiesceUntilUpToDate = true,
		})
		:expectData { }
		:dispose()

		-- Make sure the restored asset still belongs to both albums after oz
		-- deletes the updated album_assets and adds new ones.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? ORDER BY albumId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData( sortByFirstElement( { { albumId }, { albumId2 } } ) )
		:dispose()


		-- Make sure the restored asset has new comments and favorites attached as oz will delete
		-- the old ones and create new ones for compatibility with older clients.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM spacefavorite where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 1 )
		:dispose()

		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT docId FROM comment where assetId == ? ORDER BY docId]], restoredAssetId,
			quiesceUntilUpToDate = true,
		}:map( arraySize ))
		:expectData( 2 )
		:dispose()

		-- 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 )

		-- Make sure no album assets are laying around for the original or trash asset.
		rxTest:observe( catalog:indexStore():query {
			sql = [[SELECT albumId FROM ]] .. albumAssetTable .. [[ where assetId == ? or assetId == ? ]], assetId, trashAssetId,
			quiesceUntilUpToDate = true,
		})
		:expectData { }
		:dispose()

	end

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddToAlbums_OtherDelete_assocDocs( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest, { mock = true } )
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddToAlbums_OtherDelete_assocDocs_testNightly( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest )
end

--==============================================================================
-- @mark tests that involve setting the cover on assets that are being deleted
--==============================================================================

function WFCatTrashTest.rxTest_CT_MockSetCoverSyncDelete( rxTest )

	-- NMB-10225
	-- Scenario:
	-- 1 Set and asset to be the album cover
	-- 2 let album update sync up to oz
	-- 3 Delete the asset.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- Only the delete asset is sent to oz
		-- The album update isn't sent to oz as oz will do it for us.
	rxTest:expectEqual( false, failStubbedTests )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzSetCoverSyncDelete_testNightly( rxTest )

	-- NMB-10225
	-- Scenario:
	-- 1 Set and asset to be the album cover
	-- 2 let album update sync up to oz
	-- 3 Delete the asset.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- Only the delete asset is sent to oz
		-- The album update isn't sent to oz as oz will do it for us.
	rxTest:expectEqual( false, failStubbedTests )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockSetCoverDelete( rxTest )

	-- NMB-10226
	-- Scenario:
	-- While offline the user sets the album cover to an asset
	-- User then deletes the asset and then turns on sync.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- The album update and the delete asset are sent.
		-- if we receive an error related to 'asset must exist' it is ignored.

	-- Oz can send this error as well depending on how the updates
	-- get processed:
		-- {"code":1005,"description":"Input Validation Error","errors":{"cover":["asset must exist"]},"subtype":"InputValidationError"}

-- TODO [kcarlson 2018-10-09]: Look into not sending the album update in this case. (2.0 currently does)

	rxTest:expectEqual( false, failStubbedTests )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzSetCoverDelete_testNightly( rxTest )

	-- NMB-10226
	-- Scenario:
	-- While offline the user sets the album cover to an asset
	-- User then deletes the asset and then turns on sync.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- The album update and the delete asset are sent.
		-- if we receive an error related to 'asset must exist' it is ignored.

	-- Oz can send this error as well depending on how the updates
	-- get processed:
	-- {"code":1005,"description":"Input Validation Error","errors":{"cover":["asset must exist"]},"subtype":"InputValidationError"}

	rxTest:expectEqual( false, failStubbedTests )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzDeleteOtherSetCover_testNightly( rxTest )

	-- NMB-10224
	-- Scenario:
	-- Client1 while offline deletes asset A
	-- Client2 while online set album cover to asset A
	-- Client1 turns on sync.
	-- Expected: client1 will not show album with deleted asset, and will automatically pick a new cover
	rxTest:expectEqual( false, failStubbedTests )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockDeleteOtherSetCover( rxTest )

	-- NMB-10224
	-- Scenario:
	-- Client1 while offline deletes asset A
	-- Client2 while online set album cover to asset A
	-- Client1 turns on sync.
	-- Expected: client1 will not show album with deleted asset, and will automatically pick a new cover
	rxTest:expectEqual( false, failStubbedTests )

end

--==============================================================================
-- @mark tests that involve assets with partially synced data (from oz wiki)
--==============================================================================

local function testAddPartialDelete( 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 = 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 expectedUserCreated = userCreated
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )
	
	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype
	
	
	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil 
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Delete asset before binaries (master, xmp and xidecar) are uploaded
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Get the revisionId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

	local waitForRenditions = not options.noWaitForRenditions

	-- Get the trashed asset back in the sync feed with the links to upload the binaries
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, { master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Make sure trash has original
	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 )

	uploader:startContinuousUploads()
		
	-- Upload binaries to trash asset	
	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, trashAssetId, trashRevId, xmpDevelopFilePath ) )
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded before master is uploaded
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, trashAssetId, trashRevId, xmpSidecarFilePath ) )

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, options.xmpDevelop, options.xmpSidecar ),
		}, stripForAssetUpdate( false ) ), 'success' )
	end
	
	trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, trashAssetId, trashRevId, originalFilePath, nil, options.xmpSidecar ) )

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

	-- Wait for the upload master to complete, (it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()
	
	if waitForRenditions then
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )
	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
	}, stripForAssetUpdate( waitForRenditions ) ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
		: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( trashAssetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()
	end

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartial_Delete( rxTest )

-- NMB-9960
--Scenario:
--
--1 Add a photo (Alternate step 1: add photo that has a sidecar file)
--2 Pause Binary Uploader using debug tools panel (new debug option to be written)
--3 Modify the photo with a lot of brush strokes to cause it to need to use external develop settings (we could add a debug tool to show when this happens if needed)
--4 Delete the photo
--5 Turn on the Debug Tools Binary Uploader
--6 View trash on another client and make sure the photo shows up
--
--Expected Result:
--
--Binaries are uploaded to trash
--Trash asset has master, sidecar xmp, and external xmp_develop file.

	testAddPartialDelete( rxTest, { mock = true, xmpDevelop = true, xmpSidecar = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartial_Delete_testNightly( rxTest )

-- NMB-9960
--Scenario:
--
--1 Add a photo (Alternate step 1: add photo that has a sidecar file)
--2 Pause Binary Uploader using debug tools panel (new debug option to be written)
--3 Modify the photo with a lot of brush strokes to cause it to need to use external develop settings (we could add a debug tool to show when this happens if needed)
--4 Delete the photo
--5 Turn on the Debug Tools Binary Uploader
--6 View trash on another client and make sure the photo shows up
--
--Expected Result:
--
--Binaries are uploaded to trash
--Trash asset has master, sidecar xmp, and external xmp_develop file.

	testAddPartialDelete( rxTest, { xmpDevelop = true, xmpSidecar = true } )
	
end

--------------------------------------------------------------------------------

local function testAddPartialDeleteRestore( 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 = 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 expectedUserCreated = userCreated
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )
	
	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype
	
	
	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil 
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Delete asset before binaries (master, xmp and xidecar) are uploaded
	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 with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, '_localOnly', 'universal', 'trash', 'createFrom', 'id' ))
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Restore asset before binaries (master, xmp and xidecar) are uploaded
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )

	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( restoredDocValue, '_localOnly', 'universal', 'trash', 'createFrom', 'id' ) )
	rxTest:expectEqual( revisionId, derefOpt( restoredDocValue, '_localOnly', 'universal', 'trash', 'createFrom', 'revision_id' ) )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Verify we send up the delete of the original asset, and the create of the restored asset
	catalogSpec:expectHttpRequest( pushDeleteRestoreAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId, restoredAssetId, restoredRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )
	
	if not options.mock then
		-- wait for the restore to occur on Oz. This can take a bit and sometimes gets split across two puller updates which
		-- will confuse the intermediate results the tests are expecting to see
		rxTest:expectSingleValue( pollUntilAssetExistsWithLinks( 3 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, {}, options ), true )
	end
	
	local waitForRenditions = not options.noWaitForRenditions

	-- Get the restored asset back in the sync feed with the links to upload the binaries
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, false, false, false, false, expectedDevelopSettings )
	} ), 'success' )

	-- Make sure restored asset has original
	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 )


	-- Upload binaries to restored asset	
	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredRevId, xmpDevelopFilePath ) )
	end

	if options.xmpSidecar then
		-- /rels/xmp gets uploaded before master
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, restoredAssetId, restoredRevId, xmpSidecarFilePath ) )
	end
	
	uploader:startContinuousUploads()

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end
	
	if options.xmpSidecar then

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		-- Get the updated metadata with the master link.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, false, false, options.xmpDevelop, options.xmpSidecar ),
		}, stripForAssetUpdate( false ) ), 'success' )
	end
	
	restoredRevId = getLatestRevisionId( rxTest, catalog, restoredAssetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()
	
	if waitForRenditions then
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )
	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop, options.xmpSidecar ),
	}, stripForAssetUpdate( waitForRenditions ) ), '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 xmpDevelop
	if options.xmpDevelop then
		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()
	end



end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartial_DeleteRestore( rxTest )

-- NMB-10672
--Scenario:
--
--1 Add a photo (Alternate step 1: add photo that has a sidecar file)
--2 Pause Binary Uploader using debug tools panel (new debug option to be written)
--3 Modify the photo with a lot of brush strokes to cause it to need to use external develop settings (we could add a debug tool to show when this happens if needed)
--4 Delete the photo
--5 Restore the photo
--6 Turn on the Debug Tools Binary Uploader
--7 View restored on another client and make sure the photo shows up
--
--Expected Result:
--
--Binaries are uploaded to restored
--Restored asset has master, sidecar xmp, and external xmp_develop file.

	testAddPartialDeleteRestore( rxTest, { mock = true, xmpDevelop = true, xmpSidecar = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartial_DeleteRestore_testNightly( rxTest )

	testAddPartialDeleteRestore( rxTest, { xmpDevelop = true, xmpSidecar = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartial_Sync_testOtherDeleteSyncRestore( rxTest )

-- Add an asset that requires the use of external develop settings go offline
-- before the master or xmp get uploaded delete the asset from LrWeb then restore
-- the asset. Note other LrCC clients won't show the asset until it has the
-- master uploaded.
-- Go online and make sure the edits get applied to the asset.

-- Can also add a sidecar xmp file which isn't uploaded until master completes.

	rxTest:expectEqual( false, failStubbedTests )
end

--==============================================================================
-- @mark tests that involve assets with partially synced metadata (confirmation not received from Oz)
--==============================================================================

local function testAddDeletePartialSync( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 10 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local expectedUserCreated = userCreated
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )
	
	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype
	
	local docStore = catalog:docStore()
	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Delete asset before binaries (master, xmp and xidecar) are uploaded
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Make sure we create trash asset on Oz, but not wait the update response
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( docStore:docValue( assetId )
		:pluck( '_deleted' ),
	true )

	if options.delTrash then

		-- Delete trash asset before receiving trash update from Oz
		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

		-- Make sure the trash asset is deleted
		rxTest:expectSingleValue( docStore:docValue( trashAssetId ):pluck( '_deleted' ), true )

		-- Get the trashed asset back in the sync feed, we should ignore the update
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		}, stripForAssetUpdate( false ) ), 'success' )

		-- Conflict is expected when trash asset is deleted locally, but receive update from Oz
		rxTest:expectSingleValue( docStore:docValue( trashAssetId ):pluck( '_conflict' ), true)

		-- Wait the conflict to be solved by conflict resolver
		rxTest:observe( docStore:ongoingDocValue( trashAssetId ):pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()

		-- Get the latest revisionId for trash asset
		rxTest:expectSingleValue( docStore:docValue( trashAssetId )
			:map( function( trashDocValue )
				trashRevId = trashDocValue._revId
				return trashDocValue
			end )
			:pluck( '_deleted' ),
		true)

		-- Make sure we send purge trash to Oz after receiving trash update
		catalogSpec:expectHttpRequest( pushDeleteTrashReqResp( catalogId, trashAssetId, trashRevId ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the trash delete success from Oz
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, nil, userId ),
		} ), 'success' )

	elseif options.restore then

		local restoredAssetId, restoredRevId, restoredDocValue
		local restoredCreateWithRevId, restoredUserUpdated

		-- restore trash asset before receiving trash update from Oz
		rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )

		restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )
		restoredCreateWithRevId = WFCatTrashUtils.getCreateWithRevisionId( restoredDocValue )
		restoredUserUpdated = restoredDocValue.userUpdated

		-- Make sure the trash asset is deleted
		rxTest:expectSingleValue( docStore:docValue( trashAssetId ):pluck( '_deleted' ), true )

		-- Get the trashed asset back in the sync feed, we should ignore the update
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		}, stripForAssetUpdate( false ) ), 'success' )

		-- Conflict is expected when trash asset is deleted locally, but receive update from Oz
		rxTest:expectSingleValue( docStore:docValue( trashAssetId ):pluck( '_conflict' ), true)

		-- Wait the conflict to be solved by conflict resolver
		rxTest:observe( docStore:ongoingDocValue( trashAssetId ):pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()

		-- Get the latest revisionId for trash asset
		rxTest:expectSingleValue( docStore:docValue( trashAssetId )
			:map( function( trashDocValue )
				trashRevId = trashDocValue._revId
				return trashDocValue
			end )
			:pluck( '_deleted' ),
		true)

		-- Send create restored asset with trash info.
		catalogSpec:expectHttpRequest( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredCreateWithRevId, trashAssetId, trashRevId, restoredUserUpdated ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the trash delete success from Oz
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId ),
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		}, stripForAssetUpdate( false ) ), 'success' )

		-- Make sure the restored asset has local files
		rxTest:expectSingleValue( docStore:docValue( restoredAssetId )
			:map( function( docValue )
				restoredRevId = docValue._revId

				local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
				return originalPath and WFFileUtils.exists( originalPath ) == 'file'
			end ),
		true )

		-- Upload the master
		catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, restoredAssetId, restoredRevId, originalFilePath ) )

		uploader:startContinuousUploads()

		-- Wait for the upload master to complete, it will upload after the xmp_develop if there is one.
		rxTest:observe( pollUploadCount( uploader, 'master' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()

		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		-- Get the updated metadata with the master link.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop )
		}, stripForAssetUpdate( true ) ), 'success' )

		-- Wait for the update to get applied to the assets docValue
		rxTest:observe( docStore:ongoingDocValue( restoredAssetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()

		-- Make sure the original asset was deleted
		rxTest:expectSingleValue( docStore:docValue( assetId ):pluck( '_deleted' ), true )

		-- Make sure the trash asset was deleted
		rxTest:expectSingleValue( docStore:docValue( trashAssetId ):pluck( '_deleted' ), true )

	end

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddDeletePartialSync_DelTrash( rxTest )

-- NMB-10416
--Scenario:
--
--1 Pause Sync
--2 Add a photo
--3 Delete the photo
--4 Turn on the Debug tools metadataPusher to push the trash asset creation
--5 Purge Trash
--6 Turn on Sync
--
--Expected Result:
--
--The trash asset should be purged on Oz and on every device

	testAddDeletePartialSync( rxTest, { mock = true, delTrash = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddDeletePartialSync_DelTrash_testNightly( rxTest )

-- NMB-10416

	testAddDeletePartialSync( rxTest, { delTrash = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddDeletePartialSync_Restore( rxTest )

-- NMB-10414
--Scenario:
--
--1 Pause Sync
--2 Add a photo
--3 Delete the photo
--4 Turn on the Debug tools metadataPusher to push the trash asset creation
--5 Restore asset
--6 Turn on Sync
--
--Expected Result:
--
--The restored asset should be in All Photo grid, and trash asset is removed

	testAddDeletePartialSync( rxTest, { mock = true, restore = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddDeletePartialSync_Restore_testNightly( rxTest )

-- NMB-10414

	testAddDeletePartialSync( rxTest, { restore = true } )
	
end

--------------------------------------------------------------------------------

local function testAddPartialSyncDelete( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 10 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local expectedUserCreated = userCreated
	local trashPurgeDate
	
	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )
	
	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype
	
	
	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	-- Send the metadata for the newly added asset, but don't wait for the response
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Delete asset before binaries (master, xmp and xidecar) are uploaded
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )	
	
	if options.pullConflict then
	
		-- Get the metadata for original asset
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
									{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
		}, stripForAssetUpdate( false ) ), 'success' )
		
		-- Conflict is expected when original asset is deleted locally, but receive update from Oz
		rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
			:map( function( docValue ) 
				return docValue
			end )
			:pluck( '_conflict' ),
		true)
		
		-- Wait the conflict to be solved by conflict resolver
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId ):pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()
		
	end

	-- Get the latest revisionId for original
		rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
			:map( function( docValue ) 
				revisionId = docValue._revId
				return docValue
			end )
			:pluck( '_deleted' ),
		true)

	-- Make sure we send delete original asset with trash info, based on pusher counter data.
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, options.xmpDevelop ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
	}, transformOrderAndStripAssetUpdate( false ) ), 'success' )
	
	-- Update the trashRevId with the latest revision id before issuing the createMasterReqResp
	trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )

	-- Make sure master file exists with trash asset
	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 )

	-- Upload the master
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the proxy and renditions before requesting next update
	rxTest:expectSingleValue( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true )
	}), 'success' )
	
	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartialSyncDelete( rxTest )

--Scenario: (LRMI-4190377 / NMB-10383)
--
--1 Pause Sync
--2 Add a photo
--3 Turn on the Debug tools metadataPusher to push the trash asset creation
--4 Delete asset
--6 Turn on Sync
--
--Expected Result:
--
--The trash asset should be in trash can, and original asset is deleted

	testAddPartialSyncDelete( rxTest, { mock = true } )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartialSyncDelete_testNightly( rxTest )

-- LRMI-4190377 / NMB-10383

	testAddPartialSyncDelete( rxTest )

end
	
--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartialSyncDelete_PullConflict_testNightly( rxTest )

-- LRMI-4190377 / NMB-10383

	testAddPartialSyncDelete( rxTest, { pullConflict = true } )

end	

--------------------------------------------------------------------------------

local function testAddPartialSyncAddAssocDocsPartialSyncDelete( 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 = 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 subtype = 'image'
	local fileName = 'IMG_7763.JPG'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype
	
	
	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		-- TODO [kcarlson 2018-07-09]:  Not sure why this doesn't get issued when using oz
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 0: Create album and space

	-- Add album
	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId
	local albumName = 'Album1'
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumId,
			type = 'album',
			subtype = 'collection',
			name = albumName,
		}
		:map( function( revId )
			albumRevId = revId
			return revId
		end )
	, ':ignore:' )
	
	-- Sync up the album
	catalogSpec:expectHttpRequest( pushCreateAlbumReqResp( catalogId, albumId, albumName ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added album
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )

	-- Add spce 
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceAlbumId = KSIdGenerator.uuidInLowercaseHex()
	
	local now = KSDate.timeInIso8601Format()

	local spaceAlbumRevId
	rxTest:expectSingleValue( catalog:docStore():bulkApplyOverlay(
		{
			label = 'oz',
			{
				_id = spaceId,
				userCreated = now,
				userUpdated = now,
				type = 'space',
				subtype = 'shared',
				private = false,
				download = false,
				metadata = false,
				location = false,
				presentationFilters = true,
			},
			{
				_id = spaceAlbumId,
				type = 'space_album',
				space = { id = spaceId },
				album = { id = albumId },
			}
		} ) :map( function( overlayResults )
				spaceAlbumRevId = derefOpt( overlayResults, 2, 'revId' )
				return overlayResults
			end )
	, ':ignore:' )

	-- Now create the space via a direct put as MetadataPusher doesn't handle 'space' docs.
	local createSpaceUrl = string.format( '%s/spaces/%s', catalogSpec:catalogHref(), spaceId )
	local createSpaceOptions = {
		requestMethod = 'PUT',
		requestHeaders = {
			{ header = 'Content-Type', value = 'application/json' },
		},
	}
	local createSpaceData = KSJsonEncoder.encodeOzUploadJson( {
		payload = {
			private = false,
			download = false,
			metadata = false,
			location = false,
			presentationFilters = true,
		},
		subtype = 'shared',
		resources = {
			{
				id = albumId,
				type = 'album',
				space_album_id = spaceAlbumId,
			}
		}
	} )

	catalogSpec:expectHttpRequest( putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		spaceUpdateResponse( catalogId, spaceId )
	}), 'success' )
	

-- Step 1: Add asset and album_asset

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil 
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end
	
	-- Add album_asset
	local albumAssetId = KSIdGenerator.uuidInLowercaseHex()
	local albumAssetRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			_id = albumAssetId,
			type = 'album_asset',
			album = { id = albumId },
			asset = { id = assetId }
		}
		:map( function( revId )
			albumAssetRevId = revId
			return revId
		end )
	, ':ignore:' )

-- Step 2: Add Assoc docs and push create request to Oz. Don't pull confirmation

	-- Add comment
	local commentId = KSIdGenerator.uuidInLowercaseHex()
	local commentBody = "Comment1 Mumble, mumble, mumble"
	local commentRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'comment',
			subtype = 'user',
			_id = commentId,
			body = commentBody,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = { created = '2018-12-15T21:21:21Z' },
		}
		:map( function( revId )
			commentRevId = revId
			return revId
		end )
	, ':ignore:' )

	-- Add favorite
	local favoriteId = KSIdGenerator.uuidInLowercaseHex()
	local favoriteRevId
	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
			type = 'favorite',
			subtype = 'user',
			_id = favoriteId,
			space = { id = spaceId },
			asset = { id = assetId },
			_localOnly = { created = '2018-12-15T21:21:21Z' },
		}
		:map( function( revId )
			favoriteRevId = revId
			return revId
		end )
	, ':ignore:' )
	
	catalogSpec:expectHttpRequest( pushCreateAssetAndAssocDocsReqResp( catalogId, assetId, fileName, subtype, albumId, spaceId, commentId, commentBody, favoriteId ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 4, kTimeoutForSendingUpdates ) )

	-- Make sure the album_asset, comment and favorite are correctly stored
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM ]] .. albumAssetTable .. [[ where albumId == ?]], albumId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { assetId } } )
	:dispose()
	
-- Step 3: Delete Asset

	-- Delete the asset which is on Oz
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created
	local trashAssetId, trashRevId = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectEqual( 'string', type( trashAssetId ) )
	rxTest:expectEqual( 'string', type( trashRevId ) )
	
	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
			return docValue
		end )
	, ':ignore:' )
	
	-- Make sure comment is against trash asset
	rxTest:expectSingleValue( catalog:docStore():docValue( commentId )
		:map( function( commentDocValue ) 
			return commentDocValue.asset.id == trashAssetId
		end ),
	true)
	
	-- Make sure favorite is against trash asset
	rxTest:expectSingleValue( catalog:docStore():docValue( favoriteId )
		:map( function( favoriteDocValue ) 
			return favoriteDocValue.asset.id == trashAssetId
		end ),
	true)


-- Step 4: Push changes / Sync down confirmation of assoc docs. Solve conflict

	if options.pullConflict then
	
		-- Clients receive confirmation before pushing updates. Should resolve conflict and then push correct updates
	
		-- Get the confirmation for asset and assoc docs: album_asset, comment and favorite
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
									{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings ),
			albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody ),
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId ),
			feedbackUserUpdate(),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
		
		-- Conflict is expected when original asset is deleted locally, but receive update from Oz
		rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
			:map( function( assetDocValue )
				revisionId = assetDocValue._revId
				return assetDocValue
			end )
			:pluck( '_conflict' ),
		true)
		
		-- Conflict is expected when album_asset is updated locally, but receive old update from Oz
		rxTest:expectSingleValue( catalog:docStore():docValue( albumAssetId )
			:map( function( albumAssetDocValue ) 
				return albumAssetDocValue
			end )
			:pluck( '_conflict' ),
		true)
		
		-- Conflict is expected when comment is updated locally, but receive old update from Oz
		rxTest:expectSingleValue( catalog:docStore():docValue( commentId )
			:map( function( commentDocValue ) 
				return commentDocValue
			end )
			:pluck( '_conflict' ),
		true)
		
		-- Conflict is expected when favorite is updated locally, but receive old update from Oz
		rxTest:expectSingleValue( catalog:docStore():docValue( favoriteId )
			:map( function( favoriteDocValue ) 
				return favoriteDocValue
			end )
			:pluck( '_conflict' ),
		true)
		
		-- Wait for the conflict to be resolved
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()
		
		-- Wait for the conflict to be resolved
		rxTest:observe( catalog:docStore():ongoingDocValue( albumAssetId )
		:map( function( albumAssetDocValue ) 
			albumAssetRevId = albumAssetDocValue._revId
			return albumAssetDocValue
		end )
		:pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()
		
		-- Wait for the conflict to be resolved
		rxTest:observe( catalog:docStore():ongoingDocValue( commentId ):pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()
		
		-- Wait for the conflict to be resolved
		rxTest:observe( catalog:docStore():ongoingDocValue( favoriteId ):pluck( '_conflict' ) )
		:expectDataWithinTime( 5 * waitMultiplier, nil )
		:dispose()
	
		-- Sync up delete original asset and album_asset update (don't update favorite and comment if client has _serverProvidedMetadata)
		catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
		-- Get the confirmation for original asset deletion, trash asset update. 
		-- New album_asset, comment and favorite update, and old album_asset, comment and favorite update error and delete	
		local newAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
		local newAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		local newCommentId = KSIdGenerator.uuidInLowercaseHex()
		local newCommentRevId = KSIdGenerator.uuidInLowercaseHex()
		local newFavoriteId = KSIdGenerator.uuidInLowercaseHex()
		local newFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()
		
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, false, false, expectedDevelopSettings ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			albumAssetUpdateResponse( newAlbumAssetId, newAlbumAssetRevId, albumId, trashAssetId ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, newCommentId, newCommentRevId, spaceId, trashAssetId, commentBody ),
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, newFavoriteId, newFavoriteRevId, spaceId, trashAssetId ),
			albumAssetDeletedResponse( albumAssetId, userId, newAlbumAssetId ),
			commentDeletedResponse( commentId, userId, newCommentId ),
			favoriteDeletedResponse( favoriteId, userId, assetId, newFavoriteId ),
		}, function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
	
				-- Remove the sporadic feedback user / space update before running through
				-- stripUpdatesAndLinks to make sure things line up properly.
				local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
				removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

				mockDataAsTable, ozDataAsTable = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
				local mockUpdateTable = derefOpt( mockDataAsTable, 'updates' ) or {}
				ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
				if #mockUpdateTable == 0 or ( ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable ) then
					return {}, {} -- don't compare partial updates
				end
				
				for i, update in pairs( mockUpdateTable ) do
					if update.type == "album_asset" or update.type == "comment" or update.type == "favorite" then
						if update.deleted then
							stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, "new_id" )
						else
							if update.type == "album_asset" then
								newAlbumAssetId = derefOpt( ozUpdateTable[i], "id" )
							elseif update.type == "comment" then
								newCommentId = derefOpt( ozUpdateTable[i], "id" )
							elseif update.type == "favorite" then
								newFavoriteId = derefOpt( ozUpdateTable[i], "id" )
							end
							
							stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, "id" )
							stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, "links", "self", "href" )
						end
					elseif update.type == "error" then
						stripBothUpdateFields( stripped, mockUpdateTable, ozUpdateTable, i, "id" )
					end
					
				end
				
				stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
				
				return mockDataAsTable, ozDataAsTable
			
			end 
		), 'success' )
		
		rxTest:sleep( .5 )
		
		-- Make sure there's no more update pushed
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0 ) )
		
		albumAssetId = newAlbumAssetId
		commentId = newCommentId
		favoriteId = newFavoriteId
	
	elseif options.resourceExistsError then
		
		-- Only mocked unit test, because we cannot predict Oz response to be assetGoneError or ResourceExistsError for comment and favorite
		
		-- Sync up delete original asset and assoc docs create
		catalogSpec:expectHttpRequest( pushDeleteAssetAndCreateAssocsReqResp( catalogId, assetId, trashAssetId, trashRevId, spaceId, albumId, commentId, commentBody, favoriteId ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 4, kTimeoutForSendingUpdates ) )
	
		-- Get the confirmation for original asset deletion, trash asset update. 
		-- New album_asset, comment and favorite update, and old album_asset, comment and favorite update error and delete
		local newAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
		local newAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		local newCommentId = KSIdGenerator.uuidInLowercaseHex()
		local newCommentRevId = KSIdGenerator.uuidInLowercaseHex()
		local newFavoriteId = KSIdGenerator.uuidInLowercaseHex()
		local newFavoriteRevId = KSIdGenerator.uuidInLowercaseHex()
		
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, false, false, expectedDevelopSettings ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			
			joinIdAlreadyInUseErrorUpdateResponse( "create", albumAssetId, albumAssetRevId, "album_asset" ),
			resourceExistsErrorUpdateResponse( "create", commentId, commentRevId, "comment", catalogId ),
			resourceExistsErrorUpdateResponse( "create", favoriteId, favoriteRevId, "favorite", catalogId ),
			
			albumAssetUpdateResponse( newAlbumAssetId, newAlbumAssetRevId, albumId, trashAssetId ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, newCommentId, newCommentRevId, spaceId, trashAssetId, commentBody ),
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, newFavoriteId, newFavoriteRevId, spaceId, trashAssetId ),
			
			albumAssetDeletedResponse( albumAssetId, userId, newAlbumAssetId ),
			commentDeletedResponse( commentId, userId, newCommentId ),
			favoriteDeletedResponse( favoriteId, userId, assetId, newFavoriteId ),
			
			feedbackUserUpdate(),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
		
		rxTest:sleep( .5 )
		
		-- Make sure there's no more update pushed
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0 ) )
		
		albumAssetId = newAlbumAssetId
		commentId = newCommentId
		favoriteId = newFavoriteId
	
	elseif options.assetGoneError then
		
		-- Only mocked unit test, because we cannot predict Oz response to be assetGoneError or ResourceExistsError for comment and favorite
		
		-- Sync up delete original asset and assoc docs create
		catalogSpec:expectHttpRequest( pushDeleteAssetAndCreateAssocsReqResp( catalogId, assetId, trashAssetId, trashRevId, spaceId, albumId, commentId, commentBody, favoriteId ) )
		
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 4, kTimeoutForSendingUpdates ) )
	
		-- Get the confirmation for original asset deletion, trash asset update. 
		-- New album_asset, comment and favorite update, and old album_asset, comment and favorite update error and delete
		local newAlbumAssetId = KSIdGenerator.uuidInLowercaseHex()
		local newAlbumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, false, false, expectedDevelopSettings ),
			assetDeletedResponse( assetId, trashAssetId, userId ),
			
			joinIdAlreadyInUseErrorUpdateResponse( "create", albumAssetId, albumAssetRevId, "album_asset" ),
			assetGoneErrorUpdateResponse( commentId, commentRevId, 'comment', trashAssetId ),
			assetGoneErrorUpdateResponse( favoriteId, favoriteRevId, 'favorite', trashAssetId ),
			
			albumAssetUpdateResponse( newAlbumAssetId, newAlbumAssetRevId, albumId, trashAssetId ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, trashAssetId, commentBody ),
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, trashAssetId ),
			
			albumAssetDeletedResponse( albumAssetId, userId, newAlbumAssetId ),
			
			feedbackUserUpdate(),
		}, transformOrderAndStripAssetUpdate( false )), 'success' )
		
		rxTest:sleep( .5 )
		
		-- Make sure there's no more update pushed
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0 ) )
		
		albumAssetId = newAlbumAssetId
		
	end


-- Step 5: 
--		Sync up assoc doc update and upload binaries. 
-- 		Make sure trash asset is with correct assoc docs and binaries
	
	-- Make sure the album_asset, comment and favorite are correctly against trash asset
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT assetId FROM ]] .. albumAssetTable .. [[ where albumId == ?]], albumId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { trashAssetId } } )
	:dispose()
	
	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId, assetId FROM spacefavorite where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { favoriteId, trashAssetId } } )
	:dispose()

	rxTest:observe( catalog:indexStore():query {
		sql = [[SELECT docId, assetId FROM comment where spaceId == ?]], spaceId,
		quiesceUntilUpToDate = true,
	})
	:expectData( { { commentId, trashAssetId } } )
	:dispose()
	
	-- Make sure the restored asset has local files
	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 )
	
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
	:map( function( docValue )
		local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
		local numRevisionIds = #revisionIds
		trashRevId = revisionIds[ numRevisionIds ]
		return true
	end ),
	true )
	
	uploader:startContinuousUploads()
	
	local function stripForAssetUpdateAndRemoveSporadicFeedbackUser( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
		ozDataAsTable = removeSporadicSpaceOrFeedbackFromOzUpdates( ozDataAsTable )
		local assetStripFn = stripForAssetUpdate( true )
		return assetStripFn( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )
	end
	
	if options.xmpDevelop then
		-- /rels/xmp/develop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, trashAssetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, trashAssetId, trashRevId, xmpDevelopFilePath ) )
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded before master
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, trashAssetId, trashRevId, xmpSidecarFilePath ) )

		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false, options.xmpDevelop, options.xmpSidecar  ),
		}, stripForAssetUpdateAndRemoveSporadicFeedbackUser ), 'success' )
	end
	
	trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, trashAssetId, trashRevId, originalFilePath, nil, options.xmpSidecar ) )

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	end

	-- Wait for the upload master to complete, it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, options.xmpDevelop, options.xmpSidecar ),
	}, stripForAssetUpdateAndRemoveSporadicFeedbackUser ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
		: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( trashAssetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', '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 )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartialSyncAddAssocDocsPartialSyncDelete_AssetGoneError( rxTest )

-- LRMI-4190611
--Scenario:
--
--1 Pause Sync
--2 Add a photo into a shared album
--3 Add favorite and comment
--4 Turn on Debug tools metadataPusher to push the asset, album_asset, comment and favorite creation
--5 Delete asset
--6 Turn on Sync
--7 Restore asset
--
-- Comment and favorite receives AssetGoneError:
-- Delete happened before "Create F1 against A": So, A has no favorite at the time of delete
-- Delete happened on Oz: A has no favorite on Oz
-- Client sends Create F1 against A: AssetGoneError with B id
-- Client sends Create F1 against B: Will be created
-- Client sends Create F1 against B again ? (because of AssetGoneError): ResourceExists error
-- Documents in sync feed related to favorites: F1 and AssetGoneError
--
--Expected Result:
--
--The restored asset should be in the same album, with comment and favorite

	testAddPartialSyncAddAssocDocsPartialSyncDelete( rxTest, { mock = true, assetGoneError = true, xmpSidecar = true, xmpDevelop = true } )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartialSyncAddAssocDocsPartialSyncDelete_ResourceExistsError( rxTest )

-- LRMI-4190611
--Scenario:
--
--1 Pause Sync
--2 Add a photo into a shared album
--3 Add favorite and comment
--4 Turn on Debug tools metadataPusher to push the asset, album_asset, comment and favorite creation
--5 Delete asset
--6 Turn on Sync
--7 Restore asset
--
-- Comment and favorite receives ResourceExistsError:
-- Delete happened after "Create F1 against A" but before client had confirmation: A has favorite F1
-- Client sends Create F1 against A: No Error. Will be created
-- Delete happened on Oz: When delete happens, Oz creates F2 against B and F1 tombstone
-- Client sends Create F1 against B: ResourceExistsError as there is a tombstone for F1
-- Documents in sync feed related to favorites: F1 tombstone, F2
--
--Expected Result:
--
--The restored asset should be in the same album, with comment and favorite

	testAddPartialSyncAddAssocDocsPartialSyncDelete( rxTest, { mock = true, resourceExistsError = true, xmpSidecar = true, xmpDevelop = true } )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockAddPartialSyncAddAssocDocsPartialSyncDelete_PullConflict( rxTest )

-- LRMI-4190611
--Scenario:
--
--1 Pause Sync
--2 Add a photo into a shared album
--3 Add favorite and comment
--4 Turn on Debug tools metadataPusher to push the asset, album_asset, comment and favorite creation
--5 Delete asset
--6 Turn on Sync
--7 Restore asset
--
--Delete happened after "Create F1 against A" but before client had confirmation: A has favorite F1
--Client sends Create F1 against A: No Error. Will be created
--Clients Delete A
--Clients receive confirmation for A, F1 confirmation
--Clients locally conflict for F1, F1 against B
--Clients push delete A with B info, but no update for F1 (we don't handle updates for favorite if client has confirmation for favorite)
--Delete happened on Oz: When delete happens, Oz creates F2 against B and F1 tombstone
--Documents in sync feed related to favorites: F1 tombstone, F2
--
--Expected Result:
--
--The restored asset should be in the same album, with comment and favorite

	testAddPartialSyncAddAssocDocsPartialSyncDelete( rxTest, { mock = true, pullConflict = true, xmpSidecar = true, xmpDevelop = true } )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzAddPartialSyncAddAssocDocsPartialSyncDelete_PullConflict_testNightly( rxTest )

-- LRMI-4190611
--Scenario:
--
--1 Pause Sync
--2 Add a photo into a shared album
--3 Add favorite and comment
--4 Turn on Debug tools metadataPusher to push the asset, album_asset, comment and favorite creation
--5 Delete asset
--6 Turn on Sync
--7 Restore asset
--
--Delete happened after "Create F1 against A" but before client had confirmation: A has favorite F1
--Client sends Create F1 against A: No Error. Will be created
--Clients Delete A
--Clients receive confirmation for A, F1 confirmation
--Clients locally conflict for F1, F1 against B
--Clients push delete A with B info, but no update for F1 (we don't handle updates for favorite if client has confirmation for favorite)
--Delete happened on Oz: When delete happens, Oz creates F2 against B and F1 tombstone
--Documents in sync feed related to favorites: F1 tombstone, F2
--
--Expected Result:
--
--The restored asset should be in the same album, with comment and favorite

	testAddPartialSyncAddAssocDocsPartialSyncDelete( rxTest, { pullConflict = true, xmpSidecar = true, xmpDevelop = 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 = 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'

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )

	local originalFileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize

	local files_xmp_develop

	if options.xmpDevelop then

		local xmpDevelopString = 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 = 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
	rxTest:expectSingleValue( catalog:docStore():setDocValue( 
		{
			_id = trashAssetId,
			type = 'asset',
			subtype = trashAssetSubtype,
			userCreated = trashAssetCreated,
			userUpdated = trashAssetCreated,
			captureDate = "2018-01-01T00:00:00Z",
			purgeDate = purgeDate,
			_serverProvidedMetadata = 
			{
				links = {
					['/rels/master'] = {
						href = '/asset/revision/master'
					}
				}
			}
		} )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )


	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 countTrashAssetsObs = rxTest:observe( WFCatTrash.autoPurge( catalog ) )
			:expectData { purgeDate }
			
		countTrashAssetsObs
			:expectDataWithinTime( 3 * waitMultiplier, {} )
			:dispose()
		
		-- make sure the trash asset is purged
		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:pluck( '_deleted' ),
		true )
	
	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


--==============================================================================
-- @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 = 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 = getOriginalFilePath( 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 = 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )


	local function debugPollUntilHasRequiredLinks( timeoutSeconds, catalogSpec, catalogId, assetId, revisionId, subtype, isMock )

		local fileName = 'IMG_7763.JPG' -- just need to have one to satisfy getAssetReqResp we won't use the value as we are just looking for links
		local assetCreated
		local userCreated
		local importSource

		local httpRequest = 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(
					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 guarentee 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 hasRequiredLinks( docValue, expectedLinks )

		end

		return pollHttpGetAsset( timeoutSeconds, debugHasRequiredLinks, catalogSpec, catalogId, assetId, httpRequest )

	end

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end ),
	':ignore:' )

	-- Make sure we sync up the newly created asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false, xmpDevelop = false } )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the upload master to complete, it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- 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, _ = 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( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- Step 1: Add photo

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	-- 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 = 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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 = 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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
		}, transformOrderAndStripAssetUpdate( false ) ), 'success' )

		rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId } ) )
		local trashAssetId2, trashRevId2 = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
		rxTest:expectEqual( 'string', type( trashAssetId2 ) )
		rxTest:expectEqual( 'string', type( trashRevId2 ) )

		-- Get the RestoreAssetError
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, 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( 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( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, 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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
			restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, transformOrderAndStripAssetUpdate( false ) ), 'success' )

		restoredDocHasSPM:expectDataWithinTime( 2 * kOzWaitMultiplier, true )

	else

		-- Just get a RestoreAssetError to test
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, 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( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T20:20:20Z'
	local trashAssetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( trashAssetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local importSource = makeImportSource( catalog, fileName, importTimestamp )

-- 1) Pause Sync
-- 2) Add a large photo (to force it to upload the file in chunks).

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop } )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath ) )
	uploader:startContinuousUploads()

	-- Wait for the uploader to issue the create master request
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = true } )
	}, stripForAssetUpdate( true ) ), 'success' )

	-- 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 = 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( assetId, trashAssetId, userId ),
		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 = 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 = 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( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, assetId, true, false, false ),
			restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, transformOrderAndStripAssetUpdate( false ) ), 'success' )

	else

		-- Get a RestoreAssetError without a partial response
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId ),
		}, 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( 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( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		assetDeletedResponse( restoredAssetId, trashAssetId2, userId ),
		trashAssetUpdateResponse2( trashAssetId2, trashRevId2, fileName, trashAssetSubtype, trashAssetId, trashPurgeDate, userCreated, importSource, restoredAssetId, { master = true, renditions = true } ),
	}, 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 Tests that involve deleting synced assets
--==============================================================================

local function testAddSyncDeleteSyncWithVersion( 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 = 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 trashAssetSubtype = 'deleted_' .. subtype

	local expectedUserCreated = userCreated
	if not userCreated then
		expectedUserCreated = importTimestamp
		if importTimestamp == nil then
			expectedUserCreated = assetCreated
		end
	end

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		-- TODO [kcarlson 2018-07-09]:  Not sure why this doesn't get issued when using oz
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )

	local importSource = makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	-- Create the Version Document
	
	local versionName = 'Foo'
	local versionId
	local versionRevId
	local versionBody
	
	rxTest:waitFor( WFCatVersions.createVersionForAsset( catalog, assetId, false, versionName )
		:map( function( newId )
			versionId = newId
			return true
		end ) )
	
	local versionXmpDevelopFilePath
	rxTest:waitFor( catalog:docStore():docValue( versionId )
		:map( function( docValue )
			versionXmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )
			-- on mobile this path is relative so we have to derelativize it for the upload xmp call
			if WFPlatformInfo.isiOS then
				versionXmpDevelopFilePath = KSPathUtils.child( require 'OzFilePaths'.getCatalogBasePath( catalogId ), versionXmpDevelopFilePath )
			elseif WFPlatformInfo.isAndroid then
				versionXmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), versionXmpDevelopFilePath )
			end
			versionRevId = docValue._revId
						
			versionBody = {
				userCreated = docValue.userCreated,
				userUpdated = docValue.userUpdated,
				name = docValue.name,
				develop = docValue.develop,
				createdManually = false
			}
			
			return true
		end ) )
	
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateVersionReqResp( catalogId, versionId, assetId, versionName ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody )
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )
	
	-- Start the binary uploads
	
	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
	end
	
	if options.xmpDevelop then
		-- xmp_develop for the version document
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadVersionXmpDevelopReqResp( catalogId, versionId, versionRevId, versionXmpDevelopFilePath ) )
	end
	
	uploader:startContinuousUploads()
	
	-- Wait for the upload of xmp_develop for the asset
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 2 )
		:dispose()
	end
	
	if options.xmpSidecar then
		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
	
		local updates = {}
		if options.xmpDevelop then
			table.insert( updates, 
				WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true ) )		
		end
		table.insert( updates,
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = options.xmpSidecar } ) )
	
		-- if there's a sidecar the uploader is waiting for the sidecar to appear before it uploads the master so mark it so
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, 
			updates, 
			stripForAssetUpdate( false ) ), 'success' )
			
	end
	
	-- Upload the master
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 20 * waitMultiplier, 1)
	:dispose()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )
	end
	
	local metadataUpdates = {
				WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true, true ),
				assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, xmpSidecar = options.xmpSidecar } )
	}
	
	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller,
		metadataUpdates, 
		stripForAssetUpdate( waitForRenditions ) ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

	-- Wait for the update to get applied to the assets docValue for xmpDevelop
	if options.xmpDevelop then
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()
	end

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Get the revId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

	-- Make sure the version's asset ID was updated appropriately.
	
	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
		:pluck( 'asset', 'id' ),
	trashAssetId )
	
	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, trashAssetId, sha256, versionBody, true, true ),
		assetDeletedResponse( assetId, trashAssetId, userId )
    }, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )

	-- Make sure renditions oz generated renditions for the trash asset, as we may not
	-- have waited for them to be generated before we first deleted the asset.  There
	-- was a bug on oz where it didn't know to generate them if they weren't there when
	-- copying an asset to a trash asset.
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

	-- Make sure the trash asset still has the _localOnly.file for the master (could
	-- have done this check right after the moveAssetsToTrash but checking here after
	-- the metadata puller gets updates is good as well).
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )

			trashDocValue = docValue

			local originalPath = 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:expectSingleValue( httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

				for k, v in pairs( assetAsJson.links ) do
					if k ~= '/rels/main_create/parts' then
						v.href = string.format( "/v2/catalogs/%s/%s", catalogId, v.href )
					end
				end

				hasNewRenditionLinks = not KSTableUtils.deepEquals( trashDocValue._serverProvidedMetadata.links, assetAsJson.links )
				return assetAsJson

			end )
		, ':ignore:' )

		if hasNewRenditionLinks then
			-- Get the trashed asset back in the sync feed with the links to upload the original
			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
			}, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )
		end

	end

	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 = 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 )

		-- Make sure the version's asset ID was updated appropriately.
	
		rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
			:pluck( 'asset', 'id' ),
		restoredAssetId )

		-- Verify we send up the restore.

		catalogSpec:expectHttpRequest( pushRestoreAssetReqResp( catalogId, restoredAssetId, trashAssetId, trashRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Make sure renditions are generated for the restored asset
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		-- Get the newly created image asset from oz.
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, restoredAssetId, sha256, versionBody, true, true ),
			restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop )
		} ), '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

	if options.deleteTrash then

		-- Delete the asset after syncing up so the deleted asset also gets deleted on oz.
		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				-- Get the last revision of the original asset (revision when applyOverlay with _deleted = true).
				trashRevId = docValue._revId
				return docValue
			end ), ':ignore:' )

		catalogSpec:expectHttpRequest( pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId ) )

		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

		-- Get the trash delete success from Oz
		rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetDeletedResponse( trashAssetId, nil, userId ),
		} ), 'success' )
		
		
		-- Make sure the version was deleted
	
		rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
			:pluck( '_deleted' ),
		true )

	end

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockDeleteWithVersion_image( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest, {
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzDeleteWithVersion_image_testNightly( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{
		xmpDevelop = true,
		xmpSidecar = true,
	})
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockDeleteSyncRestoreWithVersion( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest, 
	{ 
		mock = true, 
		xmpDevelop = true,
		xmpSidecar = true,
		restore = true 
	} )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzDeleteSyncRestoreWithVersion_testNightly( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{ 
		xmpDevelop = true,
		xmpSidecar = true,
		restore = true 
	} )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_MockDeleteTrashWithVersion( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest, 
	{ 
		mock = true, 
		xmpDevelop = true,
		xmpSidecar = true,
		deleteTrash = true 
	} )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_OzDeleteTrashWithVersion_testNightly( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{ 
		xmpDevelop = true,
		xmpSidecar = true,
		deleteTrash = true 
	} )

end


--------------------------------------------------------------------------------

local function testAddSyncRestoreOtherDeleteTrashSyncVersions( 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 = 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 trashAssetSubtype = 'deleted_' .. subtype

	local expectedUserCreated = userCreated
	if not userCreated then
		expectedUserCreated = importTimestamp
		if importTimestamp == nil then
			expectedUserCreated = assetCreated
		end
	end

	local userId = catalog:ozUserId()
	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		-- TODO [kcarlson 2018-07-09]:  Not sure why this doesn't get issued when using oz
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )

	local importSource = makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end )
	, ':ignore:' )
	
	local expectedDevelopSettings = options.xmpDevelop and WFCatTrashTestUtils.getXmpDevelopSettings() or nil
	local xmpDevelopFilePath = child( parent( originalFilePath ), 'testXmpDevelop.xmp' )
	if options.xmpDevelop then
		revisionId = addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath )
	end

	local xmpSidecarFilePath = child( parent( originalFilePath ), 'testXmp.xmp' )
	if options.xmpSidecar then
		revisionId = addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )
	end

	-- Send the metadata for the newly added asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings )
	}, stripForAssetUpdate( false ) ), 'success' )
	
	-- Create the Version Document
	
	local versionName = 'Foo'
	local versionId
	local versionRevId
	local versionBody
	
	rxTest:waitFor( WFCatVersions.createVersionForAsset( catalog, assetId, false, versionName )
		:map( function( newId )
			versionId = newId
			return true
		end ) )
	
	local versionXmpDevelopFilePath
	rxTest:waitFor( catalog:docStore():docValue( versionId )
		:map( function( docValue )
			versionXmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )
			if WFPlatformInfo.isiOS then
				-- on mobile this path is relative so we have to derelativize it for the upload xmp call
				versionXmpDevelopFilePath = KSPathUtils.child( require 'OzFilePaths'.getCatalogBasePath( catalogId ), versionXmpDevelopFilePath )
			elseif WFPlatformInfo.isAndroid then
				versionXmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), versionXmpDevelopFilePath )
			end
			versionRevId = docValue._revId
						
			versionBody = {
				userCreated = docValue.userCreated,
				userUpdated = docValue.userUpdated,
				name = docValue.name,
				develop = docValue.develop,
				createdManually = false
			}
			
			return true
		end ) )
	
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateVersionReqResp( catalogId, versionId, assetId, versionName ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody )
	}, removeSporadicSpaceAndAssetUpdates ), 'success' )
	
	-- Start the binary uploads
	
	local expectedOriginalUploadCount = 0
	
	if options.xmpDevelop then
		-- /rels/xmpDevelop gets uploaded before master
		revisionId = getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1
	end
	
	if options.xmpSidecar then
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1
	end
	
	if options.xmpDevelop then
		-- xmp_develop for the version document
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadVersionXmpDevelopReqResp( catalogId, versionId, versionRevId, versionXmpDevelopFilePath ) )
	end
	
	uploader:startContinuousUploads()
	
	if options.xmpSidecar then
		-- Wait for sidecar to complete if there is one.
		rxTest:observe( pollUploadCount( uploader, 'xmp_sidecar' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, xmpSidecar = options.xmpSidecar } ),
			WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true )
		}, stripForAssetUpdate( false ) ), 'success' )
	end
	
	-- Upload the master
	
	revisionId = getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( createMasterReqResp( catalogId, assetId, revisionId, originalFilePath, nil, options.xmpSidecar ) )
	expectedOriginalUploadCount = expectedOriginalUploadCount + 1

	-- Wait for the upload of xmp_develop.
	if options.xmpDevelop then
		rxTest:observe( pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 2)
		:dispose()
	end

	-- Wait for the upload master to complete, (it will upload after the xmp_develop if there is one.
	rxTest:observe( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 20 * waitMultiplier, 1)
	:dispose()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then
		rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype ), true )
	end

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, xmpSidecar = options.xmpSidecar } ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true, true )
	}, stripForAssetUpdate( waitForRenditions ) ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

	-- Wait for the update to get applied to the assets docValue for xmpDevelop
	if options.xmpDevelop then
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' )
			:map( type ))
		:expectDataWithinTime( 5 * waitMultiplier, 'string' )
		:dispose()
	end

	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { assetId }, pc ) )

	-- Make sure a deleted_image was created with the correct original info and type/subtype
	local trashAssetId, trashRevId, trashDocValue = getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Get the revId of the original deleted asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			revisionId = docValue._revId
			return docValue
		end ), ':ignore:' )

	-- Verify we only send up the delete, the trash asset should not be sent if the original asset was already synced up to oz
	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
	
	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

	-- Make sure the version's asset ID was updated appropriately.
	
	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
		:pluck( 'asset', 'id' ),
	trashAssetId )
	
	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, trashAssetId, sha256, versionBody, true, true ),
		assetDeletedResponse( assetId, trashAssetId, userId ),
    }, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )

	-- Make sure renditions oz generated renditions for the trash asset, as we may not
	-- have waited for them to be generated before we first deleted the asset.  There
	-- was a bug on oz where it didn't know to generate them if they weren't there when
	-- copying an asset to a trash asset.
	rxTest:expectSingleValue( pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, trashAssetSubtype ), true )

	-- Make sure the trash asset still has the _localOnly.file for the master (could
	-- have done this check right after the moveAssetsToTrash but checking here after
	-- the metadata puller gets updates is good as well).
	
	local trashUserCreated

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )

			trashDocValue = docValue
			trashUserCreated = docValue.userCreated
			trashPurgeDate = docValue.purgeDate
			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:expectSingleValue( httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

				for k, v in pairs( assetAsJson.links ) do
					if k ~= '/rels/main_create/parts' then
						v.href = string.format( "/v2/catalogs/%s/%s", catalogId, v.href )
					end
				end

				hasNewRenditionLinks = not KSTableUtils.deepEquals( trashDocValue._serverProvidedMetadata.links, assetAsJson.links )
				return assetAsJson

			end )
		, ':ignore:' )

		if hasNewRenditionLinks then
			-- Get the trashed asset back in the sync feed with the links to upload the original
			rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, waitForRenditions, options.xmpDevelop ),
			}, transformOrderAndStripAssetUpdate( waitForRenditions ) ), 'success' )
		end

	end
	
-- Other Client ------------------------------------------------------------

-- On a second client, remove the asset from the trash on that second client
-- so that it's totally gone on Oz

	simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
	}

	rxTest:expectSingleValue( pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )

-- Original Client ------------------------------------------------------------

	-- Restore the trash asset
	rxTest:expectOnlyCompleted( WFCatTrash.restoreTrashAssets( catalog, { trashAssetId }, pc ) )
	
	-- Make sure the trash asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:pluck( '_deleted' ),
	true )
	
	local restoredAssetId, restoredRevId, restoredDocValue = getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
	rxTest:expectEqual( 'asset', restoredDocValue.type )
	rxTest:expectEqual( subtype, restoredDocValue.subtype )
	rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

	-- Make sure the Version asset was updated.

	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
			:map( function( docValue )
				return docValue 
			end )
			:pluck( 'asset', 'id' ),
		restoredAssetId )

	-- Send create restored asset from original asset
	
	catalogSpec:expectHttpRequest( pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, restoredDocValue.userUpdated ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetDeletedResponse( trashAssetId, userId ),
		resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
	}, transformOrderAndStripAssetUpdate( false )), 'success' )
	
	-- The error handler should process the RNFE and turn the restore into a proper asset create

	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates * 10 ) )

	if options.dropLocalMaster and not options.dropLocalProxy then
		importSource.willNotUploadMaster = true
	end

	-- Make sure a new version was created that points at the restored asset.
	
	local restoredVersionId
	
	local observer = rxTest:observe( 
			WFCatVersions.getAllVersionsStreamForAsset( catalog, restoredAssetId )
				:map( function( versions )
				
					assert( #versions == 1 )
					
					local version = versions[ 1 ] 
				
					restoredVersionId = version.id

					return restoredVersionId ~= versionId
				
				end ) )
				
	observer:expectDataWithinTime( 5 * waitMultiplier, true )
				
	observer:dispose()
				
	local restoredVersionRevId
	
	rxTest:waitFor( catalog:docStore():docValue( restoredVersionId )
			:map( function( docValue )
				
				restoredVersionRevId = docValue._revId
				return true
			
			end ) )

	-- Verify the original version was deleted.
	
	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
			:pluck( '_deleted' ),
	true )

	-- pull from Oz to get the asset/version create response
	rxTest:expectSingleValue( pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, 
								{ master = false, renditions = false, xmpDevelop = false }, expectedDevelopSettings, true --[[alwaysHaveLocationData]] ),
			WFCatTrashTestUtils.versionUpdateResponse( catalogId, restoredVersionId, restoredVersionRevId, restoredAssetId, sha256, versionBody ),
		},  stripAssetAndLocation ), 'success' )
		
	-- validate the restored asset is on oz
	local isRestoredDocOnOz = rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
		:map( function( docValue )
			local receivedUpdateFromOz = docValue._serverProvidedMetadata ~= nil
			
			-- stash the latest oz revisionId so we can validate the upload
			local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
			if revisionIds then
				restoredRevId = revisionIds[ #revisionIds ]
			end
			
			return receivedUpdateFromOz
		end ) )
	isRestoredDocOnOz:expectDataWithinTime( 5 * waitMultiplier, true )
	
end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_VersionsMoveAssociatedDocsToNewDocs( rxTest )

	testAddSyncRestoreOtherDeleteTrashSyncVersions( rxTest, 
	{ 
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_VersionsMoveAssociatedDocsToNewDocs_testNightly( rxTest )

	testAddSyncRestoreOtherDeleteTrashSyncVersions( rxTest,
	{ 
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

--------------------------------------------------------------------------------

local function testAddDeleteSyncOtherRestoreVersions( 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 = setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local userCreated = '2018-04-15T18:18:18Z'
	local importTimestamp = '2018-04-15T19:19:19Z'
	local assetCreated = '2018-04-15T21:21:21Z'
	local trashPurgeDate

	rxTest:expectSingleValue( catalog:ozUser():purgeAssetsAfterValue()
		:map( function( purgeAssetsAfter )
			trashPurgeDate = KSDate.shiftIso8601DateTime( assetCreated, purgeAssetsAfter )
			return true
		end ),
	true )

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local trashAssetSubtype = 'deleted_' .. subtype

	local expectedUserCreated = userCreated

	local catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = getOriginalFilePath( tempTestFolder, fileName )

	-- First pull gets the album prefetch, catalog prefetch, and an empty pull
	catalogSpec:expectHttpRequest( prefetchReqResp( catalogId ) )

	if options.mock then
		catalogSpec:expectHttpRequest( catalogPrefetchReqResp( catalogId ) )
	end

	catalogSpec:expectHttpRequest( emptyPullReqResp( catalogId ) )
	rxTest:expectOnlyCompleted( puller:getUpdatesOnce() )

	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )

	local importSource = makeImportSource( catalog, fileName, importTimestamp, sha256 )

	-- Add an image asset
	rxTest:expectSingleValue( addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			revisionId = revId
			return revId
		end ),
	':ignore:' )
	
	-- Create the Version Document
	
	local versionName = 'Foo'
	local versionId = KSIdGenerator.uuidInLowercaseHex()
	local versionRevId
	local versionBody

	local baseDocValue = require 'WFCatUtils'.createBaseOverlayWithUserCreatedFields( versionId )
	
	local versionDocValue = {

		type = 'version',
		subtype = 'edit',
		name = versionName,
		
		asset = {
			id = assetId,
			sha256 = sha256
		},
		importedOnDevice = {
			formFactor = WFPlatformInfo.isDesktop and "desktop" or 
				WFPlatformInfo.isPhone and "phone" or 
				WFPlatformInfo.isTablet and "tablet"
		},
		develop = WFCatTrashTestUtils.getXmpDevelopSettings()
	}
	
	versionDocValue = require 'WFTableMerge'.applyOverlay( versionDocValue, baseDocValue )
	
	rxTest:expectSingleValue( catalog:docStore():setDocValue( versionDocValue ), ':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 = 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 latest version rev and extra info.
	
	rxTest:waitFor( catalog:docStore():docValue( versionId )
		:map( function( docValue )
		
			versionRevId = docValue._revId
						
			versionBody = {
				userCreated = docValue.userCreated,
				userUpdated = docValue.userUpdated,
				name = docValue.name,
				develop = docValue.develop
			}
			
			rxTest:expectEqual( docValue.asset.id, trashAssetId )
			
			return true
			
		end ) )

	-- Sync up the deleted asset
	catalogSpec:expectHttpRequest( pushCreateAssetReqResp( catalogId, fileName, trashAssetSubtype ) )

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, trashAssetId, sha256, versionBody )
	}, stripForAssetUpdate( false ) ), 'success' )

	-- Update the trashRevId with the latest revision id before issuing the createMasterReqResp
	trashRevId = getLatestRevisionId( rxTest, catalog, trashAssetId )
	
	-- Upload the master
	catalogSpec:expectHttpRequest( 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( pollUploadCount( uploader, 'master' ))
	:expectDataWithinTime( 5 * waitMultiplier, 1)
	:dispose()

	-- Wait for oz to generate the proxy and renditions before requesting next update
	rxTest:expectSingleValue( 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( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true )
	}, stripForAssetUpdate( true ) ), 'success' )

	-- Make sure the original asset was deleted
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:pluck( '_deleted' ),
	true )

	-- Restore asset on other client
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevision = KSIdGenerator.uuidInLowercaseHex()

	simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = restoredAssetId,
		revisionId = restoredAssetRevision,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}

	rxTest:expectSingleValue( pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
		assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, restoredAssetId, sha256, versionBody )
	} ), 'success' )

	-- Check that trash asset is now deleted.
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId ):pluck( '_deleted' ), true )

	-- Check that there is no conflict.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):pluck( '_conflict' ), nil )

	-- Make sure the restored asset has a local master
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):map( LrAssetUtils.hasMasterLocally ), true )

	-- check that we think the current revision exists on oz to ensure we don't have a stuck sync
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId ):map( LrAssetUtils.isLatestMetadataUploaded ), true )

	-- check that there is nothing to push
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

	-- Make sure that the version doc now points at the new restoredAssetId.
	
	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
			:pluck( 'asset', 'id' ),
		restoredAssetId )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_VersionsAddDeleteSyncOtherRestore( rxTest )

	testAddDeleteSyncOtherRestoreVersions( rxTest, 
	{ 
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

--------------------------------------------------------------------------------

function WFCatTrashTest.rxTest_CT_VersionsAddDeleteSyncOtherRestore_testNightly( rxTest )

	testAddDeleteSyncOtherRestoreVersions( rxTest,
	{
		xmpDevelop = true,
		xmpSidecar = true,
	} )

end

--==============================================================================
-- @mark setup
--==============================================================================


function WFCatTrashTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

--------------------------------------------------------------------------------

return WFRxTestCase.wrapSuite( WFCatTrashTest )
