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

WFCatTrashAssociatedDocsTest.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 WFCatTrash = require 'WFCatTrash'
local WFCatTrashTestUtils = require 'WFCatTrashTestUtils'
local WFCatStorage = require 'WFCatStorage'
local WFCatVersions = require 'WFCatVersions'

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

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

local coveragePath

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

local failStubbedTests = false

local stripped = '<<stripped>>'

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

local kOzWaitMultiplier = 30
local kTimeoutForSendingUpdates = 5

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

local WFCatTrashAssociatedDocsTest = {}

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

local function setTimeoutAndRetryLimit( rxTest, options, timeoutValue )

	rxTest:setTimeLimit( timeoutValue )

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

end

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

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

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

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

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

local function testAddDeleteRestoreSyncAssocDocs( rxTest, options )

	options = options or {}

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	-- Make sure we sync up the newly restored asset, album, and album_asset (shouldn't issue a restore in this case)
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, restoredAssetId, albumId, albumName, fileName, subtype ) )

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

	-- Get the restored asset, album and album_asset back in the sync feed with the links to upload the original
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 7 * waitMultiplier, catalogSpec, catalogId, puller, {
		WFCatTrashTestUtils.assetUpdateResponse( restoredAssetId, restoredRevId, fileName, subtype, assetCreated, userCreated, importSource, { master = false } ),
		WFCatTrashTestUtils.albumUpdateResponse( catalogId, albumId, albumRevId, albumName ),
		WFCatTrashTestUtils.albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, restoredAssetId )
	}), 'success' )

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

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

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

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

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

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

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

			return haveOriginal
		end ),
	true )

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockAddDeleteRestore_AssocDocs( rxTest )

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

	testAddDeleteRestoreSyncAssocDocs( rxTest, { mock = true } )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzAddDeleteRestore_AssocDocs_testNightly( rxTest )

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

	testAddDeleteRestoreSyncAssocDocs( rxTest )

end

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

local function testAddAssocDocsSyncAddAssocDocsDeleteRestoreSync( rxTest, options )

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

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

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

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

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

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

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )

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

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

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

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

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

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

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )

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

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

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

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

-- Step 3:

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

	-- Sync up the album
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAlbumReqResp( catalogId, albumId2, albumName2 ) )

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

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

-- Step 4:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	local function transformOrderAndStrip_DeleteRestore_AssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

		WFCatTrashTestUtils.removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

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

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

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

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

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

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

		return mockDataAsTable, ozDataAsTable

	end

	local userId = catalog:ozUserId()

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

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

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

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

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

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

	local function arraySize( queryResults )
		return #queryResults
	end

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

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


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

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

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

			return haveOriginal
		end )
	, true )

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteRestore_AssocDocs( rxTest )

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

end

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

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

function WFCatTrashAssociatedDocsTest.DISABLED_rxTest_CT_OzDeleteRestore_AssocDocs_testNightly( rxTest )

	-- NMB-9375
	testAddAssocDocsSyncAddAssocDocsDeleteRestoreSync( rxTest )

end

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

local function testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest, options )

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

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

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

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

	local catalogId = catalog:ozCatalogId()
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local 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:' )

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

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )

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

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

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

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

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

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

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )

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

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

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

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

-- Step 3:

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

	-- Sync up the album
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAlbumReqResp( catalogId, albumId2, albumName2 ) )

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

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

-- Step 4:

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

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


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

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

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

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAssetAndCreateAssocDocsReqResp( catalogId, assetId, trashAssetId, trashRevId, albumId2, spaceId, commentId2, commentBody2 ) )

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

	local function transformOrderAndStrip_DeleteAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

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

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

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

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

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

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

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

		return mockDataAsTable, ozDataAsTable

	end

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

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

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

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

	local function arraySize( queryResults )
		return #queryResults
	end

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

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

	if options.restore then

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

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

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

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

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

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

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

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


		local function transformOrderAndStrip_RestoreAssocDocs( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

			mockDataAsTable, ozDataAsTable = 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

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

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

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

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

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

			return mockDataAsTable, ozDataAsTable

		end

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

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

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

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

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

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


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

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

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

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

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

				return haveOriginal
			end )
		, true )

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

	end

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDelete_AssocDocs_testNightly( rxTest )

	-- NMB-10089
	testAddSyncDeleteSyncRestoreSyncAssocDocs( rxTest )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDelete_AssocDocs( rxTest )

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

end

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

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

function WFCatTrashAssociatedDocsTest.DISABLED_rxTest_CT_OzDeleteSyncRestore_AssocDocs_testNightly( rxTest )

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteSyncRestore_AssocDocs( rxTest )

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

end

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

local function testAddSyncDeleteDelTrashSyncAssocDocs( rxTest, options )

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

	options = options or {}

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

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

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

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

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

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

	-- Sync up the newly created asset, album, and album_asset
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAssetAlbumAndAlbumAssetReqResp( catalogId, assetId, albumId, albumName, fileName, subtype ) )

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

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

	uploader:stopContinuousUploads()

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

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

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

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

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

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

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

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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody1 ) )

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

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


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

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId ) )

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

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

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

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

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

	-- Sync up the album
	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateAlbumReqResp( catalogId, albumId2, albumName2 ) )

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

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

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

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

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

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

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

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

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

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

	-- Now delete the trash asset (Oz refers to this as purging it)
	rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

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

	rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
		:map( function( docValue )
			-- Get the last revision of the original asset (revision when applyOverlay with _deleted = true).
			trashRevId = docValue._revId
			return docValue
		end ), ':ignore:' )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteAndDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId, assetId, revisionId, userCreated, purgeDate ) )

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

	local userId = catalog:ozUserId()

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

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

	local function removeNonAssetUpdates( updates )

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

		return updates

	end

	local assetUpdateCount = 0
	rxTest:observe( WFCatTrashTestUtils.pollHttpGetMetadataUpdates( catalogSpec, catalog:docStore(), '200', expectedResponse )
		:map( function( statusCode, response )
			local assetUpdates = removeNonAssetUpdates( response.updates )
			-- we might get the asset updates split across metadata pulls so keep track of the total so far
			assetUpdateCount = assetUpdateCount + #assetUpdates
			return statusCode, assetUpdateCount
		end ) )
	:expectDataWithinTime( 5 * waitMultiplier, '200', 2 )
	:dispose()

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

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

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

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

		return mockDataAsTable, ozDataAsTable

	end ), 'success' )

	local function arraySize( queryResults )
		return #queryResults
	end

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

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteDelTrash( rxTest )

	-- NMB-10294

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

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

	testAddSyncDeleteDelTrashSyncAssocDocs( rxTest, { mock = true } )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDeleteDelTrash_testNightly( rxTest )

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

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

	testAddSyncDeleteDelTrashSyncAssocDocs( rxTest )

end

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

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

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

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

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

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

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

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

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

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

	local waitMultiplier = options.mock and 2 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 15 * waitMultiplier )

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

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

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

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

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

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

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

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

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

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


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

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

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

	-- Validate trash asset, or leave that to other tests?

	-- Remove the asset from the trash.
	rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )


	-- 'Turn on' sync and validate that 'the right thing' happens, which should
	-- be nothing?
	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( 0, kTimeoutForSendingUpdates ) )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockAddDeleteDelTrash_AssocDocs( rxTest )
	-- This shouldn't send anything to oz at all
	testAddDeleteDelTrashSync( rxTest, { mock = true, assoc = true } )
end

--==============================================================================
-- @mark tests that involve setting the cover on assets that are being deleted
--==============================================================================

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockSetCoverSyncDelete( rxTest )

	-- NMB-10225
	-- Scenario:
	-- 1 Set and asset to be the album cover
	-- 2 let album update sync up to oz
	-- 3 Delete the asset.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- Only the delete asset is sent to oz
		-- The album update isn't sent to oz as oz will do it for us.
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzSetCoverSyncDelete_testNightly( rxTest )

	-- NMB-10225
	-- Scenario:
	-- 1 Set and asset to be the album cover
	-- 2 let album update sync up to oz
	-- 3 Delete the asset.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- Only the delete asset is sent to oz
		-- The album update isn't sent to oz as oz will do it for us.
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockSetCoverDelete( rxTest )

	-- NMB-10226
	-- Scenario:
	-- While offline the user sets the album cover to an asset
	-- User then deletes the asset and then turns on sync.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- The album update and the delete asset are sent.
		-- if we receive an error related to 'asset must exist' it is ignored.

	-- Oz can send this error as well depending on how the updates
	-- get processed:
		-- {"code":1005,"description":"Input Validation Error","errors":{"cover":["asset must exist"]},"subtype":"InputValidationError"}

-- TODO [kcarlson 2018-10-09]: Look into not sending the album update in this case. (2.0 currently does)

	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzSetCoverDelete_testNightly( rxTest )

	-- NMB-10226
	-- Scenario:
	-- While offline the user sets the album cover to an asset
	-- User then deletes the asset and then turns on sync.
	-- Expected Result:
		-- The album cover is removed from the document and a new album cover
		-- is automatically picked (by capture date or sort order).
		-- The album update and the delete asset are sent.
		-- if we receive an error related to 'asset must exist' it is ignored.

	-- Oz can send this error as well depending on how the updates
	-- get processed:
	-- {"code":1005,"description":"Input Validation Error","errors":{"cover":["asset must exist"]},"subtype":"InputValidationError"}

	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDeleteOtherSetCover_testNightly( rxTest )

	-- NMB-10224
	-- Scenario:
	-- Client1 while offline deletes asset A
	-- Client2 while online set album cover to asset A
	-- Client1 turns on sync.
	-- Expected: client1 will not show album with deleted asset, and will automatically pick a new cover
	rxTest:expectEqual( false, failStubbedTests )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteOtherSetCover( rxTest )

	-- NMB-10224
	-- Scenario:
	-- Client1 while offline deletes asset A
	-- Client2 while online set album cover to asset A
	-- Client1 turns on sync.
	-- Expected: client1 will not show album with deleted asset, and will automatically pick a new cover
	rxTest:expectEqual( false, failStubbedTests )

end

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

local function testAddSyncDeleteSyncWithVersion( rxTest, options )

	options = options or {}

	options.aux = options.xmpDevelop

	local waitMultiplier = options.mock and 1 or kOzWaitMultiplier

	setTimeoutAndRetryLimit( rxTest, options, 20 * waitMultiplier )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	local auxMockHttpRequests, auxInfo

	if options.aux then

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

		local auxId = auxInfo and next( auxInfo )

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

	end

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

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

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

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

	-- Create the Version Document

	local versionName = 'Foo'
	local versionId
	local versionRevId
	local versionBody

	rxTest:waitFor( WFCatVersions.createVersionForAsset( catalog, assetId, false, versionName )
		:map( function( newId )
			versionId = newId
			return true
		end ) )

	local versionXmpDevelopFilePath
	rxTest:waitFor( catalog:docStore():docValue( versionId )
		:map( function( docValue )
			versionXmpDevelopFilePath = derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' )
			-- on mobile this path is relative so we have to derelativize it for the upload xmp call
			if WFPlatformInfo.isiOS then
				versionXmpDevelopFilePath = KSPathUtils.child( require 'OzFilePaths'.getCatalogBasePath( catalogId ), versionXmpDevelopFilePath )
			elseif WFPlatformInfo.isAndroid then
				versionXmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), versionXmpDevelopFilePath )
			end
			versionRevId = docValue._revId

			versionBody = {
				userCreated = docValue.userCreated,
				userUpdated = docValue.userUpdated,
				name = docValue.name,
				develop = docValue.develop,
				createdManually = false
			}

			return true
		end ) )

	catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushCreateVersionReqResp( catalogId, versionId, assetId, versionName ) )

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

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

	-- Start the binary uploads

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

	uploader:startContinuousUploads()

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

		local updates = {}

		table.insert( updates,
			WFCatTrashTestUtils.assetUpdateResponse(
					assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
					{ master = false, renditions = false, xmpDevelop = false, aux = false, xmpSidecar = options.xmpSidecar },
					expectedDevelopSettings, nil, auxInfo ) )

		-- if there's a sidecar the uploader is waiting for the sidecar to appear before it uploads the master so mark it so
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller,
			updates,
			WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	if options.aux then

		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		
		for _, mock in ipairs( auxMockHttpRequests ) do
			catalogSpec:expectHttpRequest( mock )
		end
		
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'aux-direct' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()

		local updates = {}

		table.insert( updates,
			WFCatTrashTestUtils.assetUpdateResponse(
					assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
					{ master = false, renditions = false, xmpDevelop = false, aux = options.aux, xmpSidecar = options.xmpSidecar },
					expectedDevelopSettings, nil, auxInfo ) )

		-- if there's a sidecar the uploader is waiting for the sidecar to appear before it uploads the master so mark it so
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller,
			updates,
			WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

	-- Wait for the upload of xmp_develop for the asset
	if options.xmpDevelop then
	
		revisionId = WFCatTrashTestUtils.getLatestRevisionId( rxTest, catalog, assetId )
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadXmpDevelopReqResp( catalogId, assetId, revisionId, xmpDevelopFilePath ) )
	
		rxTest:observe( WFCatTrashTestUtils.pollUploadCount( uploader, 'xmp_develop' ))
		:expectDataWithinTime( 5 * waitMultiplier, 1 )
		:dispose()
		
		local updates = {}
		
		table.insert( updates,
			WFCatTrashTestUtils.assetUpdateResponse(
					assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
					{ master = false, renditions = false, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
					expectedDevelopSettings, nil, auxInfo ) )
					
		-- if there's a sidecar the uploader is waiting for the sidecar to appear before it uploads the master so mark it so
		rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 10 * waitMultiplier, catalogSpec, catalogId, puller,
			updates,
			WFCatTrashTestUtils.stripForAssetUpdate( false ) ), 'success' )

	end

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

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

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

	local metadataUpdates = {
				WFCatTrashTestUtils.assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
								{ master = true, renditions = waitForRenditions, xmpDevelop = options.xmpDevelop, aux = options.aux, xmpSidecar = options.xmpSidecar },
								expectedDevelopSettings, nil, auxInfo )
	}

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller,
		metadataUpdates,
		WFCatTrashTestUtils.stripForAssetUpdate( waitForRenditions ) ), 'success' )

	if options.xmpDevelop then
		-- xmp_develop for the version document
		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.uploadVersionXmpDevelopReqResp( catalogId, versionId, versionRevId, versionXmpDevelopFilePath ) )
	end

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

	metadataUpdates = {
		WFCatTrashTestUtils.versionUpdateResponse( catalogId, versionId, versionRevId, assetId, sha256, versionBody, true, true )
	}

	-- Get the updated metadata with the master link.
	rxTest:expectSingleValue( WFCatTrashTestUtils.pollForUpdateWith( 5 * waitMultiplier, catalogSpec, catalogId, puller,
		metadataUpdates,
		WFCatTrashTestUtils.stripForAssetUpdate( waitForRenditions ) ), 'success' )

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

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

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

		local auxId = auxInfo and next( auxInfo )

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

	end

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

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

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

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

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

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

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

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

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

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

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

			trashDocValue = docValue

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

	-- Check to see if oz updated the renditions because we may have deleted before they were fully generated.
	do

		local hasNewRenditionLinks = false

		rxTest:expectSingleValue( WFCatTrashTestUtils.httpGetAssetAsDecodedJson( catalogSpec, catalogId, trashAssetId )
			:map( function( assetAsJson )

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

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

			end )
		, ':ignore:' )

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

	end

	if options.restore then

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

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

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

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

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

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

		-- Verify we send up the restore.

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

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

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

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

		-- Since we don't actually download files in this test, don't make this check when
		-- performing the out of storage or expired tests as they are removing the binary
		-- to make sure we indicate that it needs to be downloaded.

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

	end

	if options.deleteTrash then

		-- Delete the asset after syncing up so the deleted asset also gets deleted on oz.
		rxTest:expectOnlyCompleted( WFCatTrash.deleteTrashAssets( catalog, { trashAssetId }, pc ) )

		rxTest:expectSingleValue( catalog:docStore():docValue( trashAssetId )
			:map( function( docValue )
				-- Get the last revision of the original asset (revision when applyOverlay with _deleted = true).
				trashRevId = docValue._revId
				return docValue
			end ), ':ignore:' )

		catalogSpec:expectHttpRequest( WFCatTrashTestUtils.pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId ) )

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

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


		-- Make sure the version was deleted

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

	end

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteWithVersion_image( rxTest )

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDeleteWithVersion_image_testNightly( rxTest )

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

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteSyncRestoreWithVersion( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
		restore = true
	} )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDeleteSyncRestoreWithVersion_testNightly( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{
		xmpDevelop = true,
		xmpSidecar = true,
		restore = true
	} )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_MockDeleteTrashWithVersion( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{
		mock = true,
		xmpDevelop = true,
		xmpSidecar = true,
		deleteTrash = true
	} )

end

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

function WFCatTrashAssociatedDocsTest.rxTest_CT_OzDeleteTrashWithVersion_testNightly( rxTest )

	testAddSyncDeleteSyncWithVersion( rxTest,
	{
		xmpDevelop = true,
		xmpSidecar = true,
		deleteTrash = true
	} )

end

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


function WFCatTrashAssociatedDocsTest.setUp( inCoveragePath )

	coveragePath = inCoveragePath

end

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

return WFRxTestCase.wrapSuite( 'WFCatTrashAssociatedDocsTest', WFCatTrashAssociatedDocsTest )
