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

WFCatBinaryUploaderTest.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

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

	-- WF.core.lua
local KSData = require 'KSData'

	-- WF.core.os
local KSPathUtils = require 'KSPathUtils'
local KSTableUtils = require 'KSTableUtils'
local WFFileUtils = require 'WFFileUtils'
local WFPlatformInfo = require 'WFPlatformInfo'

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

	-- WF.core.self_test
local WFRxTestCase = require 'WFRxTestCase'
local WFTestFileUtils = require 'WFTestFileUtils'
local WFCatAssetTestUtils = require 'WFCatAssetTestUtils'

	-- WF.core.utils
local KSDate = require 'KSDate'
local KSIdGenerator = require 'KSIdGenerator'
local WFArguments = require 'WFArguments'
local WFCapabilities = require "WFCapabilities"
local WFCatAssetUtils = require 'WFCatAssetUtils'
local WFCrypto = require 'WFCrypto'
local WFRingBuffer = require 'WFRingBuffer'
local WFTableMerge = require 'WFTableMerge'

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

	-- WF.oz_client.managed_catalog
local WFCatBinaryUploader = require 'WFCatBinaryUploader'
local WFCatCopies = require 'WFCatCopies'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatPhotoImportance = require 'WFCatPhotoImportance'
local WFCatSpecMock = require 'WFCatSpecMock'
local WFCatTestSession = require 'WFCatTestSession'
local WFCatWatermark = require 'WFCatWatermark'
local WFOzDocUtils = require 'WFOzDocUtils'

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

local MB = 1024 * 1024

local kChunkSize = 5 * MB

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

local check = assert( WFArguments.check )
local derefOpt = assert( KSTableUtils.derefOpt )

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

local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ),
										"import-samples" )

local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
local testAssetsFolder = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "assets" )

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

local xmpSidecarData = [[<?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21 (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:Rating="1"
   xmp:CreatorTool="Adobe Nimbus Test"
   exif:GPSAltitude="10000/2"
   exif:GPSAltitudeRef="1"
   exif:GPSLongitude="100,5.125S"
   exif:GPSLatitude="20,1.5"
   crs:RawFileName="IMG_9876.cr2">
   <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>
  </rdf:Description>
 </rdf:RDF>
</x:xmpmeta>
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                           
<?xpacket end="w"?>
]]

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

local WFCatBinaryUploaderTest = {}

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

local sCoveragePath

function WFCatBinaryUploaderTest.setUp( inCoveragePath )
	sCoveragePath = inCoveragePath
end

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

local function removeEmptyErrorHeader( errorMessage )

	return string.gsub( errorMessage, '<h4>(.[^<]+)</h4><ul></ul>', '' )

end

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

local function removeGetCleanOzAccountError( errorMessage )

	-- Remove intentionally generated error related to getCleanOzAccount
	return string.gsub( errorMessage, '<li>(.[^<]+)getCleanOzAccount(.[^<]+)</li>', '' )

end

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

local function remove60SecondsWarning( errorMessage )

	return string.gsub( errorMessage, "<li>(.[^<]+)Doc store hasn't updated with the presence of uploaded(.[^<]+)</li>", '' )

end

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

local function remove10SecondsWarning( errorMessage )

	local first, _ = string.find(errorMessage, "YOU SHOULD NOT USE WFRxLowFidelityTimer WITH DELAYS LESS THAN 10 SECONDS")

	if first then
		errorMessage = ''

	end 
	return errorMessage
end

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


local function getDiagnosticLogErrorsIgnoringAccountError( uploader, remove10SecError )

	-- Get errors encountered from diagnostic log, ignore account error.
	local diagnosticLogErrors = removeGetCleanOzAccountError( uploader:DEBUG_getDiagnosticLogErrors() )
	diagnosticLogErrors = remove60SecondsWarning( diagnosticLogErrors )
	if remove10SecError then
		diagnosticLogErrors = remove10SecondsWarning( diagnosticLogErrors )
	end 
	return diagnosticLogErrors
end

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

local function removeMarkingOriginalInvalidError( errorMessage )

	return string.gsub( errorMessage, '<li>(.[^<]+)Marking original as invalid(.[^<]+)</li>', '' )

end

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

local function removeOriginalMissingError( errorMessage )
	
	return string.gsub( errorMessage, '<li>(.*)but original is missing.</li>', '' )
	
end

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

local function removeUhOhWarning( errorMessage )

	return string.gsub( errorMessage, '<li>(.[^<]+)Uh oh. Trying to upload(.[^<]+)</li>', '' )

end

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

local options = {

	continuousTaskDelays = {
		progressiveBackoff = false,
		noWorkDelay = 1,
		maxNoWorkDelay = 1,
		errorDelay = 1,
		maxDelay = 1,
	},
	
	isCatalogPuller = true
	
}

local uploaderTaskDelays = {
		progressiveBackoff = false,
		noWorkDelay = 1,
		maxNoWorkDelay = 1,
		errorDelay = 1,
		maxDelay = 1,
}

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

local function getOriginalAndProxyPathsFor( rxTest, filename )
	-- TODO: Move this to the top and use this all over the place
	if filename == 'IMG_7763.JPG' then
		local testOriginalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
		local testProxyFilePath = KSPathUtils.child( testAssetsFolder, "IMG_7763.DNG" )

		local uploadTestFolder = WFTestFileUtils.createTestFolder( rxTest )
		
		local originalFilePath = KSPathUtils.child( uploadTestFolder, "IMG_7763.JPG" )
		local proxyFilePath = KSPathUtils.child( uploadTestFolder, "IMG_7763.DNG" )
		
		WFFileUtils.copy( testOriginalFilePath, originalFilePath )
		WFFileUtils.copy( testProxyFilePath, proxyFilePath )
		
		return originalFilePath, proxyFilePath
	else
		error( "unrecognized test filename " .. filename )
	end

end

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

local function getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	local copiedRevisionId, copiedOriginalFilePath, copiedProxyFilePath, copiedXmpSidecarFilePath, copiedXmpDevelopFilePath

	-- find the newly copied document and update the expected urlResponses
	rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
		:map( function( docValue )
			copiedRevisionId = docValue._revId
			copiedOriginalFilePath = WFCatAssetUtils.getFullOriginalPathFromDocValue( docValue )
			copiedProxyFilePath = WFCatAssetUtils.getFullProxyPathFromDocValue( docValue )
			copiedXmpSidecarFilePath = WFCatAssetUtils.getXmpSidecarPathFromDocValue( docValue )
			copiedXmpDevelopFilePath = WFCatAssetUtils.getLocalExternalDevelopSettingsPath( docValue )
			return true
		end ),
	true )

	return copiedRevisionId, copiedOriginalFilePath, copiedProxyFilePath, copiedXmpSidecarFilePath, copiedXmpDevelopFilePath

end

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

local function generateSidecarFile( rxTest, importSidecarPath )
	
	local sidecarData = KSData.make( xmpSidecarData )
	KSData.saveToFile( sidecarData, importSidecarPath )

	rxTest:finally( function()
		WFFileUtils.delete( importSidecarPath )
	end )
	
	return KSData.length( sidecarData )

end

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

local function pollBinariesInFlight( uploader )

	return Observable.timer( .1, 1, WFRxScheduler.sharedScheduler() )
	:map( function()
		return uploader._numberOfScheduledBinaryUploads
	end )

end

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

local function makeWFCatSpecSpy( catalogSpec, putBinaryFn, getBinaryFn  )

	putBinaryFn = putBinaryFn or function( self, ... )
		return catalogSpec:putBinary( ... )
	end

	getBinaryFn = getBinaryFn or function( self, ... )
		return catalogSpec:getBinary( ... )
	end
	
	local putFn = function( self, ... )
		return catalogSpec:put( ... )
	end

	local getFn = function( self, ... )
		return catalogSpec:get( ... )
	end

	local WFCatSpecSpy = {
		originalCatalogSpec = catalogSpec,
		getBinary = getBinaryFn,
		putBinary = putBinaryFn,
		get = getFn,
		put = putFn
	}

	return Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

end

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

local function noRetryOnPutError( self, url, data, options, progressReporter )
	options.maxAttempts = 1
	return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
end

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

	
function WFCatBinaryUploaderTest.rxTestUploadRaceConditionWithChangingSha256_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog, catalogSpec, docStore

	rxTest:waitFor(
		catalogTestSession:catalog()
			:tap( function( _catalog )
				check( 'WFCatalog' )
				catalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				docStore = catalog:docStore()
			end ) )

	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )
	
	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )

	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, noRetryOnPutError )

	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )

	-- Import a file.

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFileName = "test.xmp"
	local testFilePath = KSPathUtils.child( importFolder, testFileName )

	local file = io.open( testFilePath, 'wb' )
	file:write( "<xmp/>" )
	file:close()

	local initialSha256 = require 'WFCrypto'.digestFile( testFilePath, 'SHA256' )
	local initialSize = WFFileUtils.fileAttributes( testFilePath ).fileSize
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()

	rxTest:expectSingleValue(
		docStore:setDocValue {
			_id = assetId,
			type = 'asset',
			subtype = 'preset',
			name = 'Test',
			_localOnly = {
				binaryUpload = {
					waitForInvalid = initialSha256
				},
				files = {
					original = {
						master = {
							volumeGuid = WFFileUtils.getVolumeGuidForPath( testFilePath ),
							relativePath = WFFileUtils.removeVolumeFromPath( testFilePath ),
							sha256 = initialSha256,
							size = initialSize,
						}
					}
				}
			},
			importSource = {
				fileSize = initialSize,
				sha256 = initialSha256, 
				contentType = 'application/rdf+xml',
			}
		},
		":ignore:" )

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( docStore:ongoingDocValue( assetId )
			:map( function( docValue )
				return not not docValue._serverProvidedMetadata 
			end ) )
			
	docObserver:expectDataWithinTime( 15, true )
	rxTest:sleep(.25)
	-- change the file
	file = io.open( testFilePath, 'wb' )
	file:write( "<xmp>updated</xmp>" )
	file:close()
	
	
	local updatedSha256 = require 'WFCrypto'.digestFile( testFilePath, 'SHA256' )
	local updatedSize = WFFileUtils.fileAttributes( testFilePath ).fileSize
	
	rxTest:waitFor( 
		docStore:applyOverlay( {
			_id = assetId,
			_localOnly = {
				binaryUpload = {
					waitForInvalid = updatedSha256,
				},
				files = {
					original = {
						master = {
							volumeGuid = WFFileUtils.getVolumeGuidForPath( testFilePath ),
							relativePath = WFFileUtils.removeVolumeFromPath( testFilePath ),
							sha256 = updatedSha256,
							size = updatedSize,
						}
					}
				}
			},
			importSource = {
				fileSize = updatedSize,
				sha256 = updatedSha256,
			}
		} ) )
		
	local finalDocObserver = rxTest:observe( docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			return not not ( ( derefOpt( docValue, 'importSource', 'sha256' ) == updatedSha256 )
				and derefOpt( docValue,  '_serverProvidedMetadata', 'links', '/rels/master' )
				and	( derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'invalid' ) ~= true ) or docValue )
		end ) )
		
	finalDocObserver:expectDataWithinTime( 360, true )
	
end

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

local function addWillNotUploadMasterToDoc( catalog, assetId )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		importSource = {
			willNotUploadMaster = true,
		},
	}

end

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

local function removeOriginalPathFromDoc( catalog, assetId )
	
	if WFPlatformInfo.isDesktop then
		return catalog:docStore():applyOverlay {
			_id = assetId,
			_localOnly = {
				files = {
					original = {
						master = { _delete = true },
					},
				}
			}
		}
	elseif WFPlatformInfo.isiOS then
		return catalog:docStore():applyOverlay {
			_id = assetId,
			_localOnly = {
				files = {
					master = { _delete = true },
					latestMaster = { _delete = true }
				}
			}
		}
	elseif WFPlatformInfo.isAndroid then
		
		return catalog:docStore():applyOverlay {
			_id = assetId,
			_localOnly = {
				latestOriginal = { _delete = true },
				original = { _delete = true },
				originalFilePath = { _delete = true },
			}
		}
	end

end

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

local function removeProxyPathFromDoc( catalog, assetId )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_localOnly = {
			files = {
				proxy = { _delete = true },
			}
		}
	}

end

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

local function addXmpDevelopSettingsToDocument( catalog, 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 catalog:docStore():applyOverlay {
			_id = assetId,
			develop = {
				processingModel = 'lightroom',
				xmpCameraRaw = {
					sha256 = xmpDevelopSha256,
				},
			},
			_localOnly = {
				files = {
						xmp_develop = {
							path = newXmpDevelopFilePath,
							fileSize = xmpDevelopFileSize,
							[contentIdOrSha256] = xmpDevelopSha256,
						},
				},
			},
		}

end

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

local function createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath, xmpSidecarFilePath, allowDirectUpload )

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

	if allowDirectUpload then
		WFCatAssetTestUtils.addBasicOzMetadataUpdateForDirectUpload( baseAssetDoc, catalogId, assetId, revisionId )
	else
		WFCatAssetTestUtils.addBasicOzMetadataUpdate( baseAssetDoc, catalogId, assetId, revisionId )
	end
	
	if originalFilePath then
		WFCatAssetTestUtils.addOriginalPathToDocument( baseAssetDoc, sha256, originalFilePath )
	end
	
	if proxyFilePath then 
		WFCatAssetTestUtils.addProxyPathToDocument( baseAssetDoc, proxyFilePath ) 
	end
	
	if xmpSidecarFilePath then
		WFCatAssetTestUtils.addXmpSidecarPathToDocument( baseAssetDoc, xmpSidecarFilePath )
	end
	
	return baseAssetDoc

end

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

local function addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath, xmpSidecarFilePath, maybeOverlay )

	local baseAssetDoc = createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath, xmpSidecarFilePath )
	
	if maybeOverlay then
	
		baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, maybeOverlay )
	
	end
	
	return catalog:docStore():setDocValue( baseAssetDoc )

end

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

local function addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath, xmpSidecarFilePath, maybeOverlay )
	
	local allowDirectUpload = true
	
	local baseAssetDoc = createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath, xmpSidecarFilePath, allowDirectUpload )
	
	if maybeOverlay then
	
		baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, maybeOverlay )
	
	end
	
	return catalog:docStore():setDocValue( baseAssetDoc )

end

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

local function createIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, 
												proxyFilePath, sourceAssetId, sourceRevisionId, optRedactionsTable )
												
	local baseAssetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalog:ozCatalogId(), assetId, revisionId, sha256, originalFilePath )
	
	baseAssetDoc._spaceId = spaceId
	WFCatAssetTestUtils.addBasicOzMetadataUpdateForIACAsset( baseAssetDoc, spaceId, assetId, revisionId )
	
	if originalFilePath then
		WFCatAssetTestUtils.addOriginalPathToDocument( baseAssetDoc, sha256, originalFilePath )
	end
	
	if proxyFilePath then
		WFCatAssetTestUtils.addProxyPathToDocument( baseAssetDoc, proxyFilePath )
	end
	
	local maybeSourceOriginalLink, maybeSourceXmpLink, maybeSourceXmpDevelopLink
	if sourceAssetId and sourceRevisionId then
		maybeSourceOriginalLink = '/v2/catalogs/' .. catalog:ozCatalogId() .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/master'
		maybeSourceXmpLink = '/v2/catalogs/' .. catalog:ozCatalogId() .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/xmp'
		maybeSourceXmpDevelopLink = '/v2/catalogs/' .. catalog:ozCatalogId() .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/xmp/develop'
	end
	
	local universal
	if optRedactionsTable then
		universal = {
			sharedwithyou = {
				contribution = optRedactionsTable
			}
		}
	end
	
	baseAssetDoc = WFTableMerge.applyOverlay( baseAssetDoc, {
		_localOnly = {
			sourceOriginalLink = maybeSourceOriginalLink,
			sourceXmpLink = maybeSourceXmpLink,
			sourceXmpDevelopLink = maybeSourceXmpDevelopLink,
			universal = universal,
		}
	} )
	
	return baseAssetDoc

end

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

local function addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, 
												proxyFilePath, sourceAssetId, sourceRevisionId, optRedactionsTable )
	
	local iacAssetDoc = createIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath,
												proxyFilePath, sourceAssetId, sourceRevisionId, optRedactionsTable )

	
	return catalog:docStore():setDocValue( iacAssetDoc )

end

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

local function addIACWillNotUploadMasterDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, proxyFilePath, sourceAssetId, sourceRevisionId, sourceProxyId, optRedactionsTable )
	
	local iacAssetDoc = createIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, nil,
												proxyFilePath, sourceAssetId, sourceRevisionId, optRedactionsTable )

	KSTableUtils.setNested( iacAssetDoc, true, 'importSource', 'willNotUploadMaster' )
	KSTableUtils.setNested( iacAssetDoc, nil, '_localOnly', 'sourceOriginalLink' )
	
	local maybeSourceProxyLink
	if sourceAssetId and sourceRevisionId and sourceProxyId then
		maybeSourceProxyLink = '/v2/catalogs/' .. catalog:ozCatalogId() .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/proxies/' .. sourceProxyId
	end
	KSTableUtils.setNested( iacAssetDoc, maybeSourceProxyLink, '_localOnly', 'sourceProxyLink' )
	
	return catalog:docStore():setDocValue( iacAssetDoc )
end

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

local function addCreateLinksToDoc( catalog, catalogId, assetId, revisionId )

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

	return catalog:docStore():applyOverlay {
		_id = assetId,
		-- add the oz metadata overlay so we know where to upload it to
		_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,
			},
		},
	}

end

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

local function addCreateLinksToVersionDoc( catalog, catalogId, versionId, versionRevisionId )
	
	local hrefPrefaceForCreate = '/v2/catalogs/' .. catalogId .. '/versions/' .. versionId .. '/revisions/{revision_id}/'

	return catalog:docStore():applyOverlay {
		_id = versionId,
		-- add the oz metadata overlay so we know where to upload it to
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp_develop_create' ] = {
					href = hrefPrefaceForCreate .. 'xmp/develop',
					templated = true,
				},
			},
			revision_ids = {
				versionRevisionId,
			},
		},
	}

end

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

local function addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/master' ] = {
					href = "/v2/catalogs/" .. catalogId .. "/assets/".. assetId .. "/revisions/" .. revisionId .. "/master",
				},
				[ '/rels/xmp' ] = {
					href = "/v2/catalogs/" .. catalogId .. "/assets/".. assetId .. "/revisions/" .. revisionId .. "/xmp",
				},
			},
		},
	}

end

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

local function addXmpLinkToDoc( catalog, catalogId, assetId, revisionId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp' ] = {
					href = "/v2/catalogs/" .. catalogId .. "/assets/".. assetId .. "/revisions/" .. revisionId .. "/xmp",
				},
			},
		},
	}

end

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

local function addProxyLinkToDoc( catalog, catalogId, assetId, revisionId, maybeProxyId )

	local proxyId = maybeProxyId or KSIdGenerator.uuidInLowercaseHex()

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/proxy_type/2560' ] = {
					href = "/v2/catalogs/" .. catalogId .. "/assets/".. assetId .. "/revisions/" .. revisionId .. "/proxies/" .. proxyId,
				},
			},
		},
	}

end

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

local function addMasterExistsToDoc( catalog, catalogId, assetId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/master' ] = {
					exists = true,
				},
			},
		},
	}

end

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

local function addProxyExistsToDoc( catalog, catalogId, assetId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/proxy_type/2560' ] = {
					exists = true,
				},
			},
		},
	}

end

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

local function addXmpExistsToDoc( catalog, catalogId, assetId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp' ] = {
					exists = true,
				},
			},
		},
	}

end


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

local function addXmpDevelopExistsToDoc( catalog, catalogId, assetId )

	-- Uploading and copying a master will also set the /xmp field so set that as well

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp/develop' ] = {
					exists = true,
				},
			},
		},
	}

end


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

local function addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId )

	return catalog:docStore():applyOverlay {
		_id = assetId,
		_serverProvidedMetadata = {
			links = {
				[ '/rels/xmp/develop' ] = {
					href = "/v2/catalogs/" .. catalogId .. "/assets/".. assetId .. "/revisions/" .. revisionId .. "/xmp/develop",
				},
			},
		},
	}

end

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

local function addOriginalToDocPath( catalog, assetId, sha256, originalFilePath )
	
	local overlay = WFCatAssetTestUtils.getOriginalPathOverlay( assetId, sha256, originalFilePath )
	
	return catalog:docStore():applyOverlay( overlay )
	
end

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

local function lowercaseDataForWindows( receivedData, expectedData )

	if WFPlatformInfo.isWindows then
		-- on windows the build machine is putting is using a lower case c for
		-- the drive, local machines may have upper case and run fine without this.
		receivedData = string.lower( receivedData )
		expectedData = string.lower( expectedData )
	end

	return receivedData, expectedData

end

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

local function generateCreateMasterResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, filePath, errorCode, optAdditionalHeaders, optBypassRateLimiter )

	local url = '/v2/'.. catalogsOrSpaces.. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/master'

	errorCode = errorCode or '201'
	local requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'Content-Type',
				value = 'image/jpeg',
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'X-Generate-Proxies',
				value = 'all',
			},
		}
		
	for _, requestHeader in ipairs( optAdditionalHeaders or {} ) do
		table.insert( requestHeaders, requestHeader )
	end
			
	return {
		url = url,
		data = filePath,
		dataTransformation = lowercaseDataForWindows,
		options = {
			dataFormat = "filePath",
			requestCost = 2,
			notifyWhenHttpRequestIsInProgress = true,
			requestPriority = 3,
			requestHeaders = requestHeaders,
			bypassRateLimiter = optBypassRateLimiter
		},

		headers = {
			status_code = errorCode,
		},
	}

end

local function generateCreateMasterResponseWithXReadMeta( catalogId, assetId, revisionId, filePath, errorCode )
	local xReadMetaHeader = {
		{
			header = 'X-Read-Meta',
			value = 'xmp,location',
		},
	}
	return generateCreateMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, filePath, errorCode, xReadMetaHeader )
end

local function generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, filePath, errorCode, optRedactionsHeaderValue, optBypassRateLimiter )
	
	local optionalHeaders = {
		{
			header = 'X-Read-Meta',
			value = 'xmp,location',
		},
	}
	
	if optRedactionsHeaderValue then
		table.insert( optionalHeaders, {
			header = 'X-Redact-Meta',
			value = optRedactionsHeaderValue
		} )
	end
	
	return generateCreateMasterResponseImp( 'spaces', spaceId, assetId, revisionId, filePath, errorCode, optionalHeaders, optBypassRateLimiter )

end

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

local function generateCopyMasterResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, optAdditionalHeaders, isVideo )

	local url = '/v2/' .. catalogsOrSpaces .. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/master'

	errorCode = errorCode or '202'
	
	local requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'Content-Type',
				value = 'application/json',
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'X-Generate-Proxies',
				value = 'all',
			},
			{
				header = 'X-Async-Copy',
				value = "true",
			},
		}
		
	if isVideo then
		requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'Content-Type',
				value = 'application/json',
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'X-Async-Copy',
				value = "true",
			},
		}
	end

	for _, requestHeader in ipairs( optAdditionalHeaders or {} ) do
		table.insert( requestHeaders, requestHeader )
	end

	local response = {
		[ 'Content-Length' ] = 0,
		Location = "",
	}

	local fail
	if errorCode == '404' then
		fail = {
			{
				code = 1000,
				description = "Resource not found",
				errors = {
					revision = {
						"does not exist",
					},
				},
				subtype = "ResourceNotFoundError",
			},
			{
				status_code = errorCode,
				[ 'X-Result-Code' ] = '1000',
			}
		}
	end
	
	return {
		url = url,
		data = '{ "file_href": "/v2/catalogs/' .. catalogId .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/master" }',
		options = {
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 1,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		response = response,
		fail = fail,

		headers = {
			status_code = errorCode,
		},
	}

end

local function generateCopyMasterResponse( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
	local standardXReadMetaHeader = {
		{
				header = 'X-Read-Meta',
				value = 'xmp,location',
		},
	}
	return generateCopyMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, standardXReadMetaHeader )
end

local function generateCopyMasterResponseNoXReadMeta( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
	
	local xReadMetaHeader = {
		{
				header = 'X-Read-Meta',
				value = 'none',
		},
	}
	
	return generateCopyMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, xReadMetaHeader )
end

local function generateCopyMasterResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, optRedactionsHeaderValue )

	local optionalHeaders = {
		{
			header = 'X-Read-Meta',
			value = 'none',
		},
	}
	
	if optRedactionsHeaderValue then
		table.insert( optionalHeaders, {
			header = 'X-Redact-Meta',
			value = optRedactionsHeaderValue
		} )
	end
	
	return generateCopyMasterResponseImp( 'spaces', spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, optionalHeaders )
end

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

local function generateCopyMasterResponseForVideo( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
   local standardXReadMetaHeader = {
	   {
		   header = 'X-Read-Meta',
		   value = 'xmp,location',
		},
	}
   local isVideo = true
   return generateCopyMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, standardXReadMetaHeader, isVideo )
end


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

local function generateCopyProxyResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId, errorCode, optAdditionalHeaders )

	local url = '/v2/' .. catalogsOrSpaces .. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/proxies/' .. proxyId

	errorCode = errorCode or '202'

	local response = {
		[ 'Content-Length' ] = 0,
		Location = "",
	}

	local fail
	if errorCode == '404' then
		fail = {
			{
				code = 1000,
				description = "Resource not found",
				errors = {
					revision = {
						"does not exist",
					},
				},
				subtype = "ResourceNotFoundError",
			},
			{
				status_code = errorCode,
				[ 'X-Result-Code' ] = '1000',
			}
		}
	end
	
	local requestHeaders =  {
				{
					header = 'X-Generate-Renditions',
					value = 'all',
				},
				{
					header = 'Content-Type',
					value = 'application/json',
				},
				{
					header = 'Accept',
					value = 'application/json',
				},
				{
					header = 'X-Async-Copy',
					value = "true",
				},
			}
	
	for _, requestHeader in ipairs( optAdditionalHeaders or {} ) do
		table.insert( requestHeaders, requestHeader )
	end

	return {
		url = url,
		urlFilter = function( str )
			-- filter out the new proxy id that is created but not stored anywhere
			str = str:gsub( '/([^/]*)$', '/' .. proxyId )
			return str
		end,
		data = '{ "file_href": "/v2/catalogs/' .. catalogId .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/proxies/' .. sourceProxyId .. '" }',
		options = {
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 1,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		response = response,
		fail = fail,

		headers = {
			status_code = errorCode,
		},
	}

end

local function generateCopyProxyResponse( catalogId, assetId, revisionId, proxyId, sourceAssetId, sourceRevisionId, sourceProxyId, errorCode )
	
	local xReadMetaHeader = {
		{
				header = 'X-Read-Meta',
				value = 'none',
		},
	}
	
	return generateCopyProxyResponseImp( 'catalogs', catalogId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId, errorCode, xReadMetaHeader )
end

local function generateCopyProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId, errorCode, optRedactionsHeaderValue )
	
	local optionalHeaders = {
		{
			header = 'X-Read-Meta',
			value = 'none',
		},
	}
	
	if optRedactionsHeaderValue then
		table.insert( optionalHeaders, {
			header = 'X-Redact-Meta',
			value = optRedactionsHeaderValue
		} )
	end
	
	return generateCopyProxyResponseImp( 'spaces', spaceId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId, errorCode, optionalHeaders )
end

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

local function generateCreateProxyResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, proxyId, filePath, errorCode, optAdditionalHeaders )

	local url = '/v2/' .. catalogsOrSpaces .. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/proxies/' .. proxyId

	errorCode = errorCode or '201'
	
	local requestHeaders = {
				{
					header = "X-Proxy-Type",
					value = "2560",
				},
				{
					header = "Content-Type",
					value = "image/dng",
				},
				{
					header = "X-Generate-Renditions",
					value = "all",
				},
				{
					header = "X-Read-Meta",
					value = "xmp"
				},
				{
					header = "Accept",
					value = "application/json",
				},
			}
			
	for _, requestHeader in ipairs( optAdditionalHeaders or {} ) do
		table.insert( requestHeaders, requestHeader )
	end
	
	return {
		url = url,
		urlFilter = function( str )
			-- filter out the new proxy id that is created but not stored anywhere
			str = str:gsub( '/([^/]*)$', '/' .. proxyId )
			return str
		end,

		data = filePath,
		dataTransformation = lowercaseDataForWindows,
		options = {
			dataFormat = "filePath",
			requestCost = 2,
			notifyWhenHttpRequestIsInProgress = true,
			requestHeaders = requestHeaders,
			requestPriority = 3,
		},
		response = errorCode == '201' and {
			[ 'Content-Length' ] = 0,
			Location = "",
		} or nil,
		headers = {
			status_code = errorCode,
		},
	}

end

local function generateCreateProxyResponse( catalogId, assetId, revisionId, proxyId, filePath, optHttpResponseCode )
	return generateCreateProxyResponseImp( 'catalogs', catalogId, assetId, revisionId, proxyId, filePath, optHttpResponseCode )
end

local function generateCreateProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, filePath, optHttpResponseCode, optRedactionsHeaderValue )

	local fullRedactionsHeader
	if optRedactionsHeaderValue then
		fullRedactionsHeader = {
			header = 'X-Redact-Meta',
			value = optRedactionsHeaderValue
		}
	end
	
	return generateCreateProxyResponseImp( 'spaces', spaceId, assetId, revisionId, proxyId, filePath, optHttpResponseCode, { fullRedactionsHeader } )
end

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

local function generateUploadXmpDevelopFileResponseImp( catalogsOrSpaces, assetsOrVersions, catalogOrSpaceId, assetId, revisionId, filePath )

	local url = '/v2/' .. catalogsOrSpaces.. '/' .. catalogOrSpaceId .. '/' .. assetsOrVersions .. '/' .. assetId .. '/revisions/' .. revisionId .. '/xmp/develop'

	return {
		url = url,
		data = filePath,
		options = {
			dataFormat = "filePath",
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 2,
			requestHeaders = {
				{
					header = 'X-Generate-Renditions',
					value = 'all',
				},
				{
					header = 'X-Generate-Proxies',
					value = 'all',
				},
				{
					header = 'Content-Type',
					value = 'application/rdf+xml',
				},
				{
					header = 'Accept',
					value = 'application/json',
				},
			},
			requestPriority = 3,
		},
		response = {
			status_code = '201'
		},
	}

end

local function generateUploadXmpDevelopFileResponse( catalogId, assetId, revisionId, filePath )
	return generateUploadXmpDevelopFileResponseImp( 'catalogs', 'assets', catalogId, assetId, revisionId, filePath )
end

local function generateUploadXmpDevelopFileResponseForInAppContribute( spaceId, assetId, revisionId, filePath )
	return generateUploadXmpDevelopFileResponseImp( 'spaces', 'assets', spaceId, assetId, revisionId, filePath )
end

local function generateUploadXmpDevelopFileResponseForVersion( catalogId, versionId, revisionId, filePath )
	return generateUploadXmpDevelopFileResponseImp( 'catalogs', 'versions', catalogId, versionId, revisionId, filePath )
end

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

local function generateCopyXmpDevelopResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, sourceUrl, errorCode )

	local url = '/v2/' .. catalogsOrSpaces .. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp/develop'

	errorCode = errorCode or '202'

	local response = {
		[ 'Content-Length' ] = 0,
		Location = "",
	}

	local fail
	
	if errorCode == '404' then
		fail = {
			{
				code = 1000,
				description = "Resource not found",
				errors = {
					revision = {
						"does not exist",
					},
				},
				subtype = "ResourceNotFoundError",
			},
			{
				status_code = errorCode,
				[ 'X-Result-Code' ] = '1000',
			}
		}
	end
	
	local requestHeaders =  {
				{
					header = "Accept",
					value = "application/json",
				},
				{
					header = "Content-Type",
					value = "application/json",
				},
				{
					header = "X-Generate-Proxies",
					value = "all",
				},
				{
					header = "X-Generate-Renditions",
					value = "all",
				}
			}
	
	return {
		url = url,
		data = '{ "file_href": "' .. sourceUrl .. '" }',
		options = {
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 1,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		response = response,
		fail = fail,

		headers = {
			status_code = errorCode,
		},
	}

end

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

local function generateCopyXmpDevelopResponseWithUrl( catalogId, assetId, revisionId, sourceUrl )
	return generateCopyXmpDevelopResponseImp( 'catalogs', catalogId, assetId, revisionId, sourceUrl )
end

local function generateCopyXmpDevelopResponse( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
	local sourceUrl = '/v2/catalogs/' .. catalogId .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/xmp/develop'
	return generateCopyXmpDevelopResponseImp( 'catalogs', catalogId, assetId, revisionId, sourceUrl, errorCode )
end

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

local function generateCopyXmpSidecarResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, optAdditionalHeaders, errorCode )

	local url = '/v2/' .. catalogsOrSpaces .. '/' .. catalogOrSpaceId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp'

	errorCode = errorCode or '202'

	local response = {
		[ 'Content-Length' ] = 0,
		Location = "",
	}

	local fail
	
	if errorCode == '404' then
		fail = {
			{
				code = 1000,
				description = "Resource not found",
				errors = {
					revision = {
						"does not exist",
					},
				},
				subtype = "ResourceNotFoundError",
			},
			{
				status_code = errorCode,
				[ 'X-Result-Code' ] = '1000',
			}
		}
	end
	
	local requestHeaders =  {
				{
					header = "Accept",
					value = "application/json",
				},
				{
					header = "Content-Type",
					value = "application/json",
				}
			}
	
	for _, requestHeader in ipairs( optAdditionalHeaders or {} ) do
		table.insert( requestHeaders, requestHeader )
	end

	return {
		url = url,
		data = '{ "file_href": "/v2/catalogs/' .. catalogId .. '/assets/' .. sourceAssetId .. '/revisions/' .. sourceRevisionId .. '/xmp" }',
		options = {
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 1,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		response = response,
		fail = fail,

		headers = {
			status_code = errorCode,
		},
	}

end

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

local function generateCopyXmpSidecarResponse( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
	return generateCopyXmpSidecarResponseImp( 'catalogs', catalogId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode )
end

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

local function generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode, optRedactionsHeaderValue )
	
	local fullRedactionsHeader
	
	if optRedactionsHeaderValue then
		fullRedactionsHeader = { {
			header = 'X-Redact-Meta',
			value = optRedactionsHeaderValue
		} }
	end
	
	return generateCopyXmpSidecarResponseImp( 'spaces', spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, fullRedactionsHeader, errorCode )
	
end

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

local function watchForXmpAndMasterUploads( rxTest, catalog, catalogSpec, uploader, catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId )
	
	local httpRequestsSoFar
	rxTest:waitFor( catalogSpec._httpMock:requestsProcessed():map( function( numRequests ) httpRequestsSoFar = numRequests end ):take( 1 ) )
	
	-- update the expected url/response pairs
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId ) )
	catalogSpec:expectHttpRequest( generateCopyMasterResponseNoXReadMeta( catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId ) )
	
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur (the xmp sidecar)
	httpRequestsProcessed:expectDataWithinTime( 2, httpRequestsSoFar + 1 )
	
	-- Make it look like metadatapuller got an update with the xmp link in it to let the
	-- uploader know the xmp has been succesfully copied and the master can be uploaded (with no X-Read-Meta)
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	-- expect the second PUT to occur (the master)
	httpRequestsProcessed:expectDataWithinTime( 2, httpRequestsSoFar + 2 )
	
	-- Make it look like metadatapuller got an update with the master link in it to let the uploader know the
	-- master has been successfully copied. In the xmp sidecar case we end up seting the xmp link again here, 
	-- but that's a no-op and is no big deal
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
end

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

local function watchForXmpAndMasterUploadsForIAC( rxTest, catalog, catalogSpec, uploader, spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, optErrorCode, optRedactionsHeader )
	
	local httpRequestsSoFar
	rxTest:waitFor( catalogSpec._httpMock:requestsProcessed():map( function( numRequests ) httpRequestsSoFar = numRequests end ):take( 1 ) )
	
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, optErrorCode, optRedactionsHeader ) )
	catalogSpec:expectHttpRequest( generateCopyMasterResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, optErrorCode, optRedactionsHeader ) )
	
	uploader:startContinuousUploads()
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, httpRequestsSoFar + 1 )

	-- Make it look like metadatapuller got an update with the /rels/xmp exists: true in it to let the
	-- uploader know the xmp is copied and the master can now be copied/uploaded.
	rxTest:waitFor( addXmpExistsToDoc( catalog, catalogId, assetId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, httpRequestsSoFar + 2 )
	
	-- Make it look like the metadatapuller got an update with the /rels/master exists: true For In App 
	-- Contribute we don't always get a /rels/master href link due to sharing restrictions so IAC 
	-- uploads must look for the exists: true field on /rels
	rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) ) 
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

end

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

local function setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, enableRateLimiter, allowDirectUpload )

	local userId = '87f526b44f9e41eca6cb5a550f84ceb6'

	local indexSpecScript

	if WFPlatformInfo.isMobile() then
		indexSpecScript = 'LrmCatalogIndexDefinitions.lua'
	end

	local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, urlResponses, enableRateLimiter )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec, nil, indexSpecScript )
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
			clearFromRecentlyUploadedSeconds = 1,
			waitForOtherUploadSeconds = .5,
			waitForCopyFailedSeconds = .5,
			useDirectUploadIfPossible = allowDirectUpload and true or nil,
			timer = function( seconds )
				return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
			end,
		} )

	uploader:DEBUG_resetDiagnosticLogErrors()

	rxTest:finally( function()

		uploader:stopContinuousUploads()
		uploader:close() 

		-- Work around an async close issue causing WFCatImportDuplicate not to
		-- get it's closeAction called before the next test is started.
		if sCoveragePath then
			catalog:indexStore():DEBUG_dumpCoverage( sCoveragePath )
		end

		catalog:indexStore():close()

	end )

	return catalog, catalogSpec, uploader

end

--------------------------------------------------------------------------------
local function setupCatalogAndDocStore( rxTest )
	
	local catalogTestSession = WFCatTestSession( rxTest )
	
	local catalog, catalogSpec, catalogId, docStore
	
	rxTest:waitFor(
	
		catalogTestSession:catalog()
			:tap( function( _catalog )
				check( 'WFCatalog' )
				catalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				catalogId = _catalog:ozCatalogSpec():catalogId()
				docStore = catalog:docStore()
			end ) )
	
	return catalogTestSession, catalog, catalogSpec, catalogId, docStore
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadAdd_testNightly( rxTest )
	
	rxTest:setTimeLimit( 360 )
	
	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
		
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		-- WF-1813 need to check presence of the master link & no "invalid=true" in master link & no sha256 error in importsource
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)

end

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

local previousStartTimes = {}
local previousEndTimes = {}
local previousProcessedCounts = {}
	
local function verifyEventAndMapAwayCruft( rxTest, event, assetId, optOffset )

	local previousKey = string.format( "%s:%s", assetId, optOffset )

	if event then
	
		-- Processed count show only ever increase.
	
		if previousProcessedCounts[ previousKey ] then
			rxTest:expectEqual( true, event.processedCount >= previousProcessedCounts[ previousKey ] )
		end
		
		previousProcessedCounts[ previousKey ] = event.processedCount
		
		-- Processed count show never be greater than the total count.
		
		rxTest:expectEqual( true, event.processedCount <= event.totalCount )
		
		-- Start time is immutable
		
		local startTime = derefOpt( event, 'extraInfo', 'startTime' )
		
		if previousStartTimes[ previousKey ] then
			rxTest:expectEqual( previousStartTimes[ previousKey ], startTime )
		end
		
		previousStartTimes[ previousKey ] = startTime
		
		-- End time is immutable.
		
		local endTime = derefOpt( event, 'extraInfo', 'endTime' )
		
		if previousEndTimes[ previousKey ] then
			rxTest:expectEqual( previousEndTimes[ previousKey ], endTime )
		end
		
		previousEndTimes[ previousKey ] = endTime
		
		local timestamp = derefOpt( event, 'extraInfo', 'timestamp' )
		
		local percentComplete = event.percentComplete
		
		if percentComplete > 0 and percentComplete < 1 then
			percentComplete = '<number>'
		end
		
		local processedCount = event.processedCount
		
		if processedCount > 0 and processedCount < event.totalCount then
			processedCount = '<number>'
		end
		
		return {
			errorCount = event.errorCount,
			extraInfo = {
				timestamp = timestamp and '<number>',
				endTime = endTime and '<number>',
				startTime = startTime and '<number>',
			},
			percentComplete = percentComplete,
			processedCount = processedCount,
			totalCount = event.totalCount,
		}
		
	end
	
	return {}

end

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

function WFCatBinaryUploaderTest.rxTestUploadAdd_WithProgress_testNightly( rxTest )
	
	rxTest:setTimeLimit( 60 )

	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
		
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
				forceIncludeProgress = true
			} )
			
	-- Time remaining is hard to verify, so we just verify that the upload reported
	-- taking longer than 0 seconds at some point in time.
	
	local timeRemainingObserver = rxTest:observe( uploader:getEstimatedTimeRemaining()
		:map( function( timeRemaining )
			return timeRemaining > 0
		end ) )
		
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument( assetDoc, sha256, originalFilePath )
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ) )

	local progressEventObserver = rxTest:observe( uploader:DEBUG_getInFlightUploadProgressUpdateStream()
	
		:map( function( events )
		
			local event = derefOpt( events, assetId, 'master' )
		
			return verifyEventAndMapAwayCruft( rxTest, event, assetId )
			
		end ) )
			
	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	progressEventObserver:expectDataWithinTime( 100, {
		errorCount = 0,
		extraInfo = {
			timestamp = '<number>',
			startTime = "<number>",
		},
		percentComplete = 0,
		processedCount = 0,
		totalCount = 2072173
	} )

	progressEventObserver:expectDataWithinTime( 50, {
		errorCount = 0,
		extraInfo = {
			timestamp = '<number>',
			startTime = '<number>',
		},
		percentComplete = '<number>',
		processedCount = '<number>',
		totalCount = 2072173
	} )

	timeRemainingObserver:expectDataWithinTime( 10, true )

	progressEventObserver:expectDataWithinTime( 10, {
		errorCount = 0,
		extraInfo = {
			endTime = '<number>',
			timestamp = '<number>',
			startTime = '<number>',
		},
		percentComplete = 1,
		processedCount = 2072173,
		totalCount = 2072173
	} )

	progressEventObserver:dispose()

	timeRemainingObserver:expectDataWithinTime( 60, false )

	timeRemainingObserver:dispose()
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadAdd_DirectUpload_WithProgress_testNightly( rxTest )
	
	rxTest:setTimeLimit( 120 )
	
	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
		
	rxTest:waitFor( catalog:ozUser():enableFeature( 'allow_direct_upload' ) )
	rxTest:waitFor( catalog:ozUser():getAccountDocFromOz() )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				useDirectUploadIfPossible = true,
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
				forceIncludeProgress = true
			} )
			
	-- Time remaining is hard to verify, so we just verify that the upload reported
	-- taking longer than 0 seconds at some point in time.
	
	local timeRemainingObserver = rxTest:observe( uploader:getEstimatedTimeRemaining()
		:map( function( timeRemaining )
			return timeRemaining > 0
		end ) )
			
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))

	local progressEventObserver = rxTest:observe( uploader:DEBUG_getInFlightUploadProgressUpdateStream()
	
		:map( function( events )
		
			local wrapperEvent = derefOpt( events, assetId, 'master' )
			
			local strippedWrapperProgress = verifyEventAndMapAwayCruft( rxTest, wrapperEvent, assetId )
			
			local allStrippedProgress = { strippedWrapperProgress }
			
			local chunkedEvents = derefOpt( wrapperEvent, 'extraInfo', 'progressOfChunks' )  or {}
			
			for offset, chunkedProgress in pairs( chunkedEvents ) do
				allStrippedProgress[ #allStrippedProgress + 1 ] = verifyEventAndMapAwayCruft( rxTest, chunkedProgress, assetId, offset )
			end
			
			return allStrippedProgress
			
		end ) )

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	progressEventObserver:expectDataWithinTime( 10, { 
		{
			errorCount = 0,
			extraInfo = {},
			percentComplete = 0,
			processedCount = 0,
			totalCount = 2072173
		} 
	} )

	progressEventObserver:expectDataWithinTime( 5, {
		{
			errorCount = 0,
			extraInfo = {},
			percentComplete = '<number>',
			processedCount = '<number>',
			totalCount = 2072173
		},
		{
			errorCount = 0,
			extraInfo = {
				startTime = "<number>",
				timestamp = "<number>",
			},
			percentComplete = "<number>",
			processedCount = "<number>",
			totalCount = 2072173,
		},
	} )

	timeRemainingObserver:expectDataWithinTime( 60, true )

	progressEventObserver:expectDataWithinTime( 10, {
		{
			errorCount = 0,
			extraInfo = {},
			percentComplete = 1,
			processedCount = 2072173,
			totalCount = 2072173
		},
		{
			errorCount = 0,
			extraInfo = {
				timestamp = "<number>",
				endTime = "<number>",
				startTime = "<number>",
			},
			percentComplete = 1,
			processedCount = 2072173,
			totalCount = 2072173,
		},
	} )

	progressEventObserver:dispose()

	timeRemainingObserver:expectDataWithinTime( 60, false )

	timeRemainingObserver:dispose()
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadAdd( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local urlResponses = {
		generateCreateMasterResponseWithXReadMeta( catalogId, assetId, revisionId, originalFilePath ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	local syncDiagnosticsObserver = rxTest:observe( uploader:syncDiagnostics( 0.001 )
		:map( function( diagnostics )
			return diagnostics
		end ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	local completionStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {},
			summaryStr = "Catalog: Uploaded 0 binaries to Oz.",
			uploadsByType = {}
		} )

	uploader:startContinuousUploads()
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {
				[ assetId ] = {
					master = true,
				},
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 1,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			scheduledUpload = {
				binaryType = "master",
				docId = assetId,
				priority = 0,
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	completionStream:expectDataWithinTime( 5, assetId )
	completionStream:dispose()
	
	-- Upload Finished 
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1, -- haven't yet received Oz response with the master link
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 0,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				}
			}
		} )

	syncDiagnosticsObserver:dispose()

end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpDevelop( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local xmpDevelopSha256 = "23406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa652"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local origXmpDevelopFilePath = KSPathUtils.child( testDataFolder1, 'IMG_7763.XMP' )
	local xmpDevelopFileSize = generateSidecarFile( rxTest, origXmpDevelopFilePath )
	
	local xmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), 'IMG_7763.XMP' )
	WFFileUtils.delete( xmpDevelopFilePath )
	WFFileUtils.copy( origXmpDevelopFilePath, xmpDevelopFilePath )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	-- create a document with xmp_develop settings
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	
	local latestRevisionId
	rxTest:waitFor( catalog:docStore():docValue( assetId )
		:flatMap( function( docValue )
			-- the revision id has to be in the Oz revision_ids list for the client
			-- to upload the xmp_develop so that it knows Oz has matching data for
			-- what it's trying to upload
			latestRevisionId = docValue._revId 
			local spm = docValue._serverProvidedMetadata
			local revIds = spm.revision_ids
			table.insert( revIds, latestRevisionId )
			spm.revision_ids = revIds
			
			return catalog:docStore():applyOverlay( {
				_id = docValue._id,
				_serverProvidedMetadata = spm
			} )
		end ) )
	
	-- expect the upload of the xmp_develop file and then the master
	catalogSpec:expectHttpRequest( generateUploadXmpDevelopFileResponse( catalogId, assetId, latestRevisionId, xmpDevelopFilePath ) )
	catalogSpec:expectHttpRequest( generateCreateMasterResponseWithXReadMeta( catalogId, assetId, latestRevisionId, originalFilePath ) )
		
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- make it look like the metadatapuller got an update with the xmp_develop link in it
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpDevelop_copy( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local xmpDevelopSha256 = "23406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa652"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local origXmpDevelopFilePath = KSPathUtils.child( testDataFolder1, 'IMG_7763.XMP' )
	local xmpDevelopFileSize = generateSidecarFile( rxTest, origXmpDevelopFilePath )
	
	local xmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), 'IMG_7763.XMP' )
	WFFileUtils.delete( xmpDevelopFilePath )
	WFFileUtils.copy( origXmpDevelopFilePath, xmpDevelopFilePath )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	-- create a document with xmp_develop settings
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	
	-- create a sourceXmpDevLink reference so that the uploader issues a copy 
	local sourceUrl = '/this/is/a/sourceXmpDevelopLink'
	rxTest:waitFor( catalog:docStore():applyOverlay( { _id = assetId, _localOnly = { sourceXmpDevelopLink = sourceUrl } } ) )
	
	local latestRevisionId
	rxTest:waitFor( catalog:docStore():docValue( assetId )
		:flatMap( function( docValue )
			-- the revision id has to be in the Oz revision_ids list for the client
			-- to upload the xmp_develop so that it knows Oz has matching data for
			-- what it's trying to upload
			latestRevisionId = docValue._revId 
			local spm = docValue._serverProvidedMetadata
			local revIds = spm.revision_ids
			table.insert( revIds, latestRevisionId )
			spm.revision_ids = revIds
			
			return catalog:docStore():applyOverlay( {
				_id = docValue._id,
				_serverProvidedMetadata = spm
			} )
		end ) )
	
	-- expect the upload of the xmp_develop file and then the master
	catalogSpec:expectHttpRequest( generateCopyXmpDevelopResponseWithUrl( catalogId, assetId, latestRevisionId, sourceUrl ) )
	catalogSpec:expectHttpRequest( generateCreateMasterResponseWithXReadMeta( catalogId, assetId, latestRevisionId, originalFilePath ) )
		
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- make it look like the metadatapuller got an update with the xmp_develop link in it
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )
	
	uploader:stopContinuousUploads()
	
	-- WF-1323: If the binary uploader has a sourceXmpDevelopLink but there's already been
	-- an XMP upload, the sourceXmpDevelopLink has already been uploaded and should be ignored
	-- for all future uploads. This should force the uploader to look at the actual file info 
	-- and upload that instead
	rxTest:waitFor( catalog:docStore():docValue( assetId )
		:flatMap( function( docValue )
			local spm = docValue._serverProvidedMetadata
			KSTableUtils.setNested( spm, true, 'links', '/rels/xmp/develop', 'invalid' )
			return catalog:docStore():applyOverlay( { _id = assetId, _serverProvidedMetadata = spm } )
		end ) )
	
	catalogSpec:expectHttpRequest( generateUploadXmpDevelopFileResponse( catalogId, assetId, latestRevisionId, xmpDevelopFilePath ) )
	
	uploader:startContinuousUploads()
	
	-- wait for the latest PUT of the xmp_develop file to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 3 )
	
	-- remove the invalid flag to make it look like it's been uploaded
	rxTest:waitFor( catalog:docStore():docValue( assetId )
		:flatMap( function( docValue )
			local spm = docValue._serverProvidedMetadata
			KSTableUtils.setNested( spm, { _delete = true }, 'links', '/rels/xmp/develop', 'invalid' )
			return catalog:docStore():applyOverlay( { _id = assetId, _serverProvidedMetadata = spm } )
		end ) )
	
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )
	
	uploader:stopContinuousUploads()
	
end

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

function WFCatBinaryUploaderTest.rxTestQuicklyAbortUploadOnDocumentDeletion( rxTest, fileName )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local createMasterResponse = generateCreateMasterResponseWithXReadMeta( catalogId, assetId, revisionId, originalFilePath )
	createMasterResponse.responseDelayBySeconds = 15
		-- Delay the response by 15 seconds so we have time to cancel it.
	createMasterResponse.shouldNotComplete = true
		-- Marked the request as 'shouldNotComplete' to indicate that we should not expect this
		-- HTTP request to finish executing.
	
	local urlResponses = {
		createMasterResponse
	}

	local catalog, _, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	-- Delete the asset
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
			_id = assetId,
			_deleted = true,
		} )

	-- As soon as we delete the asset, we should cancel the HTTP request
	-- There should be no more work. Because the response in the mock
	-- request is delayed for 15s, the fact the the following succesfully
	-- resolves in 5 seconds shows that the request was succesfully cancelled.
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

end

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

local function testMockedUploadMissingOriginal( rxTest, fileName )

	-- Test for user deleting a file from their originals directory before upload
	-- occurred, which they shouldn't be doing.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	-- copy the test file to a new folder so we can delete it.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local testFilePath = KSPathUtils.child( testDataFolder1, fileName )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, fileName )
	WFFileUtils.copy( testFilePath, originalFilePath )


	local urlResponses = {
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	-- Now delete the file.
	rxTest:expectEqual( 'file', WFFileUtils.exists( originalFilePath ) )
	WFFileUtils.delete( originalFilePath )
	rxTest:expectEqual( false, WFFileUtils.exists( originalFilePath ) )

	uploader:startContinuousUploads()
	
	if WFPlatformInfo.isMobile() then
		-- mobile currently deletes the asset if the original is not present
		-- WF-686 should align WFCatBinaryUploader's behavior with desktop and
		-- not delete
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_empty' )
		):expectDataWithinTime( 8, true )
	
	else
		-- desktop marks the document as missing an original so that the client
		-- can show this info to the user and the user can take action rather
		-- than silently deleting
		rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_localOnly', 'localOriginalError' )
		):expectDataWithinTime( 8, 'UploadFailedMissingOriginal' )
	
	end
	
	-- Make sure no http activity took place
	rxTest:expectEqual( 0, catalogSpec._httpMock:requestCount() )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	errorsRecordedInDiagnosticLog = removeMarkingOriginalInvalidError( errorsRecordedInDiagnosticLog )
	errorsRecordedInDiagnosticLog = removeUhOhWarning( errorsRecordedInDiagnosticLog )
	
	if WFPlatformInfo.isMobile() then
		errorsRecordedInDiagnosticLog = removeOriginalMissingError( errorsRecordedInDiagnosticLog )
	end
	
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadMissingOriginal( rxTest )

	-- This is to guard against a user deleting a file from their originals
	-- directory.

	-- Mocked version of this is good enough since it also ensures that no
	-- Http traffic takes place.

	testMockedUploadMissingOriginal( rxTest, "IMG_7763.JPG" )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadMissingOriginalLargeFile( rxTest )

	-- Doesn't execute different than non-chunked version above but since this
	-- is a quick test run it with a large file that would be chunked to guard
	-- against future changes.

	testMockedUploadMissingOriginal( rxTest, "IMG_9876.CR2" )

end


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

function WFCatBinaryUploaderTest.rxTestMockedUploadFailedContentTypeMismatch( rxTest )

	-- Test for user deleting a file from their originals directory before upload
	-- occurred, which they shouldn't be doing.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	-- copy the test file to a new folder
	local fileName = "IMG_7763.JPG"
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local testFilePath = KSPathUtils.child( testDataFolder1, fileName )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, fileName )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local urlResponses = {
		{
			url = string.format( "/v2/catalogs/38d6f02c0177438aa3552161cc3f427f/assets/%s/revisions/%s/master", assetId, revisionId ),
			
			data = originalFilePath,
			
			fail = {
				[[
					{
						"errors": {
							"content_type": [
								"should match the subtype of the asset"
							]
						},
						"code": 1003,
						"description": "Invalid content type"
					}
				]],
				{
					status_code = '415',
					[ 'X-Result-Code' ] = '1003',
				}
			},
			
			returnHttpErrorsAsData = false,
	
		}
	}

	local catalog, _, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	uploader:startContinuousUploads()
	
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_localOnly', 'localOriginalError' )
		):expectDataWithinTime( 8, 'UploadFailedContentTypeMismatch' )

end

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

function WFCatBinaryUploaderTest.rxTestUploadProxyOnly_testNightly( rxTest )
	
	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, noRetryOnPutError )
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy )
	rxTest:addCloseTarget( uploader )
	
	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	
	-- Create a fake proxies folder for documents we set directly, copy image to that
	-- directory.
	local originalFilePath, proxyFilePath = getOriginalAndProxyPathsFor( rxTest, 'IMG_7763.JPG' )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addProxyPathToDocument(assetDoc, proxyFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))
	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )
	
	-- start up sync engine
	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/proxy_type/2560', 'href') and derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/rendition_type/2048', 'href') then
				return true
			end
		end ) )
	
	docObserver:expectDataWithinTime(360, true)
end
	

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

function WFCatBinaryUploaderTest.rxTestMockedUploadProxyOnly( rxTest )

	-- Maybe could happen when a lightroom classic asset is copied though in those cases there really
	-- should be a remote proxy already. But there may be edge cases where the proxy on Oz is 
	-- unavailable for some reason but we have it locally

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	-- Create a fake proxies folder for documents we set directly, copy image to that
	-- directory.
	local originalFilePath, proxyFilePath = getOriginalAndProxyPathsFor( rxTest, 'IMG_7763.JPG' )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, proxyFilePath ) )

	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )

	-- Remove original from the doc as we're pretending this is a proxy only asset (like from LrClassic)
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )

	-- update the expected url/response pairs
	catalogSpec:expectHttpRequest( generateCreateProxyResponse( catalogId, assetId, revisionId, KSIdGenerator.uuidInLowercaseHex(), proxyFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Make it look like metadatapuller got an update with a proxy link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

--==============================================================================
-- @mark Copy
--==============================================================================

local function testUploadCopy( rxTest, enableDirectUpload )

	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local sawCopyRequest = false
	local copyRequestUrl
	
	local function watchForCopyRequest( self, url, data, options, progressReporter )
		if options.requestHeaders then
			for _, header in ipairs( options.requestHeaders ) do
				if header.header == 'X-Async-Copy' then
					sawCopyRequest = true
					copyRequestUrl = url
				end
			end
		end
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	
	local catalogSpecSpy  = makeWFCatSpecSpy( catalogSpec, watchForCopyRequest )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy, 
											{ useDirectUploadIfPossible = enableDirectUpload } )
	rxTest:addCloseTarget( uploader )
	
	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath )
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))
	
	-- start up sync engine
	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Import a duplicate of the asset 
	local copiedAssetId1 = KSIdGenerator.uuidInLowercaseHex()
	local copiedRevisionId1 = KSIdGenerator.uuidInLowercaseHex()
	local assetDoc2 = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) 
	assetDoc2 = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc2, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc2 ))
	
	local masterCreateUrl
	
	docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId1 )
	:map( function( docValue )
		if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') then
			masterCreateUrl = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master_create', 'href')
			masterCreateUrl = masterCreateUrl:replace( '{revision_id}', copiedRevisionId1 )
			return true
		end
	end ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Copy master api will use "X-Async-Copy" in the request HEAD
	rxTest:expectEqual( true, sawCopyRequest )
	rxTest:expectEqual( copyRequestUrl, masterCreateUrl )

end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_testNightly( rxTest )

	local enableDirectUpload = false
	
	testUploadCopy( rxTest, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_enableDirectUpload_testNightly( rxTest )

	local enableDirectUpload = true
	
	testUploadCopy( rxTest, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy( rxTest )

	-- This test verifies the uploader will detect an asset with the same
	-- sha256 and use the Copy Master api instead of the Create Master api
	-- to uplaod the image.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId1 = '10be4f816aa149a7a3e8b148145d5135'
	local copiedRevisionId1 = '11e880f718a348b89663cf38f9f29d23'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local urlResponses = {
		generateCopyMasterResponse( catalogId, copiedAssetId1, copiedRevisionId1, assetId, revisionId ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId1, copiedRevisionId1 ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_VideoOne_NoProxiesHeader( rxTest )

	-- WF-1786 This test verifies Copy API for video 1.0  would not have x-generate-proxies:all header

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId1 = '10be4f816aa149a7a3e8b148145d5135'
	local copiedRevisionId1 = '11e880f718a348b89663cf38f9f29d23'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7772.mp4" )

	-- Just check mock http headers do not contain x-generate-proxies:all header
	local urlResponses = {
		generateCopyMasterResponseForVideo( catalogId, copiedAssetId1, copiedRevisionId1, assetId, revisionId ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId1, copiedRevisionId1 ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

local function testUploadCopyAfterAdd( rxTest, enableDirectUpload )
	
	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local sawCopyRequest = false

	local function watchForCopyRequest( self, url, data, options, progressReporter )
		if options.requestHeaders then
			for _, header in ipairs( options.requestHeaders ) do
				if header.header == 'X-Async-Copy' then
					sawCopyRequest = true
				end
			end
		end
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, watchForCopyRequest )
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy,
											{ useDirectUploadIfPossible = enableDirectUpload })
	rxTest:addCloseTarget( uploader )
	
	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local copiedAssetId1 = KSIdGenerator.uuidInLowercaseHex()
	local copiedRevisionId1 = KSIdGenerator.uuidInLowercaseHex()
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath )
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))
	local assetDoc2 = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) 
	assetDoc2 = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc2, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc2 ))
	
	-- start up sync engine
	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()
	
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
	:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)
		
	docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId1 )
	:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Copy master api will use "X-Async-Copy" in the request HEAD
	rxTest:expectEqual( true, sawCopyRequest )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopyAfterAdd_testNightly( rxTest )

	local enableDirectUpload = false
	
	testUploadCopyAfterAdd( rxTest, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopyAfterAdd_enableDirectUpload_testNightly( rxTest )

	local enableDirectUpload = true
	
	testUploadCopyAfterAdd( rxTest, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopyAfterAdd( rxTest )

	-- This test demonstrates the uploader will detect an asset with the same
	-- sha256 and use the Copy Master api instead of the Create Master api
	-- to upload the image.

	rxTest:setTimeLimit( 30 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId1 = 'z0be4f816aa149a7a3e8b148145d5135'
	local copiedRevisionId1 = '11e880f718a348b89663cf38f9f29d23'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local urlResponses = {
		generateCreateMasterResponseWithXReadMeta( catalogId, assetId, revisionId, originalFilePath ),
		generateCopyMasterResponse( catalogId, copiedAssetId1, copiedRevisionId1, assetId, revisionId ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Update the first document with a master link to let the other assets see it and copy it.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	uploading:expectDataWithinTime( 5, true )

	-- wait for the copy master PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )

	-- Make it look like metadatapuller got an update with a master link to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId1, copiedRevisionId1 ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 10, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

local function testMockedUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	if noLocalMaster then
		-- Handle case where master wasn't downloaded or was purged.
		rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )
	end

	if noLocalProxy then
		-- Handle case where proxy wasn't downloaded or was purged.
		rxTest:waitFor( removeProxyPathFromDoc( catalog, assetId ) )
	end

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )
	
	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	watchForXmpAndMasterUploads( rxTest, catalog, catalogSpec, uploader, catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_localMasterRemoteMaster( rxTest )

	-- Typical case where you copied an asset after importing it.

	return testMockedUploadCopyWithRemoteMaster( rxTest, false )

end

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

local function testUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy, enableDirectUpload )	

	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local sawCopyRequest = false
	local copyRequestUrl
	
	local function watchForCopyRequest( self, url, data, options, progressReporter )
		if options.requestHeaders then
			for _, header in ipairs( options.requestHeaders ) do
				if header.header == 'X-Async-Copy' then
					sawCopyRequest = true
					copyRequestUrl = url
				end
			end
		end
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, watchForCopyRequest )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy )
	rxTest:addCloseTarget( uploader )
	
	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local originalFilePath, proxyFilePath = getOriginalAndProxyPathsFor( rxTest, 'IMG_7763.JPG' )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))
	
	-- start up sync engine
	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local sourceOriginalLink
	local isValidOriginalAvailableOnOz
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') then
			-- if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/proxy_type/2560', 'href') and derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/rendition_type/2048', 'href') then
				
				-- WF-1813 need to check presence of the master link & no "invalid=true" in master link & no sha256 error in importsource
				isValidOriginalAvailableOnOz = WFOzDocUtils.isValidOriginalAvailableOnOz( docValue )
				sourceOriginalLink = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href')
				
				return isValidOriginalAvailableOnOz
				
			end
		end ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	if noLocalMaster then
		-- Handle case where master wasn't downloaded or was purged.
		rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )
	end

	if noLocalProxy then
		-- Handle case where proxy wasn't downloaded or was purged.
		rxTest:waitFor( removeProxyPathFromDoc( catalog, assetId ) )
	end
	
	local copiedAssetId1 = KSIdGenerator.uuidInLowercaseHex()
	local copiedRevisionId1 = KSIdGenerator.uuidInLowercaseHex()
	local assetDoc2 = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, copiedAssetId1, copiedRevisionId1, sha256, originalFilePath ) 
	
	KSTableUtils.setNested( assetDoc2, assetId, 'copy', 'copy_id' )
	KSTableUtils.setNested( assetDoc2, assetId, '_localOnly', 'sourceAssetId' )
	KSTableUtils.setNested( assetDoc2, sourceOriginalLink, '_localOnly', 'sourceOriginalLink' )
	
	if not noLocalMaster then
		-- Handle case where master doesn't exist locally, use Oz copy API to copy master binary
		assetDoc2 = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc2, sha256, originalFilePath)
	end
	
	if not noLocalProxy then
		-- Handle case where proxy doesn't exist locally, use Oz copy API to copy proxy binary
		assetDoc2 = WFCatAssetTestUtils.addProxyPathToDocument(assetDoc2, proxyFilePath)
	end
	
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc2 ))
	
	local masterCreateUrl
	docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId1 )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') then
				masterCreateUrl = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master_create', 'href')
				masterCreateUrl = masterCreateUrl:replace( '{revision_id}', copiedRevisionId1 )
				return true
			end
		end ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Copy master api will use "X-Async-Copy" in the request HEAD
	rxTest:expectEqual( true, sawCopyRequest )
	rxTest:expectEqual( copyRequestUrl, masterCreateUrl )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_localProxyRemoteMaster_testNightly( rxTest )

	local noLocalMaster = true
	local noLocalProxy = false
	
	return testUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_noLocalProxyRemoteMaster_testNightly( rxTest )

	local noLocalMaster = true
	local noLocalProxy = true
	
	return testUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_localProxyRemoteMaster_enableDirectUpload_testNightly( rxTest )

	local noLocalMaster = true
	local noLocalProxy = false
	local enableDirectUpload = false
	
	return testUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestUploadCopy_noLocalProxyRemoteMaster_enableDirectUpload_testNightly( rxTest )

	local noLocalMaster = true
	local noLocalProxy = true
	local enableDirectUpload = true
	
	return testUploadCopyWithRemoteMaster( rxTest, noLocalMaster, noLocalProxy, enableDirectUpload )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_localProxyRemoteMaster( rxTest )

	-- Typical case that happens if you synced down your collection on another
	-- machine and didn't pull the master down yet (or if master was purged).

	return testMockedUploadCopyWithRemoteMaster( rxTest, true )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_noLocalProxyRemoteMaster( rxTest )

	-- Typical case that happens if you synced down your collection on another
	-- machine and didn't pull the master down yet (or if master was purged).

	return testMockedUploadCopyWithRemoteMaster( rxTest, true, true )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_noLocalMasterRemoteBad( rxTest )

	-- Copy an asset that has an invalid sha that was never checked and without
	-- having a master locally on the disk so we can't recalculate the sha256.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	assert( catalogSpec )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	-- In this case the master wasn't downloaded or was purged.
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	-- Also add the sha error because the asset that was copied resulted in
	-- the sha hash as being different than was was original asset (who's
	-- sha hash was never checked when it was originally uploaded).
	rxTest:waitFor( catalog:docStore():applyOverlay( {
		_id = copiedAssetId,
		importSource = {
			status =  {
				error = {
					all = {
						reason = {
							sha256 = "does not match"
						}
					}
				}
			}
		}
	} ) )

	local uploading = rxTest:observe( uploader:isDoingWork()
		:replay(100)
		:refCount()
	)
	
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	uploader:startContinuousUploads()

	-- Make sure the uploader tried to do something (it'll at least upload/copy the xmp)
	uploading:expectDataWithinTime( 5, true )
	
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) ) 
	
	uploading:expectDataWithinTime( 5, false )

	-- Make sure the uploader completed the attempted upload (actually marked the asset as invalid).
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure the asset is no longer in the needsBinaryUpload table.
	rxTest:observe( catalog:indexStore():query {
		sql = "SELECT docId FROM needsBinaryUpload",
		quiesceUntilUpToDate = true,
	} )
	:expectDataWithinTime( 5, {} )
	:dispose()

	-- Make sure we didn't run into any unexpected errors.
	local markInvalidErrorCount
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	errorsRecordedInDiagnosticLog, markInvalidErrorCount = removeMarkingOriginalInvalidError( errorsRecordedInDiagnosticLog )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

	-- Make sure we did mark the asset as invalid
	rxTest:expectEqual( 1, markInvalidErrorCount )

end

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

if not WFPlatformInfo.isAndroid then

-- Android explicitly wants to see more items in it's binary uploader as it
-- does further processing elsewhere.  See canAssetBeUploaded comment
-- "Additional check for Android."

function WFCatBinaryUploaderTest.rxTestMocked_syncCopyRemoteBad( rxTest )

	-- Scenario is syncing down an asset marked as invalid and has copy.is_copy set
	-- No local files will be present.

	-- Make sure we don't attempt to upload via copy the asset that wasn't copied
	-- locally but has a copy_id and an invalid sha that doesn't have a master
	-- locally on disk.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	assert( catalogSpec )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	-- In this case the master wasn't downloaded or was purged.
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	-- Also add the sha error because the asset that was copied resulted in
	-- the sha hash as being different than was was original asset (who's
	-- sha hash was never checked when it was originally uploaded).
	rxTest:waitFor( catalog:docStore():applyOverlay( {
		_id = copiedAssetId,
		importSource = {
			status =  {
				error = {
					all = {
						reason = {
							sha256 = "does not match"
						}
					}
				}
			}
		}
	} ) )

	-- Since this scenario is just syncing down this copy remove all of the _localOnly data
	rxTest:waitFor( catalog:docStore():applyOverlay {
			_id = copiedAssetId,
			_localOnly = { _delete = true },
		}
	)

	local uploading = rxTest:observe( uploader:isDoingWork()
		:replay(100)
		:refCount()
	)

	-- Make sure the binary uploader doesn't think it needs to upload the synced down asset
	rxTest:observe( catalog:indexStore():query {
		sql = "SELECT docId FROM needsBinaryUpload",
		quiesceUntilUpToDate = true,
	} )
	:expectDataWithinTime( 5, {} )
	:dispose()

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 5, false )

	-- Make sure the uploader completed the attempted upload (actually marked the asset as invalid).
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	rxTest:observe( catalog:indexStore():query {
		sql = "SELECT docId FROM needsBinaryUpload",
		quiesceUntilUpToDate = true,
	} )
	:expectDataWithinTime( 5, {} )
	:dispose()

	-- Make sure we didn't run into any unexpected errors.
	local markInvalidErrorCount
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	errorsRecordedInDiagnosticLog, markInvalidErrorCount = removeMarkingOriginalInvalidError( errorsRecordedInDiagnosticLog )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

	-- Make sure WFCatBinaryUploader didn't mark the asset as invalid
	rxTest:expectEqual( 0, markInvalidErrorCount )

end

end -- not WFPlatformInfo.isAndroid

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_localMasterRemoteProxy( rxTest )

	-- Shouldn't happen from a copy operation, but can happen after importing an
	-- asset that was simultaneously synced from Lightroom on another client and
	-- the sha256 matches up with that one prior to upload.

	-- This test could named rxTestMockedUploadAddAssetSimulatenouslySyncedByLightroom,
	-- and be rewritten to not use createCopiesOfAssets, but this was easier.

	-- Expected result is to upload the master and not do a copy.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempAssetId'
	local copiedRevisionId = 'tempRevisionId'
	local copiedOriginalPath = 'tempOriginalPath'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local urlResponses = {
		generateCreateMasterResponseWithXReadMeta( catalogId, copiedAssetId, copiedRevisionId, copiedOriginalPath ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId, copiedOriginalPath = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	rxTest:expectEqual( nil, copiedOriginalPath )

	-- This can't happen when using createCopiesOfAssets, but add a local original path now to simulate
	-- the case described at the top of this test.
	rxTest:waitFor( addOriginalToDocPath( catalog, copiedAssetId, sha256, originalFilePath ) )
	copiedOriginalPath = originalFilePath

	-- update the expected url/response pairs
	urlResponses[ 1 ] = generateCreateMasterResponseWithXReadMeta( catalogId, copiedAssetId, copiedRevisionId, originalFilePath )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_localProxyRemoteProxy( rxTest )

	-- Happens when a lightroom asset is copied.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local proxyId = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempAssetId'
	local copiedRevisionId = 'tempRevisionId'
	local copiedProxyId = 'tempProxyId'
	local copiedOriginalPath = 'tempOriginalPath'

	-- Create a fake proxies folder for documents we set directly, copy image to that
	-- directory.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local proxyFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, proxyFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, proxyFilePath ) )
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, assetId, revisionId, proxyId ) )
	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )

	-- Handle case where master wasn't downloaded or was purged.
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, assetId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId, copiedOriginalPath = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	rxTest:expectEqual( nil, copiedOriginalPath )
	
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyProxyResponse( catalogId, copiedAssetId, copiedRevisionId, copiedProxyId, assetId, revisionId, proxyId ) )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- Make it look like metadatapuller got an update with the xmp link in it to let the
	-- uploader know it can continue with the proxy
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- Make it look like metadatapuller got an update with the proxy link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_noLocalProxyRemoteProxy( rxTest )

	-- Happens when a lightroom asset is copied.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sourceProxyId = 'b2e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempAssetId'
	local copiedRevisionId = 'tempRevisionId'
	local copiedProxyId = 'tempProxyId'
	local copiedOriginalPath = 'tempOriginalPath'

	-- Create a fake proxies folder for documents we set directly, copy image to that
	-- directory.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local proxyFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, proxyFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, sourceAssetId, sourceRevisionId, sha256, proxyFilePath ) )
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, sourceAssetId, sourceRevisionId ) )
	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId ) )
	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, sourceAssetId ) )

	-- Handle case where master and proxy weren't downloaded or were purged.
	rxTest:waitFor( removeOriginalPathFromDoc( catalog, sourceAssetId ) )
	rxTest:waitFor( removeProxyPathFromDoc( catalog, sourceAssetId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { sourceAssetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId, copiedOriginalPath = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	rxTest:expectEqual( nil, copiedOriginalPath )

	-- update the expected url/response pairs
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, sourceAssetId, sourceRevisionId ) )
	catalogSpec:expectHttpRequest( generateCopyProxyResponse( catalogId, copiedAssetId, copiedRevisionId, copiedProxyId, sourceAssetId, sourceRevisionId, sourceProxyId ) )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- Make it look like the metadata puller got an update with the xmp link in it to let the
	-- uploader know that it can upload or copy the proxy
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- Make it look like metadatapuller got an update with the proxy link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addProxyLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

local function rxTestMockedUploadCopy_xmpSidecarRemote( rxTest, xmpSidecarExists )

	-- copy master also copies rels/xmp .

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	if xmpSidecarExists == nil then
		rxTest:waitFor( catalog:docStore():applyOverlay {
				_id = assetId,
				_localOnly = {
					xmpSidecarExists = { _delete = true },
				},
			}
		)
	else
		rxTest:waitFor( catalog:docStore():applyOverlay {
				_id = assetId,
				_localOnly = {
					xmpSidecarExists = xmpSidecarExists,
				},
			}
		)
	end

	if xmpSidecarExists == false then
		-- it it's false we're trying to explicitly test the case where we know on the copy that there is no xmp sidecar file so the
		-- the xmp that *will* exist on Oz for the source asset must have been extracted by Oz. So make the copies, add the master
		-- and xmp link to doc, then expect only a copyMaster upload
		
		rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
			:map( function( copiedAssetIds )
				copiedAssetId = copiedAssetIds[ 1 ]
				return #copiedAssetIds
			end ),
		1 )
		
		copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

		rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
		
		
		-- Add the expected url/response pairs with copiedAssetId and copiedRevisionId with master copy + download and upload of xmp data
		catalogSpec:expectHttpRequest( generateCopyMasterResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
		
	else
		-- if it's nil or true we want to add the master and xmp links and we'll assume that we have to upload/copy the xmp. 
		rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
		
		rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
			:map( function( copiedAssetIds )
				copiedAssetId = copiedAssetIds[ 1 ]
				return #copiedAssetIds
			end ),
		1 )
		
		copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )
	
		-- Add the expected url/response pairs with copiedAssetId and copiedRevisionId with copy of xmp data and the subsequent copy of the master (with no X-Read-Meta header)
		catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
		catalogSpec:expectHttpRequest( generateCopyMasterResponseNoXReadMeta( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
		
	end


	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur (the xmp sidecar)
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	if not ( xmpSidecarExists == false ) then  -- have to account for true or nil cases
	
		-- Make it look like metadatapuller got an update with the xmp link in it to let the
		-- uploader know the xmp has been succesfully copied and the master can be uploaded (with no X-Read-Meta)
		rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
		-- expect the second PUT to occur (the master)
		httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	end
	
	-- Make it look like metadatapuller got an update with the master link in it to let the uploader know the
	-- master has been successfully copied. In the xmp sidecar case we end up seting the xmp link again here, 
	-- but that's a no-op and is no big deal
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
	
	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_xmpSidecarRemote_exists_true( rxTest )
	return rxTestMockedUploadCopy_xmpSidecarRemote( rxTest, true )
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_xmpSidecarRemote_exists_false( rxTest )
	return rxTestMockedUploadCopy_xmpSidecarRemote( rxTest, false )
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_xmpSidecarRemote_exists_nil( rxTest )
	return rxTestMockedUploadCopy_xmpSidecarRemote( rxTest, nil )
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_xmpDevelopSettingsLocal( rxTest )

	-- Uploads rels/xmp/develop instead of copying it.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local xmpDevelopSha256 = "23406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa652"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local xmpDevelopFilePath = KSPathUtils.child( originalsTestFolder, 'IMG_7763.XMP' )
	local xmpDevelopFileSize = generateSidecarFile( rxTest, xmpDevelopFilePath )
	local copiedXmpDevelopFilePath

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )

	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	local _
	
	copiedRevisionId, _, _, _, copiedXmpDevelopFilePath = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )
	
	if WFPlatformInfo.isAndroid then
		copiedXmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), copiedXmpDevelopFilePath )
	end
	
	-- Update the expected url/response pairs with copiedAssetId and copiedRevisionId.
	catalogSpec:expectHttpRequest( generateCopyXmpDevelopResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyMasterResponseNoXReadMeta( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )
	
	-- wait for the PUTs to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- make it look like the puller got the xmp develop link
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- make it look like the puller got an update with the xmp link and can now upload the master
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 3 )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know the master has been succesfully uploaded.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_xmpDevelopSettingsRemote( rxTest )

	-- Copies rels/xmp/develop as there is no local file at time of upload.

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local xmpDevelopSha256 = "23406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa652"

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'

	-- Create a fake originals folder for documents we set directly, copy image to that
	-- directory as createCopiesOfAssets will place new originals in the same directory
	-- as the copied file.
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalFilePath = KSPathUtils.child( originalsTestFolder, "IMG_7763.JPG" )
	WFFileUtils.copy( testFilePath, originalFilePath )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	rxTest:waitFor( catalog:docStore():applyOverlay {
			_id = assetId,
			develop = {
				xmpCameraRaw = {
					sha256 = xmpDevelopSha256,
				},
			},
			_localOnly = {
				xmpSidecarExists = false,
			},
		}
	)

	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:map( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			return #copiedAssetIds
		end ),
	1 )

	copiedRevisionId = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	-- Update the expected url/response pairs with copiedAssetId and copiedRevisionId.
	catalogSpec:expectHttpRequest( generateCopyXmpDevelopResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyMasterResponseNoXReadMeta( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )

	-- Update copied document with _serverProvidedMetadata create links as if the metadata was downloaded.
	rxTest:waitFor( addCreateLinksToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- make it look like the puller received the xmp develop link
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- make it look like the puller received the xmp link and the uploader can proceed to the 
	-- master
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 2, 3 )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know the master is up and we should be done
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

--==============================================================================
-- @mark Direct upload
--==============================================================================

function WFCatBinaryUploaderTest.rxTestMockedDirectUploadAdd( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local fileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
	
	local contentType = 'image/jpeg'

	local mainPartsRequestQuery = '?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}'
	local mainPartsRequestUrl = string.format( '/v2/catalogs/%s/assets/%s/revisions/%s/main/parts%s', catalogId, assetId, revisionId, mainPartsRequestQuery )

	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{upload_status}', 'pending' )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{file_size}', tostring( fileSize ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{part_size}', tostring( 5 * 1024 * 1024 ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{content_type}', WFURI.escape( contentType, 'http_query_key' ) )

	local urlResponses = {
		{
			url = mainPartsRequestUrl,
			
			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			dataTransformation = lowercaseDataForWindows,
			options = {
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				dataFormat = "filePath",
				requestCost = 2,
				notifyWhenHttpRequestIsInProgress = true,
				withoutAdobeAuthorization = true,
				requestPriority = 3,
				offset = 1,
				length = fileSize,
			},
			headers = {
				status_code = '200',
			},
		},
		{
			url = string.format( "https://photos-stage.adobe.io/v2/main/parts?file_size=%s", tostring( fileSize ) ),
			
			data = "",
			
			headers = {
				status_code = '202',
			},
			
			options = {
				requestMethod = "POST",
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				requestCost = 1,
				requestPriority = 3,
				bypassRateLimiter = true,
			}
		}
	}

	local allowDirectUpload = true
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, nil, allowDirectUpload )
	
	local syncDiagnosticsObserver = rxTest:observe( uploader:syncDiagnostics( 0.001 )
		:map( function( diagnostics )
			return diagnostics
		end ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	local completionStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {},
			summaryStr = "Catalog: Uploaded 0 binaries to Oz.",
			uploadsByType = {}
		} )

	uploader:startContinuousUploads()
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {
				[ assetId ] = {
					master = true,
				},
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 1,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			scheduledUpload = {
				binaryType = "master",
				docId = assetId,
				priority = 0,
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 1,
					master = {
						["S3 Uploads"] = 1,
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 5, 3 )

	completionStream:expectDataWithinTime( 5, assetId )
	completionStream:dispose()
	
	-- Upload Finished 
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1, -- haven't yet received Oz response with the master link
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 1,
					master = {
						["S3 Uploads"] = 1,
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 0,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 1,
					master = {
						["S3 Uploads"] = 1,
						total = 1,
					},
					total = 1,
				}
			}
		} )

	syncDiagnosticsObserver:dispose()

end

--------------------------------------------------------------------------------
local function generateChunkResponse( filePath, chunkNumber, kMasterUrl, resultHeaders, parts, chunkSize, chunkLength, contentRangeStrOverride )

	local fileSize = WFFileUtils.fileAttributes( filePath ).fileSize or 0

	chunkSize = chunkSize or kChunkSize
	chunkLength = chunkLength or chunkSize

	local startOffset = (chunkNumber - 1) * chunkSize
	local endOffset = startOffset + chunkLength - 1

	local contentRangeStr = string.format( "bytes %s-%s/%s", startOffset, endOffset, fileSize)
	
	contentRangeStr = contentRangeStrOverride or contentRangeStr
	
	local kDefaultPutArgs = {
		requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'X-Generate-Proxies',
				value = 'all',
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'Content-Type',
				value = 'application/octet-stream',
			},
			{
				header = "X-Read-Meta",
				value = "xmp,location",
			},
		},
		dataFormat = 'filePath',
		requestPriority = 3,
		notifyWhenHttpRequestIsInProgress = true,
		requestCost = 2,
	}
	
	local options = KSTableUtils.deepCopy( kDefaultPutArgs )
	options.length = chunkLength
	options.offset = startOffset + 1
	table.insert( options.requestHeaders, {
		header = 'Content-Range',
		value = contentRangeStr,
	} )

	if parts == nil then
		parts = {}
		for x = 0, chunkNumber - 1 do
			table.insert( parts, { first = x * chunkSize, last = ( x + 1 ) * chunkSize - 1 } )
		end
	end

	local response = KSJsonEncoder.encodeWebSafeJson( {
		parts = parts
	} )

	local headers = nil
	local fail = nil
	if resultHeaders then

		if resultHeaders.status_code >= '4' then
			fail = { 'responseData', resultHeaders }
		else
			headers = resultHeaders
		end

	else
		headers = { status_code = '201' }
	end

	return {
		url = kMasterUrl,
		options = options,
		data = filePath,

		response = response,

		fail = fail,
		headers = headers,
	}

end

--------------------------------------------------------------------------------
local function generateLastChunkResponse( filePath, chunkNumber, kMasterUrl, chunkSize, contentRangeStrOverride )

	chunkSize = chunkSize or kChunkSize

	local fileSize = WFFileUtils.fileAttributes( filePath ).fileSize or 0

	local lastChunkLength = ( fileSize - ( chunkNumber - 1 ) * chunkSize )

	return generateChunkResponse( filePath, chunkNumber, kMasterUrl, nil, nil, chunkSize, lastChunkLength, contentRangeStrOverride )

end

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

function WFCatBinaryUploaderTest.rxTestMockedDirectUpload_Failure_BlockedByEnterprise( rxTest )

	rxTest:setTimeLimit( 120 )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_9876.CR2" )
	local fileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
	
	-- There is a failure in windows machine due to drive letter's upper case and lower case issue. We manually fix it by always changing it to uppercase for the first character in originalFilePath.
	if WFPlatformInfo.isWindows then
		originalFilePath = originalFilePath:gsub("^%l", string.upper)
	end

	local mainPartsRequestQuery = '?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}'
	local mainPartsRequestUrl = string.format( '/v2/catalogs/%s/assets/%s/revisions/%s/main/parts%s', catalogId, assetId, revisionId, mainPartsRequestQuery )

	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{upload_status}', 'pending' )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{file_size}', tostring( fileSize ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{part_size}', tostring( 5 * 1024 * 1024 ) )
	
	local kMasterUrl= string.format('/v2/catalogs/%s/assets/%s/revisions/%s/master', catalogId, assetId, revisionId )
	local contentType = 'application/octet-stream'
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{content_type}', WFURI.escape( contentType, 'http_query_key' ) )

	local urlResponses = {
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Can't reach AWS S3"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Invalid content type"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Invalid content type"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		generateChunkResponse( originalFilePath, 1, kMasterUrl ),
		generateChunkResponse( originalFilePath, 2, kMasterUrl ),
		generateChunkResponse( originalFilePath, 3, kMasterUrl ),
		generateChunkResponse( originalFilePath, 4, kMasterUrl ),
		generateChunkResponse( originalFilePath, 5, kMasterUrl ),
		generateLastChunkResponse( originalFilePath, 6, kMasterUrl ),
	}
	
	local allowDirectUpload = true
	local catalog, _, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, nil, allowDirectUpload )
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	
	uploader:startContinuousUploads()
	
	local binaryType = 'master'
	
	local observeIsSkipDirectUpload  = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_localOnly', 'binaryUpload', binaryType, 'isSkipDirectUpload' )
		)
		
	observeIsSkipDirectUpload:expectDataWithinTime( 60, true )
		
	-- continue with normal single upload, after succeed, 'isSkipDirectUpload' should become nil
	
	observeIsSkipDirectUpload:expectDataWithinTime( 60, nil )

end

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

function WFCatBinaryUploaderTest.rxTestMockedDirectUpload_Failure_FetchMainPartsUrl( rxTest )
	
	rxTest:setTimeLimit( 20 )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local fileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
	
	-- There is a failure in windows machine due to drive letter's upper case and lower case issue. We manually fix it by always changing it to uppercase for the first character in originalFilePath.
	if WFPlatformInfo.isWindows then
		originalFilePath = originalFilePath:gsub("^%l", string.upper)
	end

	local mainPartsRequestQuery = '?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}'
	local mainPartsRequestUrl = string.format( '/v2/catalogs/%s/assets/%s/revisions/%s/main/parts%s', catalogId, assetId, revisionId, mainPartsRequestQuery )

	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{upload_status}', 'pending' )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{file_size}', tostring( fileSize ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{part_size}', tostring( 5 * 1024 * 1024 ) )
	
	local contentType = 'image/jpeg'
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{content_type}', WFURI.escape( contentType, 'http_query_key' ) )

	local urlResponses = {
		{
			url = mainPartsRequestUrl,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Can't Fetch MainPartsUrl"
						}
					]],
					{
						status_code = '401',
					}
				},
				
				returnHttpErrorsAsData = false,

		},		
		{
			url = mainPartsRequestUrl,
			
			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			dataTransformation = lowercaseDataForWindows,
			options = {
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				dataFormat = "filePath",
				requestCost = 2,
				notifyWhenHttpRequestIsInProgress = true,
				withoutAdobeAuthorization = true,
				requestPriority = 3,
				offset = 1,
				length = fileSize,
			},
			headers = {
				status_code = '200',
			},
		},
		{
			url = string.format( "https://photos-stage.adobe.io/v2/main/parts?file_size=%s", tostring( fileSize ) ),
			
			data = "",
			
			headers = {
				status_code = '202',
			},
			
			options = {
				requestMethod = "POST",
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				requestCost = 1,
				requestPriority = 3,
				bypassRateLimiter = true,
			}
		},	
	}
	
	local allowDirectUpload = true
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, nil, allowDirectUpload )
	
	local syncDiagnosticsObserver = rxTest:observe( uploader:syncDiagnostics( 0.001 )
		:map( function( diagnostics )
			return diagnostics
		end ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	local completionStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	syncDiagnosticsObserver:expectDataWithinTime( 15,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {},
			summaryStr = "Catalog: Uploaded 0 binaries to Oz.",
			uploadsByType = {}
		} )

	uploader:startContinuousUploads()
	
	syncDiagnosticsObserver:expectDataWithinTime( 15,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 2 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 2,
					master = {
						["S3 Uploads"] = 2,
						total = 2,
					},
					total = 2,
				},
			},
		} )
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 15, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 15, 3 )

	completionStream:expectDataWithinTime( 15, assetId )
	completionStream:dispose()
	
	-- Upload Finished 
	
	syncDiagnosticsObserver:expectDataWithinTime( 15,
		{
			documentsNeedingBinaryUpload = 1, -- haven't yet received Oz response with the master link
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 2 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 2,
					master = {
						["S3 Uploads"] = 2,
						total = 2,
					},
					total = 2,
				},
			},
		} )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 15, 0 )

	uploading:expectDataWithinTime( 15, false )

	syncDiagnosticsObserver:expectDataWithinTime( 15,
		{
			documentsNeedingBinaryUpload = 0,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 2 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 2,
					master = {
						["S3 Uploads"] = 2,
						total = 2,
					},
					total = 2,
				}
			}
		} )

	syncDiagnosticsObserver:dispose()

end

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

function WFCatBinaryUploaderTest.rxTestMockedDirectUpload_Failure_OtherThanBlockedByEnterprise( rxTest )

	rxTest:setTimeLimit( 30 )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local fileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
	
	-- There is a failure in windows machine due to drive letter's upper case and lower case issue. We manually fix it by always changing it to uppercase for the first character in originalFilePath.
	if WFPlatformInfo.isWindows then
		originalFilePath = originalFilePath:gsub("^%l", string.upper)
	end

	local mainPartsRequestQuery = '?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}'
	local mainPartsRequestUrl = string.format( '/v2/catalogs/%s/assets/%s/revisions/%s/main/parts%s', catalogId, assetId, revisionId, mainPartsRequestQuery )

	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{upload_status}', 'pending' )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{file_size}', tostring( fileSize ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{part_size}', tostring( 5 * 1024 * 1024 ) )
	
	local contentType = 'image/jpeg'
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{content_type}', WFURI.escape( contentType, 'http_query_key' ) )

	local urlResponses = {

		{
			url = mainPartsRequestUrl,
			
			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Can't reach AWS S3"
						}
					]],
					{
						status_code = '400',
					}
				},

			returnHttpErrorsAsData = false,
		},
		
		{
			url = mainPartsRequestUrl,
			
			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			dataTransformation = lowercaseDataForWindows,
			options = {
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				dataFormat = "filePath",
				requestCost = 2,
				notifyWhenHttpRequestIsInProgress = true,
				withoutAdobeAuthorization = true,
				requestPriority = 3,
				offset = 1,
				length = fileSize,
			},
			headers = {
				status_code = '200',
			},
		},
		{
			url = string.format( "https://photos-stage.adobe.io/v2/main/parts?file_size=%s", tostring( fileSize ) ),

			data = "",

			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Can't post complete URL"
						}
					]],
					{
						status_code = '401',
					}
				},

				returnHttpErrorsAsData = false,

		},
		
		{
			url = mainPartsRequestUrl,
			
			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			dataTransformation = lowercaseDataForWindows,
			options = {
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				dataFormat = "filePath",
				requestCost = 2,
				notifyWhenHttpRequestIsInProgress = true,
				withoutAdobeAuthorization = true,
				requestPriority = 3,
				offset = 1,
				length = fileSize,
			},
			headers = {
				status_code = '200',
			},
		},
		{
			url = string.format( "https://photos-stage.adobe.io/v2/main/parts?file_size=%s", tostring( fileSize ) ),
			
			data = "",
			
			headers = {
				status_code = '202',
			},
			
			options = {
				requestMethod = "POST",
				requestHeaders = {
					{
						header = 'X-Generate-Renditions',
						value = 'all',
					},
					{
						header = 'Content-Type',
						value = contentType,
					},
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				requestCost = 1,
				requestPriority = 3,
				bypassRateLimiter = true,
			}
		},	
	}
	
	local allowDirectUpload = true
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, nil, allowDirectUpload )
	
	local syncDiagnosticsObserver = rxTest:observe( uploader:syncDiagnostics( 0.001 )
		:map( function( diagnostics )
			return diagnostics
		end ) )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )

	local completionStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	syncDiagnosticsObserver:expectDataWithinTime( 25,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {},
			summaryStr = "Catalog: Uploaded 0 binaries to Oz.",
			uploadsByType = {}
		} )

	uploader:startContinuousUploads()
	
	syncDiagnosticsObserver:expectDataWithinTime( 25,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 2 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 2,
					master = {
						["S3 Uploads"] = 2,
						total = 2,
					},
					total = 2,
				},
			},
		} )
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 25, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 25, 3 )

	completionStream:expectDataWithinTime( 25, assetId )
	completionStream:dispose()
	
	-- Upload Finished 
	
	syncDiagnosticsObserver:expectDataWithinTime( 25,
		{
			documentsNeedingBinaryUpload = 1, -- haven't yet received Oz response with the master link
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 3 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 3,
					master = {
						["S3 Uploads"] = 3,
						total = 3,
					},
					total = 3,
				},
			},
		} )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 25, 0 )

	uploading:expectDataWithinTime( 25, false )

	syncDiagnosticsObserver:expectDataWithinTime( 25,
		{
			documentsNeedingBinaryUpload = 0,
			inFlightUploads = {},
			isCurrentlySyncing = false,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 3 binaries to Oz.",
			uploadsByType = {
				catalog = {
					["S3 Uploads"] = 3,
					master = {
						["S3 Uploads"] = 3,
						total = 3,
					},
					total = 3,
				}
			}
		} )

	syncDiagnosticsObserver:dispose()

end

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

function WFCatBinaryUploaderTest.rxTestMockedDirectUpload_Failure_FallBackToOzUpload( rxTest )
	
	rxTest:setTimeLimit( 120 )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_9876.CR2" )
	local fileSize = WFFileUtils.fileAttributes( originalFilePath ).fileSize
	
	-- There is a failure in windows machine due to drive letter's upper case and lower case issue. We manually fix it by always changing it to uppercase for the first character in originalFilePath.
	if WFPlatformInfo.isWindows then
		originalFilePath = originalFilePath:gsub("^%l", string.upper)
	end

	local mainPartsRequestQuery = '?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}'
	local mainPartsRequestUrl = string.format( '/v2/catalogs/%s/assets/%s/revisions/%s/main/parts%s', catalogId, assetId, revisionId, mainPartsRequestQuery )

	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{upload_status}', 'pending' )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{file_size}', tostring( fileSize ) )
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{part_size}', tostring( 5 * 1024 * 1024 ) )
	
	local kMasterUrl= string.format('/v2/catalogs/%s/assets/%s/revisions/%s/master', catalogId, assetId, revisionId )
	local contentType = 'application/octet-stream'
	mainPartsRequestUrl = mainPartsRequestUrl:replace( '{content_type}', WFURI.escape( contentType, 'http_query_key' ) )

	local urlResponses = {
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Can't reach AWS S3"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Invalid content type"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		
		{
			url = mainPartsRequestUrl,

			response = string.format( [[{
			
				"base": "https://photos-stage.adobe.io/v2/",
				"part_size": 5242880,
				"file_size": %d,
				"num_parts": 1,
				"expires": "2100-07-25T22:27:41.466149Z",
				"parts": [
					{
						"part_num": 1,
			 			"href": "xyz.s3-us-west-2.amazonaws.com/1",
						"href_expires": "2100-07-25T22:27:41.466149Z",
						"upload_status": "pending",
						"byte_start": 0,
						"byte_end": %d
					}
				],
				"links": {
					"rels/complete_upload": {
						"href": "main/parts?file_size=%s"
					},
					"rels/abort_upload": {
						"href": "main/parts"
					}
				}
			}]], fileSize, fileSize-1, fileSize ),
			
			headers = {
				status_code = '200',
			},
			
			options = {
				requestHeaders = {
					{
						header = 'Accept',
						value = 'application/json',
					},
					{
						header = 'X-Generate-Renditions',
						value = 'all'
					},
					{
						header = 'X-Generate-Proxies',
						value = 'all'
					},
					{
						header = "Content-Type",
						value = contentType, 
					},
					{
						header = 'X-Read-Meta',
						value = 'xmp,location',
					},
				},
				notifyWhenHttpRequestIsInProgress = true,
				requestCost = 1,
				requestPriority = 3,
			}
		},
		
		{
			url = "xyz.s3-us-west-2.amazonaws.com/1",
			data = originalFilePath,
			fail = {
					[[
						{
							"errors": {
								"content_type": [
									"should match the subtype of the asset"
								]
							},
							"code": 1003,
							"description": "Invalid content type"
						}
					]],
					{
						status_code = '400',
					}
				},
				
				returnHttpErrorsAsData = false,
		},
		generateChunkResponse( originalFilePath, 1, kMasterUrl ),
		generateChunkResponse( originalFilePath, 2, kMasterUrl ),
		generateChunkResponse( originalFilePath, 3, kMasterUrl ),
		generateChunkResponse( originalFilePath, 4, kMasterUrl ),
		generateChunkResponse( originalFilePath, 5, kMasterUrl ),
		generateLastChunkResponse( originalFilePath, 6, kMasterUrl ),
	}
	
	local allowDirectUpload = true
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, nil, allowDirectUpload )
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	
	uploader:startContinuousUploads()
	
	local binaryType = 'master'
	
	local observeIsSkipDirectUpload  = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
			:pluck( '_localOnly', 'binaryUpload', binaryType, 'isSkipDirectUpload' )
		)
		
	observeIsSkipDirectUpload:expectDataWithinTime( 60, true )
		
	-- continue with normal single upload, after succeed, 'isSkipDirectUpload' should become nil
	
	observeIsSkipDirectUpload:expectDataWithinTime( 60, nil )
	
	-- Add another asset, and it should use oz-upload to upload
	local assetId2 = KSIdGenerator.uuidInLowercaseHex()
	local revisionId2 = KSIdGenerator.uuidInLowercaseHex()
	local a2_sha256 = "a97e791724857a969cbe0416b10f8e6828694d51060269e0db5d2279d3978693"
	
	local testDataPath2 = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder2 = KSPathUtils.child( testDataPath2, "folder-1" )
	local originalFilePath2 = KSPathUtils.child( testDataFolder2, "IMG_7763.JPG" )

	local syncDiagnosticsObserver = rxTest:observe( uploader:syncDiagnostics( 0.001 )
		:map( function( diagnostics )
			return diagnostics
		end ) )
	
	table.insert( urlResponses, generateCreateMasterResponseWithXReadMeta( catalogId, assetId2, revisionId2, originalFilePath2 ) )
	rxTest:waitFor( addDocumentWithOzMetadataUpdate_allowDirectUplaod( catalog, catalogId, assetId2, revisionId2, a2_sha256, originalFilePath2 ) )

	local completionStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 2,
			inFlightUploads = {},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
				[ assetId2 ] = {
					master = a2_sha256,
				}
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				},
			},
		} )

	uploader:startContinuousUploads()
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 2,
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
				[ assetId2 ] = {
					master = a2_sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	-- Upload Finished 
	
	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 2, 
			inFlightUploads = {
			},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
				[ assetId2 ] = {
					master = a2_sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				},
			},
		} )
	
	-- Make it look like metadatapuller got an update with a master link in it to let the
	-- uploader know there is no more work to do.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId2, revisionId2 ) )

	syncDiagnosticsObserver:expectDataWithinTime( 5,
		{
			documentsNeedingBinaryUpload = 1,
			inFlightUploads = {},
			isCurrentlySyncing = true,
			numberOfInFlightUploads = 0,
			numberOfScheduledBinaryUploads = 0,
			recentlyUploaded = {
				[ assetId ] = {
					master = sha256,
				},
				[ assetId2 ] = {
					master = a2_sha256,
				},
			},
			summaryStr = "Catalog: Uploaded 1 binaries to Oz.",
			uploadsByType = {
				catalog = {
					master = {
						total = 1,
					},
					total = 1,
				}
			}
		} )

	syncDiagnosticsObserver:dispose()
end

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

function WFCatBinaryUploaderTest.rxTestDirectUploadAdd_testNightly( rxTest )
	
	rxTest:setTimeLimit( 360 )
	
	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
		
	rxTest:waitFor( catalog:ozUser():enableFeature( 'allow_direct_upload' ) )
	rxTest:waitFor( catalog:ozUser():getAccountDocFromOz() )	
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				useDirectUploadIfPossible = true,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	
	-- make sure we have direct upload required '/rels/main_create/parts' link
	
	rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
		
			local hasMainCreatePartsLink = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/main_create/parts', 'href') ~= nil
			
			return hasMainCreatePartsLink
			
		end ) )
		:expectDataWithinTime( 60, true )
		:dispose()

	-- start uploading
	
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
		
			local masterOnOz = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') ~= nil
			
			return masterOnOz
			
		end ) )
	
	docObserver:expectDataWithinTime( 300, true )

end

--==============================================================================
-- @mark In App Contribute
--==============================================================================

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

local redactionSetsToTest = { 
		{	
			redactionsTable = { redactKeywords = true },
			redactionsHeader = 'keywords',
		},
		{
			redactionsTable = { redactLocation = true },
			redactionsHeader = 'location',
		},
		{
			redactionsTable = { redactRatingsReviews = true },
			redactionsHeader = 'ratings,reviews'
		},
		{ 
			redactionsTable = { redactKeywords = true, redactLocation = true },
			redactionsHeader = 'keywords,location'
		},
		{ 
			redactionsTable = { redactKeywords = true, redactRatingsReviews = true },
			redactionsHeader = 'keywords,ratings,reviews'
		},
		{
			redactionsTable = { redactLocation = true, redactRatingsReviews = true },
			redactionsHeader = 'location,ratings,reviews'
		},
		{
			redactionsTable = { redactKeywords = true, redactLocation = true, redactRatingsReviews = true },
			redactionsHeader = 'keywords,location,ratings,reviews'
		}
	}

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

function WFCatBinaryUploaderTest.rxTestMockedUploadAdd_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local urlResponses = {
		generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, originalFilePath ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )

	-- Make it look like metadatapuller got an update with the /rels/master exists: true in it to let the
	-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/master
	-- href link so we look for the exists: true
	rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )
	
	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )

end

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

function WFCatBinaryUploaderTest.rxTestMockUploadAdd_InAppContribute_redactions( rxTest )
	
	rxTest:setTimeLimit( 15 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	
	for i, redactionSet in ipairs( redactionSetsToTest ) do
	
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
	
		catalogSpec:expectHttpRequest( generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, originalFilePath, '201', redactionSet.redactionsHeader ) )
		
		rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, nil, nil, nil, redactionSet.redactionsTable ) )
		
		uploader:startContinuousUploads()
		
		local uploading = rxTest:observe( uploader:isDoingWork() )
		uploading:expectDataWithinTime( 5, true )
	
		-- Make it look like metadatapuller got an update with the /rels/master exists: true in it to let the
		-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/master
		-- href link so we look for the exists: true
		rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )
	
		local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
		observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
		uploading:expectDataWithinTime( 5, false )
		
		-- Make sure there's nothing in the binary upload table
		rxTest:expectSingleValue( catalog:indexStore():queryOnce {
				sql = "SELECT COUNT(*) FROM needsBinaryUpload",
			}
			:pluck(1, 1),
		0 )
	
		uploader:stopContinuousUploads()
	end
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadCopy_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId )
	
	rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, nil, sourceAssetId, sourceRevisionId ) )
	
	watchForXmpAndMasterUploadsForIAC( rxTest, catalog, catalogSpec, uploader, spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId )

	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )

end

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

function WFCatBinaryUploaderTest.rxTestMockUploadCopy_InAppContribute_redactions( rxTest )
	
	rxTest:setTimeLimit( 15 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	for i, redactionSet in ipairs( redactionSetsToTest ) do
		
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
	
		rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, nil, sourceAssetId, sourceRevisionId, redactionSet.redactionsTable ) )
		
		watchForXmpAndMasterUploadsForIAC( rxTest, catalog, catalogSpec, uploader, spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, '201', redactionSet.redactionsHeader )
	
	end
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpDevelop_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local revisionId2 = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local xmpDevelopFilePath = KSPathUtils.child( originalsTestFolder, 'settings' )
	local xmpDevelopSettings = KSData.make( '<xmp />' )
	KSData.saveToFile( xmpDevelopSettings, xmpDevelopFilePath )
	
	rxTest:finally( function()
		WFFileUtils.delete( xmpDevelopFilePath )
	end )
	
	local xmpDevelopFileSize = KSData.length( xmpDevelopSettings )
	
	local xmpDevelopSha256 = WFCrypto.SHA256.digest( tostring( xmpDevelopSettings ) )
	
	local urlResponses = {
		generateUploadXmpDevelopFileResponseForInAppContribute( spaceId, assetId, revisionId2, xmpDevelopFilePath ),
		generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId2, originalFilePath ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )
	
	-- no source asset listings here mean that we're going to need to be asking Oz to extract the metadata from the master
	rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath ) )
	
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_newRevId = revisionId2,
		userUpdated = KSDate.timeInIso8601FormatNearestSecond(), -- this is just to ensure that the applyOverlay succeeds and sets a new revision
		_serverProvidedMetadata = {
			revision_ids = {
				revisionId,
				revisionId2
			}
		}
	} )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 5, 1 )
	
	-- Make it look like metadatapuller got an update with xmp_develop. For In App Contribute we don't
	-- always get a /rels/xmp/develop link so the uploader should look for exists: true to determine whether they're on Oz
	rxTest:waitFor( addXmpDevelopExistsToDoc( catalog, catalogId, assetId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 5, 2 )
	-- Make it look like metadatapuller got an update with rendition links in it to let the
	-- uploader know there is no more work to do. For In App Contribute we don't always get a 
	-- /rels/master link so the uploader should look for exists: true to determine whether 
	-- they're on Oz
	rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_proxyOnly_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local proxyId = KSIdGenerator.uuidInLowercaseHex()
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local proxyFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local urlResponses = {
		generateCreateProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, proxyFilePath ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addIACWillNotUploadMasterDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, proxyFilePath ) )

	uploader:startContinuousUploads()
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )
	httpRequestsProcessed:expectDataWithinTime( 2, 1 ) 
	
	-- Make it look like metadatapuller got an update with the /rels/proxy_type/2560 exists: true in it to let the
	-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/proxy_type/2560
	-- href link so we look for the exists: true
	rxTest:waitFor( addProxyExistsToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )
	
	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
			quiesceUntilUpToDate = true
		}
		:pluck(1, 1),
	0 )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_proxyOnly_InAppContribute_redactions( rxTest )
	
	rxTest:setTimeLimit( 15 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local proxyFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	uploader:startContinuousUploads()
	
	for i, redactionSet in ipairs( redactionSetsToTest ) do
		
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
		local proxyId = KSIdGenerator.uuidInLowercaseHex()
	
		catalogSpec:expectHttpRequest( generateCreateProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, proxyFilePath, '201', redactionSet.redactionsHeader ) )
		
		rxTest:waitFor( addIACWillNotUploadMasterDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, proxyFilePath, nil, nil, nil, redactionSet.redactionsTable ) )
	
		local uploading = rxTest:observe( uploader:isDoingWork() )
		uploading:expectDataWithinTime( 5, true )
		
		-- Make it look like metadatapuller got an update with the /rels/proxy_type/2560 exists: true in it to let the
		-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/proxy_type/2560
		-- href link so we look for the exists: true
		rxTest:waitFor( addProxyExistsToDoc( catalog, catalogId, assetId, revisionId ) )
	
		local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
		observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
		uploading:expectDataWithinTime( 5, false )
		
	end
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_proxyOnlyCopy_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()
	local sourceProxyId = KSIdGenerator.uuidInLowercaseHex()
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local proxyId = KSIdGenerator.uuidInLowercaseHex()
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local proxyFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local urlResponses = {
		generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId ),
		generateCopyProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId )
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	rxTest:waitFor( addIACWillNotUploadMasterDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, proxyFilePath, sourceAssetId, sourceRevisionId, sourceProxyId ) )

	uploader:startContinuousUploads()
	
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )
	httpRequestsProcessed:expectDataWithinTime( 2, 1 ) 
	
	-- Make it look like metadatapuller got an update with the /rels/xmp exists: true so that we know we can
	-- upload the proxy now
	rxTest:waitFor( addXmpExistsToDoc( catalog, catalogId, assetId, revisionId ) ) 
	
	httpRequestsProcessed:expectDataWithinTime( 2, 2 )
	
	-- Make it look like metadatapuller got an update with the /rels/proxy_type/2560 exists: true in it to let the
	-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/proxy_type/2560
	-- href link so we look for the exists: true
	rxTest:waitFor( addProxyExistsToDoc( catalog, catalogId, assetId, revisionId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )
	
	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
			quiesceUntilUpToDate = true
		}
		:pluck(1, 1),
	0 )
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_proxyOnlyCopy_InAppContribute_redactions( rxTest )
	
	rxTest:setTimeLimit( 15 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()
	local sourceProxyId = KSIdGenerator.uuidInLowercaseHex()
	
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local proxyFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	uploader:startContinuousUploads()
	
	local expectHttpRequestsCount = 0
	
	for i, redactionSet in ipairs( redactionSetsToTest ) do
	
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
		local proxyId = KSIdGenerator.uuidInLowercaseHex()
		
		catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, '201', redactionSet.redactionsHeader ) )
		catalogSpec:expectHttpRequest( generateCopyProxyResponseForInAppContribute( spaceId, assetId, revisionId, proxyId, catalogId, sourceAssetId, sourceRevisionId, sourceProxyId, '201', redactionSet.redactionsHeader ) )

		rxTest:waitFor( addIACWillNotUploadMasterDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, proxyFilePath, sourceAssetId, sourceRevisionId, sourceProxyId, redactionSet.redactionsTable ) )
		
		local uploading = rxTest:observe( uploader:isDoingWork() )
		uploading:expectDataWithinTime( 5, true )
		
		expectHttpRequestsCount = expectHttpRequestsCount + 1
		
		local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )
		httpRequestsProcessed:expectDataWithinTime( 2, expectHttpRequestsCount ) 
		
		-- Make it look like metadatapuller got an update with the /rels/xmp exists: true so that we know we can
		-- upload the proxy now
		rxTest:waitFor( addXmpExistsToDoc( catalog, catalogId, assetId, revisionId ) ) 
		
		expectHttpRequestsCount = expectHttpRequestsCount + 1
		httpRequestsProcessed:expectDataWithinTime( 2, expectHttpRequestsCount ) 
		
		-- Make it look like metadatapuller got an update with the /rels/proxy_type/2560 exists: true in it to let the
		-- uploader know there is no more work to do. For In App Contribute we don't always get a /rels/proxy_type/2560
		-- href link so we look for the exists: true
		rxTest:waitFor( addProxyExistsToDoc( catalog, catalogId, assetId, revisionId ) )
	
		local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
		observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
		uploading:expectDataWithinTime( 5, false )
		
		-- Make sure there's nothing in the binary upload table
		rxTest:expectSingleValue( catalog:indexStore():queryOnce {
				sql = "SELECT COUNT(*) FROM needsBinaryUpload",
				quiesceUntilUpToDate = true
			}
			:pluck(1, 1),
		0 )
	
	end
	
end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpCopy_InAppContribute( rxTest )
	
	rxTest:setTimeLimit( 10 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )

	rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, nil, sourceAssetId, sourceRevisionId ) )
	
	-- make it look like the master file is already there so we don't try to upload it now, we're only concerned about the xmp file
	rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )
	
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId ) )
	
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	rxTest:waitFor( addXmpExistsToDoc( catalog, catalogId, assetId ) )
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpCopy_InAppContribute_redactions( rxTest )
	
	rxTest:setTimeLimit( 20 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local sourceAssetId = KSIdGenerator.uuidInLowercaseHex()
	local sourceRevisionId = KSIdGenerator.uuidInLowercaseHex()

	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	local expectHttpRequestsCount = 0
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )
	
	uploader:startContinuousUploads()
	
	for i, redactionSet in ipairs( redactionSetsToTest ) do
	
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
		rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath, nil, sourceAssetId, sourceRevisionId, redactionSet.redactionsTable ) )
		
		catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponseForInAppContribute( spaceId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, nil, redactionSet.redactionsHeader ) )
		
		-- make it look like the master file is already there so we don't try to upload it now, we're only concerned about the xmp file
		rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )

		local uploading = rxTest:observe( uploader:isDoingWork() )
		uploading:expectDataWithinTime( 5, true )
		
		expectHttpRequestsCount = expectHttpRequestsCount + 1
		httpRequestsProcessed:expectDataWithinTime( expectHttpRequestsCount )
		
		rxTest:waitFor( addXmpExistsToDoc( catalog, catalogId, assetId ) )
		
		local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
		observeBinariesInFlight:expectDataWithinTime( 5, 0 )
	
		uploading:expectDataWithinTime( 5, false )
	
		-- Make sure there's nothing in the binary upload table
		rxTest:expectSingleValue( catalog:indexStore():queryOnce {
				sql = "SELECT COUNT(*) FROM needsBinaryUpload",
			}
			:pluck(1, 1),
		0 )
		
	end

end

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

function WFCatBinaryUploaderTest.rxTestMockedUpload_xmpDevelop_InAppContribute_Editor( rxTest )
	
	rxTest:setTimeLimit( 300 )
	
	local catalogId = KSIdGenerator.uuidInLowercaseHex()
	local spaceId = KSIdGenerator.uuidInLowercaseHex()

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local revisionId2 = KSIdGenerator.uuidInLowercaseHex()
	local revisionId3 = KSIdGenerator.uuidInLowercaseHex()
	local revisionId4 = KSIdGenerator.uuidInLowercaseHex()
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	-- xmp_develop 1
	local xmpDevelopFilePath = KSPathUtils.child( originalsTestFolder, 'settings' )
	local xmpDevelopSettings = KSData.make( '<xmp />' )
	KSData.saveToFile( xmpDevelopSettings, xmpDevelopFilePath )
	
	local xmpDevelopFileSize = KSData.length( xmpDevelopSettings )
	
	local xmpDevelopSha256 = WFCrypto.SHA256.digest( tostring( xmpDevelopSettings ) )
	
	-- xmp_develop 2
	local xmpDevelopFilePath2 = KSPathUtils.child( originalsTestFolder, 'settings' )
	local xmpDevelopSettings2 = KSData.make( '<xmp2 />' )
	KSData.saveToFile( xmpDevelopSettings2, xmpDevelopFilePath2 )
	
	rxTest:finally( function()
		WFFileUtils.delete( xmpDevelopFilePath )
		WFFileUtils.delete( xmpDevelopFilePath2 )
	end )
	
	local xmpDevelopFileSize2 = KSData.length( xmpDevelopSettings2 )
	
	local xmpDevelopSha256_2 = WFCrypto.SHA256.digest( tostring( xmpDevelopSettings2 ) )
	
	local urlResponses = {
		generateUploadXmpDevelopFileResponseForInAppContribute( spaceId, assetId, revisionId2, xmpDevelopFilePath ),
		generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId2, originalFilePath ),
		generateUploadXmpDevelopFileResponseForInAppContribute( spaceId, assetId, revisionId4, xmpDevelopFilePath2 ),
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )
	
	-- no source asset listings here mean that we're going to need to be asking Oz to extract the metadata from the master
	rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath ) )
	
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_newRevId = revisionId2,
		userUpdated = KSDate.timeInIso8601FormatNearestSecond(), -- this is just to ensure that the applyOverlay succeeds and sets a new revision
		_serverProvidedMetadata = {
			revision_ids = {
				revisionId,
				revisionId2
			}
		}
	} )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 20, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 20, 1 )
	
	-- Make it look like metadatapuller got an update with xmp_develop. For In App Contribute we don't
	-- always get a /rels/xmp/develop link so the uploader should look for exists: true to determine whether they're on Oz
	rxTest:waitFor( addXmpDevelopExistsToDoc( catalog, catalogId, assetId ) )
	
	httpRequestsProcessed:expectDataWithinTime( 20, 2 )
	-- Make it look like metadatapuller got an update with rendition links in it to let the
	-- uploader know there is no more work to do. For In App Contribute we don't always get a 
	-- /rels/master link so the uploader should look for exists: true to determine whether 
	-- they're on Oz
	rxTest:waitFor( addMasterExistsToDoc( catalog, catalogId, assetId ) )

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 20, 0 )

	uploading:expectDataWithinTime( 20, false )

	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )
	
	uploader:stopContinuousUploads()
	
	-- Update xmp_develop and make sure it's uploaded
	
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256_2, xmpDevelopFilePath2, xmpDevelopFileSize2 ) )
	
	-- asset is not editable, so the new xmp_develop is not uploaded
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_newRevId = revisionId3,
		userUpdated = KSDate.timeInIso8601FormatNearestSecond(), -- this is just to ensure that the applyOverlay succeeds and sets a new revision
		_serverProvidedMetadata = {
			revision_ids = {
				revisionId,
				revisionId2,
				revisionId3
			}
		}
	} )

	uploader:startContinuousUploads()
	
	uploading:expectDataWithinTime( 20, false )
	
	-- asset is editable, make sure new xmp_develop is uploaded
	
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId3 ) )
	
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_newRevId = revisionId4,
		userUpdated = KSDate.timeInIso8601FormatNearestSecond(), -- this is just to ensure that the applyOverlay succeeds and sets a new revision
		_serverProvidedMetadata = {
			editable = true,
			revision_ids = {
				revisionId,
				revisionId2,
				revisionId3,
				revisionId4
			}
		}
	} )

	uploading:expectDataWithinTime( 20, true )
	
	-- wait for the third PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 20, 3 )
	
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId4 ) )

	observeBinariesInFlight:expectDataWithinTime( 20, 0 )

	uploading:expectDataWithinTime( 20, false )

	-- Make sure there's nothing in the binary upload table
	rxTest:expectSingleValue( catalog:indexStore():queryOnce {
			sql = "SELECT COUNT(*) FROM needsBinaryUpload",
		}
		:pluck(1, 1),
	0 )
	
end

--==============================================================================
-- @mark Versions
--==============================================================================

function WFCatBinaryUploaderTest.rxTestMockedUploadVersion( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	local xmpDevelopFilePath = KSPathUtils.child( originalsTestFolder, 'settings' )
	local xmpDevelopSettings = KSData.make( '<xmp />' )
	KSData.saveToFile( xmpDevelopSettings, xmpDevelopFilePath )
	
	rxTest:finally( function()
		WFFileUtils.delete( xmpDevelopFilePath )
	end )
	
	local xmpDevelopFileSize = KSData.length( xmpDevelopSettings )
	local xmpDevelopSha256 = WFCrypto.SHA256.digest( tostring( xmpDevelopSettings ) )
	
	-- create an asset and make it look like it's fully uploaded
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, assetId, revisionId ) ) 
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )
	
	local versionId, versionRevisionId
	rxTest:waitFor( require 'WFCatVersions'.createVersionForAsset( catalog, assetId, false, 'Simple Version' )
						:flatMap( function( id )
							versionId = id
							return catalog:docStore():docValue( id )
								:map( function( docValue )
									versionRevisionId = docValue._revId
								end )
						end ) )
	
	rxTest:waitFor( addCreateLinksToVersionDoc( catalog, catalogId, versionId, versionRevisionId ) )
	
	local versionXmpDevelopFilePath
	rxTest:waitFor( catalog:docStore():docValue( versionId )
					:map( function( docValue )
						versionXmpDevelopFilePath = WFCatAssetUtils.getLocalExternalDevelopSettingsPath( docValue )
						if WFPlatformInfo.isiOS then
							local catalogBasePath = require 'OzFilePaths'.getCatalogBasePath( catalogId )
							-- mobile stores relative paths, so turn this into a full path for mobile
							versionXmpDevelopFilePath = KSPathUtils.child( catalogBasePath, versionXmpDevelopFilePath )
						elseif WFPlatformInfo.isAndroid then
							versionXmpDevelopFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ), versionXmpDevelopFilePath )
						end
						return true
					end ) )
					
	catalogSpec:expectHttpRequest( generateUploadXmpDevelopFileResponseForVersion( catalogId, versionId, versionRevisionId, versionXmpDevelopFilePath ) )
	
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for the first PUT to occur
	httpRequestsProcessed:expectDataWithinTime( 2, 1 )
	
	-- make it look like the metadatapuller get an update with an xmp develop link so we know we're don
	rxTest:waitFor( addXmpDevelopLinkToDoc( catalog, catalogId, versionId, versionRevisionId ) )
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 0 )

	uploading:expectDataWithinTime( 5, false )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestUploadVersion_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local catalogTestSession = WFCatTestSession( rxTest )
	
	local catalog, catalogSpec, catalogId, docStore
	
	rxTest:waitFor(
		catalogTestSession:catalog()
			:tap( function( _catalog )
				check( 'WFCatalog' )
				catalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				catalogId = _catalog:ozCatalogSpec():catalogId()
				docStore = catalog:docStore()
			end ) )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalSha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	local xmpDevelopFilePath = KSPathUtils.child( WFTestFileUtils.createTestFolder( rxTest ), 'IMG_7763.XMP' )
	local xmpDevelopFileSize = generateSidecarFile( rxTest, xmpDevelopFilePath )
	local xmpDevelopSha256 = require 'WFCrypto'.digestFile( xmpDevelopFilePath, 'SHA256' )
	
	local assetDocWithExternalDevelopSettings = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, originalSha256, originalFilePath )
	WFCatAssetTestUtils.addOriginalPathToDocument( assetDocWithExternalDevelopSettings, originalSha256, originalFilePath )
	
	rxTest:waitFor( catalog:docStore():setDocValue( assetDocWithExternalDevelopSettings ) )
	rxTest:waitFor( addXmpDevelopSettingsToDocument( catalog, assetId, xmpDevelopSha256, xmpDevelopFilePath, xmpDevelopFileSize ) )
	
	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') and
			derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' ) then
				return true 
			end
		end ) )
	
	docObserver:expectDataWithinTime(120, true)
	
	local versionId, versionRevisionId
	rxTest:waitFor( require 'WFCatVersions'.createVersionForAsset( catalog, assetId, false, 'Simple Version' )
						:flatMap( function( id )
							versionId = id
							return catalog:docStore():docValue( id )
								:map( function( docValue )
									versionRevisionId = docValue._revId
								end )
						end ) )
						
	local versionObserver = rxTest:observe( catalog:docStore():ongoingDocValue( versionId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp/develop', 'href' ) then
				return true
			end
		end ) )
	
	versionObserver:expectDataWithinTime( 120, true )
						
end

--==============================================================================
-- @mark Other
--==============================================================================

function WFCatBinaryUploaderTest.rxTestMockedUserOwnedSharedAlbumUploadPriority( rxTest )

	rxTest:setTimeLimit( 10 )

	local docsToInsertIntoDocStore = {}

	-- Construct a user owned shared album.
	
	local albumId = KSIdGenerator.uuidInLowercaseHex()
	local albumRevId = KSIdGenerator.uuidInLowercaseHex()
	
	local albumDoc = {

		_id = albumId,
		_newRevId = albumRevId,
		type = 'album',
		subtype = 'collection',
		name = 'foo',
		_serverProvidedMetadata = {
			revision_ids = {
				albumRevId,
			},
		}

	}
	
	docsToInsertIntoDocStore[ #docsToInsertIntoDocStore + 1 ] = albumDoc
	
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	local spaceRevId = KSIdGenerator.uuidInLowercaseHex()
	
	local spaceDoc = {
		_id = spaceId,
		_newRevId = spaceRevId,
		type = 'space',
		subtype = 'shared',
		download = true,
		private = false,
		location = true,
		metadata = true,
	}
	
	docsToInsertIntoDocStore[ #docsToInsertIntoDocStore + 1 ] = spaceDoc
	
	local spaceAlbumRevId = KSIdGenerator.uuidInLowercaseHex()
	
	local spaceAlbumDoc = {
		type = 'space_album',
		_id = KSIdGenerator.uuidInLowercaseHex(),
		_newRevId = spaceAlbumRevId,
		space = {
			id = spaceId,
		},
		album = {
			id = albumId,
		},
		_serverProvidedMetadata = {
			revision_ids = {
				spaceAlbumRevId,
			},
		}
	}
	
	docsToInsertIntoDocStore[ #docsToInsertIntoDocStore + 1 ] = spaceAlbumDoc
	
	-- Create a number of different assets, all of which should have a different upload priority.
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local urlResponses = {}

	for i = 1, 4, 1 do
	
		local assetId = KSIdGenerator.uuidInLowercaseHex()
		local revisionId = KSIdGenerator.uuidInLowercaseHex()
		local sha256 =  string.format( "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa65%d", i )

		local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
		local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
		local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

		urlResponses[ #urlResponses + 1 ] = generateCreateMasterResponseWithXReadMeta( catalogId, assetId, revisionId, originalFilePath )

		local assetDoc = createDocumentWithOzMetadataUpdate( catalogId, assetId, revisionId, sha256, originalFilePath )

		if i == 3 or i == 4 then
		
			-- Asset belongs to the user-owned shared album, so create the album asset.
		
			local albumAssetRevId = KSIdGenerator.uuidInLowercaseHex()
		
			docsToInsertIntoDocStore[ #docsToInsertIntoDocStore + 1 ] = {
				_id = KSIdGenerator.uuidInLowercaseHex(),
				_newRevId = albumAssetRevId,
				type = 'album_asset',
				asset = {
					id = assetId,
				},
				album = {
					id = albumId,
				},
				_serverProvidedMetadata = {
					revision_ids = {
						albumAssetRevId,
					},
				}

			}
		
		end
		
		if i == 1 or i == 3 then
		
			-- Asset is a pick and therefore should have a high upload priority.
		
			assetDoc.reviews = {
				[ 'foo' ] = {
					flag = 'pick'
				},
			}
		
		end

		docsToInsertIntoDocStore[ #docsToInsertIntoDocStore + 1 ] = assetDoc

	end

	local prioritizedUrlResponses = {
		urlResponses[ 3 ], -- This should be uploaded first since it's in a user-owned shared album, and is a pick.
		urlResponses[ 4 ], -- This should be uploaded second since it's in a user-owned shared album.
		urlResponses[ 1 ], -- This should be uploaded third because it's a pick.
		urlResponses[ 2 ] -- This should be uploaded last.
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, prioritizedUrlResponses )

	rxTest:waitFor( catalog:docStore():bulkSetDocValue( docsToInsertIntoDocStore ) )

	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )

	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	-- wait for all the PUTs to occur.
	
	httpRequestsProcessed:expectDataWithinTime( 5, 4 )

	-- Make sure we didn't run into any unexpected errors.
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestGetEstimatedTimeRemainingImp( rxTest )

	local kOzUploadConfirmationDelay = 5

	local sizeOfRemainingUploadsStream = Rx.Subject()
	local sizeOfRemainingCopiesStream = Rx.Subject()
	local ozUploadSpeedRingBuffer = WFRingBuffer.make( 1 )

	local observer = rxTest:observe( WFCatBinaryUploader._TEST_getEstimatedTimeRemainingImp(
			sizeOfRemainingUploadsStream,
			sizeOfRemainingCopiesStream,
			ozUploadSpeedRingBuffer ) )

	-- Test Only Uploads
	
	sizeOfRemainingCopiesStream:onNext( { 0, 0, 0, 0, 0 } ) -- Nothing to copy
	sizeOfRemainingUploadsStream:onNext( { 0, 0, 0 } ) -- Nothing to upload
	
	observer:expectDataWithinTime( 1, -1 ) -- -1 indicates nothing to upload or copy.
	
	sizeOfRemainingUploadsStream:onNext( { 0, 0, 1 } ) -- File finished uploading, but uploads are not yet confirmed by Oz.
	
	local expectedUploadSpeed = 0
	
	observer:expectDataWithinTime( 1, kOzUploadConfirmationDelay, expectedUploadSpeed ) -- Nothing to upload, just waiting on Oz.
	
	sizeOfRemainingUploadsStream:onNext( { 100, 0, 1 } ) -- One file to upload, 0 upload speed.
	
	observer:expectDataWithinTime( 1, math.huge, expectedUploadSpeed ) -- infinite upload time
	
	sizeOfRemainingUploadsStream:onNext( { 100, 5, 1 } ) -- One 100 byte file to upload, 5bps upload speed.
	
	expectedUploadSpeed = 5
	
	observer:expectDataWithinTime( 1, 20 + kOzUploadConfirmationDelay, expectedUploadSpeed ) -- 100b / 5bps = 20 seconds
	
	-- Test Only Copies
	
	sizeOfRemainingUploadsStream:onNext( { 0, 0, 0 } ) -- Nothing to upload
	sizeOfRemainingCopiesStream:onNext( { 0, 1, 5, 0, 1 } ) -- File finished copying, waiting for Oz.
	
	expectedUploadSpeed = 0
	
	observer:expectDataWithinTime( 1, kOzUploadConfirmationDelay, 0 ) -- Just waiting on Oz.
	
	sizeOfRemainingCopiesStream:onNext( { 100, 1, 5, 0, 1 } ) -- 100 bytes to copy, 5bps copy speed.
	
	expectedUploadSpeed = 0
	
	observer:expectDataWithinTime( 1, 20 + kOzUploadConfirmationDelay, 0 ) -- 100b / 5bps = 20 seconds.
	
	sizeOfRemainingCopiesStream:onNext( { 100, 10, 5, 0, 1 } ) -- 100 bytes to copy, 5bps copy speed, 10 in-flight copies.
	
	expectedUploadSpeed = 0
	
	observer:expectDataWithinTime( 1, 2 + kOzUploadConfirmationDelay, expectedUploadSpeed ) -- 100b / 5bps / 10 (in-flight) = 2 seconds.
	
	sizeOfRemainingCopiesStream:onNext( { 100, 10, 5, 5, 1 } ) -- 100 bytes to copy, 5bps copy speed, 10 in-flight copies, 5 second buffer.
	
	expectedUploadSpeed = 0
	
	observer:expectDataWithinTime( 1, 2.5 + kOzUploadConfirmationDelay, expectedUploadSpeed ) -- ( ( 100b / 5bps ) + 5 ) / 10 (in-flight) = 2.5 seconds.
	
	-- Test Both Uploads And Copies
	
	sizeOfRemainingCopiesStream:onNext( { 100, 10, 5, 0, 1 } ) -- 100 bytes to copy, 5bps copy speed, 10 in-flight copies.
	sizeOfRemainingUploadsStream:onNext( { 100, 5, 1 } ) -- One 100 byte file to upload, 5bps upload speed.
	
	expectedUploadSpeed = 5
	
	observer:expectDataWithinTime( 1, 20 + 2 + kOzUploadConfirmationDelay, expectedUploadSpeed )
	
end

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

function WFCatBinaryUploaderTest.rxTestCalculateUploadSpeed( rxTest )

	-- Nothing to upload
	
	local rows = {}
	local firstMeasurement = {}
	local previousRemainingBinaryUploads = {}
	local inFlightUploadProgress = {}

	local result = WFCatBinaryUploader._TEST_calculateUploadSpeed(
			rows, firstMeasurement, previousRemainingBinaryUploads,inFlightUploadProgress )
		
	local expected = { 0, 0, 0 }
		
	rxTest:expectEqual( expected, result )
	
	-- Stuff to upload, but nothing scheduled to upload
	
	rows = {
		{ '1', 'original', 10 }
	}
	
	firstMeasurement = {}
	previousRemainingBinaryUploads = {}
	inFlightUploadProgress = {}

	result = WFCatBinaryUploader._TEST_calculateUploadSpeed(
			rows, firstMeasurement, previousRemainingBinaryUploads,inFlightUploadProgress )
		
	expected = { 10, 0, #rows }
		
	rxTest:expectEqual( expected, result )
	
	-- Upload in flight
	
	rows = {
		{ '1', 'original', 10 }
	}
	
	firstMeasurement = {
		timestamp = 0,
		size = 10
	}
	
	previousRemainingBinaryUploads = {
		[ '1::original' ] = true
	}
	
	local processedCount = 5
	local progessTimestamp = 100
	
	inFlightUploadProgress = {
		[ '1' ] = {
			original = {
				processedCount = processedCount,
				extraInfo = {
					startTime = 100,
					timestamp = progessTimestamp
				}
			}
		}
	}
	
	local expectedSpeed = ( firstMeasurement.size - processedCount ) / ( progessTimestamp - firstMeasurement.timestamp )

	result = WFCatBinaryUploader._TEST_calculateUploadSpeed(
			rows, firstMeasurement, previousRemainingBinaryUploads,inFlightUploadProgress )
		
	expected = { 5, expectedSpeed, #rows }
		
	rxTest:expectEqual( expected, result )
	
end

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

function WFCatBinaryUploaderTest.rxTestSanitizeDirectUploadUrlForAnalytics( rxTest )

	local exampleS3Url = [[https://oz-prod-masters.s3.us-west-2.amazonaws.com/5cda448fc27a4502ab90e5f79bbf7d13/6f0608f378a6456197383f691921e386/master?partNumber=1&uploadId=EdCnRgobfelrIPPZose27UgQlqKsfrldl57VJRMvnBpyXUm7pLhB9vfTB36F88Z2HKPyd.vWIoH6ZszUv9n7IsmzgwSsW_yeBhT.36mRgtOb1eiK1JvuxrR_tH1I_Z2agOXblb4QNVY2vAZwYzccOrwSz111vboEdQiUPl4BfE8-&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIA3PR2V2HL3KH5I4A7%2F20210921%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210921T211621Z&X-Amz-Expires=21600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJz%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJHMEUCIQCZHzXke4MwwNWDuy5Xuyb2zs%2BAduGahwXHQWTGvNEx0wIgE4SczUsf21nUpGla2De2vChAFAofxBw5VIBJLhG8JVkqogII9f%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw3ODkzMjMzMDU0MzEiDOA7%2F7EfO4aWptNm%2Fyr2AfoL6UOPfH11XbjH5GA8jAGx86LoQf%2FOBzldIXLsmdFHL2I6cuQfsDEJznAqzPNet0ct0wJTJsd%2FDtXXUxk8x9hIcBIt5wMiWaUJgdlOpyC81zAIQpVh4E28WZfXAwA1nZobnjvUi5nxee4XwxrV0%2BpFjjBhg5sQcHqJn5yMy7437jWQNU0XoehEg0n7fVD0A9tWC4rW7sAmwKl%2FNx0Z1bzdVvkzLm%2FelQbOFnGHPZA3%2BqbadOuhAbzfDEJ4mJBLVRa10Qv0OcfnOfOYqNbWaC%2F%2BmNW6iRfsVAPKDRszcYQTQ1HjicVxxbp93bP%2F6zbx%2BbfOWgef4TCY7qiKBjqdASlMpVTWyVupMtkLJ%2BHynbORUMpjyzjvdqroSxpWwlMGRKibhBczOSx%2FE95dh%2FzQFmt%2BNf6fDV0zkgzO%2BzQIDhxzZ6GN0FUo4QDrlbgGs3dpSzZtf1%2BLub%2BSzeiiG2IIp6QWMHqQbgtH4tWi4Y0adoa0wDHCp1CF3Yzouq2l5ppMWHYDyKQHvtnIh4vms8XV7VEgG2Wi%2FypU%2FFuFX1s%3D&X-Amz-SignedHeaders]]
			
	local sanitizedUrl = WFCatBinaryUploader._DEBUG_sanitizeDirectUploadUrlForAnalytics( exampleS3Url )

	local expectedUrl = [[https://oz-prod-masters.s3.us-west-2.amazonaws.com/5cda448fc27a4502ab90e5f79bbf7d13/6f0608f378a6456197383f691921e386/master?partNumber=1&uploadId=EdCnRgobfelrIPPZose27UgQlqKsfrldl57VJRMvnBpyXUm7pLhB9vfTB36F88Z2HKPyd.vWIoH6ZszUv9n7IsmzgwSsW_yeBhT.36mRgtOb1eiK1JvuxrR_tH1I_Z2agOXblb4QNVY2vAZwYzccOrwSz111vboEdQiUPl4BfE8-&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIA3PR2V2HL3KH5I4A7%2F20210921%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210921T211621Z&X-Amz-Expires=21600&X-Amz-Security-Token=***&X-Amz-SignedHeaders]]

	rxTest:expectEqual( expectedUrl, sanitizedUrl )

end

--==============================================================================
-- @mark Watermarks
--==============================================================================

local function createTestWatermark( rxTest, catalog )

	local name = "My First Watermark"

	local settings = {
		opacity = 73,
		rotation = 90,
		position = "center",
		xOffset = 0.23,
		yOffset = 0.40,
		size = 22,
		text = "@ saravana",
		font = {
			name = "SourceSansPro",
			bold = true,
			italic = true,
		},
		shadow = {
			enabled = "off"
		},
		rgbColor = { 0.4, 0.33, 0.0 },
		activeWatermarkType = "image"
	}

	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name, settings ) )
			
end

function WFCatBinaryUploaderTest.rxTestMockedUploadWatermark( rxTest )
	-- For June/August 2020 releases we don't actually need to upload image watermark
	-- data. But we are trying to get the WFCatBinaryUploader map fn ready for October
	-- when we will want to upload these but also want to avoid a reindexing due to 
	-- map fn changes. So we've made changes in WFCatBinaryUploader's map fn to be ready
	-- to index watermark documents but to fail to do so (all changes for October will be 
	-- outside of the map fn). 
	--
	-- So what we need to do here is make sure that a watermark document that gets
	-- fed into the uploader indexing map fn does not throw an error and break the indexing
	-- for documents we do care about.
	-- 
	-- This test cannot validate that it *did* index the watermark document (but came up with 
	-- nothing) - that will have to be hand verified that we do enter the map fn for watermark
	-- documents.
	--
	-- But it can verify that if we have a watermark document and another uploadable document
	-- after that, that we do wind up indexing the second document. This should validate that 
	-- a watermark document does not throw a nil dereference or similar error. Though, again,
	-- some of this has to be taken on trust as to *why* it's not throwing an error...

	rxTest:setTimeLimit( 10 )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )

	local catalog = setupMockedCatalogAndUploader( rxTest, catalogId, {} )
	
	local xmpDevelopFilePath = KSPathUtils.child( originalsTestFolder, 'settings' )
	local xmpDevelopSettings = KSData.make( '<xmp />' )
	KSData.saveToFile( xmpDevelopSettings, xmpDevelopFilePath )
	
	rxTest:finally( function()
		WFFileUtils.delete( xmpDevelopFilePath )
	end )
	
	-- create a watermark
	createTestWatermark( rxTest, catalog )
	
	-- create an asset after the watermark
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath ) )
	
	-- we don't actually care about upload, just select the rows in the WFCatBinaryUploader table and make
	-- sure we have one for the asset. This should mean that the watermark was indexed (and ignored) safely
	local uploadTableObserver = rxTest:observe( catalog:indexStore():query {
		sql = "SELECT docId FROM needsBinaryUpload ORDER BY binaryType ASC"
	} )
	
	uploadTableObserver:expectDataWithinTime( 5, { { assetId } } )

end

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

function WFCatBinaryUploaderTest.rxTestUploadVerifyManualCheckWithOz_testNightly( rxTest )
	
	-- WF-921: This test case verifies after max wait time, 
	-- whether WF triggers a manual check with Oz to see if there is a master before attempting to upload again.
	-- This is to avoid duplicate uploads.
	
	rxTest:setTimeLimit( 360 )

	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )

	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )

	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				uploadBlackoutWindowInSeconds = 5,
				uploadBlackoutWindowCount = 0,
				timer = function( seconds )
					return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
				
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )

	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	-- Larger file size is choosen ( file size is 22MB )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_9876.CR2" )

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

	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath )
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ))

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	-- Do not start Uploader here
	--uploader:startContinuousUploads() 
	
	-- wait for meta data sync
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata') then
				return true
			else 
				return false
			end
		end ) )
	
	docObserver:expectDataWithinTime( 60, true )
	
	docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
		
	-- make sure no master link yet
	docObserver:expectDataWithinTime( 60, false )
	
	-- Now Stop the puller and start the uploader
	puller:stopContinuousUpdates()
	uploader:startContinuousUploads()
	
	-- make sure master link got updated, when puller is off.
	docObserver:expectDataWithinTime( 350, true )
	
	local errorsRecordedInDiagnosticLog = getDiagnosticLogErrorsIgnoringAccountError( uploader, true )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatBinaryUploaderTest.rxTestUploadWatermark_testNightly( rxTest )
	
	rxTest:setTimeLimit( 360 )
	
	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, _ = setupCatalogAndDocStore( rxTest )
		
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )

	local name = "BinaryUploadTestWithRealOz"

	local settings = {
		opacity = 73,
		rotation = 90,
		position = "center",
		xOffset = 0.23,
		yOffset = 0.40,
		size = 22,
		text = "@ saravana",
		font = {
			name = "SourceSansPro",
			bold = true,
			italic = true,
		},
		shadow = {
			enabled = "off"
		},
		rgbColor = { 0.4, 0.33, 0.0 },
		activeWatermarkType = "image"
	}
	
	--local testDataPath = KSPathUtils.getStandardFilePath( "testdata" )
	local relativePath = KSPathUtils.child( 'assets', 'IMG_7706.PNG' )
	--local absolutePath = KSPathUtils.child( testDataPath, relativePath )
	--local fileSize = absolutePath and WFFileUtils.fileAttributes( absolutePath ).fileSize
	local fileName = 'IMG_7706.PNG'
	--local sha256 = WFCrypto.digestFile( absolutePath, 'SHA256' )
	
	local wmId
	
	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name, settings, relativePath, fileName )
			:tap( function( watermarkId )
				-- capture the watermark id returned, it will be used for validation later.
				wmId = watermarkId
			end ) )
	
	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()
	
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( wmId )
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Also verify whether originalLinkAvailable set to true
	local WFCatWatermark = require 'WFCatWatermark'
	rxTest:expectSingleValue( WFCatWatermark.getWatermarkInfo( catalog, wmId )
		:map( function( watermarkInfo )
			return ( watermarkInfo.originalLinkAvailable )
		end ), true )
	
end

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

function WFCatBinaryUploaderTest.rxTestImageWatermarkNoCopy_testNightly( rxTest )
	
	-- This test case verifies incase of watermark binary already uploaded, and if
	-- the same binary being requested to be uploaded by clients
	-- avoid issuing copying as there is no support from Oz for copying and
	-- re upload again.
	
	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local sawCopyRequest = false
	
	local function watchForCopyRequest( self, url, data, options, progressReporter )
		if options.requestHeaders then
			for _, header in ipairs( options.requestHeaders ) do
				if header.header == 'X-Async-Copy' then
					sawCopyRequest = true
				end
			end
		end
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	
	local catalogSpecSpy  = makeWFCatSpecSpy( catalogSpec, watchForCopyRequest )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpecSpy )
	rxTest:addCloseTarget( uploader )
	
	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local name = "BinaryUploadTestWithRealOz"

	local settings = {
		opacity = 73,
		rotation = 90,
		position = "center",
		xOffset = 0.23,
		yOffset = 0.40,
		size = 22,
		text = "@ saravana",
		font = {
			name = "SourceSansPro",
			bold = true,
			italic = true,
		},
		shadow = {
			enabled = "off"
		},
		rgbColor = { 0.4, 0.33, 0.0 },
		activeWatermarkType = "image"
	}
	
	local relativePath = KSPathUtils.child( 'assets', '1.5MB.jpg' )
	local fileName = '1.5MB.jpg'
	
	local wmId
	
	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name, settings, relativePath, fileName )
			:tap( function( watermarkId )
				-- capture the watermark id returned, it will be used for validation later.
				wmId = watermarkId
			end ) )
	
	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()
	
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( wmId )
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Also verify whether originalLinkAvailable set to true
	local WFCatWatermark = require 'WFCatWatermark'
	rxTest:expectSingleValue( WFCatWatermark.getWatermarkInfo( catalog, wmId )
		:map( function( watermarkInfo )
			return ( watermarkInfo.originalLinkAvailable )
		end ), true )
		
	-----------------------------------------------------------------------
	--- <<<< Now again trigger create watermark with the same binary >>>>
	------------------------------------------------------------------------
	name = "BinaryUploadTestWithRealOzNoCopy"
	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name, settings, relativePath, fileName )
			:tap( function( watermarkId )
				-- capture the watermark id returned, it will be used for validation later.
				wmId = watermarkId
			end ) )
	

	docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( wmId )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') then
				return true
			end
		end ) )
	
	docObserver:expectDataWithinTime(360, true)
	
	-- Copy master api will use "X-Async-Copy" in the request HEAD: So, make sure that is not being used.
	rxTest:expectEqual( false, sawCopyRequest )
	
	-- Also verify whether originalLinkAvailable set to true
	--WFCatWatermark = require 'WFCatWatermark'
	rxTest:expectSingleValue( WFCatWatermark.getWatermarkInfo( catalog, wmId )
		:map( function( watermarkInfo )
			return ( watermarkInfo.originalLinkAvailable )
		end ), true )
	
	
end

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

function WFCatBinaryUploaderTest.rxTestNotifyOriginalLinkAvailable_testNightly( rxTest )
	
	-- This test case verifies that when href available from Oz, WF notifies the client.
	
	rxTest:setTimeLimit( 360 )
	
	-- Have a reference to catalogTestSession in case it will be GCed before the puller use it.
	local _, catalog, catalogSpec, _ = setupCatalogAndDocStore( rxTest )
	
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
					return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	-- start up sync engine
	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	
	local WFCatWatermark = require 'WFCatWatermark'
	local emptyTbl = {}
	
	--<<--------------------------------------------------------------------------->>--
	--- This is THE beginning.---
	-- Even before creating watermark, kick off the getAllWatermarksInfo observer.
	--<<----------------------------------------------------------------------------->>--
	
	local observer = rxTest:observe(
		WFCatWatermark.getAllWatermarksInfo( catalog )
			:map( function( watermarksInfoList )
				if( next( watermarksInfoList ) == nil ) then
					return watermarksInfoList
				end
				return watermarksInfoList
		end ), true )
		
	-- As there is no watermark, expect the empty Tbl
	observer:expectDataWithinTime( 3, emptyTbl )
	
	-- Now create first watermark
	local name1 = "NotifyOriginalLinkAvailable-1"

	local settings = {
		opacity = 73,
		rotation = 90,
		position = "center",
		xOffset = 0.23,
		yOffset = 0.40,
		size = 22,
		shadow = {
			enabled = "off"
		},
		rgbColor = { 0.4, 0.33, 0.0 },
		activeWatermarkType = "image"
	}
	local relativePath1 = KSPathUtils.child( 'assets', 'IMG_7706.PNG' )
	local fileName1 = 'IMG_7706.PNG'

	local wmId1
	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name1, settings, relativePath1, fileName1 )
			:tap( function( watermarkId )
				-- capture the watermark id returned, it will be used for validation later.
				wmId1 = watermarkId
			end ) )
	
	local watermarkListExpected = {}
	
	-- prepare expected content for validating received content.
	rxTest:expectSingleValue( WFCatWatermark.getWatermarkInfo( catalog, wmId1 )
		:map( function( watermarkInfo )
			watermarkListExpected[#watermarkListExpected + 1] = watermarkInfo
			return true
		end ), true )
	
	-- set the expectation of originalLinkAvailable = false exclusively
	watermarkListExpected[1].originalLinkAvailable = false
	
	-- expect the firing should happen for the created watermark
	-- expect that the received data should indicate originalLinkAvailable = false
	observer:expectDataWithinTime( 3, watermarkListExpected )
	
	-- Now start the uploader. Let the binary sync.
	uploader:startContinuousUploads()
	
	local isValidOriginalAvailableOnOz
	local docObserver1 = rxTest:observe( catalog:docStore():ongoingDocValue( wmId1 )
		:map( function( docValue )
			if derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href') then
				-- WF-1813 need to check presence of the master link & no "invalid=true" in master link & no sha256 error in importsource
				isValidOriginalAvailableOnOz = WFOzDocUtils.isValidOriginalAvailableOnOz( docValue )

				return isValidOriginalAvailableOnOz
			end
		end ) )
	docObserver1:expectDataWithinTime(360, true)

	-- This time OriginalLinkAvailable must be seen true
	-- set the expectation accordingly.
	watermarkListExpected[1].originalLinkAvailable = true
	
	-- check does it met the expectation
	observer:expectDataWithinTime( 3, watermarkListExpected )
	
	-- Now stop the uploader.
	uploader:stopContinuousUploads()

	--<<------------------------------------------------------------->>--
	
	-- Now create an another watermark text watermark,
	-- Lets name it watermark-2
	
	--<<------------------------------------------------------------->>--
	
	local name2 = "NotifyOriginalLinkAvailable-2"
	local wmId2
	local relativePath2 = KSPathUtils.child( 'assets', '1.5MB.jpg' )
	local fileName2 = '1.5MB.jpg'
	
	rxTest:waitFor(
		WFCatWatermark.createWatermark( catalog, name2, settings, relativePath2, fileName2 )
			:tap( function( watermarkId )
				-- capture the watermark id returned, it will be used for validation later.
				wmId2 = watermarkId
			end ) )
			
		-- prepare expected content for validating received content.
		rxTest:expectSingleValue( WFCatWatermark.getWatermarkInfo( catalog, wmId2 )
		:map( function( watermarkInfo )
			watermarkListExpected[#watermarkListExpected + 1] = watermarkInfo
			return true
		end ), true )
	
	-- set the expectation
	-- received data should indicate originalLinkAvailable = false for the second watermark.
	-- and for the first watermark originalLinkAvailable shall continue to show true
	watermarkListExpected[1].originalLinkAvailable = true
	watermarkListExpected[2].originalLinkAvailable = false
	
	observer:expectDataWithinTime( 3, watermarkListExpected )
	
	-- Now start the uploader. Let the binary sync.
	uploader:startContinuousUploads()
	
	local docObserver2 = rxTest:observe( catalog:docStore():ongoingDocValue( wmId2 )
		:map( WFOzDocUtils.isValidOriginalAvailableOnOz ) )
	docObserver2:expectDataWithinTime(360, true)

	-- This time both the watermarks originalLinkAvailable must be seen true
	-- set the expectation accordingly.
	watermarkListExpected[1].originalLinkAvailable = true
	watermarkListExpected[2].originalLinkAvailable = true
	
	-- check does it met the expectation
	observer:expectDataWithinTime( 3, watermarkListExpected )
	
	-- All done, happy ending!
	observer:dispose()
	
end


--==============================================================================
-- @mark Reprioritization
--==============================================================================

function WFCatBinaryUploaderTest.rxTestMockedUploadReprioritization_ReplaceWithHigherPriority( rxTest, fileName )

	rxTest:setTimeLimit( 10 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	-- Define a lower priority asset:
	
	local lowerPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local lowerPriorityRevisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local lowerPrioritySha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local testFile = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local lowerPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset.JPG" )
	WFFileUtils.copy( testFile, lowerPriorityOriginalFilePath )

	-- First define the request/response for the request that will be rescheduled.
	
	local createMasterForLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowerPriorityAssetId, lowerPriorityRevisionId, lowerPriorityOriginalFilePath )
		
	createMasterForLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForLowPriorityAsset.sitInQueueForNSeconds = 3
		-- Delay the scheduling so we have time to reschedule it.
		
	-- Next define the request/response for the rescheduled request.
	local createMasterForLowPriorityAsset2 = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowerPriorityAssetId, lowerPriorityRevisionId, lowerPriorityOriginalFilePath )
			
	createMasterForLowPriorityAsset2.notifyWhenHttpRequestIsInProgress = true
	
	-- Define the higher priority asset:
	
	local higherPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local higherPriorityRevisionId = 'b1e880f718a348b89663cf38f9f29d23'
	local higherPrioritySha256 = "43426490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local higherPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset2.JPG" )
	WFFileUtils.copy( testFile, higherPriorityOriginalFilePath )
	
	local createMasterForHighPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, higherPriorityAssetId, higherPriorityRevisionId, higherPriorityOriginalFilePath )
	
	createMasterForHighPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	
	-- Setup the mocked HTTP responses
	
	local urlResponses = {
		createMasterForLowPriorityAsset,
		createMasterForHighPriorityAsset,
		createMasterForLowPriorityAsset2
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	-- Create the lower priority asset document first and let it start uploading.

	local uploading = rxTest:observe( uploader:isDoingWork() )

	local uploadSuccessStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			lowerPriorityAssetId, lowerPriorityRevisionId, lowerPrioritySha256, lowerPriorityOriginalFilePath ) )

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 5, true )

	-- Now create the higher priority asset document.
	
	local overlay = {
		ratings = {
			[ 'aec4af47d8114158ac494aa530848430' ] = {
				rating = 5,
			}
		}
	}
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			higherPriorityAssetId, higherPriorityRevisionId, higherPrioritySha256, higherPriorityOriginalFilePath, nil, nil, overlay ) )

	-- Once we create the higher-priority asset, the current upload should be rescheduled.
	-- The order of the requests is what verifies that this test was successful. The request
	-- order in `urlResponses` shows the expected order. First, we'll start to upload the low
	-- priority asset, then will cancel it and upload the high priority asset, then we'll
	-- resume ulpoading the low priority asset.
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	uploadSuccessStream:expectDataWithinTime( 5, higherPriorityAssetId )
	
	uploadSuccessStream:expectDataWithinTime( 5, lowerPriorityAssetId )

	uploadSuccessStream:dispose()

	httpRequestsProcessed:expectDataWithinTime( 5, 3 )

	httpRequestsProcessed:dispose()

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadReprioritization_DontCancelInFlightRequest( rxTest, fileName )

	rxTest:setTimeLimit( 30 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	-- Define a lower priority asset:
	
	local lowerPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local lowerPriorityRevisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local lowerPrioritySha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local testFile = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local lowerPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset.JPG" )
	WFFileUtils.copy( testFile, lowerPriorityOriginalFilePath )

	local createMasterForLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowerPriorityAssetId, lowerPriorityRevisionId, lowerPriorityOriginalFilePath )
		
	createMasterForLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForLowPriorityAsset.responseDelayBySeconds = 3
		-- Prevent it from completing right away.
	
	-- Define the higher priority asset:
	
	local higherPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local higherPriorityRevisionId = 'b1e880f718a348b89663cf38f9f29d23'
	local higherPrioritySha256 = "43426490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local higherPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset2.JPG" )
	WFFileUtils.copy( testFile, higherPriorityOriginalFilePath )
	
	local createMasterForHighPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, higherPriorityAssetId, higherPriorityRevisionId, higherPriorityOriginalFilePath )
	
	createMasterForHighPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForHighPriorityAsset.responseDelayBySeconds = 3
	
	-- Setup the mocked HTTP responses
	
	local urlResponses = {
		createMasterForLowPriorityAsset,
		createMasterForHighPriorityAsset,
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	-- Create the lower priority asset document first and let it start uploading.

	local uploading = rxTest:observe( uploader:isDoingWork() )

	local uploadSuccessStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			lowerPriorityAssetId, lowerPriorityRevisionId, lowerPrioritySha256, lowerPriorityOriginalFilePath ) )

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 10, true )

	-- Now create the higher priority asset document.
	
	local overlay = {
		ratings = {
			[ 'aec4af47d8114158ac494aa530848430' ] = {
				rating = 5,
			}
		}
	}
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			higherPriorityAssetId, higherPriorityRevisionId, higherPrioritySha256, higherPriorityOriginalFilePath, nil, nil, overlay ) )

	-- Once we create the higher-priority asset, the current upload should NOT be
	-- rescheduled before it is already in-flight. It should finish uploading.
	
	uploadSuccessStream:expectDataWithinTime( 10, lowerPriorityAssetId )
	
	uploadSuccessStream:expectDataWithinTime( 10, higherPriorityAssetId )

	uploadSuccessStream:dispose()
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	httpRequestsProcessed:expectDataWithinTime( 10, 2 )

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadReprioritization_ReplaceWithHigherPriorityWhenCurrentPriorityLowers( rxTest, fileName )

	rxTest:setTimeLimit( 30 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	-- Define the first asset. Though it will eventually have a lower priority, it starts
	-- out with a normal priority.
	
	local lowerPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local lowerPriorityRevisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local lowerPrioritySha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local testFile = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local lowerPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset.JPG" )
	WFFileUtils.copy( testFile, lowerPriorityOriginalFilePath )

	-- First define the request/response for the request that will be rescheduled.
	
	local createMasterForLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowerPriorityAssetId, lowerPriorityRevisionId, lowerPriorityOriginalFilePath )
		
	createMasterForLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForLowPriorityAsset.sitInQueueForNSeconds = 3
		-- Delay the scheduling so we have time to reschedule it.
		
	-- Next define the request/response for the rescheduled request.
	local createMasterForLowPriorityAsset2 = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowerPriorityAssetId, lowerPriorityRevisionId, lowerPriorityOriginalFilePath )
			
	createMasterForLowPriorityAsset2.notifyWhenHttpRequestIsInProgress = true
	
	-- Define the normal priority asset:
	
	local normalPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local normalPriorityRevisionId = 'b1e880f718a348b89663cf38f9f29d23'
	local normalPrioritySha256 = "43426490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local normalPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset2.JPG" )
	WFFileUtils.copy( testFile, normalPriorityOriginalFilePath )
	
	local createMasterForNormalPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, normalPriorityAssetId, normalPriorityRevisionId, normalPriorityOriginalFilePath )
	
	createMasterForNormalPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	
	-- Setup the mocked HTTP responses
	
	local urlResponses = {
		createMasterForLowPriorityAsset,
		createMasterForNormalPriorityAsset,
		createMasterForLowPriorityAsset2
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	-- Create the first normal priority asset document first and let it start uploading.

	local uploading = rxTest:observe( uploader:isDoingWork() )

	local uploadSuccessStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	local normalPriorityOverlay = {
		ratings = {
			[ 'aec4af47d8114158ac494aa530848430' ] = {
				rating = 5,
			}
		}
	}

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			lowerPriorityAssetId, lowerPriorityRevisionId, lowerPrioritySha256, lowerPriorityOriginalFilePath, nil, nil, normalPriorityOverlay ) )

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 10, true )

	-- Now create another normal priority asset document, and modify the existing asset document to lower it's priority.
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			normalPriorityAssetId, normalPriorityRevisionId, normalPrioritySha256, normalPriorityOriginalFilePath, nil, nil, normalPriorityOverlay ) )

	rxTest:waitFor( catalog:docStore():applyOverlay( {
		_id = lowerPriorityAssetId,
		ratings = {
			[ 'aec4af47d8114158ac494aa530848430' ] = {
				rating = 1,
			}
		}
	} ) )

	-- Once we create the higher-priority asset, the current upload should be rescheduled.
	-- The order of the requests is what verifies that this test was successful. The request
	-- order in `urlResponses` shows the expected order. First, we'll start to upload the low
	-- priority asset, then will cancel it and upload the high priority asset, then we'll
	-- resume ulpoading the low priority asset.
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	uploadSuccessStream:expectDataWithinTime( 10, normalPriorityAssetId )
	
	uploadSuccessStream:expectDataWithinTime( 10, lowerPriorityAssetId )

	uploadSuccessStream:dispose()

	httpRequestsProcessed:expectDataWithinTime( 10, 3 )

	httpRequestsProcessed:dispose()

end

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

function WFCatBinaryUploaderTest.rxTestMockedUploadReprioritization_DontCancelForSamePriority( rxTest, fileName )

	rxTest:setTimeLimit( 30 )

	local catalogId = '38d6f02c0177438aa3552161cc3f427f'

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	
	-- Define a lower priority asset:
	
	local lowPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local lowPriorityRevisionId = 'a1e880f718a348b89663cf38f9f29d23'
	local lowPrioritySha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local testFile = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local lowPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset.JPG" )
	WFFileUtils.copy( testFile, lowPriorityOriginalFilePath )
	
	local createMasterForLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowPriorityAssetId, lowPriorityRevisionId, lowPriorityOriginalFilePath )
		
	createMasterForLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForLowPriorityAsset.sitInQueueForNSeconds = 2
		-- Prevent it from completing right away.
	
	-- Define another low priority asset:
	
	local secondLowPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local secondLowPriorityRevisionId = 'b1e880f718a348b89663cf38f9f29d23'
	local secondLowPrioritySha256 = "43426490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local secondLowPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "asset2.JPG" )
	WFFileUtils.copy( testFile, secondLowPriorityOriginalFilePath )
	
	local createMasterForSecondLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, secondLowPriorityAssetId, secondLowPriorityRevisionId, secondLowPriorityOriginalFilePath )
	
	createMasterForSecondLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	
	-- Setup the mocked HTTP responses
	
	local urlResponses = {
		createMasterForLowPriorityAsset,
		createMasterForSecondLowPriorityAsset,
	}

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	-- Create the lower priority asset document first and let it start uploading.

	local uploading = rxTest:observe( uploader:isDoingWork() )

	local uploadSuccessStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			lowPriorityAssetId, lowPriorityRevisionId, lowPrioritySha256, lowPriorityOriginalFilePath ) )

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 10, true )

	-- Now create another asset with the same priority.
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			secondLowPriorityAssetId, secondLowPriorityRevisionId, secondLowPrioritySha256, secondLowPriorityOriginalFilePath ) )

	-- Once we create the second asset, the current upload should NOT be
	-- rescheduled before it has the same upload priority.
	
	uploadSuccessStream:expectDataWithinTime( 10, lowPriorityAssetId )
	
	uploadSuccessStream:expectDataWithinTime( 10, secondLowPriorityAssetId )

	uploadSuccessStream:dispose()
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	httpRequestsProcessed:expectDataWithinTime( 10, 2 )

end

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

function WFCatBinaryUploaderTest.rxTestMockUploadPrioritization_TimeCriticalUpload( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalsTestFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testFile = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )

	-- Define the Time Critical upload:
	
	local higherPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local higherPriorityRevisionId = KSIdGenerator.uuidInLowercaseHex()
	local higherPrioritySha256 = "a3426490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local higherPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "assethp.JPG" )
	WFFileUtils.copy( testFile, higherPriorityOriginalFilePath )
	
	local createMasterForHighPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, higherPriorityAssetId, higherPriorityRevisionId, higherPriorityOriginalFilePath )
	
	createMasterForHighPriorityAsset.notifyWhenHttpRequestIsInProgress = true
	createMasterForHighPriorityAsset.options.bypassRateLimiter = true
		-- This request should bypass the rate limiter because it is critical
		-- This is the KEY. 
		
	-- Define a lower priority asset:
	
	local lowPriorityAssetId = KSIdGenerator.uuidInLowercaseHex()
	local lowPriorityRevisionId = KSIdGenerator.uuidInLowercaseHex()
	local lowPrioritySha256 = "b3406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	local lowPriorityOriginalFilePath = KSPathUtils.child( originalsTestFolder, "assetlp.JPG" )
	WFFileUtils.copy( testFile, lowPriorityOriginalFilePath )
	
	local createMasterForLowPriorityAsset = generateCreateMasterResponseWithXReadMeta( 
			catalogId, lowPriorityAssetId, lowPriorityRevisionId, lowPriorityOriginalFilePath )
		
	createMasterForLowPriorityAsset.notifyWhenHttpRequestIsInProgress = true
		
	local urlResponses = {
		createMasterForHighPriorityAsset,
		createMasterForLowPriorityAsset
	}
	
	-- Setup the mocked HTTP responses

	local catalog, catalogSpec, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	local uploading = rxTest:observe( uploader:isDoingWork() )

	local uploadSuccessStream = rxTest:observe( uploader:binaryUploadCompletionStream() )

	-- Now create the time sensitive asset document.
	
	local overlay = {
		_localOnly = {
			universal = {
				uploadPriorityModifier = 'CRITICAL'
			}
		}
	}
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			higherPriorityAssetId, higherPriorityRevisionId, higherPrioritySha256, higherPriorityOriginalFilePath, nil, nil, overlay ) )

	-- And the low priority asset:
	
	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, 
			lowPriorityAssetId, lowPriorityRevisionId, lowPrioritySha256, lowPriorityOriginalFilePath ) )

	uploader:startContinuousUploads()

	uploading:expectDataWithinTime( 5, true )
	
	-- Make sure the critical binary is uploaded.
	
	uploadSuccessStream:expectDataWithinTime( 5, higherPriorityAssetId )
	
	-- And that we succesfully upload a non critical binary afterwards. This guarantees
	-- we're properly clearing the scheduled-and-waiting state in the binary uploader
	-- for critical uploads.
	
	uploadSuccessStream:expectDataWithinTime( 5, lowPriorityAssetId )
	
	uploadSuccessStream:dispose()
	
	local httpRequestsProcessed = rxTest:observe( catalogSpec._httpMock:requestsProcessed() )

	httpRequestsProcessed:expectDataWithinTime( 5, 2 )

end


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

function WFCatBinaryUploaderTest.rxTestUploadPrioritization_TimeCriticalUpload_testNightly( rxTest )
	
	rxTest:setTimeLimit( 360 )
	
	local _, catalog, catalogSpec, catalogId = setupCatalogAndDocStore( rxTest )
		
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

	local puller = WFCatMetadataPuller( catalog, "Test Puller", options )
	rxTest:addCloseTarget( puller )
	
	local uploader = WFCatBinaryUploader( catalog, nil, uploaderTaskDelays, catalogSpec, {
				clearFromRecentlyUploadedSeconds = 1,
				waitForOtherUploadSeconds = .5,
				waitForCopyFailedSeconds = .5,
				timer = function( seconds )
				       return Observable.timer( seconds, WFRxScheduler.sharedScheduler() )
				end,
			} )
			
	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()
	
	rxTest:finally( function()
		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()
	end )
	
	local assetId = KSIdGenerator.uuidInLowercaseHex()
	local revisionId = KSIdGenerator.uuidInLowercaseHex()
		
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	local sha256 = require 'WFCrypto'.digestFile( originalFilePath, 'SHA256' )
	
	local assetDoc = WFCatAssetTestUtils.createBaseAssetDocument( catalogId, assetId, revisionId, sha256, originalFilePath ) 	
	assetDoc = WFCatAssetTestUtils.addOriginalPathToDocument(assetDoc, sha256, originalFilePath)
	
	rxTest:waitFor( catalog:docStore():setDocValue( assetDoc ) )

	-- Mark the asset as time critical.
	
	rxTest:waitFor( WFCatPhotoImportance.increaseUploadPriorityForAsset( catalog, assetId, true ) )

	-- Verify there is no master link and that the asset has a priority modifier.
	
	local docObserver = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
	
		:map( function( docValue )
			
			local hasMasterHref = not not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href')
			
			local hasUploadPriorityModifier = not not derefOpt( docValue, '_localOnly', 'universal', 'uploadPriorityModifier' )
				
			return { hasMasterHref, hasUploadPriorityModifier }
				
		end ) )

	docObserver:expectDataWithinTime( 10, { false, true } )

	-- start up sync engine

	pusher:sendUpdatesContinuously()
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()
	
	-- Verify there is a link and the priority modifier was removed.
	
	docObserver:expectDataWithinTime( 360, { true, false } )
	
	docObserver:dispose()

end

--==============================================================================
-- @mark Background upload
--==============================================================================

function WFCatBinaryUploaderTest.rxTestMockedBackgroundUploadScheduledTasks_testNightly( rxTest )

	rxTest:setTimeLimit( 90 )
	
	WFCapabilities.setCapability( 'WFCatBinaryUploaderTest.debug_allowSameSha256Upload', 'true' )

	rxTest:finally( function()
		WFCapabilities.setCapability( 'WFCatBinaryUploaderTest.debug_allowSameSha256Upload', nil )
	end )
	
	local catalogId = '38d6f02c0177438aa3552161cc3f427f'
	local spaceId = KSIdGenerator.uuidInLowercaseHex()
	
	local testDataPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "import-samples" )
	local testDataFolder1 = KSPathUtils.child( testDataPath, "folder-1" )
	local originalFilePath = KSPathUtils.child( testDataFolder1, "IMG_7763.JPG" )
	
	local assetIdAndRevIds = {}
	
	for i = 0, 9 do
		for j = 0, 9 do
			local assetId = "docId_" .. tostring( i ) .. tostring( j )
			local revisionId = KSIdGenerator.uuidInLowercaseHex()
			
			assetIdAndRevIds[ #assetIdAndRevIds + 1 ] = {
				assetId = assetId,
				revisionId = revisionId
			}
		end
	end
	
	-- urlResponses 
	
	local urlResponses = {}
	
	-- The first foreground upload
	for i = 1, 6 do
	
		local assetInfo = assetIdAndRevIds[ i ]
		local assetId = assetInfo.assetId
		local revisionId = assetInfo.revisionId
		
		local createMasterResponse = generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, originalFilePath )
		
		-- delay the response, so we will have time to cancel it
		createMasterResponse.responseDelayBySeconds = 60
		
		urlResponses[ #urlResponses + 1 ] = createMasterResponse
	
	end
	
	-- upload in background
	for i = 6, 100 do
	
		local assetInfo = assetIdAndRevIds[ i ]
		local assetId = assetInfo.assetId
		local revisionId = assetInfo.revisionId
		
		local createMasterResponse = generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, originalFilePath, nil, nil, true )
		
		-- delay the response, so we will have time to cancel it
		createMasterResponse.responseDelayBySeconds = 60
		
		urlResponses[ #urlResponses + 1 ] = createMasterResponse
	
	end
	
	-- upload when we goes back to foreground again, we would dispose all uploads and reschedule
	for i = 1, 6 do
	
		local assetInfo = assetIdAndRevIds[ i ]
		local assetId = assetInfo.assetId
		local revisionId = assetInfo.revisionId
		
		local createMasterResponse = generateCreateMasterResponseForInAppContributeWithXReadMeta( spaceId, assetId, revisionId, originalFilePath )
		
		-- delay the response, so we will have time to cancel it
		createMasterResponse.responseDelayBySeconds = 60
		
		urlResponses[ #urlResponses + 1 ] = createMasterResponse
	
	end
	
	-- Create catalog, catalogSpec and uploader

	local catalog, _, uploader = setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses, true )
	
	-- Add 100 assets into catalog

	local sha256 = "43406490d179b2f3d734749996734efc5834f9045e3e91b55d9ec628894fa651"
	
	for i = 1, 100 do
	
		local assetInfo = assetIdAndRevIds[ i ]
		local assetId = assetInfo.assetId
		local revisionId = assetInfo.revisionId
		
		rxTest:waitFor( addIACDocumentWithOzMetadataUpdate( catalog, spaceId, assetId, revisionId, sha256, originalFilePath ) )
	
	end 
	
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 5, true )
	
	-- BinariesInFlight is 6 because rateLimiter pool has 5 requests, and one waiting in queue
	
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 5, 6 )
	
	rxTest:expectEqual( uploader._numberOfScheduledBinaryUploads, 6 )
	
	-- Enable background upload would allow all 100 tasks be scheduled
	
	uploader:enableBackgroundUpload( true )
	
	observeBinariesInFlight:expectDataWithinTime( 25, 100 )
	
	-- Disable background upload would dispose all active uploads. Then reschedule the upload tasks up to rateLimiter limit
	
	uploader:enableBackgroundUpload( false )
	
	observeBinariesInFlight:expectDataWithinTime( 5, 6 )
	
	rxTest:expectEqual( uploader._numberOfScheduledBinaryUploads, 6 )

end

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

-- Note: 
-- 1. Please refer WFCatLrmBinaryUploaderTest for mobile client specific test cases
-- 2. As this test suite has ACR specific and Desktop specific handling this test case can't be invoked from Mobile client.
--    Hence added in a separate file

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

return WFRxTestCase.wrapSuite( WFCatBinaryUploaderTest )
