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

WFCatTrashTestUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2019 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.

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

	-- RxLua
local Rx = require 'Rx'
local Observable = Rx.Observable

	-- WF.core.rxlua
local WFRxScheduler = require 'WFRxScheduler'

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

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

	-- WF.oz_client.managed_catalog
local WFCatBinaryUploaderTestUtils = require 'WFCatBinaryUploaderTestUtils'
local WFCatTrashTestSession = require 'WFCatTrashTestSession'
local WFCatOzTestSession = require 'WFCatOzTestSession'

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

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

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

local check = WFArguments.check
local child = KSPathUtils.child
local derefOpt = KSTableUtils.derefOpt

local stripped = '<<stripped>>'

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

local function compareUpdateEntry( a, b )

	local aDeleted = derefOpt( a, 'deleted' ) and 'deleted' or ''
	local bDeleted = derefOpt( b, 'deleted' ) and 'deleted' or ''

	if aDeleted == bDeleted then

		local aType = derefOpt( a, 'type' ) or ''
		local bType = derefOpt( b, 'type' ) or ''

		if aType == bType then
			if aDeleted == 'deleted' then

				-- deleted favorites have an asset id
				local aAssetId = derefOpt( a, 'asset', 'id' ) or ''
				local bAssetId = derefOpt( b, 'asset', 'id' ) or ''

				if aAssetId == bAssetId then
					local aNewId = derefOpt( a, 'new_id' ) or ''
					local bNewId = derefOpt( b, 'new_id' )  or ''
					return aNewId < bNewId
				else
					return aAssetId < bAssetId
				end

			else
				if aType == 'comment' then
					local aComment = derefOpt( a, 'payload', 'body' ) or ''
					local bComment = derefOpt( b, 'payload', 'body' ) or ''
					return aComment < bComment
				elseif aType == 'album_asset' then
					local aAlbumId = derefOpt( a, 'album', 'id' ) or ''
					local bAlbumId = derefOpt( b, 'album', 'id' ) or ''
					return aAlbumId < bAlbumId
				elseif aType == 'error' then

					local aSubtype = derefOpt( a, 'subtype' )
					local bSubtype = derefOpt( b, 'subtype' )

					if aSubtype == bSubtype then
						local aSourceType = derefOpt( a, 'source', 'type' )
						local bSourceType = derefOpt( b, 'source', 'type' )
						if aSourceType == bSourceType then
							local aSourceAction = derefOpt( a, 'source', 'action' )
							local bSourceAction = derefOpt( b, 'source', 'action' )
							if aSourceAction == bSourceAction then
								local aSourceId = derefOpt( a, 'source', 'id' )
								local bSourceId = derefOpt( b, 'source', 'id' )
								return aSourceId < bSourceId
							else
								return aSourceAction < bSourceAction
							end
						else
							return aSourceType < bSourceType
						end
					else
						return aSubtype < bSubtype
					end

				else -- if aType == 'asset' then
					local aId = derefOpt( a, 'id' ) or ''
					local bId = derefOpt( b, 'id' ) or ''
					return aId < bId
				end
			end
		else
			return aType < bType
		end
	else
		return aDeleted < bDeleted
	end

end

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

local function stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

	local function stripField( ... )

		if derefOpt( mockDataAsTable, ... ) then
			KSTableUtils.setNested( mockDataAsTable, stripped, ... )
		end

		if derefOpt( ozDataAsTable, ... ) then
			KSTableUtils.setNested( ozDataAsTable, stripped, ... )
		end

	end

	stripField( 'links', 'next', 'href' )
	stripField( 'links', '/rels/future_updates', 'href' )

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


	if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
		-- Ignore the results until we get all updates from oz
		return {}, {}
	end

	-- updates must be in same order in order to strip both, sort the updates
	table.sort( mockUpdateTable, compareUpdateEntry )
	table.sort( ozUpdateTable, compareUpdateEntry )

	for i = 1, math.min( #mockUpdateTable, #ozUpdateTable ) do

		local function stripUpdateField( ... )

			if derefOpt( mockUpdateTable, i, ... ) then
				KSTableUtils.setNested( mockUpdateTable, stripped, i, ... )
			end

			if derefOpt( ozUpdateTable, i, ... ) then
				KSTableUtils.setNested( ozUpdateTable, stripped, i, ... )
			end

		end

		local function nilUpdateField( ... )

			if derefOpt( mockUpdateTable, i, ... ) then
				KSTableUtils.setNested( mockUpdateTable, nil, i, ... )
			end

			if derefOpt( ozUpdateTable, i, ... ) then
				KSTableUtils.setNested( ozUpdateTable, nil, i, ... )
			end

		end

		-- strip from all types of documents

		stripUpdateField( 'created' )
		stripUpdateField( 'updated' )
		stripUpdateField( 'payload', 'userCreated' )
		stripUpdateField( 'payload', 'userUpdated' )
		stripUpdateField( 'payload', 'purgeDate' )
		-- revision_ids may change, strip to verify if they both have one or not.
		stripUpdateField( 'revision_ids' )

		-- strip from Asset document

		-- strip direct upload /rels/main_create/parts and /rels/aux_create/parts and /rels/aux_copy_to
		-- since the format doesn't match, and the url is not required for testing
		stripUpdateField( 'links', '/rels/main_create/parts', 'href' )
		stripUpdateField( 'links', '/rels/aux_create/parts', 'href' )
		stripUpdateField( 'links', '/rels/aux_copy_to', 'href' )
		stripUpdateField( 'links', '/rels/xmp_develop_create/parts' )
		stripUpdateField( 'links', '/rels/versions/xmp_develop_create/parts' )
		
		-- proxy ids and revision ids are generated by oz.
		-- So just verify if both oz and mock have one or not.
		stripUpdateField( 'links', '/rels/master', 'href' )
		stripUpdateField( 'links', '/rels/xmp', 'href' )
		stripUpdateField( 'links', '/rels/xmp/develop', 'href' )

		-- Strip aux links. Aux links have unique rels keys, so the logic here is a bit different.

		local mockLinks = derefOpt( mockUpdateTable, i, 'links' ) or {}

		for rels in pairs( mockLinks ) do

			if rels:find( '^/rels/aux/' ) then
				stripUpdateField( 'links', rels, 'href' )
			end

		end

		local ozLinks = derefOpt( ozUpdateTable, i, 'links' ) or {}

		for rels in pairs( ozLinks ) do

			if rels:find( '^/rels/aux/' ) then
				stripUpdateField( 'links', rels, 'href' )
			end

		end

		-- Strip the rest of the links

		stripUpdateField( 'links', '/rels/proxy_type/2560', 'href' )
		stripUpdateField( 'links', '/rels/proxy_type/360p', 'href' )
		stripUpdateField( 'links', '/rels/proxy_type/1080p', 'href' )
		stripUpdateField( 'links', '/rels/proxy_type/hls', 'href' )

		stripUpdateField( 'links', '/rels/rendition_generate/fullsize', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/thumbnail2x', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/640', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/1280', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/2048', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/360p', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/720p', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/1080p', 'href' )
		stripUpdateField( 'links', '/rels/rendition_type/hls', 'href' )

		-- nil out dngVersion and sha256 /from rels/proxy_type/2560

		nilUpdateField( 'links', '/rels/proxy_type/2560', 'dngVersion' )
		nilUpdateField( 'links', '/rels/proxy_type/2560', 'sha256' )

		-- nil out oz calculated location data as it can come after an undetermined time
		nilUpdateField( 'payload', 'location', 'country' )
		nilUpdateField( 'payload', 'location', 'isoCountryCode' )
		nilUpdateField( 'payload', 'location', 'state' )

		-- nil out autoTags and asthetics as they come after an undetermined time.
		nilUpdateField( 'payload', 'X-autoTags-classifiers' )
		nilUpdateField( 'payload', 'X-autoTags-hashtag2' )
		nilUpdateField( 'payload', 'X-autoTags-stock7' )
		nilUpdateField( 'payload', 'aesthetics' )
		nilUpdateField( 'payload', 'autoTags' )

		-- strip from Space document
		stripUpdateField( 'links', '/rels/web_space', 'href' )
		stripUpdateField( 'createdOnClient' )

		-- strip from Comments and Favorite Documents
		stripUpdateField( 'author' )

		-- strip from feedbackuser document
		if mockUpdateTable[ i ].type == 'feedbackuser' then
			stripUpdateField( 'id' )
			stripUpdateField( 'first_name' )
			stripUpdateField( 'full_name' )
			stripUpdateField( 'last_name' )
			stripUpdateField( 'links', '/rels/avatars/large' )
			stripUpdateField( 'links', '/rels/avatars/large2x' )
			stripUpdateField( 'links', '/rels/avatars/medium' )
			stripUpdateField( 'links', '/rels/avatars/medium2x' )
			stripUpdateField( 'links', '/rels/avatars/small' )
			stripUpdateField( 'links', '/rels/avatars/small2x' )
		end

		if mockUpdateTable[ i ].type == 'error' then
			stripUpdateField( 'source', 'links', 'self', 'href' )
			stripUpdateField( 'id' )
			nilUpdateField( 'deleted_by' )
		end

	end

	return mockDataAsTable, ozDataAsTable

end

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

local function setupCloudTrashTestSession( rxTest, options )

	options = options or {}

	options.defaultTransform = stripUpdatesAndLinks

	return WFCatTrashTestSession.setupTestSession( rxTest, options )

end

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

local function getOriginalFilePath( rxTest, tempTestFolder, fileName )

	return WFCatAssetTestUtils.getTestDataOriginalFilePath( rxTest, tempTestFolder, fileName )

end

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

local function getProxyFilePath( rxTest, tempTestFolder, proxyFilename )

	local testDataPath = child( KSPathUtils.getStandardFilePath( 'testdata' ), 'assets' )
	local testFilePath = child( testDataPath, proxyFilename )

	local proxyFilePath = child( tempTestFolder, proxyFilename )
	local copiedPath = WFFileUtils.copyToUniquelyNamedFile( testFilePath, proxyFilePath )

	rxTest:finally( function()
	
		pcall( WFFileUtils.delete, copiedPath )
	
	end )

	return copiedPath

end

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

local function contentTypeFromFilePath( filePath )

	local fileName = KSPathUtils.leafName( filePath )

	return WFCatAssetTestUtils.contentTypeFromFileName( fileName )

end

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

local function createMasterReqResp( catalogId, assetId, revisionId, filePath, errorCode, skipXReadMeta )

	errorCode = errorCode or '201'

	local contentType = contentTypeFromFilePath( filePath )
	local requestHeaders = {
		{
			header = 'X-Generate-Renditions',
			value = 'all',
		},
		{
			header = 'X-Generate-Proxies',
			value = 'all',
		},
		{
			header = 'Content-Type',
			value = contentType,
		},
		{
			header = 'Accept',
			value = 'application/json',
		},
		{
			header = 'X-Read-Meta',
			value = skipXReadMeta and 'none' or 'xmp,location',
		}

	}

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/master' )
		:withRequestOptions( {
				dataFormat = "filePath",
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 2,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:expectRequestDataAfterTransformation( string.lower( filePath ), string.lower )
		:respond( errorCode, [[]] )

end

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

local function createProxyReqResp( catalogId, assetId, revisionId, proxyId, filePath, errorCode )

	errorCode = errorCode or '201'

	local requestHeaders = {
		{
			header = "X-Proxy-Type",
			value = "2560",
		},
		{
			header = "Content-Type",
			value = "image/dng",
		},
		{
			header = "X-Generate-Renditions",
			value = "all",
		},
		{
			header = "Accept",
			value = "application/json",
		},
		{
			header = "X-Read-Meta",
			value = "xmp",
		}
	}

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/proxies/' .. proxyId )
		:requestUrlTransformation( function( str )
			-- replace the new proxy id that is created but not stored anywhere
			str = str:gsub( '/([^/]*)$', '/' .. proxyId )
			return str
		end )
		:withRequestOptions( {
				dataFormat = "filePath",
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 2,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:expectRequestDataAfterTransformation( string.lower( filePath ), string.lower )
		:respond( errorCode, [[]] )

end

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

local function uploadXmpDevelopReqResp( catalogId, assetId, revisionId, filePath )

	local requestHeaders = {
		{
			header = 'X-Generate-Renditions',
			value = 'all',
		},
		{
			header = 'X-Generate-Proxies',
			value = 'all',
		},
		{
			header = 'Content-Type',
			value = 'application/rdf+xml',
		},
		{
			header = 'Accept',
			value = 'application/json',
		},
	}

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp/develop' )
		:withRequestOptions( {
				dataFormat = "filePath",
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 2,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:expectRequestDataAfterTransformation( string.lower( filePath ), string.lower )
		:respond( '201', [[]] )

end

local function uploadVersionXmpDevelopReqResp( catalogId, versionId, versionRevisionId, filePath )

	local requestHeaders = {
		{
			header = 'X-Generate-Renditions',
			value = 'all',
		},
		{
			header = 'X-Generate-Proxies',
			value = 'all',
		},
		{
			header = 'Content-Type',
			value = 'application/rdf+xml',
		},
		{
			header = 'Accept',
			value = 'application/json',
		},
	}

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/versions/' .. versionId .. '/revisions/' .. versionRevisionId .. '/xmp/develop' )
		:withRequestOptions( {
				dataFormat = "filePath",
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 2,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:expectRequestDataAfterTransformation( string.lower( filePath ), string.lower )
		:respond( '201', [[]] )

end

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

local function uploadXmpReqResp( catalogId, assetId, revisionId, filePath )

	local requestHeaders = {
		{
			header = 'Content-Type',
			value = 'application/rdf+xml',
		},
		{
			header = 'Accept',
			value = 'application/json',
		},

	}

	return WFMockHttpRequest()
		:put( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp' )
		:withRequestOptions( {
				dataFormat = "filePath",
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 2,
				requestPriority = 3,
			} )
		:withRequestHeaders( requestHeaders )
		:expectRequestDataAfterTransformation( string.lower( filePath ), string.lower )
		:respond( '201', [[]] )

end

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

local function makeImportSource( catalog, fileName, importTimestamp, sha256 )

	local contentType = WFCatAssetTestUtils.contentTypeFromFileName( fileName )

	local width = 3264 -- IMG_7763.JPG
	local height = 2448
	local fileSize = 2072173

	if fileName == 'IMG_7772.mp4' then
		width = 480
		height = 270
		fileSize = 211332
	end

	return {
		importTimestamp = importTimestamp,
		importedBy = catalog:ozUserId(),
		fileName = fileName,
		fileSize = fileSize,
		contentType = contentType,
		originalWidth = width, 	-- must be set in order for oz to generate renditions on images
		originalHeight = height, -- must be set in order for oz to generate renditions on images
		sha256 = sha256
	}

end

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

local function addDocumentWithLocalMaster( catalog, catalogId, assetId, originalFilePath, subtype, userCreated, importSource )

	local docValue = {
		_id = assetId,
		type = 'asset',
		subtype = subtype,
		userCreated = userCreated,
		userUpdated = userCreated,
		captureDate = "2018-01-01T00:00:00Z",

		importSource = importSource,
	}

	if importSource.fileName == 'IMG_7763.JPG' then
		docValue.location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
	end

	docValue = WFCatAssetTestUtils.addOriginalPathToDocument( docValue, nil, originalFilePath )

	return catalog:docStore():setDocValue( docValue )

end

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

local function addDocumentWithLocalMasterAndProxy( catalog, catalogId, assetId, originalFilePath, proxyFilePath, subtype, userCreated, importSource )

	local docValue = {
		_id = assetId,
		type = 'asset',
		subtype = subtype,
		userCreated = userCreated,
		userUpdated = userCreated,
		captureDate = "2018-01-01T00:00:00Z",
		importSource = importSource,
	}

	if importSource.fileName == 'IMG_7763.JPG' then
		docValue.location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
	end

	docValue = WFCatAssetTestUtils.addOriginalPathToDocument( docValue, nil, originalFilePath )
	docValue = WFCatAssetTestUtils.addProxyPathToDocument( docValue, proxyFilePath )

	return catalog:docStore():setDocValue( docValue )

end

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

local function prefetchReqResp( catalogId )

	return WFMockHttpRequest()
		:get( "/v2/catalogs/" .. catalogId .. "/albums?subtype=collection%3Bcollection_set" )
		:respond( '200', KSJsonEncoder.encodeOzUploadJson( {
			base = "https://photos-stage.adobe.io/v2/catalogs/"..catalogId .."/",
			links = {
				["/rels/album"] = {
					href = "albums/{album_id}",
					templated = true,
				},
			},
			resources = {},
		} ) )

end

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

local function catalogPrefetchReqResp( catalogId )

	return WFMockHttpRequest()
		:get( "/v2/catalogs/" .. catalogId )
		:respond( '200', [[{
			"base": "https://photos-stage.adobe.io/v2/",
			"id": "]] .. catalogId .. [[",
			"type": "catalog"
			}]] )

end

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

local function emptyPullReqResp( catalogId )

	return WFMockHttpRequest()
		:get( "/v2/catalogs/" .. catalogId .. "/updates" )
		:respond( '200', [[{
			"base": "https://photos-stage.adobe.io/v2/catalogs/]] ..catalogId .. [[/",
			"updates": [],
			"links": {
				"next": {
					"href": "/v2/catalogs/]] .. catalogId .. [[/updates?updated_since=]] .. stripped .. [["
				}
			}
		}]] )

end

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

local function futureUpdatesResponse( catalogId )

	return [[{
		"base": "https://photos-stage.adobe.io/v2/catalogs/]] ..catalogId .. [[/",
		"links": {
			"/rels/future_updates": {
				"href": "/v2/catalogs/]] .. catalogId .. [[/updates?updated_since=]] .. stripped .. [["
			}
		}
	}]]
end

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

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

					if str:find( '"action":"create"' ) and
						str:find( '"fileName":"' .. fileName .. '"' ) and
						str:find( '"subtype":"' .. subtype .. '"' ) and
						str:find( '"type":"asset"' ) then

						return 'success'
					end
					return str
				end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

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

					for _, item in ipairs( fileNames ) do
						local theFileName = item.fileName
						if not( str:find( '"action":"create"' ) and
							str:find( '"fileName":"' .. theFileName .. '"' ) and
							str:find( '"subtype":"' .. subtype .. '"' ) and
							str:find( '"type":"asset"' ) ) then

							return str
						end
					end
					return 'success'
				end )
		:respond( '202', futureUpdatesResponse( catalogId ) )
end

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

local function pushCreateAlbumReqResp( catalogId, albumId, albumName )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local albumUpdate = #updatesBeingPushed  == 1 and updatesBeingPushed[ 1 ]

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

			if hasCorrectAlbumUpdate then
				return 'success'
			end

			return str

		end )

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

end

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

local function hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

	local hasCorrectUpdate = albumAssetUpdate
		and albumAssetUpdate.action == 'create'
		and albumAssetUpdate.type == 'album_asset'
		and type( albumAssetUpdate.id ) == 'string' -- this will be a new id
		and albumAssetUpdate.album.id == albumId
		and albumAssetUpdate.asset.id == assetId
		and type( albumAssetUpdate.revision_id ) == 'string'

	return hasCorrectUpdate

end

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

local function pushCreateAlbumAssetReqResp( catalogId, albumAssetId, albumId, assetId )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local albumAssetUpdate = #updatesBeingPushed  == 1 and updatesBeingPushed[ 1 ]

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

			if correctAlbumAssetUpdate then
				return 'success'
			end

			return str

		end )

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

end

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

local function stripBothUpdateFields( stripValue, mockUpdateTable, ozUpdateTable, i, ... )

	if derefOpt( mockUpdateTable, i, ... ) then
		KSTableUtils.setNested( mockUpdateTable, stripValue, i, ... )
	end

	if derefOpt( ozUpdateTable, i, ... ) then
		KSTableUtils.setNested( ozUpdateTable, stripValue, i, ... )
	end

end

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

local function pushDeleteAssetReqResp( catalogId, assetId, revId, trashAssetId, trashRevId )

	local expectedUpdates = {
		{
			action = "delete",
			deleted_asset = {
				id = trashAssetId,
				payload = {
					userCreated = "2018-04-15T18:18:18Z",
					purgeDate = "2018-06-14T18:18:18Z",
				},
				revision_id = trashRevId,
			},
			id = assetId,
			revision_id = revId, -- This should be the revision id of the asset once it has been updated with _deleted = true
			type = "asset",
		}
	}

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

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

		return actualData, expectedData
	end

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

end

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

local function transformJsonToTable( actualData, expectedData )
	actualData = KSJsonDecoder.decodeWebSafeJson( actualData )
	expectedData = KSJsonDecoder.decodeWebSafeJson( expectedData )
	return actualData, expectedData
end

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

local function pushDeleteTrashAssetReqResp( catalogId, trashAssetId, trashRevId )

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( {
			{
				type = 'asset',
				action = 'delete',
				id = trashAssetId,
				revision_id = trashRevId,
			},
		} )
		:requestDataTransformation( transformJsonToTable )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushDeleteTrashReqResp( catalogId, trashAssetId, trashRevId )

	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestData( {
			{
				type = 'asset',
				action = 'delete',
				id = trashAssetId,
				revision_id = trashRevId,
			},
		} )
		:requestDataTransformation( transformJsonToTable )
		:respond( '202', futureUpdatesResponse( catalogId ) )

end

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

local function pushDeleteRestoreAssetReqResp( catalogId, originalAssetId, originalRevId, trashAssetId, trashRevId, restoredAssetId, restoredRevId, subtype )

	local expectedUpdates = {
		{
			action = "delete",
			deleted_asset = {
				id = trashAssetId,
				payload = {
					userCreated = "2018-04-15T18:18:18Z",
					purgeDate = "2018-06-14T18:18:18Z",
				},
				revision_id = trashRevId,
			},
			id = originalAssetId,
			revision_id = originalRevId, -- This should be the revision id of the asset once it has been updated with _deleted = true
			type = "asset",
		},
		{
			action = "create",
			id = restoredAssetId,
			payload = {
				userUpdated = "2019-01-13T14:43:34.838Z",
			},
			restore_from = {
				id = trashAssetId,
			},
			revision_id = restoredRevId,
			subtype = subtype or 'image',
			type = "asset",
		}
	}

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

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

		return actualData, expectedData
	end

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

end

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

local function hasCorrectRestoreAssetUpdate( update, assetId, trashAssetId, trashRevisionId )

	local hasCorrectUpdate = update
		and update.action == 'create'
		and update.type == 'asset'
		and update.id == assetId
		and type( update.revision_id ) == 'string'
		and KSDate.isKSDate( derefOpt( update, 'payload', 'userUpdated' ) )
		and derefOpt( update, 'restore_from', 'id' ) == trashAssetId

	return hasCorrectUpdate

end

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

local function pushRestoreAssetReqResp( catalogId, assetId, trashAssetId, trashRevisionId )
	return WFMockHttpRequest()
		:post( "/v2/catalogs/" .. catalogId .."/updates" )
		:expectRequestDataAfterTransformation( 'success', function( str )

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local update = updatesBeingPushed and updatesBeingPushed[ 1 ]

			local hasCorrectUpdate = hasCorrectRestoreAssetUpdate( update, assetId, trashAssetId, trashRevisionId )

			if hasCorrectUpdate then
				return 'success'
			end

			return str

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

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

local function pushUpdateAssetReqResp( catalogId, assetId, assetRevId, subtype, useExternalDevelopSettings, developSettings, status )

	status = status or '202'

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate = updatesBeingPushed[ 1 ]

			local correctAssetUpdate = assetUpdate.action == 'update'
				and assetUpdate.id == assetId

			if correctAssetUpdate then
				if useExternalDevelopSettings then
					correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw', 'sha256' ) ~= nil
				else
					if type( developSettings ) == 'string' then
						correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw' ) == developSettings
					elseif type( developSettings ) == 'table' then
						correctAssetUpdate = KSTableUtils.deepEquals( derefOpt( assetUpdate, 'payload', 'develop' ), developSettings )
					end

				end
			end

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

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

			return str

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

end

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

local function pushUpdateRestoredReqResp( catalogId, restoredAssetId, restoredAssetRevId, restoredAssetSubtype, useExternalDevelopSettings, developSettings )

	return pushUpdateAssetReqResp( catalogId, restoredAssetId, restoredAssetRevId, restoredAssetSubtype, useExternalDevelopSettings, developSettings )

end

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

local function getXmpDevelopSettings( smallXmp )
	return WFCatAssetTestUtils.getXmpDevelopSettings( smallXmp )
end

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

local function assetUpdateResponse( assetId, revisionId, fileName, subtype,
		assetCreated, userCreatedOrTable, importSource, links, developSettings,
		alwaysHaveLocationData, auxInfo )

	links = links or {}
	local hasMasterLink = links.master
	local hasProxyLink = links.proxy
	local hasRenditions = links.renditions
	local hasXmpDevelopLink = links.xmpDevelop
	local hasXmpSidecarLink = links.xmpSidecar
	local hasAuxLink = links.aux
	local xmpDevelopInvalid = links.xmpDevelopInvalid
	local xmpDevelopSha256 = links.xmpDevelopSha256
	local xmpDevelopFileSize = links.xmpDevelopFileSize

	local userCreated
	local userUpdated
	if type( userCreatedOrTable ) == 'table' then
		userUpdated = userCreatedOrTable.userUpdated
		userCreated = userCreatedOrTable.userCreated
	else
		userCreated = userCreatedOrTable
	end

	local content_type = WFCatAssetTestUtils.contentTypeFromFileName( fileName )

	local update = {
		id = assetId,
		type = 'asset',
		updated = "2017-09-06T18:15:10.138376Z",
		subtype = subtype,
		created = assetCreated,
		revision_ids = { revisionId },
		links = {
			self = {
				href = "assets/" .. assetId,
			},
			[ "/rels/master_create" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/master",
				templated = true,
			},
			[ "/rels/main_create/parts" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/main/parts?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}",
				templated = true,
			},
			[ "/rels/proxy_create" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/proxies/{proxy_id}",
				templated = true,
			},
			[ "/rels/rendition_create" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/renditions/{rendition_id}",
				templated = true,
			},
			[ "/rels/version_create" ] = {
				href = "assets/" .. assetId.."/versions/{version_id}",
				templated = true,
			},
			[ "/rels/versions" ] = {
				href = "assets/" .. assetId.."/versions",
			},
			[ "/rels/xmp_create" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/xmp",
				templated = true,
			},
			[ "/rels/xmp_develop_create" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/xmp/develop",
				templated = true,
			},
			[ "/rels/aux_create/parts" ] = {
				href = "assets/" .. assetId.."/sha256/{sha256}/aux/{type}/parts?access=put&file_size={file_size}&part_size={part_size}&upload_status={upload_status}&content_type={content_type}",
				templated = true,
			},
			[ "/rels/aux_copy_to" ] = {
				href = "assets/" .. assetId.."/revisions/{revision_id}/aux",
				templated = true,
			},
			[ "/rels/xmp_develop_create/parts" ] = {
				href = "assets/" .. assetId.."/sha256/{sha256}/xmp/develop/parts?access=put&file_size={file_size}&part_size={part_size}&upload_status={upload_status}&content_type={content_type}",
				templated = true,
			},
			[ "/rels/versions/xmp_develop_create/parts" ] = {
				href = "assets/" .. assetId.."versions/{version_id}/sha256/{sha256}/xmp/develop/parts?access=put&file_size={file_size}&part_size={part_size}&upload_status={upload_status}&content_type={content_type}",
				templated = true,
			},

		},
		payload = {
			userCreated = userCreated,
			userUpdated = userUpdated or "2017-09-06T18:15:00",
			captureDate = "2018-01-01T00:00:00Z",
			importSource = importSource,
		},
	}

	update.payload.develop = developSettings

	local auxId = auxInfo and next( auxInfo )

	if auxInfo then

		update.payload.aux = auxInfo

	end

	if hasXmpDevelopLink and developSettings == nil then

		update.payload.develop = getXmpDevelopSettings()

	end

	if auxInfo then

		update.payload.develop.masks = {
			[ auxId ] = {
				digest = auxInfo[ auxId ].digest
			}
		}

	end

	if hasMasterLink then

		update.links[ "/rels/master" ] = {
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/master",
			content_type = content_type,
		}
		
		update.links[ "/rels/rendition_generate/fullsize" ] = {
			href = "assets/" .. assetId.."/revisions/" .. revisionId .. "/renditions/{rendition_id}?rendition_type=fullsize",
			templated = true,
		}

		if fileName == 'IMG_7772.mp4' then
			update.payload.importSource.originalHeight = 270
			update.payload.importSource.originalWidth = 480
			update.payload.video = {
				displayAspectRatio = { 16, 9 },
				duration = { 151, 125 },
				frameRate = { 24, 1 },
				pixelAspectRatio = { 1, 1 },
				storageHeight = 270,
				storageWidth = 480,
				videoBitrate = 1224053,
				videoCodec = "AVC",
				fieldOrder = "progressive",
				audioBitrate = 125568,
				audioChannel = "stereo",
				audioCodec = "AAC",
				audioSampleRate = 48000,
				audioSampleSize = 16,
				bitDepth = 8,
				chromaSubsampling = "4:2:0",
				colorRange = "limited",
			}
			update.payload.xmp = {
				tiff = {
					Orientation = "top left",
				}
			}
		end

	end

	if hasMasterLink or hasProxyLink then

		if subtype == 'image' or subtype == 'deleted_image' then
			update.links[ "/rels/xmp" ] = {
				href = "assets/" .. assetId.."/revisions/"..revisionId.."/xmp",
			}

			update.links[ "/rels/proxy_type/2560" ] = {
				href = "assets/" .. assetId.."/revisions/"..revisionId.."/proxies/5af24c70480614fac9c8ffeab607eff5", -- oz generated proxy id
			}
		end
		
		if subtype == 'video' or subtype == 'deleted_video' then
			local sampleProxy_href = "assets/" .. assetId.."/revisions/"..revisionId.."/proxies/5af24c70480614fac9c8ffeab607eff5" -- oz generated proxy id
			
			update.links[ "/rels/proxy_type/360p" ] = {
				href = sampleProxy_href
			}
			
			update.links[ "/rels/proxy_type/1080p" ] = {
				href = sampleProxy_href
			}
			
			update.links[ "/rels/proxy_type/hls" ] = {
				href = sampleProxy_href
			}
		end

	end

	if hasAuxLink then

		local auxLinksKey = string.format( '/rels/aux/%s', auxId )

		update.links[ auxLinksKey ] = {
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/aux/" .. auxId,
		}

	end

	if hasXmpDevelopLink then

		update.links[ "/rels/xmp/develop" ] = {
			fileSize = xmpDevelopFileSize or 26104,
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/xmp/develop",
			sha256 = ( not xmpDevelopInvalid and ( xmpDevelopSha256 or update.payload.develop.xmpCameraRaw.sha256 ) ) or nil,
			invalid = ( xmpDevelopInvalid and true ) or nil,
		}

	end

	if hasXmpSidecarLink then
		update.links[ "/rels/xmp" ] = {
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/xmp",
		}
	end

	if fileName == 'IMG_7763.JPG' then
		update.payload.location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
		if hasMasterLink or hasProxyLink or hasXmpSidecarLink or alwaysHaveLocationData then
			-- reverse geo data is added by oz when it has a master/proxy/xmp
			update.payload.location.country = "United States"
			update.payload.location.isoCountryCode = "us"
			update.payload.location.state = "Washington"
		end
	end

	if subtype == 'video' or subtype == 'deleted_video' then
		update.links[ "/rels/proxy_create" ] = nil
		update.links[ "/rels/xmp" ] = nil
		update.links[ "/rels/aux_create/parts" ] = nil
		update.links[ "/rels/aux_copy_to" ] = nil
	end

	if hasRenditions then

		if subtype == 'image' or subtype == 'deleted_image' then

			local sampleRendition_href = "assets/" .. assetId.."/revisions/"..revisionId.."/renditions/17fdbeba95dff46171d4c9895ed7ca29" -- oz generated rendition id

			update.links[ "/rels/rendition_type/1280" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/2048" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/640" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/thumbnail2x" ] = {
				href = sampleRendition_href,
			}

		end

		if subtype == 'video' or subtype == 'deleted_video' then

			local sampleRendition_href = "assets/" .. assetId.."/revisions/"..revisionId.."/renditions/17fdbeba95dff46171d4c9895ed7ca29" -- oz generated rendition id

			update.links[ "/rels/rendition_type/2048" ] = {
				href = sampleRendition_href
			}
			update.links[ "/rels/rendition_type/360p" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/720p" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/1080p" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/hls" ] = {
				href = sampleRendition_href,
			}
			update.links[ "/rels/rendition_type/thumbnail2x" ] = {
				href = sampleRendition_href,
			}

		end

	end

	return update

end

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

local function ignoreRenditionLinks( mockDataAsTable, ozDataAsTable, i )

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

		if derefOpt( mockUpdateTable, i, ... ) then
			KSTableUtils.setNested( mockUpdateTable, nil, i, ... )
		end

		if derefOpt( ozUpdateTable, i, ... ) then
			KSTableUtils.setNested( ozUpdateTable, nil, i, ... )
		end

	end

	-- nil out the renditions as they may get generated as part of the
	-- first update (if we specify hasRenditions == false we are
	-- indicating they may not be there yet).

	nilUpdateField( 'links', '/rels/proxy_type/2560' )
	nilUpdateField( 'links', '/rels/proxy_type/360p' )
	nilUpdateField( 'links', '/rels/proxy_type/1080p' )
	nilUpdateField( 'links', '/rels/proxy_type/hls' )

	nilUpdateField( 'links', '/rels/rendition_type/thumbnail2x' )
	nilUpdateField( 'links', '/rels/rendition_type/640' )
	nilUpdateField( 'links', '/rels/rendition_type/1280' )
	nilUpdateField( 'links', '/rels/rendition_type/2048' )
	nilUpdateField( 'links', '/rels/rendition_type/360p' )
	nilUpdateField( 'links', '/rels/rendition_type/720p' )
	nilUpdateField( 'links', '/rels/rendition_type/1080p' )
	nilUpdateField( 'links', '/rels/rendition_type/hls' )

	return mockDataAsTable, ozDataAsTable

end

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

local function stripUpdatesAndLinksIgnoringRenditions( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

	return ignoreRenditionLinks( mockDataAsTable, ozDataAsTable, 1 )

end

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

local function stripMissingProxyInTrashAssetUpdate( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

	local ozUpdate = ozUpdateTable and #ozUpdateTable >= 1 and ozUpdateTable[ 1 ] or {}

	-- Don't expect a proxy link for deleted_image
	local subtype = ozUpdate.subtype
	if subtype == 'deleted_image' or subtype == 'deleted_video' then
		stripBothUpdateFields( nil, mockUpdateTable, ozUpdateTable, 1, 'links', '/rels/proxy_type/2560' )
	end

	return mockDataAsTable, ozDataAsTable

end

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

local function stripForAssetUpdate( haveRenditions )

	local stripFn = stripUpdatesAndLinksIgnoringRenditions
	if haveRenditions then
		stripFn = stripMissingProxyInTrashAssetUpdate
	end

	return stripFn

end

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

local function albumAssetUpdateResponse( albumAssetId, albumAssetRevId, albumId, assetId )

	return {
		album = {
			id = albumId,
			links = {
				[ "self" ] = {
					href = "albums/"..albumId,
				},
			},
		},
		asset = {
			id = assetId,
			links = {
				["self"] = {
					href = "assets/"..assetId,
				},
			},
		},
		created = "2018-08-23T20:25:31.989486Z",
		id = albumAssetId,
		links = {
			["self"] = {
				href = "albums/"..albumId.."/assets/"..assetId,
			},
		},
		revision_ids = {
			albumAssetRevId,
		},
		type = "album_asset",
		updated = "2018-08-23T20:25:31.989486Z",
	}

end

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

local function assetDeletedResponse( assetId, newAssetId, userId )

	local now = KSDate.timeInIso8601Format()

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

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

local function albumUpdateResponse( catalogId, albumId, albumRevId, albumName )

	return {
		created = "2018-08-23T20:25:31.964992Z",
		id = albumId,
		links = {
			["/rels/album_assets"] = {
				href = "albums/"..albumId.."/assets?embed=asset",
			},
			["/rels/web_album"] = {
				href = "https://stage.adobelr.com/libraries/"..catalogId.."/albums/"..albumId,
			},
			["self"] = {
				href = "albums/"..albumId,
			},
		},
		payload = {
			name = albumName,
		},
		revision_ids = {
			albumRevId,
		},
		subtype = "collection",
		type = "album",
		updated = "2018-08-23T20:25:31.964992Z",
	}

end

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

local function pullUpdatesReqResp( catalogId, updates, transformFn )

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

	local response = {
		base = "https://photos-stage.adobe.io/v2/catalogs/"..catalogId.."/",
		updates = updates,

		links = {
			next = {
				href = "/v2/catalogs/"..catalogId.."/updates?updated_since=<<stripped>>"
			}
		}
	}

	local responseData = KSJsonEncoder.encodeOzUploadJson( response )

	return WFMockHttpRequest()
		:get( "/v2/catalogs/" .. catalogId .. "/updates?updated_since=<<stripped>>" )
		:respond( '200',  responseData )
		:attachFunctionToRequest( transformFn )

end

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

local function trashAssetUpdateResponse( trashAssetId, trashRevisionId, fileName,
		subtype, trashAssetCreated, trashPurgeDate, userCreated, importSource,
		originalAssetId, hasMasterLink, hasRenditions, hasXmpDevelopLink,
		hasXmpSidecarLink, developSettings, hasAuxLink, auxInfo, xmpDevelopInvalid,
		xmpDevelopSha256, xmpDevelopFileSize )

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

	local links = {
		master = hasMasterLink,
		proxy = hasRenditions,
		renditions = hasRenditions,
		xmpDevelop = hasXmpDevelopLink,
		aux = hasAuxLink,
		xmpSidecar = hasXmpSidecarLink,
		xmpDevelopInvalid = xmpDevelopInvalid,
		xmpDevelopSha256 = xmpDevelopSha256,
		xmpDevelopFileSize = xmpDevelopFileSize,
	}

	local alwaysHaveLocationData = nil

	local response = assetUpdateResponse( trashAssetId, trashRevisionId, fileName,
			subtype, trashAssetCreated, trashAssetCreated, importSource, links, developSettings,
			alwaysHaveLocationData, auxInfo )

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

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

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

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

	return response

end

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

local function restoredAssetUpdateResponse( restoredAssetId, restoredRevisionId, fileName, subtype, assetCreated, userCreated, importSource, originalAssetId, hasMasterLink, hasRenditions, hasXmpDevelopLink, hasXmpSidecarLink, developSettings, hasAuxLink, auxInfo )

	local response = assetUpdateResponse( restoredAssetId, restoredRevisionId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = hasMasterLink, proxy = hasRenditions, renditions = hasRenditions, xmpDevelop = hasXmpDevelopLink, xmpSidecar = hasXmpSidecarLink, aux = hasAuxLink}, developSettings, nil, auxInfo )

	-- Restored assets always have an extra revision added onto them in case an asset was modified in order to create
	-- a conflict with what is locally on the restored asset which will cause a subsequent pushed update to
	-- get the modifications
	table.insert( response.revision_ids, KSIdGenerator.uuidInLowercaseHex() )

	response.original = {
		id = originalAssetId
	}

	return response

end

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

local function pollForNextUpdateWithNEntries( timeoutSeconds, numEntries, catalogSpec, puller, httpRequest )

	return Observable.defer( function()

		catalogSpec:expectHttpRequest( httpRequest )

		return puller:getLatestUpdates( 'onceWithStatus', numEntries )
		:flatMap( function( bulkUpdateStatus )

			if bulkUpdateStatus then

				return Observable.just( 'success' )

			else

				-- wait two seconds so we don't flood oz with requests
				return Observable.timer( 2, WFRxScheduler.sharedScheduler() )
				:flatMap( function()

					return Observable.throw( "empty update, try again" )

				end )

			end
		end )
		:catch( function( errorMessageOrJson, headers )

			if errorMessageOrJson == "empty update, try again" then
				return Observable.throw( errorMessageOrJson )
			end

			-- Got an invalid request or response return it.
			-- Sometimes it won't show the error in the unit test
			-- runner so re-issue it here.

			local ok, errorMessage = pcall( KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty, errorMessageOrJson )

			if not ok then
				errorMessage = errorMessageOrJson
			end

			return Observable.just( errorMessage, headers )

		end )

	end )

	-- Retry with a high limit, the timeout below will abort this for us.
	:retry( 1000 )

	-- give up after timeoutSeconds, and issue a single request to get whatever oz sent.
	:timeout( timeoutSeconds, Observable.defer( function()

		local responseTransformFn = httpRequest._attachedFunction or stripUpdatesAndLinks

		httpRequest:attachFunctionToRequest( function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

			local timeoutMessage = string.format( "TIMED OUT after %d seconds, expected %d updates got %d.", timeoutSeconds, #mockUpdateTable, #ozUpdateTable )
			mockDataAsTable._A_TIMEOUT_OCCURRED = timeoutMessage
			ozDataAsTable._A_TIMEOUT_OCCURRED = timeoutMessage

			-- don't ignore partial results if timed out
			return responseTransformFn( mockDataAsTable, ozDataAsTable, false )

		end )

		catalogSpec:expectHttpRequest( httpRequest )
		return puller:getLatestUpdates( 'onceWithStatus', 1 )
			:map( function()
				return 'timeout'
			end )

	end ), WFRxScheduler.sharedScheduler() )

end

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

local function transformOrderAndStripAssetUpdate( waitForRenditions )

	return function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

		if ignorePartialUpdates and #ozUpdateTable < #mockUpdateTable then
			return {}, {}
		end

		if not waitForRenditions then
			mockDataAsTable, ozDataAsTable = ignoreRenditionLinks( mockDataAsTable, ozDataAsTable, 1 )
		end

		return mockDataAsTable, ozDataAsTable
	end

end

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

local function pollForUpdateWith( timeoutSeconds, catalogSpec, catalogId, puller, updates, responseTransformFn )

	if responseTransformFn == nil then

		responseTransformFn = function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

			if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
				return {}, {}
			end

			return stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

		end

	end

	return pollForNextUpdateWithNEntries( timeoutSeconds, #updates, catalogSpec, puller,
			pullUpdatesReqResp( catalogId, updates, responseTransformFn ) )

end

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

local function getTrashAssetForDeletedAsset( rxTest, catalog, waitMultiplier, assetId )

	local trashAssetId, trashRevId, trashDocValue

	-- Get the current _newId.
	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
	:map( function( docValue )
		trashAssetId = docValue._newId
		return docValue
	end ), ':ignore:' )

	-- Get the current revId and docValue of the trash asset.
	rxTest:expectSingleValue( catalog:docStore()
		:docValue( trashAssetId )
		:map( function( docValue )
			trashRevId = docValue._revId
			trashDocValue = docValue
			return docValue
		end ), ':ignore:' )

	return trashAssetId, trashRevId, trashDocValue

end

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

local function getNewlyCreatedAsset( rxTest, catalog, waitMultiplier, knownAssetIds, optType )

	local newAssetId, newRevId, newDocValue

	local knownAssetIdsSet = KSTableUtils.mapArrayToSet( knownAssetIds )

	rxTest:observe(
		catalog:docStore():scannedFilteredMappedChangesFeedSince( "WFCatTrashTest" , "reportAllChanges", {}, 1 )
		:map( function( docValue )

			-- if we found the deleted asset check other values like the userCreated date
			local foundAsset = docValue.type == ( optType or 'asset' ) and not knownAssetIdsSet[ docValue._id ]
			if foundAsset then

				newAssetId = docValue._id

			end

			return foundAsset

		end ) )
	:expectDataWithinTime( 5 * waitMultiplier, true )
	:dispose()

	-- Get the current revId and docValue of the asset in case there is more than
	-- one change in the changes feed.
	rxTest:expectSingleValue( catalog:docStore()
		:docValue( newAssetId )
		:map( function( docValue )
			newRevId = docValue._revId
			newDocValue = docValue
			return docValue
		end ), ':ignore:' )

	return newAssetId, newRevId, newDocValue

end

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

local function pollUploadCount( uploader, bucket )

	return WFCatAssetTestUtils.pollBinaryUploadCount( uploader, bucket )

end

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

local function expectedBinaryLinksForImage()

	return {
		'/rels/master',
		'/rels/xmp',
		'/rels/proxy_type/2560',
		'/rels/rendition_type/1280',
		'/rels/rendition_type/2048',
		'/rels/rendition_type/640',
		'/rels/rendition_type/thumbnail2x',
	}

end

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

local function expectedBinaryLinksForProxyImage()

	return {
		'/rels/xmp',
		'/rels/proxy_type/2560',
		'/rels/rendition_type/1280',
		'/rels/rendition_type/2048',
		'/rels/rendition_type/640',
		'/rels/rendition_type/thumbnail2x',
	}

end

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

local function expectedBinaryLinksForVideo()

	return {
		'/rels/master',
		'/rels/proxy_type/360p',
		'/rels/proxy_type/1080p',
		'/rels/rendition_type/2048',
		'/rels/rendition_type/360p',
		'/rels/rendition_type/720p',
		'/rels/rendition_type/1080p',
	--	'/rels/rendition_type/hls', -- this will get a redirect when trying to download
		'/rels/rendition_type/thumbnail2x',
	}

end

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

local function expectedBinaryLinksForSubtype( subtype )
	local optExpectedLinks = ( subtype == 'video' or subtype == 'deleted_video' ) and expectedBinaryLinksForVideo() or expectedBinaryLinksForImage()
	return optExpectedLinks
end

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

local function hasRequiredLinks( docValue, optExpectedLinks )

	if optExpectedLinks == nil then
		optExpectedLinks = expectedBinaryLinksForSubtype( docValue.subtype )
	end

	-- Copy the expectedLinks as we may remove the proxy link and don't wnat to
	-- change the global value.
	local expectedLinks = KSTableUtils.deepCopy( optExpectedLinks )

	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links' )

	if docValue.subtype == 'deleted_image' or docValue.subtype == 'deleted_video' then

		-- Oz doesn't guarantee the proxy will be there for trash assets.
		-- The edge case where this comes close to being important is if you
		-- restored an asset on an over quota or expired account we would attempt
		-- to get the proxy from trash, we will fall back to getting the master
		-- in this case so it is believed not to be a problem.
		
		for i = #expectedLinks, 1, -1 do
			local linkType = expectedLinks[ i ]
			if linkType == '/rels/proxy_type/2560' 
				or linkType == '/rels/proxy_type/360p' 
				or linkType == '/rels/proxy_type/1080p' 
				or linkType == '/rels/rendition_type/1080p' then
				
				table.remove( expectedLinks, i )
			end
		end

	end

	for _, v in ipairs( expectedLinks ) do
		if derefOpt( links, v, 'href' ) == nil then
			return "Missing " .. v, docValue
		end
	end

	return true

end

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

local function getAssetReqResp( catalogId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, links, injectQuickerRetries )

	local requestOptions = {
		returnHttpErrorsAsData = true,
		ignoreMockResponse = true,
	}

	if injectQuickerRetries then
		requestOptions.requestHeaders = {
			{
				header = "X-Quicker-Retries",
				value = "true",
			}
		}
	end

	return WFMockHttpRequest()
		:get( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId )
		:withRequestOptions( requestOptions )
		:respond( '200', KSJsonEncoder.encodeOzUploadJson(
			assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource, links )
		 ) )

end

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

local function httpGetAssetAsDecodedJson( catalogSpec, catalogId, assetId, injectQuickerRetries )

	local assetUrl = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId

	-- All of this data will be ignored we just need it complete enough to make the getAssetReqResp
	local fileName = 'IMG_7763.JPG' -- just need to have one to satisfy getAssetReqResp we won't use the value as we are just looking for links
	local assetCreated
	local userCreated
	local importSource
	local subtype = 'image'
	local revisionId = KSIdGenerator.uuidInLowercaseHex() -- we are ignoring this so just dummy one up.
	local expectedLinks = { master = true, renditions = true }

	local httpRequest = getAssetReqResp( catalogId, assetId, revisionId, fileName, subtype,
							assetCreated, userCreated, importSource, expectedLinks, injectQuickerRetries )

	catalogSpec:expectHttpRequest( httpRequest )

		return catalogSpec:get( assetUrl, {
			returnHttpErrorsAsData = true,
			ignoreMockResponse = true,
		} )
		:map( function( data, resultHeaders )
			return KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )
		end )

end

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

local function getBinaryReqResp(  link )

	local mockBinary = KSData.make( "mock" )

	local responseHeaders = {
		[ 'Content-Length' ] = KSData.length( mockBinary ),
	}

	return WFMockHttpRequest()
		:get( link )
		:withResponseHeaders( responseHeaders )
		:respond( '200', mockBinary )

end

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

local function canDownloadRequiredBinaryFiles( rxTest, catalogSpec, docValue, expectedLinks, expectedSizes )

	-- This routine verifies the binary links that we are able to download the
	-- binary links that are in the docValue will actually deliver the correct
	-- binary by verifying the size downloaded.

	-- The pollUntilHasRequiredLinks call made prior to this verifies whether
	-- the links that should be there are present.

	local links = derefOpt( docValue, '_serverProvidedMetadata', 'links' )

	for _, link in ipairs( expectedLinks ) do

		local binaryUrl = derefOpt( links, link, 'href' )

		-- Trash assets may be missing renditions and proxy so we only download links
		-- that are present in the docValue.
		if binaryUrl then

			catalogSpec:expectHttpRequest( getBinaryReqResp( binaryUrl ) )

			rxTest:expectSingleValue( catalogSpec:getBinary( binaryUrl )
				:map( function( result, headers )

					local headerContentLength = tonumber( derefOpt( headers, "Content-Length" ) ) 
					local headerContentEncoding = tostring( derefOpt( headers, "Content-Encoding" ) )
					local dataLength = KSData.length( result )

					if headerContentLength and dataLength ~= headerContentLength and headerContentEncoding ~= 'gzip' then
						return false, "Downloaded data length doesn't match result headers.Content-Length for " .. link, docValue
					end

					local expectedSize = expectedSizes[ link ]
					if expectedSize then
						if dataLength ~= expectedSize then
							return false, string.format( "Downloaded data length (%d) doesn't match expected size (%d) for %s",
								dataLength, expectedSize, link ), docValue
						end
					elseif dataLength == 0 then
						return false, string.format( "Downloaded data for %s is too small", link ), docValue
					end

					return true, link, docValue

				end )

			, true, link, docValue )

		end

	end

end

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

local function pollHttpGetAsset( timeoutSeconds, pollUntilFunc, catalogSpec, catalogId, assetId, httpRequest )

	local assetUrl = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId

	local lastDocValue = {}

	return Observable.defer( function()

		catalogSpec:expectHttpRequest( httpRequest )

		return catalogSpec:get( assetUrl, {
			returnHttpErrorsAsData = true,
			ignoreMockResponse = true,
		} )
		:map( function( data, resultHeaders )

			if resultHeaders.status_code == '404' then
				-- produce a fake (and really truncated) 'deleted' doc
				return { _id = assetId, _deleted = true }
			end

			local assetInfo = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( data )

			if resultHeaders.status_code ~= '200' then
				return assetInfo -- just return the error
			end

			local doc = assetInfo.payload or {}
			doc._id = assetInfo.id
			doc.type = assetInfo.type
			doc.subtype = assetInfo.subtype
			doc._serverProvidedMetadata = {
				links = assetInfo.links,
				revision_ids = assetInfo.revision_ids
			}

			return doc

		end )

		:flatMap( function( docValue )

			lastDocValue = docValue

			local result = pollUntilFunc( docValue )
			if result == true then
				-- call the func again in case it provides other results
				return Observable.just( pollUntilFunc( docValue ) )
			else
				return Observable.throw( "Try again" )
			end

		end )

	end )

	:catch( function( errorMessage )

		if errorMessage == "Try again" then
			-- Delay a little so we don't constantly ask oz for
			-- the document.
			return Observable.timer( 2, WFRxScheduler.sharedScheduler() )
			:flatMap( function()
				return Observable.throw( errorMessage )
			end )
		end

		return Observable.just( false, errorMessage )

	end )

	-- Retry with a high limit, the timeout below will abort this for us.
	:retry( 1000 )

	-- give up after timeoutSeconds, and issue a single request to get whatever oz sent.
	:timeout( timeoutSeconds, WFRxScheduler.sharedScheduler() )

	:catch( function( errorMessage )

		return Observable.throw( errorMessage, pollUntilFunc( lastDocValue ) )

	end )

end

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

local function pollUntilHasRequiredLinks( timeoutSeconds, catalogSpec, catalogId, assetId, revisionId, subtype, options )

	options = options or {}

	local expectedLinks = { master = true, proxy = true, renditions = true }

	local expectedBinaryLinks = expectedBinaryLinksForSubtype( subtype )

	if options.proxyOnlyAsset then
		expectedLinks = { proxy = true, renditions = true }
		expectedBinaryLinks = KSTableUtils.deepCopy( expectedBinaryLinksForProxyImage() )
	end

	local function hasRequiredLinksFn( docValue )
		return hasRequiredLinks( docValue, expectedBinaryLinks )
	end

	local fileName = 'IMG_7763.JPG' -- just need to have one to satisfy getAssetReqResp we won't use the value as we are just looking for links
	local assetCreated
	local userCreated
	local importSource

	local httpRequest = getAssetReqResp( catalogId, assetId, revisionId, fileName, subtype, assetCreated, userCreated, importSource,
			expectedLinks, options.injectQuickerRetries )

	return pollHttpGetAsset( timeoutSeconds, hasRequiredLinksFn, catalogSpec, catalogId, assetId, httpRequest )

end

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

local function addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, userUpdated, smallXmp )

	-- Adds file to localonly section and sets up the develop section
	return WFCatAssetTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, userUpdated, smallXmp )

end

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

local function addAuxToDocument( rxTest, catalog, assetId, options )

	-- Add mask to assetDoc

	local auxId = KSIdGenerator.uuidInLowercaseHex()

	local auxDigest = string.upper( WFCrypto.MD5.digest( KSIdGenerator.uuidInLowercaseHex() ) )

	-- This should be close to random

	local auxBinary = KSData.make( auxDigest )

	local WFCatAssetAux = require 'WFCatAssetAux'

	local baseAuxPath = WFCatAssetAux.getBasePath( catalog )

	local auxFilePath, auxRelativeFilePath = WFCatAssetAux.calculatePathUsingDigest( baseAuxPath, auxDigest )

	local auxParentPath = KSPathUtils.parent( auxFilePath )

	WFFileUtils.mkdir( auxParentPath )

	KSData.saveToFile( auxBinary, auxFilePath )

	local auxFileSize = WFFileUtils.fileAttributes( auxFilePath ).fileSize

	local auxSha256 = WFCrypto.digestFile( auxFilePath, 'SHA256' )

	local auxParamsList = {
		{
			id = auxId,
			sha256 = auxSha256,
			filePath = auxRelativeFilePath,
			fileSize = auxFileSize,
			digest = auxDigest,
			type = "mask"
		}
	}

	local assetOverlay = WFCatAssetTestUtils.addAuxiliaryMetadataReference( { _id = assetId }, assetId, auxParamsList )

	local revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay( assetOverlay ) )

	local assetDoc = rxTest:getFirstValue( catalog:docStore():docValue( assetId ) )

	local auxMockHttpRequests = { }
	local mockOptions = { forTestNightly = not options.mock, atomicDevelopSettings = options.atomicDevelopSettings }
	local auxes = WFCatBinaryUploaderTestUtils.getAuxDirectUploadMocksForAsset( catalog, assetDoc, auxMockHttpRequests, mockOptions )

	local auxMainPartsMock = auxMockHttpRequests[ 1 ]
	auxMainPartsMock._isAuxPartsRequest = true
	auxMainPartsMock._luaResponse = KSJsonDecoder.decodeWebSafeJson(auxMainPartsMock.response)

	local auxInfo = {
		[ auxes[ 1 ].id ] = {
			sha256 = auxes[ 1 ].sha256,
			fileSize = auxes[ 1 ].fileSize,
			digest = auxes[ 1 ].digest,
			type = auxes[ 1 ].type,
		}
	}

	return revisionId, auxMockHttpRequests, auxInfo

end

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

local function updateAssetIdInAuxMockHttpRequest( request, originalAssetId, newAssetId )

	request.url = request.url:replace( originalAssetId, newAssetId )

	if request._isAuxPartsRequest then

		local auxResponse = request._luaResponse
		local href = KSTableUtils.derefOpt( auxResponse, 'links', "rels/complete_upload", 'href' )
		local updatedHref = href:replace( originalAssetId, newAssetId )
		KSTableUtils.setNested( auxResponse, updatedHref, 'links', "rels/complete_upload", 'href' )
		request.response = KSJsonEncoder.encodeWebSafeJson( auxResponse )

	end

	return request

end

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

local function addXmpSidecarToDocument( rxTest, catalog, assetId, xmpSidecarFilePath )

	local revisionId

	local xmpSidecarString = WFCatAssetTestUtils.getXmpSidecar()
	local xmpSidecarData = KSData.make( xmpSidecarString )
	local xmpSidecarFileSize = #xmpSidecarString
	KSData.saveToFile( xmpSidecarData, xmpSidecarFilePath )
	rxTest:expectEqual( 'file', WFFileUtils.exists( xmpSidecarFilePath ) )

	rxTest:finally( function()
		WFFileUtils.delete( xmpSidecarFilePath )
	end )

	rxTest:expectSingleValue( catalog:docStore():applyOverlay {
		_id = assetId,
		_localOnly = {
			files = {
				original = {
					xmp = {
						relativePath = WFFileUtils.removeVolumeFromPath( xmpSidecarFilePath ),
						volumeGuid = WFFileUtils.getVolumeGuidForPath( xmpSidecarFilePath ),
						fileSize = xmpSidecarFileSize,
					},
				},
			},
			xmpSidecarExists = true,
		}
	}
	:map( function( revId )
		revisionId = revId
		return revId
	end )
	, ':ignore:' )

	return revisionId

end

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

local function getLatestRevisionId( rxTest, catalog, assetId )

	local revisionId

	rxTest:expectSingleValue( catalog:docStore():docValue( assetId )
		:map( function( docValue )
			local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
			if revisionIds then
				revisionId = revisionIds[ #revisionIds ]
			end
			return type( revisionId )
		end ),
	'string' )

	return revisionId
end

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

local function syncStatusQuery( catalog )

	return catalog:indexStore():query( {
		sql = [[SELECT docId, needsOriginalDownload, hasLocalOriginal, needsProxyDownload,
			hasLocalProxy, proxySize, isMaybeDenied, isMarkedForOffline,
			importDate, captureDate, waitingForProxyOnOz, needsDevelopXmpDownload,
			ozHasNonThumbnailRendition
		FROM localStatusAndOzStatus]],
		quiesceUntilUpToDate = true,
	} )

end

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

local function simulateOtherClientAssetDelete( args )

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

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

	local catalogId = ozCatalog:ozCatalogId()

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

	local deleteAssetUpdate = {{
		type = 'asset',
		action = 'delete',
		revision_id = revisionId,
		id = assetId,
		deleted_asset = {
			id = trashAssetId,
			payload = {
				userCreated = userCreated,
				purgeDate = trashPurgeDate
			},
			revision_id = trashAssetRevId
		}
	}}

	catalogSpec:expectHttpRequest( pushDeleteAssetReqResp( catalogId, assetId, revisionId, trashAssetId, trashAssetRevId ) )

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

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

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

end

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

local function versionUpdateResponse( catalogId, versionId, versionRevId, assetId, assetSha256, body, hasXmpDevelopLink, hasThumbnailLink )

	local links

	if hasXmpDevelopLink then
		links = {
			["/rels/xmp/develop"] = {
				fileSize = 26104,
				href = "<<stripped>>",
			},
			self = {
				href = "assets/" .. assetId .. "/versions/" .. versionId
			}
		}
	else
		links = {
			["/rels/xmp_develop_create"] = {
				href = "versions/" .. versionId .. "/revisions/{revision_id}/xmp/develop",
				templated = true,
			},
			self = {
				href = "assets/" .. assetId .. "/versions/" .. versionId
			}
		}
	end

	if hasThumbnailLink then

		links["/rels/rendition_type/thumbnail2x"] = {
			href = "<<stripped>>",
		}

	end

	return {
		id = versionId,
		created = "2018-08-23T20:25:31.964992Z",
		updated = "2018-08-23T20:25:31.964992Z",
		type = "version",
		subtype = "edit",
		payload = body,
		revision_ids = {
			versionRevId,
		},
		importedOnDevice = {
			formFactor = WFPlatformInfo.isDesktop and "desktop" or
				WFPlatformInfo.isPhone and "phone" or
				WFPlatformInfo.isTablet and "tablet"
		},
		asset = {
			id = assetId,
			links = {
				self = {
					href = "assets/"..assetId
				}
			},
			sha256 = assetSha256,
		},

		links = links
    }

end

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

local function commentUpdateResponse( catalogId, commentId, commentRevId, spaceId, assetId, body )

	return {
		id = commentId,
		created = "2018-08-23T20:25:31.964992Z",
		updated = "2018-08-23T20:25:31.964992Z",
		type = "comment",
		subtype = "user",
		payload = {
	        body = body,
    	},
		revision_ids = {
			commentRevId,
		},
		author = { -- stripped
			id = "2858dd632858dd632858dd632858dd63",
        	full_name = "John Smith",
        	first_name = "John",
       		last_name = "Smith",
        	links = {
			},
        },
		asset = {
			id = assetId,
			links = {
				self = {
					href = "assets/"..assetId
				}
			}
		},
		space = {
			id = spaceId,
			links = {
				self = {
					href = "spaces/"..spaceId
				}
			}
		},
		links = {
			self = {
				href = "/v2/catalogs/" .. catalogId .. "/updates/" .. commentId
			}
		}
    }

end

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

local function favoriteUpdateResponse( catalogId, favoriteId, favoriteRevId, spaceId, assetId )

	return {
		id = favoriteId,
		created = "2018-08-23T20:25:31.964992Z",
		updated = "2018-08-23T20:25:31.964992Z",
		type = "favorite",
		subtype = "user",
		revision_ids = {
			favoriteRevId,
		},
		author = { -- stripped
			id = "2858dd632858dd632858dd632858dd63",
        	full_name = "John Smith",
        	first_name = "John",
       		last_name = "Smith",
        	links = {
			},
		},
		asset = {
			id = assetId,
			links = {
				self = {
					href = "assets/"..assetId
				}
			}
		},
		space = {
			id = spaceId,
			links = {
				self = {
					href = "spaces/"..spaceId
				}
			}
		},
		links = {
			self = {
				href = "/v2/catalogs/" .. catalogId .. "/updates/" .. favoriteId
			}
		}
    }

end

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

local function hasCorrectVersionUpdate( versionUpdate, versionId, assetId, versionName )

	local hasCorrectUpdate = versionUpdate
		and versionUpdate.action == 'create'
		and versionUpdate.type == 'version'
		and versionUpdate.subtype == 'edit'
		and versionUpdate.id == versionId
		and versionUpdate.asset.id == assetId
		and type( versionUpdate.revision_id ) == 'string'
		and versionUpdate.payload.name == versionName

	return hasCorrectUpdate

end

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

local function hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, assetId, commentBody )

	local hasCorrectUpdate = commentUpdate
		and commentUpdate.action == 'create'
		and commentUpdate.type == 'comment'
		and commentUpdate.subtype == 'user'
		and commentUpdate.id == commentId
		and commentUpdate.space.id == spaceId
		and commentUpdate.asset.id == assetId
		and type( commentUpdate.revision_id ) == 'string'
		and commentUpdate.payload.body == commentBody

	return hasCorrectUpdate

end

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

local function hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, assetId )

	local hasCorrectUpdate = favoriteUpdate
		and favoriteUpdate.action == 'create'
		and favoriteUpdate.type == 'favorite'
		and favoriteUpdate.subtype == 'user'
		and favoriteUpdate.id == favoriteId
		and favoriteUpdate.space.id == spaceId
		and favoriteUpdate.asset.id == assetId
		and type( favoriteUpdate.revision_id ) == 'string'

	return hasCorrectUpdate

end

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

local function pushCreateVersionReqResp( catalogId, versionId, assetId, versionName )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local update = #updatesBeingPushed == 1 and updatesBeingPushed[ 1 ]

			local hasCorrectUpdate = hasCorrectVersionUpdate( update, versionId, assetId, versionName )

			if hasCorrectUpdate then
				return 'success'
			end

			return str

		end )

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

end

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

local function pushCreateCommentReqResp( catalogId, commentId, spaceId, assetId, commentBody )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local update = #updatesBeingPushed == 1 and updatesBeingPushed[ 1 ]

			local hasCorrectUpdate = hasCorrectCommentUpdate( update, commentId, spaceId, assetId, commentBody )

			if hasCorrectUpdate then
				return 'success'
			end

			return str

		end )

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

end

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

local function pushCreateFavoriteReqResp( catalogId, favoriteId, spaceId, assetId )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )
			local update = #updatesBeingPushed == 1 and updatesBeingPushed[ 1 ]

			local hasCorrectUpdate = hasCorrectFavoriteUpdate( update, favoriteId, spaceId, assetId )

			if hasCorrectUpdate then
				return 'success'
			end

			return str

		end )

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

end

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


local function addDocumentWithProxyOnly( catalog, catalogId, assetId, proxyFilePath, subtype, userCreated, importSource )

	importSource.willNotUploadMaster = true


	local docValue = {
		_id = assetId,
		type = 'asset',
		subtype = subtype,
		userCreated = userCreated,
		userUpdated = userCreated,
		captureDate = "2018-01-01T00:00:00Z",

		importSource = importSource,
	}

	if importSource.fileName == 'IMG_7763.JPG' then
		docValue.location = {
			altitude = 18,
			latitude = 47.611714,
			longitude = -122.403008,
		}
	end

	docValue = WFCatAssetTestUtils.addProxyPathToDocument( docValue, proxyFilePath )

	return catalog:docStore():setDocValue( docValue )

end

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

local function putCreateSpaceReqResp( createSpaceUrl, createSpaceData, createSpaceOptions )

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

end

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

local function spaceUpdateResponse( catalogId, spaceId )

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

end

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

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

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

end

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

local function feedbackUserUpdate()

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

end

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

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

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

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

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

	return hasCorrectUpdate

end

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumUpdate, assetUpdate, albumAssetUpdate

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

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

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

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

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

			return str

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

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumAssetUpdate = updatesBeingPushed[ 1 ]

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )

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

			return str

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

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate, albumAssetUpdate, commentUpdate, favoriteUpdate

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

			local correctAssetUpdate = assetUpdate
				and assetUpdate.action == 'create'
				and assetUpdate.type == 'asset'
				and assetUpdate.subtype == subtype
				and assetUpdate.id == assetId
				and type( assetUpdate.revision_id ) == 'string'
				and derefOpt( assetUpdate, 'payload', 'importSource', 'fileName' ) == fileName

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )
			local hasCommentCorrectUpdate = hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, assetId, commentBody )
			local hasFavoriteCorrectUpdate = hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, assetId )

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

			return str

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

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

local function pushRestoreAssetAndCommentFavoriteDocsReqResp( catalogId, restoredAssetId, restoredRevId, trashAssetId, trashRevId,
										spaceId, commentId, commentBody, favoriteId )

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate, commentUpdate, favoriteUpdate

			for _, update in ipairs( updatesBeingPushed ) do
				if update.type == 'asset' then
					assetUpdate = update
				elseif update.type == 'comment' then
					commentUpdate = update
				elseif update.type == 'favorite' then
					favoriteUpdate = update
				end
			end

			local correctRestoreAssetUpdate = hasCorrectRestoreAssetUpdate( assetUpdate, restoredAssetId, trashAssetId, trashRevId )

			local correctCommentUpdate = hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, restoredAssetId, commentBody )
			local correctFavoriteUpdate = hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, restoredAssetId )

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

			return str

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


end

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

local function pushCreateAlbumAndCommentDocsReqResp( catalogId, assetId, assetRevisionId, albumId, spaceId, commentId, commentBody )

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local albumAssetUpdate, commentUpdate

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

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, assetId, albumId )
			local correctCommentUpdate = hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, assetId, commentBody )

			if correctAlbumAssetUpdate and correctCommentUpdate
				and #updatesBeingPushed == 2 then

				return 'success'
			end

			return str

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

end

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

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

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

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

		return actualData, expectedData
	end

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

end

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

local function pushDeleteAndDeleteTrashAssetWithTrashInfoStripReqResp( catalogId, assetId, revId )

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

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

		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'id' )
		stripBothUpdateFields( stripped, expectedData, actualData, 2, 'revision_id' )

		return actualData, expectedData
	end

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

end

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

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

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

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

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

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

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

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

		return actualData, expectedData
	end

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

end

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

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

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, assetCreateUpdate, albumAssetUpdate, commentUpdate

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

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

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

				return 'success'
			end

			return str

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

end

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

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

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, albumAssetUpdate, commentUpdate

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

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

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

				return 'success'
			end

			return str

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

end

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

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

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

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetDeleteUpdate, albumAssetUpdate, commentUpdate, favoriteUpdate

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

			local correctAlbumAssetUpdate = hasCorrectAlbumAssetUpdate( albumAssetUpdate, trashAssetId, albumId )
			local correctCommentUpdate = hasCorrectCommentUpdate( commentUpdate, commentId, spaceId, trashAssetId, commentBody )
			local correctFavoriteUpdate = hasCorrectFavoriteUpdate( favoriteUpdate, favoriteId, spaceId, trashAssetId )

			local correctDeleteAssetUpdate = hasCorrectDeleteAssetUpdate( assetDeleteUpdate, originalAssetId, trashAssetId, trashRevId )

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

				return 'success'
			end

			return str

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

end

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

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

	local status = '202'

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

			local updatesBeingPushed = KSJsonDecoder.decodeWebSafeJson_stripNull_ignoreEmpty( str )

			local assetUpdate = updatesBeingPushed[ 1 ]

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

			if correctAssetUpdate then
				if useExternalDevelopSettings then

					correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw', 'sha256' ) ~= nil
						-- check for external develop sha256 changed

					-- check that aux is included
					if auxInfo and correctAssetUpdate then
						local auxId = next( auxInfo )
						correctAssetUpdate =
							derefOpt( assetUpdate, 'payload', 'aux', auxId, 'sha256' ) == auxInfo[ auxId ].sha256 and
							derefOpt( assetUpdate, 'payload', 'develop', 'masks', auxId, 'digest' ) == auxInfo[ auxId ].digest
					end

				else

					if type( developSettings ) == 'string' then
						correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw' ) == developSettings
					elseif type( developSettings ) == 'table' then
						correctAssetUpdate = KSTableUtils.deepEquals( derefOpt( assetUpdate, 'payload', 'develop' ), developSettings )
					end
				end

			end

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

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

			return str

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

end

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

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

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

end

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

local function restoreAssetErrorResponse( restoredAssetId, restoredRevId, trashAssetId )

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

end

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

local function albumAssetDeletedResponse( docId, userId, newAlbumAssetId )

	local now = KSDate.timeInIso8601Format()

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

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

local function commentDeletedResponse( commentId, userId, newCommentId )

	local now = KSDate.timeInIso8601Format()

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

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

local function trashAssetDeletedResponse( trashAssetId, userId, maybeNewId )

	local now = KSDate.timeInIso8601Format()

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

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

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

	local now = KSDate.timeInIso8601Format()

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

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

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

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

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

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

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

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

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

	return response

end

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

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

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

	return response

end

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

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

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

	return response

end

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

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

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

	}

	return response

end

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

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

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

	return response

end

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

local function stripAssetAndLocation( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

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

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

	return mockDataAsTable, ozDataAsTable

end



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

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

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

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

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

		end )

end

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

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

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

local function pollUntilDeleted( timeoutSeconds, catalogSpec, catalogId, assetId, revisionId, options )

	local isAssetDeleted = function( docValue )
		return docValue._deleted == true
	end

	local requestOptions = {
		returnHttpErrorsAsData = true,
		ignoreMockResponse = true,
	}

	if options.injectQuickerRetries then
		requestOptions.requestHeaders = {
			{
				header = "X-Quicker-Retries",
				value = "true",
			}
		}
	end

	local httpRequest = WFMockHttpRequest()
		:get( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId )
		:withRequestOptions( requestOptions )
		:respond( '404', KSJsonEncoder.encodeOzUploadJson(
			resourceNotFoundErrorUpdateResponse( "delete", assetId, revisionId, "asset", assetId, "asset" )
		 ) )

	return pollHttpGetAsset( timeoutSeconds, isAssetDeleted, catalogSpec, catalogId, assetId, httpRequest )

end

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

local function pollUntilAssetExistsWithLinks( timeoutSeconds, catalogSpec, catalogId, assetId, expectedLinks, options )

	local doesAssetExistWithLinks = function( docValue )
		if docValue then
			return docValue.testResponse
				-- test response from mock test so let it pass
				or
				( not docValue._deleted and hasRequiredLinks( docValue, expectedLinks or {} ) )
		end
		return false
	end

	local requestOptions = {
		returnHttpErrorsAsData = true,
		ignoreMockResponse = true,
	}

	if options and options.injectQuickerRetries then
		requestOptions.requestHeaders = {
			{
				header = "X-Quicker-Retries",
				value = "true",
			}
		}
	end

	local httpRequest = WFMockHttpRequest()
		:get( '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId )
		:withRequestOptions( requestOptions )
		:respond( '200', KSJsonEncoder.encodeOzUploadJson( { testResponse = true } ) )

	return pollHttpGetAsset( timeoutSeconds, doesAssetExistWithLinks, catalogSpec, catalogId, assetId, httpRequest )
end

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

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

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

local function removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

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

end

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

local function removeSporadicUpdatesWithTypes( typesToRemove )

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

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

	return function( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

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

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

		return mockDataAsTable, ozDataAsTable

	end

end

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

local function transformResponseWithResourceNotFoundError( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

	if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
		return {}, {}
	end

	local mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'id' )
	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'new_id' )
	stripBothUpdateFields( stripped, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'source', 'revision_id' )
	stripBothUpdateFields( nil, mockDataMinusLinksAndUpdates.updates, ozDataMinusUpdatesAndLinks.updates, 2, 'deleted_by' )

	return mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks

end

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

local function transformResponseWithUnknownNewIds( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

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

	-- Remove the sporadic feedback user / space update before running through
	-- WFCatTrashTestUtils.stripUpdatesAndLinks to make sure things line up properly.
	removeSporadicSpaceOrFeedbackFromOzUpdates( ozUpdateTable )

	if #ozUpdateTable < #mockUpdateTable and ignorePartialUpdates then
		return {}, {}
	end

	local mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks = stripUpdatesAndLinks( mockDataAsTable, ozDataAsTable, ignorePartialUpdates )

	for i, mockUpdate in ipairs( mockDataMinusLinksAndUpdates.updates ) do
		local ozUpdate = ozDataMinusUpdatesAndLinks.updates[ i ]

		if mockUpdate.type == 'album_asset' and mockUpdate.deleted == true then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'new_id' )
		elseif mockUpdate.type == 'album_asset' then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'asset', 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'asset', 'links', 'self', 'href' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'id' )
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'links', 'self', 'href' )
		elseif mockUpdate.type == 'error' then
			stripBothUpdateFields( stripped, mockUpdate, ozUpdate, 'new_id' )
			stripBothUpdateFields( nil, mockUpdate, ozUpdate, 'deleted_by' )
		end
	end
	return mockDataMinusLinksAndUpdates, ozDataMinusUpdatesAndLinks

end

--==============================================================================
-- @mark Utils functions that involve other client actions
--==============================================================================

local function simulateOtherClientAssetCreate( args )

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

	local catalogId = ozCatalog:ozCatalogId()

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

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

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

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

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

end

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

local function simulateOtherClientAssetRestore( args )

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

	local catalogId = ozCatalog:ozCatalogId()

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

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

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

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

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

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

end

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

local function simulateOtherClientDeleteTrash( args )

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

	local catalogId = catalog:ozCatalogId()

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

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

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

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

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

end

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

return {
	addDocumentWithLocalMaster = addDocumentWithLocalMaster,
	addDocumentWithLocalMasterAndProxy = addDocumentWithLocalMasterAndProxy,
	addXmpDevelopToDocument = addXmpDevelopToDocument,
	addAuxToDocument = addAuxToDocument,
	addXmpSidecarToDocument = addXmpSidecarToDocument,
	albumAssetUpdateResponse = albumAssetUpdateResponse,
	albumUpdateResponse = albumUpdateResponse,
	assetDeletedResponse = assetDeletedResponse,
	assetUpdateResponse = assetUpdateResponse,
	canDownloadRequiredBinaryFiles = canDownloadRequiredBinaryFiles,
	catalogPrefetchReqResp = catalogPrefetchReqResp,
	createMasterReqResp = createMasterReqResp,
	createProxyReqResp = createProxyReqResp,
	emptyPullReqResp = emptyPullReqResp,
	expectedBinaryLinksForImage = expectedBinaryLinksForImage,
	expectedBinaryLinksForProxyImage = expectedBinaryLinksForProxyImage,
	expectedBinaryLinksForVideo = expectedBinaryLinksForVideo,
	futureUpdatesResponse = futureUpdatesResponse,
	getAssetReqResp = getAssetReqResp,
	getCameraRaw = WFCatAssetTestUtils.getCameraRaw,
	getLatestRevisionId = getLatestRevisionId,
	getNewlyCreatedAsset = getNewlyCreatedAsset,
	getOriginalFilePath = getOriginalFilePath,
	getProxyFilePath = getProxyFilePath,
	getTrashAssetForDeletedAsset = getTrashAssetForDeletedAsset,
	getXmpDevelopSettings = getXmpDevelopSettings,
	getXmpSidecar = WFCatAssetTestUtils.getXmpSidecar,
	hasCorrectAlbumAssetUpdate = hasCorrectAlbumAssetUpdate,
	hasCorrectRestoreAssetUpdate = hasCorrectRestoreAssetUpdate,
	hasRequiredLinks = hasRequiredLinks,
	httpGetAssetAsDecodedJson = httpGetAssetAsDecodedJson,
	makeImportSource = makeImportSource,
	pollForUpdateWith = pollForUpdateWith,
	pollHttpGetAsset = pollHttpGetAsset,
	pollUntilHasRequiredLinks = pollUntilHasRequiredLinks,
	pollUploadCount = pollUploadCount,
	prefetchReqResp = prefetchReqResp,
	pullUpdatesReqResp = pullUpdatesReqResp,
	pushCreateAlbumAssetReqResp = pushCreateAlbumAssetReqResp,
	pushCreateAlbumReqResp = pushCreateAlbumReqResp,
	pushCreateAssetReqResp = pushCreateAssetReqResp,
	pushCreateMultipleAssetsReqResp = pushCreateMultipleAssetsReqResp,
	pushDeleteAssetReqResp = pushDeleteAssetReqResp,
	pushDeleteRestoreAssetReqResp = pushDeleteRestoreAssetReqResp,
	pushDeleteTrashAssetReqResp = pushDeleteTrashAssetReqResp,
	pushDeleteTrashReqResp = pushDeleteTrashReqResp,
	pushRestoreAssetReqResp = pushRestoreAssetReqResp,
	pushUpdateAssetReqResp = pushUpdateAssetReqResp,
	pushUpdateRestoredReqResp = pushUpdateRestoredReqResp,
	restoredAssetUpdateResponse = restoredAssetUpdateResponse,
	setupCloudTrashTestSession = setupCloudTrashTestSession,
	simulateOtherClientAssetDelete = simulateOtherClientAssetDelete,
	stripBothUpdateFields = stripBothUpdateFields,
	stripForAssetUpdate = stripForAssetUpdate,
	stripMissingProxyInTrashAssetUpdate = stripMissingProxyInTrashAssetUpdate,
	stripUpdatesAndLinks = stripUpdatesAndLinks,
	stripUpdatesAndLinksIgnoringRenditions = stripUpdatesAndLinksIgnoringRenditions,
	syncStatusQuery = syncStatusQuery,
	transformJsonToTable = transformJsonToTable,
	transformOrderAndStripAssetUpdate = transformOrderAndStripAssetUpdate,
	trashAssetUpdateResponse = trashAssetUpdateResponse,
	updateAssetIdInAuxMockHttpRequest = updateAssetIdInAuxMockHttpRequest,
	uploadXmpDevelopReqResp = uploadXmpDevelopReqResp,
	uploadVersionXmpDevelopReqResp = uploadVersionXmpDevelopReqResp,
	uploadXmpReqResp = uploadXmpReqResp,
	xmpDevelopSha256 = WFCatAssetTestUtils.xmpDevelopSha256,
	versionUpdateResponse = versionUpdateResponse,
	commentUpdateResponse = commentUpdateResponse,
	favoriteUpdateResponse = favoriteUpdateResponse,
	hasCorrectVersionUpdate = hasCorrectVersionUpdate,
	hasCorrectCommentUpdate = hasCorrectCommentUpdate,
	hasCorrectFavoriteUpdate = hasCorrectFavoriteUpdate,
	pushCreateVersionReqResp = pushCreateVersionReqResp,
	pushCreateCommentReqResp = pushCreateCommentReqResp,
	pushCreateFavoriteReqResp = pushCreateFavoriteReqResp,
	addDocumentWithProxyOnly = addDocumentWithProxyOnly,
	putCreateSpaceReqResp = putCreateSpaceReqResp,
	spaceUpdateResponse = spaceUpdateResponse,
	spaceAlbumUpdateResponse = spaceAlbumUpdateResponse,
	feedbackUserUpdate = feedbackUserUpdate,
	pushCreateAssetAlbumAndAlbumAssetReqResp = pushCreateAssetAlbumAndAlbumAssetReqResp,
	pushAlbumAssetReqResp = pushAlbumAssetReqResp,
	pushCreateAssetAndAssocDocsReqResp = pushCreateAssetAndAssocDocsReqResp,
	pushRestoreAssetAndCommentFavoriteDocsReqResp = pushRestoreAssetAndCommentFavoriteDocsReqResp,
	pushCreateAlbumAndCommentDocsReqResp = pushCreateAlbumAndCommentDocsReqResp,
	pushDeleteAndDeleteTrashAssetReqResp = pushDeleteAndDeleteTrashAssetReqResp,
	pushDeleteAndDeleteTrashAssetWithTrashInfoStripReqResp = pushDeleteAndDeleteTrashAssetWithTrashInfoStripReqResp,
	pushRestoreAsset2ReqResp = pushRestoreAsset2ReqResp,
	pushRestoreDeleteAssetReqResp = pushRestoreDeleteAssetReqResp,
	pushDeleteRestoreAssetAndCreateAssocDocsReqResp = pushDeleteRestoreAssetAndCreateAssocDocsReqResp,
	pushDeleteAssetAndCreateAssocDocsReqResp = pushDeleteAssetAndCreateAssocDocsReqResp,
	pushDeleteAssetAndCreateAssocsReqResp = pushDeleteAssetAndCreateAssocsReqResp,
	pushUpdateTrashReqResp = pushUpdateTrashReqResp,
	assetGoneErrorUpdateResponse = assetGoneErrorUpdateResponse,
	restoreAssetErrorResponse = restoreAssetErrorResponse,
	albumAssetDeletedResponse = albumAssetDeletedResponse,
	commentDeletedResponse = commentDeletedResponse,
	trashAssetDeletedResponse = trashAssetDeletedResponse,
	favoriteDeletedResponse = favoriteDeletedResponse,
	trashAssetUpdateResponse2 = trashAssetUpdateResponse2,
	joinIdAlreadyInUseErrorUpdateResponse = joinIdAlreadyInUseErrorUpdateResponse,
	resourceExistsErrorUpdateResponse = resourceExistsErrorUpdateResponse,
	resourceNotFoundErrorUpdateResponse = resourceNotFoundErrorUpdateResponse,
	storageExceededUpdateResponse = storageExceededUpdateResponse,
	stripAssetAndLocation = stripAssetAndLocation,
	pollHttpGetMetadataUpdates = pollHttpGetMetadataUpdates,
	pollUntilDeleted = pollUntilDeleted,
	pollUntilAssetExistsWithLinks = pollUntilAssetExistsWithLinks,
	pollErrorHandlerErrorsProcessed = pollErrorHandlerErrorsProcessed,
	removeSporadicSpaceOrFeedbackFromOzUpdates = removeSporadicSpaceOrFeedbackFromOzUpdates,
	removeSporadicUpdatesWithTypes = removeSporadicUpdatesWithTypes,
	transformResponseWithResourceNotFoundError = transformResponseWithResourceNotFoundError,
	transformResponseWithUnknownNewIds = transformResponseWithUnknownNewIds,
	simulateOtherClientAssetCreate = simulateOtherClientAssetCreate,
	simulateOtherClientAssetRestore = simulateOtherClientAssetRestore,
	simulateOtherClientDeleteTrash = simulateOtherClientDeleteTrash,
}
