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

WFCatTrashMultipleDevicesTest.lua

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

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

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

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

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

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

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

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

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

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

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

local coveragePath

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

local stripped = '<<stripped>>'

local albumAssetTable = 'album_asset'
if WFPlatformInfo.isDesktop then
	albumAssetTable = 'album_asset_v2'
end

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5

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

local WFCatTrashMultipleDevicesTest = {}

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 removeSporadicSpaceAndAssetUpdates = WFCatTrashTestUtils.removeSporadicUpdatesWithTypes( { 'space', 'asset' } )

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

local removeSporadicAssetUpdates = WFCatTrashTestUtils.removeSporadicUpdatesWithTypes( { 'asset' } )

--==============================================================================
-- @mark tests that involve other client actions
--==============================================================================

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 = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"
	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( WFCatTrashTestUtils.prefetchReqResp( catalogId ) )

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

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

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

-- Step 1: Add an image asset
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = WFCatTrashTestUtils.getProxyFilePath( rxTest, tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( WFCatTrashTestUtils.addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )
		proxyFilePath = WFCatTrashTestUtils.getProxyFilePath( rxTest, tempTestFolder, proxyFileName )

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

	WFCatTrashTestUtils.simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = otherClientTrashPurgeDate,
		options = options,
	}

	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false, xmpDevelop = false } ),
		WFCatTrashTestUtils.assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
    }, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

-- Step 7: The error handler should process the ResourceExistsError.
	if options.resourceExistsError then
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceExistsErrorUpdateResponse( "create", assetId, revisionId, "asset", catalogId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )

		-- Wait for the error to be processed.
		rxTest:observe( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesTest.rxTest_CT_MockAddPush_OtherDelModify_resourceExistsErr( rxTest )
-- NMB-18214 #1
	testAddPush_ModifyOtherDel_ResourceExists( rxTest, { mock = true, resourceExistsError = true } )
end

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

function WFCatTrashMultipleDevicesTest.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, _ = WFCatTrashTestUtils.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 sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"
	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( WFCatTrashTestUtils.prefetchReqResp( catalogId ) )

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

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

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

-- Step 1: Add an image asset

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local filePath, proxyFilePath
	if options.proxyOnlyAsset then
		proxyFilePath = WFCatTrashTestUtils.getProxyFilePath( rxTest, tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( WFCatTrashTestUtils.addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )
			:tap( function( revId )
				revisionId = revId
			end ),
			':ignore:' )

	else
		filePath = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )
		proxyFilePath = WFCatTrashTestUtils.getProxyFilePath( rxTest, tempTestFolder, proxyFileName )

		rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushCreateAssetReqResp( catalogId, fileName, subtype ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

-- Step 4 pull

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource ),
		WFCatTrashTestUtils.resourceExistsErrorUpdateResponse( "create", assetId, revisionId, "asset", catalogId ),
	}, WFCatTrashTestUtils.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()

	WFCatTrashTestUtils.simulateOtherClientAssetDelete {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = assetId,
		revisionId = revisionId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userCreated = userCreated,
		trashPurgeDate = otherClientTrashPurgeDate,
		options = options,
	}

	rxTest:expectSingleValue( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, assetId, { master = false, proxy = false, renditions = false, xmpDevelop = false } ),
		WFCatTrashTestUtils.assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
    }, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

	-- Check that there is nothing to send
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashMultipleDevicesTest.rxTest_CT_MockAddPushModifyPush_OtherDel_resourceExistsErr( rxTest )
-- NMB-18214 #2
	testAddPushModifyPush_OtherDel_ResourceExists( rxTest, { mock = true, resourceExistsError = true } )
end

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

function WFCatTrashMultipleDevicesTest.rxTest_CT_OzAddPushModifyPush_OtherDel_resourceExistsErr_testNightly( rxTest )
-- NMB-18214 #2
	testAddPushModifyPush_OtherDel_ResourceExists( rxTest, { resourceExistsError = true } )
end

--==============================================================================
-- @mark tests that involve adding assoc docs and receiving other client delete
--==============================================================================

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

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

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

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

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

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

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

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

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

-- Step 1:

	-- Add an image asset
	rxTest:expectSingleValue( WFCatTrashTestUtils.addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )
		:map( function( revId )
			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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, assetRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = false, renditions = false } ),
		WFCatTrashTestUtils.albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId ),
		WFCatTrashTestUtils.albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}), 'success' )

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

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

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

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

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

-- Step 2:

	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( WFCatTrashTestUtils.putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions ) )
	rxTest:expectSingleValue(
		catalogSpec:put( createSpaceUrl, createSpaceData, createSpaceOptions )
		:map( function( responseData, headers )
			return headers.status_code
		end )
	, '201' )

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.spaceAlbumUpdateResponse( catalogId, spaceAlbumId, spaceAlbumRevId, spaceId, albumId ),
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, commentBody1 ),
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )
	}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushCreateAlbumReqResp( catalogId, albumId2, albumName2 ) )

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

	-- Sync down the album
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.albumUpdateResponse( catalogId, albumId2, albumRevId2, albumName2 )
	}, WFCatTrashTestUtils.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()

	WFCatTrashTestUtils.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( WFCatTrashTestUtils.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
		-- WFCatTrashTestUtils.stripUpdatesAndLinks to make sure things line up properly.
		local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
		WFCatTrashTestUtils.removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

		mockDataAsTable, ozDataAsTable = WFCatTrashTestUtils.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 WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 14 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.albumAssetUpdateResponse( trashAlbumAssetId, trashAlbumAssetRevId, albumId, trashAssetId ), -- new unknown id and revId will be created by oz
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.commentUpdateResponse( catalogId, trashCommentId, trashCommentRevId, spaceId, trashAssetId, commentBody1 ),
			WFCatTrashTestUtils.assetGoneErrorUpdateResponse( albumAssetId2, albumAssetRevId2, 'album_asset', trashAssetId, {} ), -- create should fail with error 410 as asset was deleted by oz.
			WFCatTrashTestUtils.assetGoneErrorUpdateResponse( commentId2, commentRevId2, 'comment', trashAssetId ), -- create should fail with error 410 as asset was deleted by oz.
			WFCatTrashTestUtils.favoriteUpdateResponse( catalogId, trashFavoriteId, trashFavoriteRevId, spaceId, trashAssetId ),
			WFCatTrashTestUtils.albumAssetDeletedResponse( albumAssetId, userId, trashAlbumAssetId ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
			WFCatTrashTestUtils.commentDeletedResponse( commentId, userId, trashCommentId ),
			WFCatTrashTestUtils.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( WFCatTrashTestUtils.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 = WFCatTrashTestUtils.getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, { assetId, trashAssetId } )
		rxTest:expectEqual( 'asset', restoredDocValue.type )
		rxTest:expectEqual( subtype, restoredDocValue.subtype )
		rxTest:expectEqual( nil, derefOpt( restoredDocValue, 'original' ) )

		-- Make sure 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( WFCatTrashTestUtils.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
			-- WFCatTrashTestUtils.stripUpdatesAndLinks to make sure things line up properly.
			local ozUpdateTable = derefOpt( ozDataAsTable, 'updates' ) or {}
			WFCatTrashTestUtils.removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

			mockDataAsTable, ozDataAsTable = WFCatTrashTestUtils.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 WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, restoredAssetId, restoredRevId, subtype ), true )

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.albumAssetUpdateResponse( restoredAlbumAssetId, restoredAlbumAssetRevId, albumId, restoredAssetId ), -- new unknown id and revId will be created by oz
				WFCatTrashTestUtils.albumAssetUpdateResponse( restoredAlbumAssetId2, restoredAlbumAssetRevId2, albumId2, restoredAssetId ), -- new unknown id and revId will be created by oz
				WFCatTrashTestUtils.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 ),
				WFCatTrashTestUtils.albumAssetDeletedResponse( trashAlbumAssetId, userId, restoredAlbumAssetId ),
				WFCatTrashTestUtils.albumAssetDeletedResponse( albumAssetId2, userId, restoredAlbumAssetId2 ),
				WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
				WFCatTrashTestUtils.commentDeletedResponse( trashCommentId, userId, restoredCommentId ),
				WFCatTrashTestUtils.commentDeletedResponse( commentId2, userId, restoredCommentId2 ),
				WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesTest.rxTest_CT_MockAddToAlbums_OtherDelete_assocDocs( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest, { mock = true } )
end

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

-- TODO [jandrew 2025-03-14]: Disabled due to instability

function WFCatTrashMultipleDevicesTest.DISABLED_rxTest_CT_OzAddToAlbums_OtherDelete_assocDocs_testNightly( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest )
end


--==============================================================================
-- @mark Tests that involve other deleting or restoring synced assets with version
--==============================================================================

local function testAddSyncRestoreOtherDeleteTrashSyncVersions( rxTest, options )

	options = options or {}

	options.aux = options.xmpDevelop

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 20 * waitMultiplier )

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

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

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

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

	local subtype = 'image'
	local fileName = 'IMG_7763.JPG'
	local 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 = WFCatTrashTestUtils.getOriginalFilePath( rxTest, tempTestFolder, fileName )

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

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

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

	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )

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

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

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

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

	local auxMockHttpRequests, auxInfo

	if options.aux then

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

		local auxId = auxInfo and next( auxInfo )

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

	end

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

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

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

	-- Get the metadata for the newly added asset that includes the links for uploading binaries.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = false },
								expectedDevelopSettings, nil, auxInfo )
	}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.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.xmpSidecar then
	
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		-- /rels/xmp gets uploaded after master is uploaded an a /rels/xmp link is present
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpReqResp( catalogId, assetId, revisionId, xmpSidecarFilePath ) )
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

	end

	uploader:startContinuousUploads()

	if options.xmpSidecar then

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

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

	if options.aux then

		-- /rels/xmpDevelop gets uploaded before master
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )

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

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

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

	end
	
	if options.xmpDevelop then

		-- /rels/xmpDevelop gets uploaded before master
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1
		
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1)
		:dispose()
		
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	-- Upload the master

	revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createMasterReqResp( catalogId, assetId, revisionId, originalFilePath, nil, options.xmpSidecar ) )
	expectedOriginalUploadCount = expectedOriginalUploadCount + 1

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

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

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

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

	if options.xmpDevelop then
	
		-- xmp_develop for the version document
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadVersionXmpDevelopReqResp( catalogId, versionId, versionRevId, versionXmpDevelopFilePath ) )
	
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 2)
		:dispose()
		
	end

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true, true )
	}, WFCatTrashTestUtils.stripForAssetUpdate( waitForRenditions ) ), 'success' )

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

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

		local auxId = auxInfo and next( auxInfo )

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

	end

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

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

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

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

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

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

	-- Make sure the version's asset ID was updated appropriately.

	rxTest:expectSingleValue( catalog:docStore():docValue( versionId )
		:pluck( 'asset', 'id' ),
	trashAssetId )

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

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

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

	-- Make sure the trash asset still has the _localOnly.file for the master (could
	-- have done this check right after the moveAssetsToTrash but checking here after
	-- the metadata puller gets updates is good as well).

	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( WFCatTrashTestUtils.httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

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

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

			end )
		, ':ignore:' )

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

	end

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

-- On a second client, remove the asset from the trash on that second client
-- so that it's totally gone on Oz

	WFCatTrashTestUtils.simulateOtherClientDeleteTrash {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
	}

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

	-- Make sure 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( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, restoredDocValue.userUpdated ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetDeletedResponse( trashAssetId, userId ),
		WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )

	-- The error handler should process the RNFE and turn the restore into a proper asset create

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = false, renditions = false, xmpDevelop = false, aux = false },
								expectedDevelopSettings, true --[[alwaysHaveLocationData]], auxInfo ),
			WFCatTrashTestUtils.versionUpdateResponse( catalogId, restoredVersionId, restoredVersionRevId, restoredAssetId, sha256, versionBody ),
		},  WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesTest.rxTest_CT_VersionsMoveAssociatedDocsToNewDocs( rxTest )

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

end

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

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

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

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

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

	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )

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

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

	-- 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 = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )
	rxTest:expectNotEqual( assetId, trashAssetId ) -- should not have the original asset id
	rxTest:expectEqual( 'asset', trashDocValue.type )
	rxTest:expectEqual( trashAssetSubtype, trashDocValue.subtype )
	rxTest:expectEqual( assetId, derefOpt( trashDocValue, 'original', 'id' ) )
	rxTest:expectEqual( expectedUserCreated, derefOpt( trashDocValue, 'original', 'userCreated' )  )

	-- Get the 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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashRevId, fileName, trashAssetSubtype, assetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, trashAssetId, sha256, versionBody )
	}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

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

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

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

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

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

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

	-- Restore asset on other client
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevision = KSIdGenerator.uuidInLowercaseHex()

	WFCatTrashTestUtils.simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = restoredAssetId,
		revisionId = restoredAssetRevision,
		trashAssetId = trashAssetId,
		trashAssetRevId = trashRevId,
		userUpdated = userCreated,
		options = options,
	}

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
		WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesTest.rxTest_CT_VersionsAddDeleteSyncOtherRestore( rxTest )

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

end

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

function WFCatTrashMultipleDevicesTest.rxTest_CT_VersionsAddDeleteSyncOtherRestore_testNightly( rxTest )

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

end

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


function WFCatTrashMultipleDevicesTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( 'WFCatTrashMultipleDevicesTest', WFCatTrashMultipleDevicesTest )
