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

WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.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 stripped = '<<stripped>>'

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5

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

local WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest = {}

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

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

local function setTimeoutAndRetryLimit( rxTest, options, timeoutValue )

	rxTest:setTimeLimit( timeoutValue )

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

end

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

local function testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, options )

	-- NMB-10868

	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- Scenario:
	-- 1 Pauses Sync
	-- 2 Modify a photo (i.e. change tint to green)
	-- 3 On another client that is online deletes the asset.
	-- 4 Still on other client restore the asset
	-- 5 Using original client turns on sync
	-- Expected results: The modifications to the asset appears correctly in the trash.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, _ = 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'

	-- set the modification dates so that the other userUpdated value is before this user.
	local initUserUpdated = KSDate.timeInIso8601Format()
	local userUpdated = KSDate.shiftIso8601DateTime( initUserUpdated, 5 )

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

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

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

	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local originalFilePath = 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

	-- 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 )
	else
		rxTest:expectSingleValue( catalog:docStore():applyOverlay {
				_id = assetId,
				userUpdated = userUpdated,
				develop = {
					processingModel = "lightroom",
					xmpCameraRaw = kApplyTintXmp
				}
			}
			:map( function( revId )
				revisionId = revId
				return revId
			end )
		, ':ignore:' )
	end


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

	-- Send an update with a delete asset without our docstore knowing about it
	-- so it appears as if a different client issued the delete.  Our code
	-- doesn't currently look at deleted_by so it shouldn't be any different
	-- that this client did it instead of an other client.

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local trashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetRevId = KSIdGenerator.uuidInLowercaseHex()

	local trashPurgeDate

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

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

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

	local deleteRestoreAssetUpdate = {
		{
			type = 'asset',
			action = 'delete',
			revision_id = revisionId,
			id = assetId,
			deleted_asset = {
				id = trashAssetId,
				payload = {
					userCreated = userCreated,
					purgeDate = trashPurgeDate,
				},
				revision_id = trashAssetRevId
			}
		},
		{
			action = "create",
			type = "asset",
			subtype = "image",
			restore_from = {
				id = trashAssetId
			},
			revision_id = restoredAssetRevId,
			id = restoredAssetId,
			payload = {
				userUpdated = "2019-01-08T08:08:08.008Z"
			},
		}
	}

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

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

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

	if options.sendUpdateBeforeTombstone then

		-- Send an update to original asset with the modifications that were made before the delete
		-- this update will fail with an error 410.

		-- The plan is to ignore the error 410 on the master, when the
		-- tombstone is received we will copy the localOnly file links
		-- for the asset's binaries (xmpDevelop, xmpSidecar) at that point.
		-- The uploader should still see the binaries as something that needs
		-- uploading.

		-- Note: album_asset, comment, and favorite will be handled by their own
		-- error 400 handling.

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateAssetReqResp( catalogId, assetId, revisionId, subtype, options.xmpDevelop, kApplyTintXmp ) )

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

	end

	if not options.splitResponse then

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

		-- NMB-11430
		-- Wait for the delete and restore to both be processed, when this happens we will have
		-- one asset update and two asset delete's.  This is done as the metadatapuller can end
		-- up getting two asset updates  (one subtype = 'image', one subtype = 'deleted_image')
		-- and a delete.  We may have an error document in here as well if we did the push above
		-- but that shouldn't affect the delete and asset update counts.
		-- This response is actually abbreviated as it doesn't contain base or links but that shouldn't matter.
		local expectedResponse = {
			updates =  {
				WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
				WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			}
		}

		rxTest:observe( WFCatTrashTestUtils.pollHttpGetMetadataUpdates( catalogSpec, catalog:docStore(), '200', expectedResponse )
			:map( function( statusCode, response )

				local numDeletes = 0
				local numUpdates = 0
				local updates = response.updates or {}
				for _, update in ipairs( updates ) do
					if update.type == 'asset' then
						if update.deleted then
							numDeletes = numDeletes + 1
						else
							numUpdates = numUpdates + 1
						end
					end
				end

				return numDeletes, numUpdates
			end ) )
		:expectDataWithinTime( 5 * waitMultiplier, 2, 1 )
		:dispose()

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

	end

	if options.sendUpdateBeforeTombstone then

		local doesNotExistErrorUpdate = {
			errors = {
				asset = {
					"does not exist",
				},
			},
			id = "c7e348dad04be59f450a47a3bfeb992e",
			source = {
				action = "update",
				id = assetId,
				revision_id = revisionId,
				type = "asset",
			},
			status = 400,
			subtype = "InputValidationError",
			type = "error",
			updated = '2019-01-02T01:02:03Z',
		}

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
				WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, options.xmpDevelop ),
				doesNotExistErrorUpdate,
				WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ), -- see comment in block below
				WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			}, 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.splitResponse then

		-- Pretend like the updates got split across the update limit bounddry so we
		-- get the delete asset in one update, and the delete and restore in another.
		-- This is a mock only only as we can't force oz to split the updates.

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

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated }
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			-- Make the second response a little bit contrived as well by placing the restoredAssetUpdate
			-- after the delete as oz doesn't guarantee order.
			WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, userCreateUpdate, userCreated, importSource, assetId, true, true ),
			} )
		, 'success' )

	elseif options.splitResponse2 then

		-- Pretend like the updates got split across the update limit bounddry so we
		-- get the delete asset in one update, and the delete and restore in another.
		-- This is a mock only only as we can't force oz to split the updates.

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

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated }
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreateUpdate, importSource, assetId, true, true ),
			} )
		, 'success' )

	elseif options.splitResponse3 then

		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( trashAssetId, restoredAssetId, userId ),
			} )
		, 'success' )

		local userCreateUpdate = { userCreated = userCreated, userUpdated = initUserUpdated }
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreateUpdate, importSource, assetId, true, true ),
			} )
		, 'success' )

	else

		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
			-- MetadataPuller will see these two tombstones and update the restored asset with the
			-- modifications that were applied to assetId. If they get split because of batch
			-- size crosssing 100 item limit and it only sees one tombstone, it will update
			-- the trash asset.  And the following update will see it needs to move the
			-- modification from the trash asset to the restored asset.
			WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
			WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, restoredAssetId, userId ),
			} )
		, 'success' )

	end

	-- Pull the tombstone from the other client.
	-- WFCatMetadataPuller should be copying the modified document to the
	-- restored document when we see the tombstone.

	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local restoredAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, restoredAssetRevId )

			-- Update restoredAssetRevId the handling of WFCatTrashTestUtils.assetDeletedResponse will cause an overlay to be
			-- applied to the restored asset with the modifications made to the original asset.
			restoredAssetRevId = docValue._revId

			return restoredAssetRevIdOnOz, localRevisionIsOnOz
		end )
	, true, false )

	-- Check to see that the restored asset has the tint modification or external develop settings that was applied earlier.
	local function haveDevelopSettings( developSettings )
		return developSettings ~= nil or 'develop settings missing'
	end

	if options.xmpDevelop then

		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( 'develop', 'xmpCameraRaw', 'sha256' )
			:map( haveDevelopSettings )
		 )
		:expectDataWithinTime( 1, true )
		:dispose()

	else

		rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
			:pluck( 'develop', 'xmpCameraRaw' )
			:map( haveDevelopSettings )
		)
		:expectDataWithinTime( 1, true )
		:dispose()

	end

	-- Verify we send up a restored asset update to include the modifications that were made before the delete.
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateRestoredReqResp( catalogId, restoredAssetId, restoredAssetRevId, subtype, options.xmpDevelop, kApplyTintXmp ) )

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

	-- Get the updated restored asset back from oz, it should have the develop settings modifications we made.

	local developSettings
	if options.xmpDevelop then
		developSettings = WFCatTrashTestUtils.getXmpDevelopSettings()
		developSettings.userUpdated = developUserUpdated
	else
		developSettings = {
			processingModel = "lightroom",
			xmpCameraRaw = kApplyTintXmp
		}
	end

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

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, false, true, 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( restoredAssetId )
		:map( function( docValue )
			local localRevisionIsOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, docValue._revId )
			local restoredAssetRevIdOnOz = WFCatIndexUtils.isAssetRevisionOnOz( docValue, restoredAssetRevId )

				-- Update restoredAssetRevId as WFCatTrashTestUtils.restoredAssetUpdateResponse will have added a new revision.
			restoredAssetRevId = docValue._revId

			return restoredAssetRevIdOnOz, localRevisionIsOnOz
		end )
	, true, true )

	-- [TODO: iOS would not preserve xmp file at the moment if the delete is from other devices]
	if options.xmpDevelop and not WFPlatformInfo.isiOS then

		-- Verify we send up the modified xmp file to the restored asset.
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, restoredAssetId, restoredAssetRevId, xmpDevelopFilePath ) )

		-- Wait for the upload xmp file to complete, (it will upload after the xmp_develop if there is one.
		uploader:startContinuousUploads()

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

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

		-- Make sure we get an update of the asset without invalid = true on the renditions.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.restoredAssetUpdateResponse( restoredAssetId, restoredAssetRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true, true, true, developSettings ),
			} )
		, 'success' )

	end

	-- make sure the binaries are there
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )

			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			local originalExists = originalPath and WFFileUtils.exists( originalPath ) or 'original file missing'

			local xmpDevelopExists = 'file'
			if options.xmpDevelop and not WFPlatformInfo.isiOS then
				local xmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )

				if WFPlatformInfo.isAndroid then
					-- android stores xmp develop path as a relative path, fix that before making sure it exists
					xmpDevelopFilePath = child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), xmpDevelopFilePath )
				end

				xmpDevelopExists = xmpDevelopFilePath and WFFileUtils.exists( xmpDevelopFilePath ) or 'xmpDevelop file missing'
			end

			return originalExists, xmpDevelopExists

		end ),
	'file', 'file' )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzModifyOtherDeleteOtherRestore_testNightly( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { } )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockModifyOtherDeleteOtherRestore( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true } )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

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

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse2( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitResponse2 = true } )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockModifyOtherDeleteOtherRestoreSplitResponse3( rxTest )

	-- NMB-12766
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitResponse3 = true } )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzModifyOtherDeleteOtherRestore_xmp_testNightly( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- This version tests with external develop settings.
	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { xmpDevelop = true,  } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockModifyOtherDeleteOtherRestore_xmp( rxTest )
	-- NMB-10868
	-- Verifies this client's modification are transfered from the original
	-- to the restored asset when a tombstone and restore is received from
	-- another client.

	-- This version tests with external develop settings.
	testAddSyncModifyOtherDeleteOtherRestoreSync( rxTest, { mock = true, xmpDevelop = true,  } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockAddSyncOtherDeleteRestoreSync_SplitResponse( rxTest )

	-- WF-1802 Stuck sync
	-- https://jira.corp.adobe.com/browse/WF-1802
	-- This bug is caused by split sync feed from Oz
	-- 1. Add asset and sync. Then go offline
	-- 2. Other device delete and restore the asset
	-- 3. Goes back online and get split sync feed from Oz
	--		- A deletion with new_id B
	--		- B deletion with new_id C and C docValue
	-- This would cause puller falsely create a local C with subtype deleted_image

	local options = { mock = true }

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader = 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 catalogId = catalog:ozCatalogId()
	local userId = catalog:ozUserId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId

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

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

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

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

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

-- Step 1:

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

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

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

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

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

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

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

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

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


-- Step 2: Receive Delete and restore action by other device from Oz sync feed

	local trashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local restoredRevId = KSIdGenerator.uuidInLowercaseHex()

	-- split resposne with first A deletion

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

	-- Make sure the asset is deleted
	-- Trash doc is created without type and subtype, only _localOnly files

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

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( tempLocalTrashDocValue )

			local originalPath = WFCatAssetUtils.getFullOriginalPathFromDocValue( tempLocalTrashDocValue )
			local haveOriginal = originalPath and WFFileUtils.exists( originalPath ) == 'file'

			return tempLocalTrashDocValue.type == nil
				and tempLocalTrashDocValue.subtype == nil
				and haveOriginal
		end ),
	true )

	-- split response with second B deletion and C creation

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

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

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

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0 ) )

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

			return haveOriginal
		end ),
	true )

end


--==============================================================================
-- @mark tests that involve other client actions that conflict with local actions
--==============================================================================

-- These tests are ultimately testing WFCatErrorHandler's resourceNotFoundHandler.
--
-- We always attempt to have just one asset (to avoid generating duplicates).
--
-- Expected result is to have asset of type indicated by local action on the left.
-- In some cases we have to directly create assets using whatever binaries are
-- available locally.
--
-- In addition we attempt to preserve local modifications if they exist.
--
-- Below is a table of conflicting actions showing jira story number if test exists.
--
-- XXXXXXXX indicates the other client can't do that action as both clients must
-- start with the same asset. - indicates a test that needs to be written.
--
---------------------------------Other Client-----------------------------------
--
--                          Delete------ Restore----- DelTrash---- Delete/Restore Delete/DelTrash Restore/Delete Restore/Delete/DelTrash Restore/Delete/Restore Delete/Restore/Delete Delete/Restore/Delete/DelTrash
-- T    Delete              NMB-9390     XXXXXXXX     XXXXXXXX     NMB-13896 #3   NMB-13896 #7    XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
-- H    Restore             XXXXXXXX     NMB-10313    NMB-9395     XXXXXXXX       XXXXXXXX        NMB-13896 #6   -                       -                      XXXXXXXX              XXXXXXXX
-- I    DelTrash            XXXXXXXX     -            -            XXXXXXXX       XXXXXXXX        -              -                       -                      XXXXXXXX              XXXXXXXX
-- S    Delete/Restore      NMB-13896 #4 XXXXXXXX     XXXXXXXX     NMB-13896 #5   NMB-14104       XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
--      Delete/DelTrash     -            XXXXXXXX     XXXXXXXX     -              -               XXXXXXXX       XXXXXXXX                XXXXXXXX               -                     -
-- C    Restore/Delete      XXXXXXXX     NMB-13896 #1 NMB-13896 #8 XXXXXXXX       XXXXXXXX        NMB-13896 #2   -                       -                      XXXXXXXX              XXXXXXXX
-- L
-- I    Restore/Delete/DelTrash (should be same as DelTrash)
-- E    Restore/Delete/Restore (should be same as Restore)
-- N    Delete/Restore/Delete (should be same as Delete)
-- T    Delete/Restore/Delete/DelTrash (should be same as Delete/DelTrash)
--

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

local function testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, options )

-- NMB-13896 #3 / NMB-14704
-- Scenario:
-- Steps:
--		1) Add photo, Sync.
--		2) Pause Sync.
--      3) Other Client Delete, Other Client Restore.
--		4) Delete the photo.
-- 		5) Resume Sync.
--
-- Expected: Should end up with the photo in the trash.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = 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, Sync.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

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

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

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

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

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

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

	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		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, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = true } ), 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 = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- 2) Pause Sync. - this test issues separate sync actions so need to pause.

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

-- 3) Other Client Delete, Other Client Restore.
	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	WFCatTrashTestUtils.simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userUpdated = userCreated,
		options = options,
	}

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, options ), true )
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- 4) Delete the photo.

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

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

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

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( otherClientTrashAssetId, otherClientRestoredAssetId, userId ),
		WFCatTrashTestUtils.restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset", otherClientTrashAssetId, userId ),
	}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

	-- The error handler should update the trash asset createFrom info to refer to the
	-- otherClientTrashAsset unfortunately this will require it to refer to an intermediate
	-- restored asset id as Oz doesn't allow making one trash asset from another.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientTrashAssetId )
	:dispose()

	-- Get the intermediate asset's id and revId created by WFCatErrorHandler
	local intermediateId, intermediateRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( localTrashDocValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( localTrashDocValue )
			intermediateId = createFromInfo.lastAssetId
			intermediateRevId = createFromInfo.lastRevisionId
			return type( intermediateId ), type( intermediateRevId )
		end )
	, 'string', 'string' )

	-- Since the subtypes are the same at this point the pusher will generate a restore/delete combo
	-- and since the otherClientTrashAssetId was deleted when making otherClientRestoredAssetId this
	-- restore request will also generate a resource not found error.
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreDeleteAssetReqResp( catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, intermediateId, intermediateRevId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

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

	else

		-- Get the error with respect to what we tried to create the trash asset from as it is no longer there.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", intermediateId, intermediateRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

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

	end

	-- Make sure trashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
	:map( function( docValue )
		return docValue._deleted == nil and docValue._empty == nil
	end ), true )

	-- The error handler should update the trash asset createFrom info one more time to refer
	-- to the otherClientRestoreAsset.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientRestoredAssetId )
	:dispose()

	-- Send up the delete request this time against the other client's restored asset id.
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- Success! Get the trash asset and tombstone for otherClientRestoredAssetId back in the sync feed.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			WFCatTrashTestUtils.assetDeletedResponse( otherClientRestoredAssetId, trashAssetId, userId ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", intermediateId, intermediateRevId, "asset", intermediateId, "asset", nil, userId ),
	    }, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

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

	else

		-- Success! Get the trash asset and tombstone for otherClientRestoredAssetId back in the sync feed.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = not options.proxyOnlyAsset, proxy = true, renditions = true, xmpDevelop = options.xmpDevelop } ),
			WFCatTrashTestUtils.assetDeletedResponse( otherClientRestoredAssetId, trashAssetId, userId ),
	    }, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

	end

	-- Make sure trashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
	:map( function( docValue )
		return docValue._deleted == nil and docValue._empty == nil
	end ), true )

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

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredSecondPartOfTrashPair_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestoreDeleted = {
					count = 1,
					sourceDocIds = {
						intermediateId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockDelete_OtherDeleteOtherRestore( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzDelete_OtherDeleteOtherRestore_testNightly( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockDelete_OtherDeleteOtherRestore_splitErrors( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitErrors = true } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzDelete_OtherDeleteOtherRestore_splitErrors_testNightly( rxTest )
-- NMB-13896 #3 / NMB-14704
	testAddSyncDeleteOtherDeleteOtherRestoreSync( rxTest, { splitErrors = true } )
end

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

local function testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, options )

-- NMB-13896 #6 / NMB-14708
-- Scenario:
-- Steps:
--		1) Add photo, Sync.
--		2) Delete the photo.
--		3) Sync, Pause.
--      4) Other Client Restore
--		5) Other Client Delete.
-- 		6) Restore the photo.
--		7) Resume Sync.
--
-- Expected: Photo should be restored

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = 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, Sync.
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

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

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

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

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

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

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

	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		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, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = true } ), 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 = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- 2) Delete the photo.

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

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

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

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true } ),
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

-- 3) Sync, Pause.

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

-- 4) Other Client Restore

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

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

-- 5) Other Client Delete.

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, options ), true )

-- Original Client ------------------------------------------------------------

-- 6) Restore the photo.

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

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

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

-- 7) Resume Sync.

	-- Send up the Restore request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAsset2ReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId, restoredDocValue.userUpdated ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, otherClientRestoredAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( otherClientRestoredAssetId, otherClientTrashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_newId'), nil )

	-- Get the errors with respect to what we tried to copy from as they are no longer there.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
	}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

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

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( WFCatTrashUtils.getCreateFromId )
	, otherClientRestoredAssetId )

	-- Get the intermediate asset's id and revId created by WFCatErrorHandler
	local intermediateId, intermediateRevId
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( docValue )
			local createFromInfo = WFCatTrashUtils.createFromInfo( docValue )
			intermediateId = createFromInfo.lastAssetId
			intermediateRevId = createFromInfo.lastRevisionId
			return type( intermediateId ), type( intermediateRevId ), intermediateId ~= createFromInfo.id
		end )
	, 'string', 'string', true )

	-- Verify the intermediateId has _newId updated with restoredAssetId.
	rxTest:expectSingleValue( catalog:docStore():docValue( intermediateId ):pluck( '_newId' ), restoredAssetId )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_empty'), true )

	-- Other Client Trash asset should still be there.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId ):pluck('_empty'), nil )

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteRestoreAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, intermediateId, intermediateRevId, restoredAssetId, restoredRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

		-- When issuing delete/restore pair oz typically sends the first resourceNotFoundError
		-- quickly and the second a minute later so make sure we handle this more common path.

		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

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

	else

		-- Get both of the errors related to trying to delete from the otherClientRestoredAssetId
		-- which no longer exists either.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", intermediateId, "deleted_asset", nil, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

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

	end

	-- Verify we will now try to restore from the otherClientTrashAssetId.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( WFCatTrashUtils.getCreateFromId )
	, otherClientTrashAssetId )

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreAssetReqResp( catalogId, restoredAssetId, otherClientTrashAssetId, otherClientTrashAssetRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then

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

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

	else

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

	end

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

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				Ignored_deleteWasSentToOzBeforeAssetExisted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						otherClientRestoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestore = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockRestore_OtherRestoreOtherDelete( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, { mock = true } )
end

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

-- FIXME [jandrew 2025-03-11]: Disabled due to instability.

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.DISABLED_rxTest_CT_OzRestore_OtherRestoreOtherDelete_testNightly( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockRestore_OtherRestoreOtherDelete_splitErrors( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = true } )
end

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

-- FIXME [jandrew 2025-03-11]: Disabled due to instability.

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.DISABLED_rxTest_CT_OzRestore_OtherRestoreOtherDelete_splitErrors_testNightly( rxTest )
-- NMB-13896 #6
	testAddDeleteSyncRestoreOtherRestoreOtherDeleteSync( rxTest,  { splitErrors = true } )
end

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

local function testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, options )

-- NMB-13896 #5 / NMB-14707
--
-- This tests the two client scenario where one client attempts to delete a
-- photo while another client deleted and restored that asset.
--
-- Scenario:
-- Steps:
--		1) Add photo, let it sync.
--		2) Sync, Pause.
--		3) Other client delete the photo.
--		4) Other client restore the photo.
-- 		5) Delete the photo.
--		6) Restore the photo.
--		7) Resume Sync.
--
-- Expected: Photo is restored.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = 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 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,
	}

-- 4) Other client restore the photo.

	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

	WFCatTrashTestUtils.simulateOtherClientAssetRestore {
		rxTest = rxTest,
		catalog = catalog,
		catalogSpec = catalogSpec,
		assetId = otherClientRestoredAssetId,
		revisionId = otherClientRestoredRevId,
		trashAssetId = otherClientTrashAssetId,
		trashAssetRevId = otherClientTrashAssetRevId,
		userUpdated = userCreated,
		options = options,
	}

-- Original Client ------------------------------------------------------------

-- 5) Delete the photo.

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

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

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

-- 6) Restore the photo.

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

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

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

-- 7) Resume Sync.

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( assetId, otherClientTrashAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( otherClientTrashAssetId, otherClientRestoredAssetId, userId ),
		WFCatTrashTestUtils.restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, 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 other client's trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( restoredAssetId )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientTrashAssetId )
	:dispose()

	-- Send up a new Restore request referring to the other client's trash asset
	catalogSpec:expectHttpRequest( 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' )

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

	end

	-- Verify the other client's restored asset exists. Any modifications should be
	-- moved over as well, which is tested by other tests but could be added here.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
		:map( function( otherClientRestoredDocValue )
			return otherClientRestoredDocValue._deleted, otherClientRestoredDocValue._empty
		end )
	, nil, nil )

	-- Get another resource not found this time for the otherClientTrashAssetId as it was also restored by the other client.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", otherClientTrashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( false )), 'success' )

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

	-- Verify the local asset has been removed in favor of keeping
	-- the other client's restored asset.
	rxTest:expectSingleValue( catalog:docStore():docValue( restoredAssetId )
		:map( function( localRestoredDocValue )
			return localRestoredDocValue._deleted
		end )
	, true )

	-- Verify the other client's restored asset exists. Any modifications should be
	-- moved over as well, which is tested by other tests but could be added here.
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
		:map( function( otherClientRestoredDocValue )
			return otherClientRestoredDocValue._deleted, otherClientRestoredDocValue._empty
		end )
	, nil, nil )

	-- Send an update for the local asset we merged on top of the otherClient's restored asset.
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushUpdateAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, subtype, options.xmpDevelop ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.restoredAssetUpdateResponse( otherClientRestoredAssetId, otherClientRestoredRevId, fileName, subtype, assetCreated, userCreated, importSource, assetId, true, true ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

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

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				IgnoredMoreThanOneOperationBehind = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalDelete = {
					count = 1,
					sourceDocIds = {
						assetId,
					},
				},
				CreatedOverlaysToMergeTheRestoredAssetsDeletingLocal_LocalRestore = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherRestore( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzDeleteRestore_OtherDeleteOtherRestore_testNightly( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockDeleteRestore_OtherDeleteOtherRestore_splitErrors( rxTest )
-- NMB-13896 #5
	testAddSyncDeleteRestoreOtherDeleteOtherRestoreSync( rxTest, { mock = true, splitErrors = true } )
end

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

local function testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, options )

-- NMB-13896 #2 / NMB-14703
-- Scenario:
-- Steps:
--		1) Add photo.
--		2) Delete the photo.
--		3) Sync, Pause.
--      4) Other Client Restore, Other Client Delete.
-- 		5) Restore and Delete the photo.
--		6) Resume Sync.
--
-- Expected: Photo in Trash
--   Should get two ResourceNotFoundError, the first of which should update the
--   trash createFrom info to restore from the oz asset and the second should be
--   ignored.
--   After a second attempt of the delete it will get one more ResourceNotFoundError
--   which will result in discarding our trash asset in favor of the other clients.

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

	local testSession, catalog, catalogSpec, pusher, puller, uploader, pc, errorHandler = 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 )

-- Step 1: Add photo
	local tempTestFolder = WFTestFileUtils.createTestFolder( rxTest )

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

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

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

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

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

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

	-- push and validate for the binary upload
	local expectedOriginalUploadCount = 0
	local expectedProxyUploadCount = 0
	if options.proxyOnlyAsset then
		expectedProxyUploadCount = expectedProxyUploadCount + 1

		-- Upload the proxy (and generate renditions)
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.createProxyReqResp( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )
		uploader:startContinuousUploads()

		-- wait for the uploader to issue the create proxy request
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'proxy' ))
		:expectDataWithinTime( 5 * waitMultiplier, expectedProxyUploadCount )
		:dispose()
	else
		expectedOriginalUploadCount = expectedOriginalUploadCount + 1

		-- Upload the master
		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, expectedOriginalUploadCount )
		:dispose()
	end
	uploader:stopContinuousUploads()

	-- Wait for oz to generate the renditions before requesting next update
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 10 * waitMultiplier, catalogSpec, catalogId, assetId, revisionId, subtype, { proxyOnlyAsset = options.proxyOnlyAsset } ), 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 = not options.proxyOnlyAsset, proxy = options.proxyOnlyAsset, renditions = true }, nil, true )
	}, WFCatTrashTestUtils.stripAssetAndLocation ), 'success' )

	-- Wait for the update to get applied to the assets docValue
	local binaryRelsKey = options.proxyOnlyAsset and '/rels/proxy_type/2560' or '/rels/master'
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', binaryRelsKey, 'href' )
		:map( type ))
	:expectDataWithinTime( 5 * waitMultiplier, 'string' )
	:dispose()

-- Step 2: Move the asset to trash and sync it

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

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

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

	-- Send up the Delete request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashRevId ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	-- Get the trashed asset back in the sync feed with the links to necessary binaries
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.trashAssetUpdateResponse2( trashAssetId, trashRevId, fileName, trashAssetSubtype, trashUserCreated, trashPurgeDate, userCreated, importSource, assetId, { master = true, renditions = true } ),
		WFCatTrashTestUtils.assetDeletedResponse( assetId, trashAssetId, userId ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

-- Step 3 Pause Sync - this test issues separate sync actions so need to pause.

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

-- Step 4: On a second client, restore the trash asset

	-- Restore asset on other client
	local otherClientRestoredAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientRestoredRevId = KSIdGenerator.uuidInLowercaseHex()

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

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, trashAssetId, trashRevId, options ), true )

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, 'image' ), true )

	local otherClientTrashAssetCreated = KSDate.timeInIso8601Format()
	local otherClientTrashPurgeDate = KSDate.shiftIso8601DateTime( otherClientTrashAssetCreated, 5184001 )
	local otherClientTrashAssetId = KSIdGenerator.uuidInLowercaseHex()
	local otherClientTrashAssetRevId = KSIdGenerator.uuidInLowercaseHex()

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

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilDeleted( 10 * waitMultiplier, catalogSpec, catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, options ), true )

	rxTest:expectSingleValue( WFCatTrashTestUtils.pollUntilHasRequiredLinks( 5 * waitMultiplier, catalogSpec, catalogId, otherClientTrashAssetId, otherClientTrashAssetRevId, 'deleted_image' ), true )

-- Original Client ------------------------------------------------------------

-- Step 5: Restore and Delete the asset locally

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

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

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

	-- Delete the locally restored asset.
	rxTest:expectOnlyCompleted( WFCatTrash.moveAssetsToTrash( catalog, { restoredAssetId }, pc ) )

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

	-- Get the new trash asset id
	local trashAssetId2, trashRevId2 = WFCatTrashTestUtils.getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, restoredAssetId )
	rxTest:expectEqual( 'string', type( trashAssetId2 ) )
	rxTest:expectEqual( 'string', type( trashRevId2 ) )

-- Step 6: Turn on Sync

	-- Send up the Restore/Delete request
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushRestoreDeleteAssetReqResp( catalogId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 2, kTimeoutForSendingUpdates ) )

	-- Get the other client's updates from the sync feed.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetDeletedResponse( trashAssetId, otherClientRestoredAssetId, userId ),
		WFCatTrashTestUtils.assetDeletedResponse( otherClientRestoredAssetId, otherClientTrashAssetId, userId ),
		WFCatTrashTestUtils.trashAssetUpdateResponse2( otherClientTrashAssetId, otherClientTrashAssetRevId, fileName, trashAssetSubtype, otherClientTrashAssetCreated, otherClientTrashPurgeDate, userCreated, importSource, otherClientRestoredAssetId, { master = true, renditions = true } ),
	}, WFCatTrashTestUtils.transformOrderAndStripAssetUpdate( true ) ), 'success' )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId )
	:map( function( otherClientDocValue )
		otherClientRestoredRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), true )

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

	if options.splitErrors then

		-- Get the errors with respect to what we tried to copy from as they are no longer there.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )

	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( "create", restoredAssetId, restoredRevId, "asset", trashAssetId, "deleted_asset", otherClientRestoredAssetId, userId ),
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )
	end

	-- Verify WFCatErrorHandler will update the createFrom trash info
	-- so that we attempt to create from the oz trash asset even though
	-- it is not there anymore, we will re-issue the restore request
	-- and get a new error later.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( WFCatTrashUtils.getCreateFromId ) )
	:expectDataWithinTime( 5 * waitMultiplier, otherClientRestoredAssetId )
	:dispose()

	-- Verify the otherClientRestoredAssetId has _newId updated with
	-- trashAssetId2.
	rxTest:observe( catalog:docStore():ongoingDocValue( otherClientRestoredAssetId )
	:map( function( otherClientRestoredDocValue )
		return otherClientRestoredDocValue._newId
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, trashAssetId2 )
	:dispose()

	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientRestoredAssetId ):pluck('_empty'), true )
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId ):pluck('_empty'), nil )
	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId2 ):pluck('_empty'), nil )

	-- Send up a new Delete request referring to the other client's restored asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetReqResp( catalogId, otherClientRestoredAssetId, otherClientRestoredRevId, trashAssetId2, trashRevId2 ) )
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 1, kTimeoutForSendingUpdates ) )

	if options.splitErrors then
		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			-- First error is leftover from our first attempt at the delete/restore.
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", restoredAssetId, restoredRevId, "asset", restoredAssetId, "asset", nil, userId ),
			-- Second error is from the WFCatTrashTestUtils.pushDeleteAssetReqResp call above.
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )
	else
		-- Get the error related to trying to delete from the otherClientRestoredAssetId which no longer exists either.
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller, {
			WFCatTrashTestUtils.resourceNotFoundErrorUpdateResponse( "delete", otherClientRestoredAssetId, otherClientRestoredRevId, "asset", otherClientRestoredAssetId, "asset", otherClientTrashAssetId, userId ),
		}, WFCatTrashTestUtils.stripUpdatesAndLinks ), 'success' )
	end


	-- Verify WFCatErrorHandler will remove the local trashAssetId and
	-- moving any mods to the otherClientTrashAssetId.
	rxTest:observe( catalog:docStore():ongoingDocValue( trashAssetId2 )
	:map( function( trashAssetDoc2 )
		return trashAssetDoc2._empty
	end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

	-- Make sure otherClientTrashAssetId still exists
	rxTest:expectSingleValue( catalog:docStore():docValue( otherClientTrashAssetId )
	:map( function( otherClientDocValue )
		otherClientTrashAssetRevId = otherClientDocValue._revId
		return otherClientDocValue._deleted
	end ), nil )

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

	-- Check to see how the ResourceNotFoundErrors were resolved
	rxTest:expectEqual( {
		ResourceNotFoundError = {
			count = 3,
			resolution = {
				DeletedLocalAsset_LocalDelete = {
					count = 1,
					sourceDocIds = {
						otherClientRestoredAssetId,
					},
				},
				IgnoredRevisionDoesNotMatch = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
				UpdatedLocalCreateFromTargetingOzDoc_LocalRestoreDeleted = {
					count = 1,
					sourceDocIds = {
						restoredAssetId,
					},
				},
			},
		},
	}, errorHandler:DEBUG_errorsResolved() )

end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockRestoreDelete_OtherRestoreOtherDelete( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, { mock = true } )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_OzRestoreDelete_OtherRestoreOtherDelete_testNightly( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest )
end

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

function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.rxTest_CT_MockRestoreDelete_OtherRestoreOtherDelete_splitErrors( rxTest )
-- NMB-13896 #2
	testAddDeleteSyncRestoreDeleteOtherRestoreOtherDeleteSync( rxTest, { mock = true, splitErrors = true } )
end

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


function WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( 'WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest', WFCatTrashMultipleDevicesOtherDeleteOtherRestoreTest )
