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

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.self_test
local WFTestFileUtils = require 'WFTestFileUtils'

	--WF.core.os
local WFDeviceInfo = require 'WFDeviceInfo'

	--WF.core.utils
local KSData = require 'KSData'
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFFileUtils = require 'WFFileUtils'
local WFPlatformInfo = require 'WFPlatformInfo'
local WFTableMerge = require 'WFTableMerge'
local WFCrypto = require 'WFCrypto'

	-- WF.oz_client.catalog
local WFCatAssetAux = require 'WFCatAssetAux'
local WFOzContentTypes = require 'WFOzContentTypes'

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

local child = KSPathUtils.child


local KSTableUtils_setNested = assert( KSTableUtils.setNested )

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

--[[

	@mark WFCatAssetTestUtils

	WFCatAssetTestUtils is used for generating platform-specific Asset documents
	for use in test cases.

]]--

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

local WFCatAssetTestUtils = {}

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

local offlinePath = '/Volumes/foo/images'
local offlineVolumeGuid = KSIdGenerator.uuidInLowercaseHex()
local offlineRelativePath = '/foo/images'

if WFPlatformInfo.isWindows then
	offlinePath = [[Q:\foo\images]]
	offlineRelativePath = [[foo\images]]
end

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

local function createXmpSidecarFile( filePath )

	local xmpDataAsStr = '<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c128 79.159124, 2016/03/18-14:01:55 (debug)">\
		   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\
		      <rdf:Description rdf:about=""\
		            xmlns:xmp="http://ns.adobe.com/xap/1.0/"\
		            xmlns:dc="http://purl.org/dc/elements/1.1/"\
		            xmlns:exif="http://ns.adobe.com/exif/1.0/"\
		            xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/">\
		         <xmp:ModifyDate>2020-12-12T11:10:09.876Z</xmp:ModifyDate>\
		         <xmp:Rating>1</xmp:Rating>\
		         <xmp:CreatorTool>Adobe Nimbus Test</xmp:CreatorTool>\
		         <dc:subject>\
		            <rdf:Seq>\
		               <rdf:li>mary</rdf:li>\
		               <rdf:li>had</rdf:li>\
		               <rdf:li>a little lamb</rdf:li>\
		            </rdf:Seq>\
		         </dc:subject>\
		         <exif:GPSAltitude>10000/2</exif:GPSAltitude>\
		         <exif:GPSAltitudeRef>1</exif:GPSAltitudeRef>\
		         <exif:GPSLongitude>100,5.125S</exif:GPSLongitude>\
		         <exif:GPSLatitude>20,1.5</exif:GPSLatitude>\
		         <crs:RawFileName>IMG_9876.cr2</crs:RawFileName>\
		      </rdf:Description>\
		   </rdf:RDF>\
		</x:xmpmeta>'

	local xmpData = KSData.make( xmpDataAsStr )
	KSData.saveToFile( xmpData, filePath )
	xmpData:close()

	return #xmpDataAsStr

end

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

WFCatAssetTestUtils.createXmpSidecarFile = createXmpSidecarFile

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

local function createDevelopSettingsData( optOverrides )

	local xmpDataAsStr = [[<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c128 79.159124, 2016/03/18-14:01:55 (debug)">\
		   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\
		      <rdf:Description rdf:about="%s"\
		            xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/">\
		         <crs:RawFileName>IMG_9876.cr2</crs:RawFileName>\
		      </rdf:Description>\
		   </rdf:RDF>\
		</x:xmpmeta>]]

	local maybeUniqueStr = optOverrides.sharedDevelopSettings
			and 'foo' or KSIdGenerator.uuidInLowercaseHex()
		-- Guarantee that the develop settings are unique, unless sharedDevelopSettings is set.

	return string.format( xmpDataAsStr, maybeUniqueStr )

end

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

local function createXmpDevelopSettings( wfCatalog, assetId, tempFolder, optOverrides )

	optOverrides = optOverrides or {}

	local xmpDataAsStr = createDevelopSettingsData( optOverrides )
	local xmpData = KSData.make( xmpDataAsStr )
	local sha256 = xmpData:digest( 'SHA256' )

	local absolutePath, relativePath

	if WFPlatformInfo.isMobile() then

		local OzFilePaths = require 'OzFilePaths'
		local catalogId = wfCatalog:ozCatalogId()
		relativePath, absolutePath = OzFilePaths.getXmpDevelopPath( catalogId, { _id = assetId }, sha256, true )

	else

		absolutePath = KSPathUtils.child( tempFolder, sha256 )
		relativePath = absolutePath

	end

	KSData.saveToFile( xmpData, absolutePath )
	xmpData:close()

	return absolutePath, relativePath, #xmpDataAsStr, xmpDataAsStr, sha256

end

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

WFCatAssetTestUtils.createXmpDevelopSettings = createXmpDevelopSettings

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

local tempFolderForTestName = {}

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

local function createFile( tempFolder, fileName, fileDataStr )

	local filePath = KSPathUtils.child( tempFolder, fileName )

	fileDataStr = fileDataStr .. KSIdGenerator.uuidInLowercaseHex()
	local fileData = KSData.make( fileDataStr )
	KSData.saveToFile( fileData, filePath )

	local fileSize = WFFileUtils.fileAttributes( filePath ).fileSize
	local contentId = fileData:digest( 'SHA256' )

	fileData:close()

	return filePath, fileSize, contentId, fileDataStr

end

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

local function createOriginalFile( tempFolder, fileName, fileDataStr )

	local folder = KSPathUtils.child( tempFolder, 'originals' )
	pcall( WFFileUtils.mkdir, folder )
	return createFile( folder, fileName, fileDataStr )

end

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

local function createProxyFile( tempFolder, fileName, fileDataStr )

	local folder = KSPathUtils.child( tempFolder, 'proxies' )
	pcall( WFFileUtils.mkdir, folder )
	return createFile( folder, fileName, fileDataStr )

end

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

local function creatAuxFile( basePath, fingerprint, auxData, optOverrides, optNotFromSource )

	optOverrides = optOverrides or {}

	local filePath, relativeFilePath = WFCatAssetAux.calculatePathUsingDigest( basePath, fingerprint )

	local fileSize

	-- If hasSourceOnlyFilters is true, filter target instance won't be created

	if optOverrides.hasMasks or optOverrides.hasC2pa or optOverrides.hasFilters 
		or ( optOverrides.hasSourceOnlyFilters and not optNotFromSource )
		or ( optOverrides.hasLocalResampledFilter and optNotFromSource ) then

		local parentPath = KSPathUtils.parent( filePath )

		if not WFFileUtils.exists( parentPath ) then
			pcall( WFFileUtils.mkdir, parentPath )
		end

		KSData.saveToFile( auxData, filePath )

		fileSize = WFFileUtils.fileAttributes( filePath ).fileSize

	else

		-- SPAM [jandrew 2023-01-30]: This change should not be needed, but it was made
		-- to try to stabilize a test (MockedVersion_ErrorAuxMissing) that fails when trying
		-- to create the mask file on disk:
		--
		-- Don't bother creating the masks if the test does not need them.
		-- FYI: Even when the masks don't exist, sometimes metadata about the masks
		-- is still used within tests.

		fileSize = KSData.length( auxData )

	end

	local sha256 = auxData:digest( 'SHA256' )

	return filePath, relativeFilePath, fileSize, sha256

end

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

local function createMaskFile( basePath, fingerprint, maskData, optOverrides )

	optOverrides = optOverrides or {}

	local overrides = {
		hasMasks = optOverrides.hasMasks
	}

	return creatAuxFile( basePath, fingerprint, maskData, overrides )

end

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

WFCatAssetTestUtils.createMaskFile = createMaskFile

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

local function createFilterInstanceFile( basePath, fingerprint, maskData, optOverrides, optNotFromSource )

	optOverrides = optOverrides or {}

	local overrides = {
		hasFilters = optOverrides.hasFilters,
		hasSourceOnlyFilters = optOverrides.hasSourceOnlyFilters,
		hasLocalResampledFilter = optOverrides.hasLocalResampledFilter
	}

	return creatAuxFile( basePath, fingerprint, maskData, overrides, optNotFromSource )

end

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

WFCatAssetTestUtils.createFilterInstanceFile = createFilterInstanceFile

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

local function createC2paFile( basePath, fingerprint, c2paData, optOverrides )

	optOverrides = optOverrides or {}

	local overrides = {
		hasC2pa = optOverrides.hasC2pa
	}

	return creatAuxFile( basePath, fingerprint, c2paData, overrides )

end

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

WFCatAssetTestUtils.createC2paFile = createC2paFile

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

local deleteFile = function( path )
	if path then
		WFFileUtils.deleteFile( path )
	end
end

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

local deleteDirectory = function( path )
	if path then
		WFFileUtils.deleteDirectory( path )
	end
end

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

local function getDefaultLocalOnlySettingsForVideo( rxTest, wfCatalog, assetId, optOverrides )

	local customId = KSIdGenerator.uuidInLowercaseHex()

	local testOriginalName = string.format( "original_%s.MP4", customId )
	local testLatestOriginalName = string.format( "latest_original_%s.MP4", customId )
	local testProxyName = string.format( "proxy_%s.MP4", customId )

	local testName = rxTest:testName()

	local tempFolder = tempFolderForTestName[ testName ]

	if not tempFolder then
		tempFolder = WFTestFileUtils.createTestFolder( rxTest )
	end

	tempFolderForTestName[ testName ] = tempFolder

	local originalFilePath, originalFileSize, originalContentId
			= createOriginalFile( tempFolder, testOriginalName, 'original_data' )

	local latestOriginalFilePath = createOriginalFile( tempFolder, testLatestOriginalName, 'latest_original_data' )

	local proxy360pFilePath, proxy360pFileSize, proxy360pContentId
	local proxy720pFilePath, proxy720pFileSize, proxy720pContentId
	local proxy1080pFilePath, proxy1080pFileSize, proxy1080pContentId
	local proxy1080p709FilePath, proxy1080p709FileSize, proxy1080p709ContentId

	if optOverrides.hasLocal1080p then

		proxy1080pFilePath, proxy1080pFileSize, proxy1080pContentId
				= createProxyFile( tempFolder, '1080'..testProxyName, 'proxy_data' )

	end

	if optOverrides.hasLocal1080p709 then

		proxy1080p709FilePath, proxy1080p709FileSize, proxy1080p709ContentId
				= createProxyFile( tempFolder, '1080p709'..testProxyName, 'proxy_data' )

	end

	if optOverrides.hasLocal720p then

		proxy720pFilePath, proxy720pFileSize, proxy720pContentId
				= createProxyFile( tempFolder, '720' .. testProxyName, 'proxy_data' )

	end

	if optOverrides.hasLocal360p then

		proxy360pFilePath, proxy360pFileSize, proxy360pContentId
				= createProxyFile( tempFolder, '360' .. testProxyName, 'proxy_data' )

	end

	-- Note: Desktop rendition information is generated in createDesktopAsset.

	local fullsizeFilePath, fullsizeFileSize, fullsizeContentId, fullsizeData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'fullsize_data' )

	local previewFilePath, previewFileSize, previewContentId, previewData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'preview_data' )

	local thumbnailFilePath, thumbnailFileSize, thumbnailContentId, thumbnailData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'thumb_data' )

	rxTest:finally( function()

		deleteFile( originalFilePath )
		deleteFile( latestOriginalFilePath )
		deleteFile( proxy720pFilePath )
		deleteFile( proxy1080pFilePath )
		deleteFile( proxy1080p709FilePath )
		deleteFile( proxy360pFilePath )
		deleteFile( previewFilePath )
		deleteFile( thumbnailFilePath )
		deleteFile( fullsizeFilePath )
		deleteDirectory( tempFolder )

		tempFolderForTestName[ testName ] = nil

	end )

	return {

		originalFilePath = originalFilePath,
		originalFileSize = originalFileSize,
		originalContentId = originalContentId,
		originalSha256 = originalContentId,

		latestOriginalFilePath = latestOriginalFilePath,

		proxy360pFilePath = proxy360pFilePath,
		proxy360pFileSize = proxy360pFileSize,
		proxy360pContentId = proxy360pContentId,

		proxy720pFilePath = proxy720pFilePath,
		proxy720pFileSize = proxy720pFileSize,
		proxy720pContentId = proxy720pContentId,

		proxy1080pFilePath = proxy1080pFilePath,
		proxy1080pFileSize = proxy1080pFileSize,
		proxy1080pContentId = proxy1080pContentId,

		proxy1080p709FilePath = proxy1080p709FilePath,
		proxy1080p709FileSize = proxy1080p709FileSize,
		proxy1080p709ContentId = proxy1080p709ContentId,

		fullsizeFilePath = fullsizeFilePath,
		fullsizeFileSize = fullsizeFileSize,
		fullsizeData = fullsizeData,
		fullsizeContentId = fullsizeContentId,

		previewFilePath = previewFilePath,
		previewFileSize = previewFileSize,
		previewData = previewData,
		previewContentId = previewContentId,

		thumbnailFilePath = thumbnailFilePath,
		thumbnailFileSize = thumbnailFileSize,
		thumbnailData = thumbnailData,
		thumbnailContentId = thumbnailContentId,

	}

end

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

local function getDefaultLocalOnlySettingsForImage_aux( rxTest, wfCatalog, optOverrides )

	local baseAuxPath = WFCatAssetAux.getBasePath( wfCatalog )

	-- Masks

	local mask1Fingerprint = optOverrides.sharedMasks
			and string.rep( 'A', 32 )
			or string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local mask1AuxId = optOverrides.sharedMasks
			and '111' .. string.rep( 'a', 29 )
			or KSIdGenerator.uuidInLowercaseHex()

	local mockMask1 = KSData.make( mask1Fingerprint )

	local mask1FilePath, mask1RelativeFilePath, mask1FileSize, mask1Sha256
			= createMaskFile( baseAuxPath, mask1Fingerprint, mockMask1, optOverrides )

	local mask2Fingerprint = optOverrides.sharedMasks
			and string.rep( 'B', 32 )
			or string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local mask2AuxId = optOverrides.sharedMasks
			and '222' .. string.rep( 'b', 29 )
			or KSIdGenerator.uuidInLowercaseHex()

	local mockMask2 = KSData.make( mask2Fingerprint )

	local mask2FilePath, mask2RelativeFilePath, mask2FileSize, mask2Sha256
			= createMaskFile( baseAuxPath, mask2Fingerprint, mockMask2, optOverrides )

	if optOverrides.noLocalMasks or not optOverrides.hasMasks then

		deleteFile( mask1FilePath )

		local parentPath = KSPathUtils.parent( mask1FilePath )

		if WFFileUtils.exists( parentPath ) and WFFileUtils.empty( parentPath ) then
			WFFileUtils.deleteDirectory( parentPath, true )
		end

		deleteFile( mask2FilePath )

		parentPath = KSPathUtils.parent( mask2FilePath )

		if WFFileUtils.exists( parentPath ) and WFFileUtils.empty( parentPath ) then
			WFFileUtils.deleteDirectory( parentPath, true )
		end

	end

	mockMask1:close()
	mockMask2:close()

	-- Filters

	local groupFingerprint = optOverrides.sharedFilters
			and string.rep( 'C', 32 )
			or string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local filter1Fingerprint = optOverrides.sharedFilters
			and string.rep( 'D', 32 )
			or string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local filter1AuxId = optOverrides.sharedFilters
			and '444' .. string.rep( 'd', 29 )
			or KSIdGenerator.uuidInLowercaseHex()

	local mockFilter1 = KSData.make( filter1Fingerprint )

	local filter1FilePath, filter1RelativeFilePath, filter1FileSize, filter1Sha256
			= createFilterInstanceFile( baseAuxPath, filter1Fingerprint, mockFilter1, optOverrides )

	local filter2Fingerprint = optOverrides.sharedFilters
			and string.rep( 'E', 32 )
			or string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local filter2AuxId = optOverrides.sharedFilters
			and '555' .. string.rep( 'e', 29 )
			or KSIdGenerator.uuidInLowercaseHex()

	local mockFilter2 = KSData.make( filter2Fingerprint )

	local notFromSource = true 

	local filter2FilePath, filter2RelativeFilePath, filter2FileSize, filter2Sha256
			= createFilterInstanceFile( baseAuxPath, filter2Fingerprint, mockFilter2, optOverrides, notFromSource )

	if optOverrides.noLocalFilters or not ( optOverrides.hasFilters or optOverrides.hasSourceOnlyFilters ) then

		deleteFile( filter1FilePath )

		local parentPath = KSPathUtils.parent( filter1FilePath )

		if WFFileUtils.exists( parentPath ) and WFFileUtils.empty( parentPath ) then
			WFFileUtils.deleteDirectory( parentPath, true )
		end

	end

	if ( optOverrides.hasSourceOnlyFilters and not optOverrides.hasLocalResampledFilter )
			or ( not optOverrides.hasLocalResampledFilter and optOverrides.noLocalFilters ) 
			or ( not optOverrides.hasFilters and not optOverrides.hasLocalResampledFilter ) then

		deleteFile( filter2FilePath )

		local parentPath = KSPathUtils.parent( filter2FilePath )

		if WFFileUtils.exists( parentPath ) and WFFileUtils.empty( parentPath ) then
			WFFileUtils.deleteDirectory( parentPath, true )
		end

	end

	mockFilter1:close()
	mockFilter2:close()
	
	-- mockC2pa

	local c2paFingerprint = string.upper( KSIdGenerator.uuidInLowercaseHex() )

	local c2paAuxId = KSIdGenerator.uuidInLowercaseHex()

	local mockC2pa = KSData.make( c2paFingerprint )

	local c2paFilePath, c2paRelativeFilePath, c2paFileSize, c2paSha256
			= createC2paFile( baseAuxPath, c2paFingerprint, mockC2pa, optOverrides )

	if optOverrides.noLocalC2pa or not optOverrides.hasC2pa then

		deleteFile( c2paFilePath )

		local parentPath = KSPathUtils.parent( c2paFilePath )

		if WFFileUtils.exists( parentPath ) and WFFileUtils.empty( parentPath ) then
			WFFileUtils.deleteDirectory( parentPath, true )
		end

	end

	mockC2pa:close()

	rxTest:finally( function()

		deleteFile( mask1FilePath )
		deleteFile( mask2FilePath )
		deleteFile( filter1FilePath )
		deleteFile( filter2FilePath )
		deleteFile( c2paFilePath )

	end )

	return {

		mask1AuxId = mask1AuxId,
		mask1Fingerprint = mask1Fingerprint,
		mask1FilePath = mask1FilePath,
		mask1RelativeFilePath = mask1RelativeFilePath,
		mask1FileSize = mask1FileSize,
		mask1Sha256 = mask1Sha256,

		mask2AuxId = mask2AuxId,
		mask2Fingerprint = mask2Fingerprint,
		mask2FilePath = mask2FilePath,
		mask2RelativeFilePath = mask2RelativeFilePath,
		mask2FileSize = mask2FileSize,
		mask2Sha256 = mask2Sha256,

		filterGroupFingerprint = groupFingerprint,

		filter1AuxId = filter1AuxId,
		filter1Fingerprint = filter1Fingerprint,
		filter1FilePath = filter1FilePath,
		filter1RelativeFilePath = filter1RelativeFilePath,
		filter1FileSize = filter1FileSize,
		filter1Sha256 = filter1Sha256,

		filter2AuxId = filter2AuxId,
		filter2Fingerprint = filter2Fingerprint,
		filter2FilePath = filter2FilePath,
		filter2RelativeFilePath = filter2RelativeFilePath,
		filter2FileSize = filter2FileSize,
		filter2Sha256 = filter2Sha256,
		
		c2paAuxId = c2paAuxId,
		c2paFingerprint = c2paFingerprint,
		c2paFilePath = c2paFilePath,
		c2paRelativeFilePath = c2paRelativeFilePath,
		c2paFileSize = c2paFileSize,
		c2paSha256 = c2paSha256,
	}

end

WFCatAssetTestUtils.getDefaultLocalOnlySettingsForImage_aux = getDefaultLocalOnlySettingsForImage_aux

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

local function getDefaultLocalOnlySettingsForImage( rxTest, wfCatalog, assetId, optOverrides )

	local customId = KSIdGenerator.uuidInLowercaseHex()

	local testOriginalName = string.format( "original_%s.RAW", customId )
	local testLatestOriginalName = string.format( "latest_original_%s.RAW", customId )
	local testProxyName = string.format( "proxy_%s.DNG", customId )
	local testFullsizeProxyName = string.format( "fullsize_proxy_%s.DNG", customId )
	local testSidecarName = string.format( "sidecar%s.xmp", customId )
	local testPresetName = string.format( "preset_%s.xmp", customId )

	local tempFolder = tempFolderForTestName[ rxTest:testName() ]

	if not tempFolder then
		tempFolder = WFTestFileUtils.createTestFolder( rxTest )

		tempFolderForTestName[ rxTest:testName() ] = tempFolder
	end

	local originalFilePath, originalFileSize, originalContentId
			= createOriginalFile( tempFolder, testOriginalName, 'original_data' )

	local latestOriginalFilePath = createOriginalFile( tempFolder, testLatestOriginalName, 'latest_original_data' )

	local proxyFilePath, proxyFileSize, proxyContentId
			= createProxyFile( tempFolder, testProxyName, 'proxy_data' )

	local fullsizeProxyFilePath, fullsizeProxyFileSize, fullsizeProxyContentId
			= createProxyFile( tempFolder, testFullsizeProxyName, 'fullsize_proxy_data' )

	local xmpSidecarFilePath, xmpSidecarFileSize

	if WFPlatformInfo.isMobile() then

		xmpSidecarFilePath = nil
		xmpSidecarFileSize = 0

	else

		xmpSidecarFilePath = KSPathUtils.child( tempFolder, testSidecarName )
		xmpSidecarFileSize = createXmpSidecarFile( xmpSidecarFilePath )

	end

	local xmpDevelopSettingsFilePath, xmpDevelopSettingsRelPath,
		xmpDevelopSettingsFileSize, xmpDataAsStr, developSettingsSha256
			= createXmpDevelopSettings( wfCatalog, assetId, tempFolder, optOverrides )

	-- Aux

	local auxLocalOnlySettings = getDefaultLocalOnlySettingsForImage_aux( rxTest, wfCatalog, optOverrides )

	-- Note: Desktop rendition information is generated in createDesktopAsset.

	local fullsizeFilePath, fullsizeFileSize, fullsizeContentId, fullsizeData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'fullsize_data' )

	local previewFilePath, previewFileSize, previewContentId, previewData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'preview_data' )

	local thumbnailFilePath, thumbnailFileSize, thumbnailContentId, thumbnailData
			= createFile( tempFolder, KSIdGenerator.uuidInLowercaseHex(), 'thumb_data' )

	local presetFilePath, presetFileSize, presetContentId
			= createFile( tempFolder, testPresetName, 'preset_data' )

	rxTest:finally( function()

		deleteFile( originalFilePath )
		deleteFile( latestOriginalFilePath )
		deleteFile( xmpSidecarFilePath )
		deleteFile( proxyFilePath )
		deleteFile( fullsizeProxyFilePath )
		deleteFile( previewFilePath )
		deleteFile( thumbnailFilePath )
		deleteFile( xmpDevelopSettingsFilePath )
		deleteFile( presetFilePath )
		deleteFile( fullsizeFilePath )

	end )

	local localOnlySettings = {

		originalFilePath = originalFilePath,
		originalFileSize = originalFileSize,
		originalContentId = originalContentId,
		originalSha256 = originalContentId,

		latestOriginalFilePath = latestOriginalFilePath,

		xmpSidecarFilePath = xmpSidecarFilePath,
		xmpSidecarFileSize = xmpSidecarFileSize,

		proxyFilePath = proxyFilePath,
		proxyFileSize = proxyFileSize,
		proxyContentId = proxyContentId,

		fullsizeProxyFilePath = fullsizeProxyFilePath,
		fullsizeProxyFileSize = fullsizeProxyFileSize,
		fullsizeProxyContentId = fullsizeProxyContentId,

		previewFilePath = previewFilePath,
		previewFileSize = previewFileSize,
		previewData = previewData,
		previewContentId = previewContentId,

		fullsizeFilePath = fullsizeFilePath,
		fullsizeFileSize = fullsizeFileSize,
		fullsizeData = fullsizeData,
		fullsizeContentId = fullsizeContentId,

		thumbnailFilePath = thumbnailFilePath,
		thumbnailFileSize = thumbnailFileSize,
		thumbnailData = thumbnailData,
		thumbnailContentId = thumbnailContentId,

		xmpDevelopSettingsFilePath = xmpDevelopSettingsFilePath,
		xmpDevelopSettingsRelPath = xmpDevelopSettingsRelPath,
		xmpDevelopSettingsFileSize = xmpDevelopSettingsFileSize,
		developSettingsSha256 = developSettingsSha256,
		xmpDevelopSettingsString = xmpDataAsStr,

		presetFilePath = presetFilePath,
		presetContentId = presetContentId,
		presetFileSize = presetFileSize,

	}

	return KSTableUtils.merge( {}, localOnlySettings, auxLocalOnlySettings )

end

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

local function getDefaultLocalOnlySettings( rxTest, wfCatalog, assetId, optOverrides )

	if optOverrides.docSubtype == 'video' then

		if not optOverrides.noLocalProxy
				and not optOverrides.hasLocal1080p
				and not optOverrides.hasLocal720p
				and not optOverrides.hasLocal360p
				and not optOverrides.hasLocal1080p709 then

			optOverrides.hasLocal1080p = true -- Default

		end

		return getDefaultLocalOnlySettingsForVideo( rxTest, wfCatalog, assetId, optOverrides )

	else

		if not optOverrides.noLocalProxy
				and not optOverrides.hasLocal2560Proxy
				and not optOverrides.hasLocalFullsizeProxy
				and not optOverrides.isPanorama then

			optOverrides.hasLocal2560Proxy = true -- Default

		end

		return getDefaultLocalOnlySettingsForImage( rxTest, wfCatalog, assetId, optOverrides )

	end

end

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

local function getBaseDocValue( assetId, optOverrides, localOnlySettings )

	local xmpCameraRaw = {
		sha256 = localOnlySettings.developSettingsSha256,
	}

	local isVideo = optOverrides.docSubtype == 'video'
	local hasMasks = optOverrides.hasMasks
	local hasC2pa = optOverrides.hasC2pa
	local hasFilters = optOverrides.hasFilters
	local hasSourceOnlyFilters = optOverrides.hasSourceOnlyFilters

	local links = {
		[ 'self' ] = { href = '/href/bogus/self' },
		[ '/rels/master' ] = { href = '/href/bogus/testing' },
		[ '/rels/xmp' ] = {
			href = '/href/bogus/xmp',
			exists = true
		}
	}

	if optOverrides.internalXmpCameraRaw or isVideo then

		xmpCameraRaw = localOnlySettings.xmpDevelopSettingsString

	elseif not optOverrides.noDevelopSettingsLink then

		KSTableUtils_setNested(
				links, '/href/bogus/xmp_develop', '/rels/xmp/develop', 'href' )

		if not optOverrides.ozDevelopSettingsWithoutSha256 then

			KSTableUtils_setNested( links, localOnlySettings.developSettingsSha256,
					'/rels/xmp/develop', 'sha256' )

		end

		if optOverrides.ozDevelopSettingsAreInvalid then

			KSTableUtils_setNested(
					links, true, '/rels/xmp/develop', 'invalid' )

		end

	end

	local revId = KSIdGenerator.uuidInLowercaseHex()

	local fileName = "Image01.jpg"
	
	local importTimestamp = '2023-12-20T17:19:16.448Z'
	
	if optOverrides.randomFileName then
		fileName = "Image"..revId..".jpg"
	end
	
	if optOverrides.currentImportTimestamp then
		importTimestamp = KSDate.timeInIso8601Format()
	end
	
	local docValue = {

		_id = assetId,
		type = 'asset',
		_newRevId = revId,
		subtype = optOverrides.docSubtype or ( isVideo and 'video' ) or 'image',
		captureDate = optOverrides.captureDate or KSDate.timeInIso8601Format(),
		develop = {
			processingModel = 'lightroom',
			croppedHeight = 1000,
			croppedWidth = 1000,
			device = not optOverrides.noDevelopDevice and WFDeviceInfo.deviceDescription() or nil,
			xmpCameraRaw = xmpCameraRaw,
			userOrientation = 1,
			userUpdated = not optOverrides.noDevelopUserUpdated and KSDate.timeInIso8601Format() or nil,
			-- Masks
			masks = not isVideo and hasMasks and
				{
					[ localOnlySettings.mask1AuxId ] = {
						digest = localOnlySettings.mask1Fingerprint
					},
					[ localOnlySettings.mask2AuxId ] = {
						digest = localOnlySettings.mask2Fingerprint
					}  
				} or nil,
			filters = not isVideo and ( hasFilters or hasSourceOnlyFilters ) and {
				[ localOnlySettings.filterGroupFingerprint ] = {
					[ localOnlySettings.filter1AuxId ] = {
						digest = localOnlySettings.filter1Fingerprint,
						sourceLongEdge = 6000,
						crsSizeX = 1024,
						crsSizeY = 768
					} or nil,
					[ localOnlySettings.filter2AuxId ] = not hasSourceOnlyFilters and {
						digest = localOnlySettings.filter2Fingerprint,
						targetLongEdge = 2560,
						crsSizeX = 437,
						crsSizeY = 328
					} or nil
				}
			} or nil
		},
		importSource = {
			sha256 = localOnlySettings.originalSha256,
			fileName = fileName,
			importedBy = '9cb9fb43d26c4691a9fd32e633434796',
			importTimestamp = importTimestamp,
			--importedBy = KSIdGenerator.uuidInLowercaseHex(),
			--importTimestamp = KSDate.timeInIso8601Format(),
				-- importedBy and importTimestamp were added to make these valid assets that can be synced
				-- to Oz. Originally tried to make them dynamically generated but switched to hard coded
				-- values to make it easier to keep WFCatCopiesTest and WFCatSpaceFeedsManagerTest tests passing. Someone else can
				-- feel free to push through and make it work!
			willNotUploadMaster = optOverrides.willNotUploadMaster and true or nil,
			c2paIngredients = not isVideo and hasC2pa and {
				aux_id = localOnlySettings.c2paAuxId,
				state = "Signed_Valid",
				version = 1,
			} or nil
		},
		_serverProvidedMetadata = {
			links = links,
			revision_ids = { revId },
			editable = optOverrides.editable,
		},
	}

	-- Aux for Masks and Filters
	if not isVideo and ( hasMasks or hasC2pa or hasFilters or hasSourceOnlyFilters ) then
		
		local aux = {}

		if hasMasks then

			aux[ localOnlySettings.mask1AuxId ] = {
                type = 'mask',
                fileSize = localOnlySettings.mask1FileSize,
                digest = localOnlySettings.mask1Fingerprint,
                sha256 = localOnlySettings.mask1Sha256,
            }

			aux[ localOnlySettings.mask2AuxId ] = {
                type = 'mask',
                fileSize = localOnlySettings.mask2FileSize,
                digest = localOnlySettings.mask2Fingerprint,
                sha256 = localOnlySettings.mask2Sha256,
            }

		end

		if hasFilters or hasSourceOnlyFilters then

			aux[ localOnlySettings.filter1AuxId ] = {
                type = 'filter',
                fileSize = localOnlySettings.filter1FileSize,
                digest = localOnlySettings.filter1Fingerprint,
                sha256 = localOnlySettings.filter1Sha256,
            }

			if not hasSourceOnlyFilters then

				aux[ localOnlySettings.filter2AuxId ] = {
					type = 'filter',
					fileSize = localOnlySettings.filter2FileSize,
					digest = localOnlySettings.filter2Fingerprint,
					sha256 = localOnlySettings.filter2Sha256,
				}

			end 

		end
		
		if hasC2pa then
		
			aux[ localOnlySettings.c2paAuxId ] = {
                type = 'c2pa',
                fileSize = localOnlySettings.c2paFileSize,
                digest = localOnlySettings.c2paFingerprint,
                sha256 = localOnlySettings.c2paSha256,
            }
		end

		docValue.aux = aux

	end

	-- Other docValue overlay

	if optOverrides.spaceId then
		docValue._spaceId = optOverrides.spaceId
	elseif optOverrides.isSpaceAsset then
		docValue._spaceId = KSIdGenerator.uuidInLowercaseHex()
	end

	if optOverrides.isCopy then
		KSTableUtils_setNested( docValue, KSIdGenerator.uuidInLowercaseHex(), 'copy', 'copy_id' )
	end

	KSTableUtils_setNested( docValue, localOnlySettings.originalFileSize, 'importSource', 'fileSize' )

	if optOverrides.noXmpSidecarLink then

		KSTableUtils_setNested( links, nil, '/rels/xmp' )

	end

	if not optOverrides.noXmp then

		docValue.xmp = {
			dc = {
				rights = 'Copyright',
			},
			exif = {
				ExposureTime = { 100, 1 },
				FNumber = { 100, 1 },
				ApertureValue = { 100, 1 },
				FocalLength = { 100, 1 },
				ISOSpeedRatings = 100,
			},
			tiff = {
				Make = 'Make',
				Model = 'Model',
			},
			aux = {
				Lens = 'Lens',
			}
		}

	end

	if optOverrides.noMasterLink then

		KSTableUtils_setNested(
				links, nil, '/rels/master' )

	end

	if not optOverrides.noProxyLink then

		if isVideo then

			-- Add Video Proxy Links

			KSTableUtils_setNested(
					links, '/href/bogus/360p', '/rels/proxy_type/360p', 'href' )

			KSTableUtils_setNested(
					links, '/href/bogus/360p_bt709', '/rels/proxy_type/360p_bt709', 'href' )

			KSTableUtils_setNested(
					links, '/href/bogus/1080p', '/rels/proxy_type/1080p', 'href' )

			KSTableUtils_setNested(
					links, '/href/bogus/1080p709', '/rels/proxy_type/1080p709', 'href' )

			if optOverrides.ozProxiesAreInvalid then

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/360p', 'invalid' )

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/360p_bt709', 'invalid' )

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/1080p', 'invalid' )

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/1080p709', 'invalid' )

			end

		else

			if optOverrides.isPanorama then

				-- Add Panorama Proxy Link

				KSTableUtils_setNested(
						links, '/href/bogus/proxy', '/rels/proxy_type/panorama', 'href' )

				if optOverrides.ozProxiesAreInvalid then

					KSTableUtils_setNested(
							links, true, '/rels/proxy_type/panorama', 'invalid' )

				end

			end

			-- Add Proxy Links

			if not optOverrides.no2560ProxyLink then
				KSTableUtils_setNested(
						links, '/href/bogus/proxy', '/rels/proxy_type/2560', 'href' )
			end

			if not optOverrides.noFullsizeProxyLink then
				KSTableUtils_setNested(
						links, '/href/bogus/proxy', '/rels/proxy_type/fullsize', 'href' )
			end

			if optOverrides.ozProxiesAreInvalid then

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/2560', 'invalid' )

				KSTableUtils_setNested(
						links, true, '/rels/proxy_type/fullsize', 'invalid' )

			end

		end

	end

	if hasMasks and not isVideo and not optOverrides.noMaskLinks then

		local auxRels = string.format( '/rels/aux/%s', localOnlySettings.mask1AuxId )

		KSTableUtils_setNested(
				links, '/href/bogus/aux1', auxRels, 'href' )

		auxRels = string.format( '/rels/aux/%s', localOnlySettings.mask2AuxId )

		KSTableUtils_setNested(
				links, '/href/bogus/aux1', auxRels, 'href' )

	end

	if ( hasFilters or hasSourceOnlyFilters ) and not isVideo and not optOverrides.noFilterLinks then

		local auxRels = string.format( '/rels/aux/%s', localOnlySettings.filter1AuxId )

		KSTableUtils_setNested(
				links, '/href/bogus/aux1', auxRels, 'href' )

		if not hasSourceOnlyFilters then
			
			auxRels = string.format( '/rels/aux/%s', localOnlySettings.filter2AuxId )

			KSTableUtils_setNested(
				links, '/href/bogus/aux1', auxRels, 'href' )

		end 

	end
	
	if hasC2pa and not isVideo and not optOverrides.noC2paLink then

		local auxRels = string.format( '/rels/aux/%s', localOnlySettings.c2paAuxId )

		KSTableUtils_setNested(
				links, '/href/bogus/aux1', auxRels, 'href' )

	end
	
	if optOverrides.ozOriginalIsInvalid then

		KSTableUtils_setNested(
				links, true, '/rels/master', 'invalid' )

	end

	if not optOverrides.noSufficientRenditionFoundOnOz then

		KSTableUtils_setNested(
				links, '/href/bogus/thumbnail2x', '/rels/rendition_type/thumbnail2x', 'href' )

		KSTableUtils_setNested(
				links, '/href/bogus/2048', '/rels/rendition_type/2048', 'href' )

		KSTableUtils_setNested(
				links, '/href/bogus/fullsize', '/rels/rendition_type/fullsize', 'href' )

		if isVideo then

			KSTableUtils_setNested(
					links, '/href/bogus/360p', '/rels/rendition_type/360p', 'href' )

			if not optOverrides.noProxyLink then

				KSTableUtils_setNested(
						links, '/href/bogus/720p', '/rels/rendition_type/720p', 'href' )

			end

		else

			KSTableUtils_setNested(
					links, '/href/bogus/640', '/rels/rendition_type/640', 'href' )

			KSTableUtils_setNested(
					links, '/href/bogus/1280', '/rels/rendition_type/1280', 'href' )

		end

	end

	if optOverrides.corruptMaster then

		KSTableUtils_setNested( docValue, "does not match",
				'importSource',
				'status',
				'error',
				'all',
				'reason',
				'sha256' )

	end

	if optOverrides.linkedLocation then

		KSTableUtils_setNested( docValue, optOverrides.linkedLocation, 'importSource', 'linkedLocation')

	end

	return docValue

end

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

local function shouldHaveLocalMaskAnnotations( optOverrides )

	return optOverrides.docSubtype ~= 'video'
				and optOverrides.hasMasks and not optOverrides.noLocalMasks

end

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

local function shouldHaveLocalFilterAnnotations( optOverrides )

	return optOverrides.docSubtype ~= 'video'
				and ( optOverrides.hasFilters or optOverrides.hasSourceOnlyFilters )
				and ( not optOverrides.noLocalFilters or optOverrides.hasLocalResampledFilter )

end

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

local function shouldHaveLocalC2paAnnotations( optOverrides )

	return optOverrides.docSubtype ~= 'video'
				and optOverrides.hasC2pa and not optOverrides.noLocalC2pa

end

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

local function maybeAddCopyLinks( docValue, localOnlySettings, optOverrides )

	if optOverrides.copyLinks then

		docValue._localOnly.sourceOriginalLink = '/href/bogus/master'
		docValue._localOnly.sourceXmpDevelopLink = '/href/bogus/xmp_develop'
		docValue._localOnly.sourceXmpDevelopSha256 = localOnlySettings.developSettingsSha256

		docValue._localOnly.sourceXmpLink = '/href/bogus/xmp'
		docValue._localOnly.sourceProxyLink = '/href/bogus/proxy'
		docValue._localOnly.sourceAssetLinkForAux = '/href/bogus/self'

		docValue._localOnly.sourceAuxLinks = {}

		local sourceAuxLinks = {}

		if shouldHaveLocalMaskAnnotations( optOverrides ) then
			sourceAuxLinks[ localOnlySettings.mask1Fingerprint ] = '/href/bogus/aux'
			sourceAuxLinks[ localOnlySettings.mask2Fingerprint ] = '/href/bogus/aux'
		end

		if shouldHaveLocalFilterAnnotations( optOverrides ) then
			sourceAuxLinks[ localOnlySettings.filter1Fingerprint ] = '/href/bogus/aux'
			sourceAuxLinks[ localOnlySettings.filter2Fingerprint ] = '/href/bogus/aux'
		end

		if shouldHaveLocalC2paAnnotations( optOverrides ) then
			sourceAuxLinks[ localOnlySettings.c2paFingerprint ] = '/href/bogus/aux'
		end

		docValue._localOnly.sourceAuxLinks = sourceAuxLinks

	end

end


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

local function getLocalOnlyAuxAnnotations( localOnlySettings, optOverrides )

	if not ( shouldHaveLocalMaskAnnotations( optOverrides ) or shouldHaveLocalC2paAnnotations( optOverrides )
			or shouldHaveLocalFilterAnnotations( optOverrides ) ) then
		
		return nil

	end

	local localOnlyAux = {}

	if shouldHaveLocalMaskAnnotations( optOverrides ) then

		localOnlyAux[ localOnlySettings.mask1Fingerprint ] = {
			relativePath = localOnlySettings.mask1RelativeFilePath,
		}

		localOnlyAux[ localOnlySettings.mask2Fingerprint ] = {
			relativePath = localOnlySettings.mask2RelativeFilePath
		}

	end

	if shouldHaveLocalFilterAnnotations( optOverrides ) then
		
		if not optOverrides.noLocalFilters then
			localOnlyAux[ localOnlySettings.filter1Fingerprint ] = {
				relativePath = localOnlySettings.filter1RelativeFilePath,
			}

			if not optOverrides.hasSourceOnlyFilters then

				localOnlyAux[ localOnlySettings.filter2Fingerprint ] = {
					relativePath = localOnlySettings.filter2RelativeFilePath
				}

			end
		end

		if  optOverrides.hasLocalResampledFilter then
			localOnlyAux[ localOnlySettings.filter2Fingerprint ] = {
				relativePath = localOnlySettings.filter2RelativeFilePath,
				groupDigest = localOnlySettings.filterGroupFingerprint,
				targetLongEdge = 2560,
				crsSizeX = 437,
				crsSizeY = 328,
				fileSize = localOnlySettings.filter2FileSize
			}
		end

	end
	
	if shouldHaveLocalC2paAnnotations( optOverrides ) then
	
		localOnlyAux[ localOnlySettings.c2paFingerprint ] = {
			relativePath = localOnlySettings.c2paRelativeFilePath,
		}

	end

	return localOnlyAux

end

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

local function createDesktopAsset( rxTest, wfCatalog, assetId, localOnlySettings, optOverrides )

	optOverrides = optOverrides or {}

	local originalRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.originalFilePath )
	local originalVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.originalFilePath )

	if optOverrides.localOriginalIsOnDisconnectedVolume then

		localOnlySettings.originalFilePath = offlinePath
		originalVolumeGuid = offlineVolumeGuid
		originalRelativePath = offlineRelativePath

	end

	local localOnlyProxySection = {}

	if optOverrides.docSubtype == 'video' then

		if optOverrides.hasLocal1080p then

			local proxy1080pRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.proxy1080pFilePath )
			local proxy1080pVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.proxy1080pFilePath )

			localOnlyProxySection[ '1080p' ] = {
				relativePath = proxy1080pRelativePath,
				volumeGuid = proxy1080pVolumeGuid,
				size = localOnlySettings.proxy1080pFileSize,
				sha256 = localOnlySettings.proxy1080pContentId,
			}

		end

		if optOverrides.hasLocal1080p709 then

			local proxy1080p709RelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.proxy1080p709FilePath )
			local proxy1080p709VolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.proxy1080p709FilePath )

			localOnlyProxySection[ '1080p709' ] = {
				relativePath = proxy1080p709RelativePath,
				volumeGuid = proxy1080p709VolumeGuid,
				size = localOnlySettings.proxy1080p709FileSize,
				sha256 = localOnlySettings.proxy1080p709ContentId,
			}

		end

		if optOverrides.hasLocal720p then

			local proxy720pRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.proxy720pFilePath )
			local proxy720pVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.proxy720pFilePath )

			localOnlyProxySection[ '720p' ] = {
				relativePath = proxy720pRelativePath,
				volumeGuid = proxy720pVolumeGuid,
				size = localOnlySettings.proxy720pFileSize,
				sha256 = localOnlySettings.proxy720pContentId,
			}

		end

		if optOverrides.hasLocal360p then

			local proxy360pRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.proxy360pFilePath )
			local proxy360pVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.proxy360pFilePath )

			localOnlyProxySection[ '360p' ] = {
				relativePath = proxy360pRelativePath,
				volumeGuid = proxy360pVolumeGuid,
				size = localOnlySettings.proxy360pFileSize,
				sha256 = localOnlySettings.proxy360pContentId,
			}

		end

	else

		if optOverrides.isPanorama or optOverrides.hasLocal2560Proxy then

			local proxyRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.proxyFilePath )
			local proxyVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.proxyFilePath )

			if optOverrides.localProxyIsOnDisconnectedVolume then

				localOnlySettings.proxyFilePath = offlinePath
				proxyVolumeGuid = offlineVolumeGuid
				proxyRelativePath = offlineRelativePath

			end

			local proxySize = '2560'

			if optOverrides.isPanorama then
				proxySize = 'panorama'
			end

			localOnlyProxySection[ proxySize ] = {
				relativePath = proxyRelativePath,
				volumeGuid = proxyVolumeGuid,
				size = localOnlySettings.proxyFileSize,
				sha256 = localOnlySettings.proxyContentId,
			}

		end

		if optOverrides.hasLocalFullsizeProxy then

			local fullsizeProxyRelativePath = WFFileUtils.removeVolumeFromPath( localOnlySettings.fullsizeProxyFilePath )
			local fullsizeProxyVolumeGuid = WFFileUtils.getVolumeGuidForPath( localOnlySettings.fullsizeProxyFilePath )

			localOnlyProxySection[ 'fullsize' ] = {
				relativePath = fullsizeProxyRelativePath,
				volumeGuid = fullsizeProxyVolumeGuid,
				size = localOnlySettings.fullsizeProxyFileSize,
				sha256 = localOnlySettings.fullsizeProxyContentId,
			}

		end

	end

	local docValue = getBaseDocValue( assetId, optOverrides, localOnlySettings )

	docValue._localOnly = {

		files = {

			original = {

				master = {
					relativePath = originalRelativePath,
					volumeGuid = originalVolumeGuid,
					size = localOnlySettings.originalFileSize,
					sha256 = localOnlySettings.originalSha256,
				},

				xmp = {
					path = localOnlySettings.xmpSidecarFilePath,
					fileSize = localOnlySettings.xmpSidecarFileSize,
				},

			},

			proxy = localOnlyProxySection,

			xmp_develop = {

				path = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsRelPath or nil,
				fileSize = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsFileSize or nil,
				sha256 = localOnlySettings.developSettingsSha256,

			},

			aux = getLocalOnlyAuxAnnotations( localOnlySettings, optOverrides ),
			
		},

		xmpSidecarExists = not optOverrides.localXmpSidecarDoesNotExist,
		
	}

	if optOverrides.sourceAssetId then
		docValue._localOnly.sourceAssetId = optOverrides.sourceAssetId
		docValue._localOnly.sourceOriginalLink = '/href/bogus/master'
		docValue._localOnly.sourceXmpDevelopLink = '/href/bogus/xmp_develop'
		docValue._localOnly.sourceXmpDevelopSha256 = localOnlySettings.developSettingsSha256
	end

	maybeAddCopyLinks( docValue, localOnlySettings, optOverrides )

	if optOverrides.markedForOfflineUse then
		docValue._localOnly.isMarkedForOfflineUse = true
	end

	if optOverrides.localXmpSidecarDoesNotExist then
		docValue._localOnly.files.original.xmp = nil
	end

	if optOverrides.noLocalOriginal then
		docValue._localOnly.files.original = nil
	elseif optOverrides.localOriginalIsInvalid then
		docValue._localOnly.files.original.master.sha256 = "INVALID"
	end

	if optOverrides.noLocalProxy then
		docValue._localOnly.files.proxy = nil
	end

	if optOverrides.noLocalDevelopSettings and not optOverrides.internalXmpCameraRaw then
		docValue._localOnly.files.xmp_develop = nil
	end

	if optOverrides.docSubtype == "preset" and not optOverrides.noLocalOriginal then
		-- on desktop a preset binary is handled like an original
		localOnlySettings.presetFilePath = localOnlySettings.originalFilePath
		localOnlySettings.presetContentId = localOnlySettings.originalContentId
		localOnlySettings.presetFileSize = localOnlySettings.originalFileSize
	end

	-- Add in rendition information. Need the fully formed doc value in order to do this.

	local NmbLocalRenditionDigest = require 'NmbLocalRenditionDigest'

	local renditionDigest = NmbLocalRenditionDigest.maybeComputeFromDocValue( docValue )

	if renditionDigest then

		local mockPreview = KSData.make( 'preview_data' )

		local previewFilePath = wfCatalog:fileSpec():pathForPreviewDigestAndLongEdge(
				docValue._id, renditionDigest, 2048, false )

		local parentFolder = KSPathUtils.parent( previewFilePath )
		WFFileUtils.mkdir( parentFolder )
		KSData.saveToFile( mockPreview, previewFilePath )

		localOnlySettings.previewFilePath = previewFilePath

		mockPreview:close()

		local mockThumb = KSData.make( 'thumb_data' )

		local thumbnailFilePath = wfCatalog:fileSpec():pathForPreviewDigestAndLongEdge(
				docValue._id, renditionDigest, 320, false )

		localOnlySettings.thumbnailFilePath = thumbnailFilePath

		parentFolder = KSPathUtils.parent( thumbnailFilePath )
		WFFileUtils.mkdir( parentFolder )
		KSData.saveToFile( mockThumb, thumbnailFilePath )

		mockThumb:close()

		rxTest:finally( function()

			deleteFile( previewFilePath )
			deleteFile( thumbnailFilePath )

		end )

	end

	return docValue

end

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

local function createiOSAsset( rxTest, assetId, localOnlySettings, optOverrides )

	optOverrides = optOverrides or {}

	local docValue = getBaseDocValue( assetId, optOverrides, localOnlySettings )

	local localOnlyProxySection = {}

	if optOverrides.docSubtype == 'video' then

		if optOverrides.hasLocal1080p then

			localOnlyProxySection[ '1080p' ] = {
				relativePath = localOnlySettings.proxy1080pFilePath,
				contentId = localOnlySettings.proxy1080pContentId,
				fileSize = localOnlySettings.proxy1080pFileSize,
			}

		end

		if optOverrides.hasLocal360p then

			localOnlyProxySection[ '360p' ] = {
				relativePath = localOnlySettings.proxy360pFilePath,
				contentId = localOnlySettings.proxy360pContentId,
				fileSize = localOnlySettings.proxy360pFileSize,
			}

		end

	else

		-- LriOS Does not currently support Panorama or Full Size Proxies

		localOnlyProxySection = {
			path = localOnlySettings.proxyFilePath,
			contentId = localOnlySettings.proxyContentId,
			fileSize = localOnlySettings.proxyFileSize,
		}

	end

	docValue._localOnly = {

		files = {

			master = {
				path = localOnlySettings.originalFilePath,
				contentId = localOnlySettings.originalContentId,
				fileSize = localOnlySettings.originalFileSize,
			},

			latestMaster = {
				filePath = localOnlySettings.latestOriginalFilePath,
			},

			proxy = optOverrides.docSubtype ~= 'video' and localOnlyProxySection or nil,

			proxies = optOverrides.docSubtype == 'video' and localOnlyProxySection or nil,

			thumbnail2x = {
				path = localOnlySettings.thumbnailFilePath,
				contentId = localOnlySettings.thumbnailContentId,
				fileSize = localOnlySettings.thumbnailFileSize,
			},

			preview = {
				path = localOnlySettings.previewFilePath,
				contentId = localOnlySettings.previewContentId,
				fileSize = localOnlySettings.previewFileSize,
			},

			fullsize = {
				path = localOnlySettings.fullsizeFilePath,
				contentId = localOnlySettings.fullsizeContentId,
				fileSize = localOnlySettings.fullsizeFileSize,
			},

			xmp_develop = {
				path = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsRelPath or nil,
				contentId = localOnlySettings.developSettingsSha256,
				fileSize = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsFileSize or nil,
			},

			aux = getLocalOnlyAuxAnnotations( localOnlySettings, optOverrides ),

		}
		
	}

	if optOverrides.sourceAssetId then
		docValue._localOnly.sourceAssetId = optOverrides.sourceAssetId
		docValue._localOnly.sourceOriginalLink = '/href/bogus/master'
		docValue._localOnly.sourceXmpDevelopLink = '/href/bogus/xmp_develop'
		docValue._localOnly.sourceXmpDevelopSha256 = localOnlySettings.developSettingsSha256
	end

	maybeAddCopyLinks( docValue, localOnlySettings, optOverrides )

	if optOverrides.noLocalOriginal then
		docValue._localOnly.files.latestMaster = nil
		docValue._localOnly.files.master = nil
	end

	if optOverrides.localOriginalIsInvalid then
		docValue._localOnly.files.master.contentId = "INVALID"
	end

	if optOverrides.noLocalProxy then
		docValue._localOnly.files.proxy = nil
		docValue._localOnly.files.proxies = nil
	end

	if optOverrides.noLocalPreview then
		docValue._localOnly.files.preview = nil
	end

	if optOverrides.noLocalThumbnail2x then
		docValue._localOnly.files.thumbnail2x = nil
	end

	if optOverrides.noLocalFullsize then
		docValue._localOnly.files.fullsize = nil
	end

	if optOverrides.noLocalDevelopSettings and not optOverrides.internalXmpCameraRaw then
		docValue._localOnly.files.xmp_develop = nil
	end

	if ( optOverrides.docSubtype == "preset"
		or optOverrides.docSubtype == "profiles"
		or optOverrides.docSubtype == "camera_profile" ) and not optOverrides.noLocalOriginal then
		docValue._localOnly = {
			files = {
				presetProfile = {
					path = localOnlySettings.presetFilePath,
					sha256 = localOnlySettings.presetContentId,
					fileSize = localOnlySettings.presetFileSize,
				}
			}
		}
	end

	return docValue

end

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

local function createAndroidAsset( rxTest, assetId, localOnlySettings, optOverrides )

	optOverrides = optOverrides or {}

	local previewData = KSData.memoryMappedContentsOfFile( localOnlySettings.previewFilePath )
	local thumbnail2xData = KSData.memoryMappedContentsOfFile( localOnlySettings.thumbnailFilePath )
	local fullsizeData = KSData.memoryMappedContentsOfFile( localOnlySettings.fullsizeFilePath )

	localOnlySettings.previewMd5 = previewData:digest( 'MD5' )
	localOnlySettings.thumbMd5 = thumbnail2xData:digest( 'MD5' )
	localOnlySettings.fullsizeMd5 = fullsizeData:digest( 'MD5' )

	previewData:close()
	thumbnail2xData:close()
	fullsizeData:close()

	local revId = KSIdGenerator.uuidInLowercaseHex()

	local docValue = getBaseDocValue( assetId, optOverrides, localOnlySettings )

	local localOnlyProxySection = {}

	if optOverrides.docSubtype == 'video' then

		if optOverrides.hasLocal1080p then

			localOnlyProxySection[ '1080p' ] = {
				fileSize = localOnlySettings.proxy1080pFileSize,
				relativePath = localOnlySettings.proxy1080pFilePath,
				contentId = localOnlySettings.proxy1080pContentId,
			}

		end

		if optOverrides.hasLocal360p then

			localOnlyProxySection[ '360p' ] = {
				fileSize = localOnlySettings.proxy360pFileSize,
				relativePath = localOnlySettings.proxy360pFilePath,
				contentId = localOnlySettings.proxy360pContentId,
			}

		end

	else

		-- LrA Does not currently support Panorama or Full Size Proxies

		localOnlyProxySection = {
			size = localOnlySettings.proxyFileSize,
			filePath = localOnlySettings.proxyFilePath,
			epoch = 1,
		}

	end

	docValue._localOnly = {

		thumbnail2x = {
			md5 = localOnlySettings.thumbMd5,
			referenceHeight = 960,
			referenceWidth = 1280,
			epoch = 1,
			revId = revId,
			size = localOnlySettings.thumbnailFileSize
		},

		preview = {
			md5 = localOnlySettings.previewMd5,
			referenceHeight = 960,
			referenceWidth = 1280,
			epoch = 1,
			revId = revId,
			size = localOnlySettings.previewFileSize
		},

		fullsize = {
			md5 = localOnlySettings.fullsizeMd5,
			referenceHeight = 960,
			referenceWidth = 1280,
			epoch = 1,
			revId = revId,
			size = localOnlySettings.fullsizeFileSize
		},

		proxy = optOverrides.docSubtype ~= 'video' and localOnlyProxySection or nil,

		originalFilePath = localOnlySettings.originalFilePath,

		latestOriginal = {
			filePath = localOnlySettings.latestOriginalFilePath,
		},

		original = {
			epoch = 1,
			size = localOnlySettings.originalFileSize,
		},

		files = {

			xmp_develop = {
				path = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsRelPath or nil,
				sha256 = localOnlySettings.developSettingsSha256,
				fileSize = not optOverrides.internalXmpCameraRaw and localOnlySettings.xmpDevelopSettingsFileSize or nil,
			},

			aux = getLocalOnlyAuxAnnotations( localOnlySettings, optOverrides ),

			proxies = optOverrides.docSubtype == 'video' and localOnlyProxySection or nil,

		}
		
	}

	if optOverrides.sourceAssetId then
		docValue._localOnly.sourceAssetId = optOverrides.sourceAssetId
		docValue._localOnly.sourceOriginalLink = '/href/bogus/master'
		docValue._localOnly.sourceXmpDevelopLink = '/href/bogus/xmp_develop'
		docValue._localOnly.sourceXmpDevelopSha256 = localOnlySettings.developSettingsSha256
	end

	maybeAddCopyLinks( docValue, localOnlySettings, optOverrides )

	if optOverrides.noLocalOriginal then
		docValue._localOnly.latestOriginal = nil
		docValue._localOnly.originalFilePath = nil
		docValue._localOnly.original = nil
	end

	if optOverrides.localOriginalIsInvalid then
		docValue._localOnly.original.contentId = "INVALID"
	end

	if optOverrides.noLocalProxy then
		docValue._localOnly.proxy = nil
		docValue._localOnly.proxies = nil
		docValue._localOnly.files.proxies = nil
	end

	if optOverrides.noLocalPreview then
		docValue._localOnly.preview = nil
	end

	if optOverrides.noLocalThumbnail2x then
		docValue._localOnly.thumbnail2x = nil
	end

	if optOverrides.noLocalFullsize then
		docValue._localOnly.fullsize = nil
	end

	if optOverrides.noLocalDevelopSettings and not optOverrides.internalXmpCameraRaw then
		docValue._localOnly.files.xmp_develop = nil
	end

	if ( optOverrides.docSubtype == "preset"
		or optOverrides.docSubtype == "profile" ) and not optOverrides.noLocalOriginal then
		docValue._localOnly = {
			files = {
				presetProfile = {
					path = localOnlySettings.presetFilePath,
					sha256 = localOnlySettings.presetContentId,
					fileSize = localOnlySettings.presetFileSize,
				}
			}
		}
	end

	if optOverrides.docSubtype == "camera_profile" and not optOverrides.noLocalOriginal then
		docValue._localOnly = {
			files = {
				original = {
					master = {
						path = localOnlySettings.originalFilePath
					}
				}
			}
		}

	end

	-- Fully override what was created in getBaseDocValue
	if optOverrides.linkedLocation then

		local linkedLocation = {
			id = optOverrides.linkedLocation.id,
			source = optOverrides.linkedLocation.source
		}

		KSTableUtils.setNested( docValue, linkedLocation, 'importSource', 'linkedLocation')
	end

	return docValue

end

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

local function refineOverrides( optOverrides )

	optOverrides = optOverrides or {}

	-- Has aux

	if optOverrides.hasAllAuxType or optOverrides.hasDevelopAux then

		optOverrides.hasMasks = true
		optOverrides.hasFilters = true

	end

	if optOverrides.hasAllAuxType then
		optOverrides.hasC2pa = true
	end

	-- No aux links

	if optOverrides.noAllAuxTypeLinks or optOverrides.noDevelopAuxLinks then

		optOverrides.noMaskLinks = true
		optOverrides.noFilterLinks = true
	
	end

	if optOverrides.noAllAuxTypeLinks then
		optOverrides.noC2paLink = true
	end

	-- No local aux

	if optOverrides.noLocalAllAuxType or optOverrides.noLocalDevelopAux then

		optOverrides.noLocalMasks = true
		optOverrides.noLocalFilters = true

	end

	if optOverrides.noLocalAllAuxType then
		optOverrides.noLocalC2pa = true
	end
	
	return optOverrides

end

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

-- @mark APIs

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

--[[!
	\brief Creates a platform specific Asset document for use in tests.

	\param rxTest (rxTest)
	\param wfCatalog (WFCatalog)
	\param docId (optional string) optional document Id.
	\param optOverrides (optional table) Optional document modifiers:

		captureDate ( String ): Overrides the capture date in the assetDocument by passing
			it to getBaseDocValue through the createAsset platform functions.
		
		currentImportTimestamp ( boolean ): Overrides the import time stamp and sets it to 
			the current time. If not enabled the import time stamp defaults to 
			'2023-12-20T17:19:16.448Z'
			
		docSubtype (string): Overrides the document subtype. Supported:
			'preset' or 'profile' or 'camera_profile' or 'video'

		noXmpSidecarLink (boolean): If true, there will be no xmp sidecar link, indicating
			that the sidecar has not been uploaded.

		noSufficientRenditionFoundOnOz (boolean): If true, there is no thumbnail
			link on Oz.

		markedForOfflineUse (boolean): DESKTOP ONLY.

		isSpaceAsset (boolean): True if the asset is a space asset, and not a catalog asset.

		spaceId (string UUID): If provided, will use as the space Id.

		editable (boolean): If true, mark the asset as editable. Useful for space assets

		isCopy (boolean): If True, the asset will have a copy Id. Note, the source asset
			may not exist.

		sourceAssetId (boolean): If provided, will use as the sourceAssetId.

		copyLinks (boolean): If true, the asset will have copy links (bogus) for all binaries.

		randomFileName( boolean ): When filename needs to be randomized set this variable 
			to true. The revId will be used to generate a new filename. Else a predetermined 
			filename will be given by default. 
			
		-- ORIGINAL OPTIONS

		noMasterLink (boolean): If true, there will be no master link, indicating the master
			has not been uploaded.

		noLocalOriginal (boolean): If true, there will be no local original.

		ozOriginalIsInvalid (boolean): If true, the original on Oz will be marked as invalid.

		corruptMaster (boolean): If true, the original will be marked as corrupt with the sha256
			'does not match' error in the importSource section of the document.

		localOriginalIsInvalid (boolean): If true, the local original will be marked as invalid.

		localOriginalIsOnDisconnectedVolume (boolean): If true, the local original will be
			stored on a non-existent (disconnected) volume.

		localXmpSidecarDoesNotExist (boolean): If true, there will be no local xmp sidecar file.

		linkedLocation (table): If provided, treat this asset
			as an implicit import. Put the provided table directly
			into the importSource section of the asset

		hasOriginalSourceLinkForCopy (boolean): if true, the asset will contain a sourceLinkForCopy
			annotation. Note, the source link will not be valid.
		
		willNotUploadMaster (boolean): If ture, importSource.willNotUploadMaster will be set to true.

		-- DEVELOP OPTIONS

		internalXmpCameraRaw (boolean): If true, the dev settings are inlined and there
			are no external develop settings.

		noDevelopSettingsLink (boolean): If true, there will be no dev settings link on Oz.

		ozDevelopSettingsAreInvalid (boolean): If true, the settings on Oz will be marked as invalid.

		ozDevelopSettingsWithoutSha256 (boolean): If true, the xmp_develop link from Oz will not have
			sha256 value included.

		noLocalDevelopSettings (boolean): If true, there will be no local externalized dev settings.

		sharedDevelopSettings (boolean): If multiple assets are generated with
			sharedDevelopSettings as true, the dev settings will match across the assets.

		-- GROUP AUX OPTIONS

		hasAllAuxType (boolean): If true, the document will include all kinds of aux type.

		noAllAuxTypeLinks (boolean): If true, all aux have not been uploaded.

		noLocalAllAuxType (boolean): if true, all aux won't be on the local device.

		hasDevelopAux (boolean): If true, the develop settings will include all kinds of
			develop aux

		noDevelopAuxLinks (boolean): If true, all develop aux have not been uploaded.

		noLocalDevelopAux (boolean): if true, all develop aux won't be on the local device.
		
		-- DEVELOP AUX OPTIONS

		hasMasks (boolean): If true, the develop settings will reference two masks.

		sharedMasks (boolean): If multiple assets are generated with sharedMasks as true, the
			mask fingerprints will match across the assets.

		noMaskLinks (boolean): If true, the masks have not been uploaded.

		noLocalMasks (boolean): if true, the masks won't be on the local device.

		hasFilters (boolean): If true, the develop settings will reference one filter group with 
			two instances. One of which will be original size while the other one will be proxy size.
			Note: If true, then hasSourceOnlyFilters should be nil.
			
		hasSourceOnlyFilters (boolean): If true, the develop settings will reference one filter 
			group with only instance from source. Note: If true, then hasFilters should be nil.

		sharedFilters (boolean): If multiple assets are generated with sharedFilters as true, the
			filter group and fingerprints will match across the assets.

		noFilterLinks (boolean): If true, the filters have not been uploaded.

		noLocalFilters (boolean): If true, the filters won't be on the local device.

		hasLocalResampledFilter (boolean): If true, the filters have resampled auxInfo in _localOnly. 
			Note: noLcoalFilter can be true at the same time hasLocalResampledFilter is true. ResampledFilter 
			would exists locally.

		noDevelopUserUpdated (boolean): if true, do not include a userUpdated timestamp in the develop section

		noDevelopDevice (boolean): if true, do not include the device in the develop section
		
		-- C2PA OPTIONS
		
		hasC2pa (boolean): If true, importSource.c2paIngredients will reference a c2pa blob.
		
		noLocalC2pa (boolean): If true, c2pa blob won't be on the local device.
		
		noC2paLink (boolean): If true, the c2pa blobs have not been uploaded.

		-- RENDITION OPTIONS

		noLocalPreview (boolean): Mobile only: If true, there will be no local preview.

		noLocalThumbnail2x (boolean): Mobile only: If true, there will be no local thumbnail.

		noLocalFullsize (boolean): Mobile only: If true, there will be no local fullsize rendition.

		-- PROXY OPTIONS

		localProxyIsOnDisconnectedVolume (boolean): If true, the local proxy will be
			stored on a non-existent (disconnected) volume.

		noLocalProxy (boolean): If true, then there will be no local proxy.

		ozProxiesAreInvalid (boolean): If true, the proxies on Oz will be marked as invalid.

		noProxyLink (boolean): If true, there will be no proxy link on Oz.

		no2560ProxyLink (boolean): If true, there will be no 2560 proxy link on Oz.

		noFullsizeProxyLink (boolean): If true, there will be no fullsize proxy link on Oz.

		hasLocal2560Proxy = (boolean): Image only. If true, there will be a local 2560 proxy.

		hasLocalFullsizeProxy = (boolean): Image only. If true, there will be a local Fullsize proxy.

		isPanorama (boolean): If true, the image will be a panorama (panorama proxy).

		hasLocal1080p (boolean): If true, and subtype is 'video', then a local 1080p proxy will exist. Defaults
			to true for videos.

		hasLocal1080p709 (boolean): LrD Only: If true, and subtype is 'video', then a local 1080p709
			proxy will exist.

		hasLocal720p (boolean): LrD only: If true, and subtype is 'video', then a local 720p proxy will exist.

		hasLocal360p (boolean): If true, and subtype is 'video', then a local 360p proxy will exist.

	\return (table) The Asset document.
--]]

function WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, assetId, optOverrides )

	assetId = assetId or KSIdGenerator.uuidInLowercaseHex()
	optOverrides = refineOverrides( optOverrides )
	
	local docValue = {}

	local localOnlySettings = getDefaultLocalOnlySettings( rxTest, wfCatalog, assetId, optOverrides )
	
	if WFPlatformInfo.isDesktop then

		docValue = createDesktopAsset( rxTest, wfCatalog, assetId, localOnlySettings, optOverrides )

	elseif WFPlatformInfo.isiOS then

		docValue = createiOSAsset( rxTest, assetId, localOnlySettings, optOverrides )

	elseif WFPlatformInfo.isAndroid then

		docValue = createAndroidAsset( rxTest, assetId, localOnlySettings, optOverrides )

	end

	if optOverrides.hasOriginalSourceLinkForCopy then
		KSTableUtils.setNested( docValue, "/href/bogus/testing", '_localOnly', 'sourceOriginalLink' )
	end

	return docValue, localOnlySettings

end

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

function WFCatAssetTestUtils.insertAssets( rxTest, wfCatalog, assetCount, optOverrides )

	local assetOverlays = {}
	local assetIds = {}

	for i = 1, assetCount do

		local docValue = WFCatAssetTestUtils.createAssetDocument( rxTest, wfCatalog, nil, optOverrides )

		assetOverlays[ #assetOverlays + 1 ] = docValue
		assetIds[ #assetIds + 1 ] = docValue._id

	end

	return wfCatalog:docStore():bulkApplyOverlay( assetOverlays )
		:map( function()
			return assetIds
		end )
end

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

function WFCatAssetTestUtils.getOverlayToRemoveLocalOriginal( assetId )
	if WFPlatformInfo.isDesktop then

		return {
			_id = assetId,
			_localOnly = {
				files = {
					original = { _delete = true }
				}
			}
		}

	elseif WFPlatformInfo.isiOS then

		return {
			_id = assetId,
			_localOnly = {
				files = {
					master = { _delete = true }
				}
			}
		}

	elseif WFPlatformInfo.isAndroid then

		return {
			_id = assetId,
			_localOnly = {
				localUrl =  { _delete = true },
				originalFilePath = { _delete = true },
				latestOriginal = { _delete = true },
				original = { _delete = true },
				xmpSidecarExists = false,
			}
		}

	end

end

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

function WFCatAssetTestUtils.getOverlayToRemoveLocalProxy( assetId )

	if WFPlatformInfo.isDesktop then

		return {
			_id = assetId,
				_localOnly = {
					files = {
						proxy = { _delete = true }
					}
				}
			}

	elseif WFPlatformInfo.isiOS then

		return {
			_id = assetId,
			_localOnly = {
				files = {
					proxy = { _delete = true }
				}
			}
		}

	elseif WFPlatformInfo.isAndroid then

		return {
			_id = assetId,
			_localOnly = {
				proxy = { _delete = true }
			}
		}

	end
end

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

function WFCatAssetTestUtils.getOriginalPathOverlay( assetId, sha256, originalFilePath )

	local originalFileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize

	local overlay = {}

	if WFPlatformInfo.isDesktop then

		overlay = {
			_id = assetId,
			_localOnly = {
				files = {
					original = {
						master = {
							relativePath = WFFileUtils.removeVolumeFromPath( originalFilePath ),
							volumeGuid = WFFileUtils.getVolumeGuidForPath( originalFilePath ),
							size = originalFileSize,
							sha256 = sha256,
						},
					},
				},
				xmpSidecarExists = false,
			}
		}

	elseif WFPlatformInfo.isiOS then

		overlay = {
			_id = assetId,
			_localOnly = {
				files = {
					master = {
						path = originalFilePath,
						contentId = sha256,
						fileSize = originalFileSize,
					},
				},
				xmpSidecarExists = false,
			}
		}

	elseif WFPlatformInfo.isAndroid then


		overlay = {
			_id = assetId,
			_localOnly = {
				-- this is a fake id (not the actual format of the field) and would apply only to local imports
				-- it's necessary for the binary uploader tests for missing originals
				localUrl = KSIdGenerator.uuidInLowercaseHex(),
				originalFilePath = originalFilePath,

				latestOriginal = {
					filePath = originalFilePath,
				},

				original = {
					epoch = 1,
					size = originalFileSize,
				},
				xmpSidecarExists = false,
			}
		}
	end

	return overlay

end

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

function WFCatAssetTestUtils.addOriginalPathToDocument( baseAssetDoc, sha256, originalFilePath )

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, WFCatAssetTestUtils.getOriginalPathOverlay( baseAssetDoc._id, sha256, originalFilePath ) )

	return baseAssetDoc
end

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

function WFCatAssetTestUtils.getProxyPathOverlay( assetId, proxyFilePath )

	local proxyFileSize = WFFileUtils.fileAttributes( proxyFilePath ).fileSize

	local overlay = {}

	if WFPlatformInfo.isDesktop then
	
		local proxySha256
		if WFPlatformInfo.isClassic() then
			proxySha256 = WFCrypto.digestFile( proxyFilePath, 'SHA256' )
		end

		overlay = {
			_id = assetId,
			_localOnly = {
				files = {
					proxy = {
						['2560'] = {
							relativePath = WFFileUtils.removeVolumeFromPath( proxyFilePath ),
							volumeGuid = WFFileUtils.getVolumeGuidForPath( proxyFilePath ),
							sha256 = proxySha256,
							size = proxyFileSize,
						},
					},
				},
			}
		}

	elseif WFPlatformInfo.isiOS then

		overlay = {
			_id = assetId,
			_localOnly = {
				files = {
					proxy = {
						path = proxyFilePath,
						size = proxyFileSize,
						epoch = 1,
					},
				}
			}
		}

	elseif WFPlatformInfo.isAndroid then

		overlay = {
			_id = assetId,
			_localOnly = {
				proxy = {
					size = proxyFileSize,
					filePath = proxyFilePath,
					epoch = 1,
				},
			},
		}

	end

	return overlay
end

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

function WFCatAssetTestUtils.getXmpSidecarPathOverlay( assetId, xmpSidecarFilePath )

	local overlay
	if WFPlatformInfo.isDesktop then

		overlay = {
			_id = assetId,
			_localOnly = {
				files = {
					original = {
						xmp = {
							relativePath = xmpSidecarFilePath and WFFileUtils.removeVolumeFromPath( xmpSidecarFilePath ),
							volumeGuid = xmpSidecarFilePath and WFFileUtils.getVolumeGuidForPath( xmpSidecarFilePath ),
						}
					}
				},
				xmpSidecarExists = true
			}
		}

	else

		overlay = {}

	end

	return overlay
end

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

function WFCatAssetTestUtils.addProxyPathToDocument( baseAssetDoc, proxyFilePath )

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, WFCatAssetTestUtils.getProxyPathOverlay( baseAssetDoc._id, proxyFilePath ) )

	return baseAssetDoc
end

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

function WFCatAssetTestUtils.addXmpSidecarPathToDocument( baseAssetDoc, xmpSidecarFilePath )

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, WFCatAssetTestUtils.getXmpSidecarPathOverlay( baseAssetDoc._id, xmpSidecarFilePath ) )

	return baseAssetDoc

end

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

local function getXmpSidecar()

	return [[<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c148 79.164036, 2019/08/13-01:06:57        ">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:xmp="http://ns.adobe.com/xap/1.0/"
            xmlns:aux="http://ns.adobe.com/exif/1.0/aux/"
            xmlns:exifEX="http://cipa.jp/exif/1.0/"
            xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
            xmlns:dc="http://purl.org/dc/elements/1.1/"
            xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
            xmlns:exif="http://ns.adobe.com/exif/1.0/">
         <xmp:CreatorTool>8.3</xmp:CreatorTool>
         <xmp:ModifyDate>2015-06-08T08:19:58</xmp:ModifyDate>
         <xmp:CreateDate>2015-06-08T08:19:58</xmp:CreateDate>
         <xmp:MetadataDate>2015-06-08T08:19:58</xmp:MetadataDate>
         <aux:LensInfo>83/20 83/20 11/5 11/5</aux:LensInfo>
         <aux:Lens>iPhone 5s back camera 4.15mm f/2.2</aux:Lens>
         <exifEX:LensModel>iPhone 5s back camera 4.15mm f/2.2</exifEX:LensModel>
         <exifEX:LensMake>Apple</exifEX:LensMake>
         <photoshop:DateCreated>2015-06-08T08:19:58.057</photoshop:DateCreated>
         <photoshop:ColorMode>3</photoshop:ColorMode>
         <photoshop:ICCProfile>sRGB IEC61966-2.1</photoshop:ICCProfile>
         <xmpMM:DocumentID>74CEFE737E075E9148CE2592E45BDAF5</xmpMM:DocumentID>
         <xmpMM:InstanceID>74CEFE737E075E9148CE2592E45BDAF5</xmpMM:InstanceID>
         <dc:format>image/jpeg</dc:format>
         <tiff:ImageWidth>3264</tiff:ImageWidth>
         <tiff:ImageLength>2448</tiff:ImageLength>
         <tiff:BitsPerSample>
            <rdf:Seq>
               <rdf:li>8</rdf:li>
               <rdf:li>8</rdf:li>
               <rdf:li>8</rdf:li>
            </rdf:Seq>
         </tiff:BitsPerSample>
         <tiff:PhotometricInterpretation>2</tiff:PhotometricInterpretation>
         <tiff:Orientation>1</tiff:Orientation>
         <tiff:SamplesPerPixel>3</tiff:SamplesPerPixel>
         <tiff:YCbCrPositioning>1</tiff:YCbCrPositioning>
         <tiff:XResolution>72/1</tiff:XResolution>
         <tiff:YResolution>72/1</tiff:YResolution>
         <tiff:ResolutionUnit>2</tiff:ResolutionUnit>
         <tiff:Make>Apple</tiff:Make>
         <tiff:Model>iPhone 5s</tiff:Model>
         <exif:ExifVersion>0221</exif:ExifVersion>
         <exif:FlashpixVersion>0100</exif:FlashpixVersion>
         <exif:ColorSpace>1</exif:ColorSpace>
         <exif:ComponentsConfiguration>
            <rdf:Seq>
               <rdf:li>1</rdf:li>
               <rdf:li>2</rdf:li>
               <rdf:li>3</rdf:li>
               <rdf:li>0</rdf:li>
            </rdf:Seq>
         </exif:ComponentsConfiguration>
         <exif:PixelXDimension>3264</exif:PixelXDimension>
         <exif:PixelYDimension>2448</exif:PixelYDimension>
         <exif:DateTimeOriginal>2015-06-08T08:19:58</exif:DateTimeOriginal>
         <exif:ExposureTime>1/2564</exif:ExposureTime>
         <exif:FNumber>11/5</exif:FNumber>
         <exif:ExposureProgram>2</exif:ExposureProgram>
         <exif:ISOSpeedRatings>
            <rdf:Seq>
               <rdf:li>32</rdf:li>
            </rdf:Seq>
         </exif:ISOSpeedRatings>
         <exif:ShutterSpeedValue>7055/623</exif:ShutterSpeedValue>
         <exif:ApertureValue>2275007/1000000</exif:ApertureValue>
         <exif:BrightnessValue>8147/752</exif:BrightnessValue>
         <exif:ExposureBiasValue>0/1</exif:ExposureBiasValue>
         <exif:MeteringMode>3</exif:MeteringMode>
         <exif:Flash rdf:parseType="Resource">
            <exif:Fired>False</exif:Fired>
            <exif:Return>0</exif:Return>
            <exif:Mode>3</exif:Mode>
            <exif:Function>False</exif:Function>
            <exif:RedEyeMode>False</exif:RedEyeMode>
         </exif:Flash>
         <exif:FocalLength>83/20</exif:FocalLength>
         <exif:SubjectArea>
            <rdf:Seq>
               <rdf:li>1444</rdf:li>
               <rdf:li>1172</rdf:li>
               <rdf:li>610</rdf:li>
               <rdf:li>612</rdf:li>
            </rdf:Seq>
         </exif:SubjectArea>
         <exif:SensingMethod>2</exif:SensingMethod>
         <exif:SceneType>1</exif:SceneType>
         <exif:ExposureMode>0</exif:ExposureMode>
         <exif:WhiteBalance>0</exif:WhiteBalance>
         <exif:FocalLengthIn35mmFilm>29</exif:FocalLengthIn35mmFilm>
         <exif:SceneCaptureType>0</exif:SceneCaptureType>
         <exif:SubSecTimeOriginal>570</exif:SubSecTimeOriginal>
         <exif:SubSecTimeDigitized>570</exif:SubSecTimeDigitized>
         <exif:LensInfo>
            <rdf:Seq>
               <rdf:li>83/20</rdf:li>
            </rdf:Seq>
         </exif:LensInfo>
         <exif:LensMake>Apple</exif:LensMake>
         <exif:Lens>iPhone 5s back camera 4.15mm f/2.2</exif:Lens>
         <exif:GPSLatitude>47,36.7028N</exif:GPSLatitude>
         <exif:GPSLongitude>122,24.1805W</exif:GPSLongitude>
         <exif:GPSAltitudeRef>0</exif:GPSAltitudeRef>
         <exif:GPSAltitude>1153/64</exif:GPSAltitude>
         <exif:GPSTimeStamp>2015-06-08T15:19:58Z</exif:GPSTimeStamp>
         <exif:GPSSpeedRef>K</exif:GPSSpeedRef>
         <exif:GPSSpeed>383/50</exif:GPSSpeed>
         <exif:GPSImgDirectionRef>T</exif:GPSImgDirectionRef>
         <exif:GPSImgDirection>5101/146</exif:GPSImgDirection>
         <exif:GPSDestBearingRef>T</exif:GPSDestBearingRef>
         <exif:GPSDestBearing>31381/146</exif:GPSDestBearing>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>]]

end

WFCatAssetTestUtils.getXmpSidecar = getXmpSidecar

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

local xmpCameraRawStart = [[
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21        ">
 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <rdf:Description rdf:about=""
    xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"
   crs:Version="10.3"
   crs:ProcessVersion="10.0"
   crs:WhiteBalance="As Shot"
   crs:IncrementalTemperature="0"
   crs:IncrementalTint="0"
   crs:Saturation="0"
   crs:Sharpness="0"
   crs:LuminanceSmoothing="0"
   crs:ColorNoiseReduction="0"
   crs:VignetteAmount="0"
   crs:ShadowTint="0"
   crs:RedHue="0"
   crs:RedSaturation="0"
   crs:GreenHue="0"
   crs:GreenSaturation="0"
   crs:BlueHue="0"
   crs:BlueSaturation="0"
   crs:Vibrance="0"
   crs:HueAdjustmentRed="0"
   crs:HueAdjustmentOrange="0"
   crs:HueAdjustmentYellow="0"
   crs:HueAdjustmentGreen="0"
   crs:HueAdjustmentAqua="0"
   crs:HueAdjustmentBlue="0"
   crs:HueAdjustmentPurple="0"
   crs:HueAdjustmentMagenta="0"
   crs:SaturationAdjustmentRed="0"
   crs:SaturationAdjustmentOrange="0"
   crs:SaturationAdjustmentYellow="0"
   crs:SaturationAdjustmentGreen="0"
   crs:SaturationAdjustmentAqua="0"
   crs:SaturationAdjustmentBlue="0"
   crs:SaturationAdjustmentPurple="0"
   crs:SaturationAdjustmentMagenta="0"
   crs:LuminanceAdjustmentRed="0"
   crs:LuminanceAdjustmentOrange="0"
   crs:LuminanceAdjustmentYellow="0"
   crs:LuminanceAdjustmentGreen="0"
   crs:LuminanceAdjustmentAqua="0"
   crs:LuminanceAdjustmentBlue="0"
   crs:LuminanceAdjustmentPurple="0"
   crs:LuminanceAdjustmentMagenta="0"
   crs:SplitToningShadowHue="0"
   crs:SplitToningShadowSaturation="0"
   crs:SplitToningHighlightHue="0"
   crs:SplitToningHighlightSaturation="0"
   crs:SplitToningBalance="0"
   crs:ParametricShadows="0"
   crs:ParametricDarks="0"
   crs:ParametricLights="0"
   crs:ParametricHighlights="0"
   crs:ParametricShadowSplit="25"
   crs:ParametricMidtoneSplit="50"
   crs:ParametricHighlightSplit="75"
   crs:SharpenRadius="+1.0"
   crs:SharpenDetail="25"
   crs:SharpenEdgeMasking="0"
   crs:PostCropVignetteAmount="0"
   crs:GrainAmount="0"
   crs:LensProfileEnable="0"
   crs:LensManualDistortionAmount="0"
   crs:PerspectiveVertical="0"
   crs:PerspectiveHorizontal="0"
   crs:PerspectiveRotate="0.0"
   crs:PerspectiveScale="100"
   crs:PerspectiveAspect="0"
   crs:PerspectiveUpright="0"
   crs:PerspectiveX="0.00"
   crs:PerspectiveY="0.00"
   crs:AutoLateralCA="0"
   crs:Exposure2012="0.00"
   crs:Contrast2012="0"
   crs:Highlights2012="0"
   crs:Shadows2012="0"
   crs:Whites2012="0"
   crs:Blacks2012="0"
   crs:Clarity2012="0"
   crs:DefringePurpleAmount="0"
   crs:DefringePurpleHueLo="30"
   crs:DefringePurpleHueHi="70"
   crs:DefringeGreenAmount="0"
   crs:DefringeGreenHueLo="40"
   crs:DefringeGreenHueHi="60"
   crs:Dehaze="0"
   crs:ToneMapStrength="0"
   crs:ConvertToGrayscale="False"
   crs:OverrideLookVignette="False"
   crs:ToneCurveName="Linear"
   crs:ToneCurveName2012="Linear"
   crs:CameraProfile="Embedded"
   crs:CameraProfileDigest="54650A341B5B5CCAE8442D0B43A92BCE"
   crs:LensProfileSetup="LensDefaults"
   crs:UprightVersion="151388160"
   crs:UprightCenterMode="0"
   crs:UprightCenterNormX="0.5"
   crs:UprightCenterNormY="0.5"
   crs:UprightFocalMode="0"
   crs:UprightFocalLength35mm="35"
   crs:UprightPreview="False"
   crs:UprightTransformCount="6"
   crs:UprightFourSegmentsCount="0"
   crs:HasSettings="True"
   crs:AlreadyApplied="False">
   <crs:ToneCurve>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurve>
   <crs:ToneCurveRed>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurveRed>
   <crs:ToneCurveGreen>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurveGreen>
   <crs:ToneCurveBlue>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurveBlue>
   <crs:ToneCurvePV2012>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurvePV2012>
   <crs:ToneCurvePV2012Red>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurvePV2012Red>
   <crs:ToneCurvePV2012Green>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurvePV2012Green>
   <crs:ToneCurvePV2012Blue>
    <rdf:Seq>
     <rdf:li>0, 0</rdf:li>
     <rdf:li>255, 255</rdf:li>
    </rdf:Seq>
   </crs:ToneCurvePV2012Blue>
   <crs:PaintBasedCorrections>
    <rdf:Seq>
     <rdf:li>
      <rdf:Description
       crs:What="Correction"
       crs:CorrectionAmount="1.000000"
       crs:CorrectionActive="true"
       crs:LocalExposure="0.000000"
       crs:LocalSaturation="0.000000"
       crs:LocalContrast="0.000000"
       crs:LocalClarity="0.000000"
       crs:LocalSharpness="0.000000"
       crs:LocalBrightness="0.000000"
       crs:LocalToningHue="0.000000"
       crs:LocalToningSaturation="0.000000"
       crs:LocalExposure2012="0.500000"
       crs:LocalContrast2012="0.000000"
       crs:LocalHighlights2012="0.000000"
       crs:LocalShadows2012="-0.042071"
       crs:LocalWhites2012="1.000000"
       crs:LocalBlacks2012="1.000000"
       crs:LocalClarity2012="0.000000"
       crs:LocalDehaze="0.000000"
       crs:LocalLuminanceNoise="0.000000"
       crs:LocalMoire="0.000000"
       crs:LocalDefringe="0.000000"
       crs:LocalTemperature="0.000000"
       crs:LocalTint="0.000000">
      <crs:CorrectionMasks>
       <rdf:Seq>
        <rdf:li>
         <rdf:Description
          crs:What="Mask/Paint"
          crs:MaskValue="1.000000"
          crs:Radius="0.013802"
          crs:Flow="0.500000"
          crs:CenterWeight="0.000000">
         <crs:Dabs>
          <rdf:Seq>
           <rdf:li>d 0.944824 0.911002</rdf:li>
           <rdf:li>d 0.940887 0.909293</rdf:li>
           <rdf:li>d 0.936950 0.907583</rdf:li>
]]

local xmpCameraRawEnd = [[
           <rdf:li>d 0.496560 0.297892</rdf:li>
          </rdf:Seq>
         </crs:Dabs>
         </rdf:Description>
        </rdf:li>
       </rdf:Seq>
      </crs:CorrectionMasks>
      <crs:CorrectionRangeMask
       crs:Version="+2"
       crs:Type="0"
       crs:ColorAmount="0.500000"
       crs:LumMin="0.000000"
       crs:LumMax="1.000000"
       crs:LumFeather="0.500000"/>
      </rdf:Description>
     </rdf:li>
    </rdf:Seq>
   </crs:PaintBasedCorrections>
   <crs:Look
    crs:Name=""/>
  </rdf:Description>
 </rdf:RDF>
</x:xmpmeta>
]]

local function getCameraRaw( useBigXmpDevelop )

	local xmpCameraRaw = xmpCameraRawStart
	if useBigXmpDevelop then
		local x = 0.900
		local y = 0.800

		for i = 1, 400 do
			x = x - .001
			y = y - .001
			local stroke =  string.format("           <rdf:li>d %f %f</rdf:li>\r\n", x, y )
			xmpCameraRaw = xmpCameraRaw .. stroke
		end
	end

	xmpCameraRaw = xmpCameraRaw .. xmpCameraRawEnd

	return xmpCameraRaw

end

WFCatAssetTestUtils.getCameraRaw = getCameraRaw

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

-- Sha256 for result of getCameraRaw( true )
local xmpDevelopSha256 = '0054ef228b1eaf3322c8caa31df6123e5851bfb3c851f81e0a09c270d51877dc'

-- Sha256 for result of getCameraRaw( false )
local smallXmpDevelopSha256 = '766b904b2667e91a39c92d11b0a100ef6ab0357a34f93307012f960ab6a0582d'

local function getXmpDevelopSettings( smallXmp )
	return {
		device = WFDeviceInfo.deviceDescription(),
		processingModel = "lightroom",
		userOrientation = 1,
		userUpdated = KSDate.timeInIso8601Format(),
		xmpCameraRaw = { sha256 = ( smallXmp and smallXmpDevelopSha256 ) or xmpDevelopSha256 }
	}
end

WFCatAssetTestUtils.xmpDevelopSha256 = xmpDevelopSha256
WFCatAssetTestUtils.getXmpDevelopSettings = getXmpDevelopSettings

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

function WFCatAssetTestUtils.addXmpDevelopToDocument( rxTest, catalog, assetId, xmpDevelopFilePath, userUpdated, smallXmp )

	local xmpDevelopString = getCameraRaw( not smallXmp )
	local xmpDevelopData = KSData.make( xmpDevelopString )
	local xmpDevelopFileSize = #xmpDevelopString
	KSData.saveToFile( xmpDevelopData, xmpDevelopFilePath )

	rxTest:expectEqual( 'file', WFFileUtils.exists( xmpDevelopFilePath ) )

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

	local newXmpDevelopFilePath = xmpDevelopFilePath
	if WFPlatformInfo.isAndroid then
		-- Android doesn't store full file path here, so remove the beginning if we have a full file path
		local basePath = KSPathUtils.getStandardFilePath( 'carouselDocuments' )
		newXmpDevelopFilePath = string.replace( xmpDevelopFilePath, basePath .. '/', '' )
	end

	local developSettings = getXmpDevelopSettings( smallXmp )

	local contentIdOrSha256 = WFPlatformInfo.isiOS and 'contentId' or 'sha256'
	local sha256 = ( smallXmp and smallXmpDevelopSha256 ) or xmpDevelopSha256

	local revisionId = rxTest:getFirstValue( catalog:docStore():applyOverlay {
			_id = assetId,
			userUpdated = userUpdated,
			develop = developSettings,
			_localOnly = {
				files = {
					xmp_develop = {
						path = newXmpDevelopFilePath,
						fileSize = xmpDevelopFileSize,
						[ contentIdOrSha256 ] = sha256,
					},
				},
			}
		} )

	rxTest:expectNotEqual( nil, revisionId )

	return revisionId, sha256, developSettings.userUpdated, newXmpDevelopFilePath, xmpDevelopFileSize

end

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

function WFCatAssetTestUtils.addBasicOzMetadataUpdate( baseAssetDoc, catalogId, assetId, revisionId )

	local hrefPrefaceForCreate = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/{revision_id}/'

	-- add the oz metadata overlay so we know where to upload it to
	local overlay = {
		_serverProvidedMetadata = {
			links = {
				[ '/rels/master_create' ] = {
					href = hrefPrefaceForCreate .. 'master',
					templated = true,
				},
				[ '/rels/proxy_create' ] = {
					href = hrefPrefaceForCreate .. 'proxies/{proxy_id}',
					templated = true,
				},
				[ '/rels/xmp_develop_create' ] = {
					href = hrefPrefaceForCreate .. 'xmp/develop',
					templated = true,
				},
				[ '/rels/xmp_create' ] = {
					href = hrefPrefaceForCreate .. 'xmp',
					templated = true,
				},
			},
			revision_ids = {
				revisionId,
			},
		}
	}

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, overlay )

	return baseAssetDoc
end

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

function WFCatAssetTestUtils.addBasicOzMetadataUpdateForDirectUpload( baseAssetDoc, catalogId, assetId, revisionId )

	local hrefPrefaceForDirectCreate = string.format( '/v2/catalogs/%s/assets/%s', catalogId, assetId )
	local hrefPrefaceForAuxCopy = string.format( '%s/revisions/%s', hrefPrefaceForDirectCreate, revisionId )
	local hrefPrefaceForCreate = string.format( '%s/revisions/{revision_id}', hrefPrefaceForDirectCreate )

	-- add the oz metadata overlay so we know where to upload it to
	local overlay = {
		_serverProvidedMetadata = {
			links = {
				[ 'self' ] = {
					href = hrefPrefaceForDirectCreate
				},
				[ '/rels/master_create' ] = {
					href = hrefPrefaceForCreate .. '/master',
					templated = true,
				},
				[ '/rels/proxy_create' ] = {
					href = hrefPrefaceForCreate .. '/proxies/{proxy_id}',
					templated = true,
				},
				[ '/rels/xmp_develop_create' ] = {
					href = hrefPrefaceForCreate .. '/xmp/develop',
					templated = true,
				},
				[ '/rels/xmp_develop_create/parts' ] = {
					href = hrefPrefaceForDirectCreate .. '/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 = hrefPrefaceForDirectCreate .. '/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,
				},
				[ '/rels/xmp_create' ] = {
					href = hrefPrefaceForCreate .. '/xmp',
					templated = true,
				},
				[ '/rels/main_create/parts' ] = {
					href = hrefPrefaceForCreate .. '/main/parts?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}',
					templated = true,
				},
				[ '/rels/aux_create/parts' ] = {
					href = hrefPrefaceForDirectCreate .. '/sha256/{sha256}/aux/{type}/parts?access={access}&file_size={file_size}&part_size={part_size}&upload_status={upload_status}&content_type={content_type}',
					templated = true,
				},
				[ '/rels/aux_copy_to' ] = {
					href = hrefPrefaceForAuxCopy .. '/aux',
					templated = true,
				}
			},
			revision_ids = {
				revisionId,
			},
		}
	}

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, overlay )

	return baseAssetDoc

end

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

function WFCatAssetTestUtils.addBasicOzMetadataUpdateForIACAsset( baseAssetDoc, spaceId, assetId, revisionId )

	local hrefPrefaceForCreate = '/v2/spaces/' .. spaceId .. '/assets/' .. assetId .. '/revisions/{revision_id}/'

	-- add the oz metadata overlay so we know where to upload it to
	local overlay = {
		_serverProvidedMetadata = {
			links = {
				[ '/rels/master_create' ] = {
					href = hrefPrefaceForCreate .. 'master',
					templated = true,
				},
				[ '/rels/proxy_create' ] = {
					href = hrefPrefaceForCreate .. 'proxies/{proxy_id}',
					templated = true,
				},
				[ '/rels/xmp_develop_create' ] = {
					href = hrefPrefaceForCreate .. 'xmp/develop',
					templated = true,
				},
				[ '/rels/xmp_create' ] = {
					href = hrefPrefaceForCreate .. 'xmp',
					templated = true,
				},

			},
			revision_ids = {
				revisionId,
			},
		}
	}

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, overlay )

	return baseAssetDoc

end

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

function WFCatAssetTestUtils.addBasicOzMetadataUpdateForDirectUploadForIACAsset( baseAssetDoc, spaceId, assetId, revisionId )

	local hrefPrefaceForDirectCreate = string.format( '/v2/spaces/%s/assets/%s', spaceId, assetId )
	local hrefPrefaceForAuxCopy = string.format( '%s/revisions/%s', hrefPrefaceForDirectCreate, revisionId )
	local hrefPrefaceForCreate = string.format( '%s/revisions/{revision_id}', hrefPrefaceForDirectCreate )

	-- add the oz metadata overlay so we know where to upload it to
	local overlay = {
		_serverProvidedMetadata = {
			links = {
				[ 'self' ] = {
					href = hrefPrefaceForDirectCreate
				},
				[ '/rels/master_create' ] = {
					href = hrefPrefaceForCreate .. '/master',
					templated = true,
				},
				[ '/rels/proxy_create' ] = {
					href = hrefPrefaceForCreate .. '/proxies/{proxy_id}',
					templated = true,
				},
				[ '/rels/xmp_develop_create' ] = {
					href = hrefPrefaceForCreate .. '/xmp/develop',
					templated = true,
				},
				[ '/rels/xmp_develop_create/parts' ] = {
					href = hrefPrefaceForDirectCreate .. '/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 = hrefPrefaceForDirectCreate .. '/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,
				},
				[ '/rels/xmp_create' ] = {
					href = hrefPrefaceForCreate .. '/xmp',
					templated = true,
				},
				[ '/rels/aux_create/parts' ] = {
					href = hrefPrefaceForDirectCreate .. '/sha256/{sha256}/aux/{type}/parts?access={access}&file_size={file_size}&part_size={part_size}&upload_status={upload_status}&content_type={content_type}',
					templated = true,
				},
				[ '/rels/aux_copy_to' ] = {
					href = hrefPrefaceForAuxCopy .. '/aux',
					templated = true,
				}
			},
			revision_ids = {
				revisionId,
			},
		}
	}

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, overlay )

	return baseAssetDoc

end

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

function WFCatAssetTestUtils.addBasicOzMetadataUpdateForProxyOnly( baseAssetDoc, catalogId, assetId, revisionId )

	local hrefPrefaceForCreate = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/{revision_id}/'

	-- add the oz metadata overlay so we know where to upload it to
	local overlay = {
		_serverProvidedMetadata = {
			links = {
				[ '/rels/proxy_create' ] = {
					href = hrefPrefaceForCreate .. 'proxies/{proxy_id}',
					templated = true,
				}
			},
			revision_ids = {
				revisionId,
			},
		}
	}

	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, overlay )

	return baseAssetDoc

end

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

--[[
	\brief Inserts a well formed auxiliary reference into an asset document, including local file references

	\param auxParams (table) A table of aux data. Below are examples of the table
		mask example
		{
			digest = digest,
			filePath = relativePath,
			fileSize = fileSize,
			type = "mask",
			sha256 = sha256
		}
		
		filter example
		{
			digest = digest,
			groupDigest = groupDigest,
			sourceLongEdge = sourceLongEdge,
			crsSizeX = crsSizeX,
			crsSizeY = crsSizeY,
			filePath = relativePath,
			fileSize = fileSize,
			type = "filter",
			sha256 = sha256
		}
	\return updated asset document
]]
local function addAux(baseAssetDoc, assetId, auxParams)

	local auxId = auxParams.id or KSIdGenerator.uuidInLowercaseHex()

	local auxDigest = auxParams.digest or KSIdGenerator.uuidInLowercaseHex()
	local isMask = auxParams.type == "mask"
	local isFilter = auxParams.type == "filter"

	local auxOverlay = {
		_localOnly = {
			files = {
				aux = {
					[auxDigest] = {
						relativePath = auxParams.filePath,
						fileSize = auxParams.fileSize
					}
				},
			},
		},

		develop = {
			processingModel = "lightroom",
			masks = isMask and {
				[auxId] = {
					digest = auxParams.digest
				}
			} or nil,
			filters = isFilter and {
				[auxParams.groupDigest] = {
					[auxId] = {
						digest = auxParams.digest,
						sourceLongEdge = auxParams.sourceLongEdge,
						targetLongEdge = auxParams.targetLongEdge,
						crsSizeX = auxParams.crsSizeX,
						crsSizeY = auxParams.crsSizeY
					}
				}
			} or nil,
		},
		aux = {
			[auxId] = {
				type = auxParams.type,
				fileSize = auxParams.fileSize,
				digest = auxParams.digest,
				sha256 = auxParams.sha256,
			},
		}
	}


	return WFTableMerge.applyOverlay( baseAssetDoc, auxOverlay )

end

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

--[[
	\brief Add any number of well formed auxiliary references to an asset document

	\param baseAssetdoc - asset Document
	\param auxParamslist - table of the form:
	{
		{
			id = <auxiliary ID>,
			sha256 = <sha256 of aux binary>,
			filePath = <local path to aux binary>,
			fileSize = <size of aux binary in bytes>,
			digest = <MD5 UPPERCASE of aux binary>,
			groupDigest = <group digest of a filter>,
			sourceLongEdge / targetLongEdge = <the longEdge of a filter instance>,
			crsSizeX = <width of a filter aux binary>,
			crsSizeY = <height of a filter aux binary>
			type = <string type of aux> (mask | filter)
		}

	}

	\return Updates asset document overlay
]]

function WFCatAssetTestUtils.addAuxiliaryMetadataReference( baseAssetDoc, assetId, auxParamsList )

	local assetDoc = baseAssetDoc

	for _, auxParams in pairs( auxParamsList ) do

		assetDoc = addAux( assetDoc, assetId, auxParams )

	end

	return assetDoc

end

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

local function contentTypeFromFileName( fileName )

	if fileName then
		local extension = fileName:match( "%.([^%.]+)$" )

		if extension then
			extension = extension:lower()
			return WFOzContentTypes.contentTypeForExtention( extension ) or WFOzContentTypes.kRawContentType
		end
	end

	return "image/jpeg"

end

WFCatAssetTestUtils.contentTypeFromFileName = contentTypeFromFileName

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

local function getTestDataOriginalFilePath( rxTest, tempTestFolder, fileName )

	local originalFilePath = child( tempTestFolder, fileName )

	if not WFFileUtils.exists( originalFilePath ) then

		local testDataPath = child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
		local testDataFolder1 = child( testDataPath, "folder-1" )
		local testFilePath = child( testDataFolder1, fileName )

		WFFileUtils.copy( testFilePath, originalFilePath )

	end

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

	return originalFilePath

end

WFCatAssetTestUtils.getTestDataOriginalFilePath = getTestDataOriginalFilePath

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

local function pollBinaryUploadCount( uploader, bucket )
	return Observable.timer( .1, .1, WFRxScheduler.sharedScheduler() )
		:map( function()
			return uploader:DEBUG_uploadCount( bucket )
		end )
end

WFCatAssetTestUtils.pollBinaryUploadCount = pollBinaryUploadCount

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

function WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath, importedBy, linkedLocation )

	local originalFileSize
	local fileName
	local contentType = 'image/jpeg'
	local subtype = 'image'

	if originalFilePath then

		originalFileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
		fileName = KSPathUtils.leafName( originalFilePath )
		contentType = contentTypeFromFileName( fileName )

		subtype = string.find( contentType, "video") and "video" or "image"

	end

	local originalHeight = 3840
	local originalWidth = 5760

	if fileName == "IMG_7763.JPG" then

		originalHeight = 2448
		originalWidth = 3264

	elseif fileName == 'IMG_7772.mp4' then

		originalHeight = 270
		originalWidth = 480

	elseif fileName == 'IMG_9876.CR2' then

		originalHeight = 3456
		originalWidth = 5184

	elseif fileName == 'IMG_6025.MOV' then

		originalHeight = 1920
		originalWidth = 1080

	end

	local linkedLocationDetails
	if linkedLocation then

		linkedLocationDetails = {
			id = linkedLocation.id,
			source = linkedLocation.source
		}

	end

	return {
		_id = assetId or KSIdGenerator.uuidInLowercaseHex(),
		_newRevId = revisionId or KSIdGenerator.uuidInLowercaseHex(),
		type = 'asset',
		subtype = subtype,
		captureDate = "2015-06-08T08:19:59Z",
		userCreated = "2018-04-15T18:18:18Z",
		userUpdated = "2018-04-15T18:18:18Z",

		importSource = {
			originalHeight = originalHeight,
			originalWidth = originalWidth,
			importedOnDevice = "Macbook Pro",
			importTimestamp = "2017-04-15T11:03:40Z",
			contentType = contentType,
			sha256 = sha256,
			fileSize = originalFileSize,
			importedBy = importedBy or "129d51066a7a3a5711fa62810b758fc8",
			fileName = fileName,
			linkedLocation = linkedLocationDetails
		},
	}

end

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

--[[!
	\brief get a table that can be applied to an asset as an overlay to add
	external xmp develop settings, including the develop info and the _localOnly
	info with the file paths, etc.

	\params assetId (string, uuid)
	\params xmpDevelopSha256 (string)
	\params xmpDevelopPath (string)
	\params xmpDevelopFileSize (number)

	\return table

--]]
local function createXmpDevelopSettingsOverlay( assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize  )

	local newXmpDevelopFilePath = xmpDevelopFilePath
	if WFPlatformInfo.isAndroid then
		-- Android doesn't store full file path here, so remove the beginning if we have a full file path
		local basePath = KSPathUtils.getStandardFilePath( 'carouselDocuments' )
		newXmpDevelopFilePath = string.replace( xmpDevelopFilePath, basePath .. '/', '' )
	end

	local contentIdOrSha256 = WFPlatformInfo.isiOS and 'contentId' or 'sha256'

	return {
		_id = assetId,
		develop = {
			processingModel = 'lightroom',
			xmpCameraRaw = {
				sha256 = xmpDevelopSha256,
			},
		},
		_localOnly = {
			files = {
					xmp_develop = {
						path = newXmpDevelopFilePath,
						fileSize = xmpDevelopFileSize,
						[contentIdOrSha256] = xmpDevelopSha256,
					},
			},
		},
	}

end

WFCatAssetTestUtils.createXmpDevelopSettingsOverlay = createXmpDevelopSettingsOverlay

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

--[[!
	\brief Apply external xmp develop settings to asset using overlay
		generaged by createXmpDevelopSettingsOverlay().

	\params catalog (WFCatalog)
	\params assetId (string, uuid)
	\params xmpDevelopSha256 (string)
	\params xmpDevelopPath (string)
	\params xmpDevelopFileSize (number)

	\return Observable

--]]
function WFCatAssetTestUtils.addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize )

	return catalog:docStore():applyOverlay( createXmpDevelopSettingsOverlay( assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )

end

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

--[[!
	\brief Wrapper for createBaseAssetDocument() that optionally allows other
		WFCatAssetTestUtils functions or overlays to be applied to the document

	\params catalogId (string, uuid)
	\params assetId (string, uuid)
	\params revisionId (string, uuid)
	\params sha256 (string): sha256 for the original binary.
	\params originalFilePath (optional string): local file path to original binary
	\params options (optional table): Optional document modifiers:

		proxyFilePath (string): local file path to proxy binary

		overlay (table): overlay that will be applied to document if provided

		auxParamsList (table): If provided will be applied to document by calling
			WFCatAssetTestUtils.addAuxiliaryMetadataReference()

		xmpDevelop (table): if provided will be applied to document by calling
			createXmpDevelopSettingsOverlay(). Note expected to contain keys
			sha256, filePath and fileSize.

		spaceId (string, uuid): If provided than add direct upload links for space
			context instead of catalog context and apply as _spaceId to the asset

		editable (boolean): If true, mark the asset as editable. Useful for space assets

	\return baseAssetDoc (table)

--]]
local function createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath, options )

	options = options or {}

	local baseAssetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath )

	if options.spaceId then

		WFCatAssetTestUtils.addBasicOzMetadataUpdateForDirectUploadForIACAsset( baseAssetDoc, options.spaceId, assetId, revisionId )

		baseAssetDoc._spaceId = options.spaceId

		if options.editable then
			baseAssetDoc._serverProvidedMetadata.editable = options.editable
		end

	else

		WFCatAssetTestUtils.addBasicOzMetadataUpdateForDirectUpload( baseAssetDoc, catalogId, assetId, revisionId )

	end

	if originalFilePath then
		WFCatAssetTestUtils.addOriginalPathToDocument( baseAssetDoc, sha256, originalFilePath )
	end

	if options.proxyFilePath then
		WFCatAssetTestUtils.addProxyPathToDocument( baseAssetDoc, options.proxyFilePath )
	end

	if options.overlay then
		baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, options.overlay )
	end

	if options.auxParamsList then
		baseAssetDoc = WFCatAssetTestUtils.addAuxiliaryMetadataReference( baseAssetDoc, assetId, options.auxParamsList )
	end

	if options.xmpDevelop then
		baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, createXmpDevelopSettingsOverlay( assetId, options.xmpDevelop.sha256, options.xmpDevelop.filePath, options.xmpDevelop.fileSize ) )
	end

	return baseAssetDoc

end

WFCatAssetTestUtils.createDocumentWithOzMetadataUpdate = createDocumentWithOzMetadataUpdate

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

--[[!
	\brief Create asset document in doc store generated by calling
		createDocumentWithOzMetadataUpdate
	\params catalogId (string, uuid)
	\params assetId (string, uuid)
	\params revisionId (string, uuid)
	\params sha256 (string): sha256 for the original binary.
	\params originalFilePath (optional string): local file path to original binary
	\params options (optional table): Optional document modifiers:

		proxyFilePath (string): local file path to proxy binary

		overlay (table): overlay that will be applied to document if provided

		auxParamsList (table): If provided will be applied to document by calling
			WFCatAssetTestUtils.addAuxiliaryMetadataReference()

		xmpDevelop (table): if provided will be applied to document by calling
			createXmpDevelopSettingsOverlay(). Note expected to contain keys
			sha256, filePath and fileSize.

		spaceId (string, uuid): If provided than add direct upload links for space
			context instead of catalog context and apply as _spaceId to the asset

	\return baseAssetDoc

--]]
function WFCatAssetTestUtils.addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, options )

	local baseAssetDoc = createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath, options )

	return catalog:docStore():setDocValue( baseAssetDoc )

end

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

return WFCatAssetTestUtils
