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

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

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

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

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

local coveragePath

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

local failStubbedTests = false

local stripped = '<<stripped>>'

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

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5

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

local WFCatTrashMultipleDevicesBasicOtherDeleteTest = {}

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

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

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

local function setTimeoutAndRetryLimit( rxTest, options, timeoutValue )

	rxTest:setTimeLimit( timeoutValue )

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

end

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

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

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

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

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

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

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

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

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

	fileName = options.fileName or fileName
	subtype = options.subtype or subtype
	local 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( 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 )
	local location = fileName == 'IMG_7763.JPG' and {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		} or nil
	-- Default to not expecting any conflicts
	local expectedConflicts = {}

	if options.otherCreateTrash then

		-- Simulate trash asset creation by other client
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	elseif options.otherCreate then

		-- Simulate asset creation by other client
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetUpdateResponse( assetId, revId, fileName, subtype, userCreated, userCreated, importSource, { master = false, renditions = false } )
		}, WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

		local appliedDevelop

		if options.modifyAssetBeforeDelete then

			-- Change video develop or image 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 now = KSDate.timeInIso8601Format()

				if subtype == 'video' then

					appliedDevelop = getApplyVideoDevelopTint( now )

				else

					appliedDevelop = {
						processingModel = "lightroom",
						xmpCameraRaw = kApplyTintXmp
					}

				end

				rxTest:expectSingleValue( catalog:docStore():applyOverlay {
						_id = assetId,
						develop = appliedDevelop,
						userUpdated = now,
					}, ':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 = WFCatTrashTestUtils.getProxyFilePath( rxTest, 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
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

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


		elseif options.splitResponse1dupe then

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

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

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

		elseif options.splitResponse2 then

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

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

			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

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

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


		else

			-- Receive Tombstone and trash asset in same batch

			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, options.xmpDevelop, appliedDevelop ) )
			rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )
		end

	else

		-- Simulate asset creation by other client
		WFCatTrashTestUtils.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
		WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

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

		elseif options.splitResponse1dupe then

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

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

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

		elseif options.splitResponse2 then

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

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

			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}), 'success' )

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

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

		else

			-- Receive Tombstone and trash asset in same batch

			rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, false, false ),
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId )
			}, WFCatTrashTestUtils.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 = WFCatTrashTestUtils.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( WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true } )
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_video( rxTest )
	testOtherDeleteSyncRestore( rxTest, {
		mock = true,
		fileName = "IMG_7772.mp4",
		subtype = "video",
		otherCreate = true,
		modifyAssetBeforeDelete = true
	} )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzOtherCreate_Modify_OtherDelete_video_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, {
		fileName = "IMG_7772.mp4",
		subtype = "video",
		otherCreate = true,
		modifyAssetBeforeDelete = true
	} )
end

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

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

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

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

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse2 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, splitResponse3dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzOtherCreate_Modify_OtherDelete_Restore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, modifyAssetBeforeDelete = true, restore = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_Modify_OtherDelete_Restore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, modifyAssetBeforeDelete = true, restore = true, splitResponse3dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzOtherCreate_GetProxy_OtherDelete_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, downloadProxyBeforeDelete = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse1 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse1dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse2 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, splitResponse3dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzOtherCreate_GetProxy_OtherDelete_Restore_testNightly( rxTest )
	testOtherDeleteSyncRestore( rxTest, { otherCreate = true, downloadProxyBeforeDelete = true, restore = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse1( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse1 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse1dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse1dupe = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse2( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse2 = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockOtherCreate_GetProxy_OtherDelete_Restore_splitResponse3dupe( rxTest )
	testOtherDeleteSyncRestore( rxTest, { mock = true, otherCreate = true, downloadProxyBeforeDelete = true, restore = true, splitResponse3dupe = true } )
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 = WFCatTrashTestUtils.setupCloudTrashTestSession( rxTest, options )
	assert( testSession )

	local now = KSDate.timeInIso8601Format()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then

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

	end

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

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

	-- Image asset is now complete and fully synced.

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

	local DevelopNow = KSDate.timeInIso8601Format()

	if subtype == 'video' then
		appliedDevelop = getApplyVideoDevelopTint( DevelopNow )
	else
		appliedDevelop = {
			processingModel = "lightroom",
			xmpCameraRaw = kApplyTintXmp
		}
	end

	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		develop = appliedDevelop,
		userUpdated = DevelopNow,
	} )

	local ratingsUuid = KSIdGenerator.uuidInLowercaseHex()
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		ratings = {
			[ ratingsUuid ] = {
				rating = 3
			}
		}
	} )

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

	WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, emptyRevId, localTrashAssetId, trashCreateWithRevId ) )

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

		local expectedUpdate = {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", trashAssetId, userId ),
		}

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, expectedUpdate,
			WFCatTrashTestUtils.transformResponseWithResourceNotFoundError )
		, 'success' )
	else

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

		-- Send the delete after pulling updates
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushUpdateTrashReqResp( catalogId, trashAssetId, trashAssetRevId, trashAssetSubtype, options.xmpDevelop, appliedDevelop ) )
		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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzDelete_OtherDelete_pushFirst_testNightly( rxTest )

	testDeleteOtherDeleteSync( rxTest, { pushFirstAfterUnpause = true } )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzDelete_OtherDelete_pushFirst_video_testNightly( rxTest )

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

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzDelete_OtherDelete_pullFirst_testNightly( rxTest )

	testDeleteOtherDeleteSync( rxTest, {} )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzDelete_OtherDelete_pullFirst_video_testNightly( rxTest )

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

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDelete_OtherDelete_pushFirst( rxTest )

	testDeleteOtherDeleteSync( rxTest, { mock = true, pushFirstAfterUnpause = true } )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDelete_OtherDelete_pushFirst_video( rxTest )

	testDeleteOtherDeleteSync( rxTest, {
		mock = true,
		fileName = "IMG_7772.mp4",
		sha256 = "71fe5d71a7a176e5e214f804ae6f750cb014646f62e162d0a0e4e1d1ec6a9963",
		subtype = "video",
		pushFirstAfterUnpause = true
	} )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDelete_OtherDelete_pullFirst( rxTest )

	testDeleteOtherDeleteSync( rxTest, { mock = true } )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDelete_OtherDelete_pullFirst_video( rxTest )

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

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 = WFCatTrashTestUtils.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'
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"

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

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

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

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

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

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

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

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


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

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

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

	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

	local waitForRenditions = not options.noWaitForRenditions
	if waitForRenditions then

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

	end

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

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

	-- 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( WFCatTrashTestUtils.pushCreateAlbumReqResp( catalogId, albumId, albumName ) )

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

	-- Get the metadata for the newly added asset and album
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.albumUpdateResponse( catalogId, albumId, albumRevId, albumName )
	}, WFCatTrashTestUtils.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()

	WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )

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

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( ozTrashAssetId, ozTrashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, ozTrashAssetId, userId ), -- This will resuilt in the trashAsset being updated
			WFCatTrashTestUtils.assetGoneErrorUpdateResponse( albumAssetId, albumAssetRevId, 'album_asset', ozTrashAssetId, {} ),
		}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( ozTrashAssetId, ozTrashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.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( WFCatTrashTestUtils.pushAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )
		rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, 5 ) )

		-- Wait for asset gone response

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetGoneErrorUpdateResponse( albumAssetId, albumAssetRevId, 'album_asset', ozTrashAssetId, {} ),
		}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.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()

	WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevision, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			-- Other client restored asset.
		WFCatTrashTestUtils.assetDeletedResponse( ozTrashAssetId, restoredAssetId, userId ),
			-- Oz deletes trash asset due to other client restore.
		WFCatTrashTestUtils.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.
		WFCatTrashTestUtils.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.
	}, WFCatTrashTestUtils.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 = WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockAddToAlbum_OtherDelete_pullFirst( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { mock = true } )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockAddToAlbum_OtherDelete_pushFirst( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { mock = true, pushFirstAfterUnpause = true } )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzAddToAlbum_OtherDelete_pullFirst_testNightly( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, {} )

end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzAddToAlbum_OtherDelete_pushFirst_testNightly( rxTest )

	testAddToAlbumOtherDeleteSync( rxTest, { pushFirstAfterUnpause = true } )

end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	uploader:startContinuousUploads()

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

	uploader:stopContinuousUploads()

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

	-- 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' )
	local developUserUpdated, _
	if options.xmpDevelop then
		revisionId, _, developUserUpdated = WFCatTrashTestUtils.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()

	WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			doesNotExistErrorUpdate,
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			}, function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

				mockDataAsTable, ozDataAsTable = WFCatTrashTestUtils.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.
				WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			} )
		, 'success' )

		-- Pull the update from the other client.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true ),
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- This will result in the trashAsset being updated
			} )
		, 'success' )

	end

	-- 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 WFCatTrashTestUtils.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( WFCatTrashTestUtils.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
	if options.xmpDevelop then
		developSettings = WFCatTrashTestUtils.getXmpDevelopSettings()
		developSettings.userUpdated = developUserUpdated
	else
		developSettings = {
			processingModel = "lightroom",
			xmpCameraRaw = kApplyTintXmp
		}
	end

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse( trashAssetId, trashAssetRevId, fileName, trashAssetSubtype, trashAssetCreated, trashPurgeDate, userCreated, importSource, assetId, true, true, false, options.xmpSidecar, developSettings ),
		},
		function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

			mockDataAsTable, ozDataAsTable = WFCatTrashTestUtils.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 WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzModifyOtherDelete_testNightly( rxTest )
	-- NMB-9394
	testAddSyncModifyOtherDeleteSync( rxTest )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockModifyOtherDelete( rxTest )
	-- NMB-9394
	testAddSyncModifyOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockModifyOtherDelete_splitTombstone( rxTest )
	-- LRMI-4193390
	testAddSyncModifyOtherDeleteSync( rxTest, { mock = true, splitTombstoneAndUpdate = true } )
end

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

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

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

	local subtype = 'image'
	local fileName = "IMG_7763.JPG"
	local sha256 = "3bd8b936cc646bf1296e5ba4a76c410a27beb80809f525a8f5072f0115c54ccc"
	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 )

-- 1) Add photo.

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

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

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

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

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

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

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

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

	local trashUserCreated, trashPurgeDate
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		: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 = 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 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( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete/Restore request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
		}, WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )

		expectedResourceNotFoundErrorCount = 2

	end

	-- Wait for the error(s) to be processed.
	rxTest:observe( WFCatTrashTestUtils.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( WFCatTrashTestUtils.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( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", nil, userId ),
		}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )
	end

	-- Get the updates from the sync feed showing the photo has been restored.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( otherClientTrashAssetId, restoredAssetId, userId ),
		WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Make sure both errors have been processed.
	rxTest:observe( WFCatTrashTestUtils.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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDeleteRestore_OtherDelete( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzDeleteRestore_OtherDelete_testNightly( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockDeleteRestore_OtherDelete_splitErrors( rxTest )
-- NMB-13896 #4
	testAddSyncDeleteRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = 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 )
	}, 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 )
	}, 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 WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_MockAddToAlbums_OtherDelete_assocDocs( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesBasicOtherDeleteTest.rxTest_CT_OzAddToAlbums_OtherDelete_assocDocs_testNightly( rxTest )
-- NMB-10394
	testAddSyncAddAlbumsOtherClientDeleteSync( rxTest )
end

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


function WFCatTrashMultipleDevicesBasicOtherDeleteTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( 'WFCatTrashMultipleDevicesBasicOtherDeleteTest', WFCatTrashMultipleDevicesBasicOtherDeleteTest )
