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

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

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

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

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

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

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 direc upload /rels/main_create/parts since the format doesn't match,
		-- and the url is not required for testing 
		stripUpdateField( 'links', '/rels/main_create/parts', 'href' )

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

		stripUpdateField( 'links', '/rels/proxy_type/2560', '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/hls', 'href' )

		-- 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( tempTestFolder, fileName )

	return WFCatAssetTestUtils.getTestDataOriginalFilePath( tempTestFolder, fileName )

end

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

local function getProxyFilePath( tempTestFolder, proxyFilename )

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

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

	return proxyFilePath

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 = 'Content-Type',
			value = contentType,
		},
		{
			header = 'Accept',
			value = 'application/json',
		},
		{
			header = 'X-Read-Meta',
			value = skipXReadMeta and 'none' or 'xmp,location',
		}

	}

	if not contentType:match( "^video" ) then
		table.insert( requestHeaders, {
			header = 'X-Generate-Proxies',
			value = 'all',
		} )
	end

	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 )

	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 = "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
					correctAssetUpdate = derefOpt( assetUpdate, 'payload', 'develop', 'xmpCameraRaw' ) == developSettings
				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()
	return WFCatAssetTestUtils.getXmpDevelopSettings()
end

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

local function assetUpdateResponse( assetId, revisionId, fileName, subtype, assetCreated, userCreatedOrTable, importSource, links, developSettings, alwaysHaveLocationData )
	
	links = links or {}
	local hasMasterLink = links.master
	local hasProxyLink = links.proxy
	local hasRenditions = links.renditions
	local hasXmpDevelopLink = links.xmpDevelop
	local hasXmpSidecarLink = links.xmpSidecar

	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,
			},
		},
		payload = {
			userCreated = userCreated,
			userUpdated = userUpdated or "2017-09-06T18:15:00",
			captureDate = "2018-01-01T00:00:00Z",
			importSource = importSource,
		},
	}

	update.payload.develop = developSettings

	if hasXmpDevelopLink and developSettings == nil then

		update.payload.develop = getXmpDevelopSettings()
		
	end

	if hasMasterLink then

		update.links[ "/rels/master" ] = {
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/master",
			content_type = content_type,
		}

		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 = 272,
				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
		
	end
	
	if hasXmpDevelopLink then
		update.links[ "/rels/xmp/develop" ] = {
			fileSize = 26104,
			href = "assets/" .. assetId.."/revisions/"..revisionId.."/xmp/develop",
		}
	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
	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/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/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/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 )

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

	local response = assetUpdateResponse( trashAssetId, trashRevisionId, fileName, subtype, trashAssetCreated, trashAssetCreated, importSource, 
				{ master = hasMasterLink, proxy = hasRenditions, renditions = hasRenditions, xmpDevelop = hasXmpDevelopLink, xmpSidecar = hasXmpSidecarLink }, developSettings )

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

	local response = assetUpdateResponse( restoredAssetId, restoredRevisionId, fileName, subtype, assetCreated, userCreated, importSource,
			{ master = hasMasterLink, proxy = hasRenditions, renditions = hasRenditions, xmpDevelop = hasXmpDevelopLink, xmpSidecar = hasXmpSidecarLink }, developSettings )
	
	-- 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/rendition_type/2048',
		'/rels/rendition_type/360p',
		'/rels/rendition_type/720p',
	--	'/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 guarentee 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, linkType in ipairs( expectedLinks ) do
			if linkType == '/rels/proxy_type/2560' then
				table.remove( expectedLinks, i )
				break
			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,
		dontCompareToMockResponse = 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,
			dontCompareToMockResponse = 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" ) ) or 0
					local headerContentEncoding = tostring( derefOpt( headers, "Content-Encoding" ) )
					local dataLength = KSData.length( result )

					if 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,
			dontCompareToMockResponse = 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 )

	return WFCatAssetTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, userUpdated )

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, isImportant, 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

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

return {
	addDocumentWithLocalMaster = addDocumentWithLocalMaster,
	addDocumentWithLocalMasterAndProxy = addDocumentWithLocalMasterAndProxy,
	addXmpDevelopToDocument = addXmpDevelopToDocument,
	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,
	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
}
