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

WFCatDesktopBinaryUploaderTest.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.

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

local WFPlatformInfo = require 'WFPlatformInfo'

if WFPlatformInfo.isMobile() then
	return {} -- not used by mobile
end

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

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

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

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

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

	-- WF.core.table
local WFTableArchiving = require 'WFTableArchiving'

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

	-- WF.oz_client.managed_catalog
local WFCatBinaryUploader = require 'WFCatBinaryUploader'
local WFCatChunkedUploaderClassName = 'WFCatContentRangeUploader'
local WFCatChunkedUploader = require( WFCatChunkedUploaderClassName )
local WFCatContentRangeUploader = require 'WFCatContentRangeUploader'
local WFCatConflictResolver = require 'WFCatConflictResolver'
local WFCatCopies = require 'WFCatCopies'
local WFCatDelete = require 'WFCatDelete'
local WFCatErrorHandler = require 'WFCatErrorHandler'
local WFCatFileSpec = require 'WFCatFileSpec'
local WFCatImport = require 'WFCatImport'
local WFCatMetadataPuller = require 'WFCatMetadataPuller'
local WFCatMetadataPusher = require 'WFCatMetadataPusher'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatSpecMock = require 'WFCatSpecMock'
local WFCatTestSession = require 'WFCatTestSession'

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

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

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

local MB = 1024 * 1024

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

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

local function getCameraRaw( useBigXmpDevelop )

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

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

	xmpCameraRaw = xmpCameraRaw .. xmpCameraRawEnd

	return xmpCameraRaw

end

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

local WFCatDesktopBinaryUploaderTest = {}

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

local sCoveragePath

function WFCatDesktopBinaryUploaderTest.setUp( inCoveragePath )
	sCoveragePath = inCoveragePath
end

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

local function nonNilToTrue( value )
	return value ~= nil
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 getDiagnosticLogErrorsIgnoringAccountError( uploader )

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

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

local function removeTooManyRequestsError( errorMessage )

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

end

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

local function removeTimeoutError( errorMessage )

	return string.gsub( errorMessage, '<li>(.[^<]+)The operation timed out(.[^<]+)</li>', '' )

end

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

local function removeMustExistError( errorMessage )

	return string.gsub( errorMessage, '<li>(.[^<]+)must exist(.[^<]+)</li>', '' )

end

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

local function removeResourceNotFoundError( errorMessage )

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

end

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

local function removeUploadMasterWhenNotAllowedError( errorMessage )

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

end

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

local function removeShaDoesNotMatchError( errorMessage )

	return string.gsub( errorMessage, '<li>(.[^<]+)does not match(.[^<]+)</li>', '' )

end

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

local function removeMarkingOriginalInvalidError( errorMessage )

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

end

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

local function removeDebugForceRestartError( errorMessage )

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

end

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

local options = {

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

	uploadBlackoutWindowInSeconds = 30,

	uploadBlackoutWindowCount = 2,

	isCatalogPuller = true

}

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

local uploaderOptions = {
	fallbackToOzUploadForOriginalsOnly = true
}

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

local function setupImportTestSession( rxTest, chunkFileSize, chunkSize, debugForceRestart, ignoreErrors )

	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 chunkedUploader = WFCatChunkedUploader( docStore, catalogSpec, chunkFileSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

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

		local diagnosticLogErrors = getDiagnosticLogErrorsIgnoringAccountError( uploader )

		if debugForceRestart then
			diagnosticLogErrors = removeDebugForceRestartError( diagnosticLogErrors )
		end

		if not ignoreErrors then
			rxTest:expectEqual( "", removeEmptyErrorHeader( diagnosticLogErrors ) )
		end

	end )

	return catalogTestSession, docStore, catalog

end

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

local function countDocs( docStore )

	return Observable.timer( .1, 1, WFRxScheduler.sharedScheduler() )

		:flatMapLatest( function()

			return docStore:changedDocs { fromChangeCounter = 0 }

		end )

		:map( function( changedDocs )

			local nAssetsWithOriginalLinks = 0
			local nAssetsWithShaError = 0
			local nAssetsWithFilePath = 0

			for _, docValue in ipairs( changedDocs ) do

				if docValue.type ~= "catalog" then

					if WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) then
						nAssetsWithOriginalLinks = nAssetsWithOriginalLinks + 1
					end

					local sha256Error = derefOpt( docValue,
												  'importSource',
												  'status',
												  'error',
												  'all',
												  'reason',
												  'sha256' ) == "does not match"

					local invalidOriginal = derefOpt( docValue,
												  '_localOnly',
												  'localOriginalError' ) == "InvalidLocalOriginal"


					if sha256Error and invalidOriginal then
						nAssetsWithShaError = nAssetsWithShaError + 1
					end

					if WFCatFileSpec.getOriginalVolumeGuidFromDocValue( docValue ) and
						WFCatFileSpec.getOriginalRelativePathFromDocValue( docValue ) then
						nAssetsWithFilePath = nAssetsWithFilePath + 1
					end

				end

			end

			return nAssetsWithOriginalLinks, nAssetsWithFilePath, nAssetsWithShaError

		end )

end

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

local function getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	local copiedRevisionId, copiedOriginalFilePath, copiedProxyFilePath, copiedXmpSidecarFilePath

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

	return copiedRevisionId, copiedOriginalFilePath, copiedProxyFilePath, copiedXmpSidecarFilePath

end

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

local function generateSidecarFile( rxTest, importSidecarPath )

	local sidecarData = KSData.make( [[<?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"?>
]] )
	KSData.saveToFile( sidecarData, importSidecarPath )

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

	return KSData.length( sidecarData )

end

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

local function downloadPayloadForAssetId( docStore, catalogSpec, assetId )

	return docStore:ongoingDocValue( assetId )
		:combineLatest( Observable.timer( .1, 8, WFRxScheduler.sharedScheduler() ) ) -- poll as well
		:pluck( '_serverProvidedMetadata', 'links', 'self', 'href' )
		:filter( function( url )
			return not not url
		end )
		:flatMapLatest( function( url )
			return catalogSpec:getBinary( url )
		end )
		:map( function( response, headers )

			if headers.status_code ~= '200' then
				return false
			end

			return {
				_id = assetId,
				_serverProvidedMetadata = KSJsonDecoder.decodeWebSafeJson( response )
			}

		end )
end

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

local function downloadXmpSidecarForAssetId( docStore, catalogSpec, assetId )

	return docStore:ongoingDocValue( assetId )
		:combineLatest( Observable.timer( .1, 8, WFRxScheduler.sharedScheduler() ) ) -- poll as well
		:map( function( docValue )
			local getXmpUrl = WFCatFileSpec.getXmpLinkFromDocValue( docValue )

			if getXmpUrl == nil then
				getXmpUrl = WFCatFileSpec.getXmpLinkFromDocValue( docValue )
			end

			return getXmpUrl
		end )
		:filter( function( url )
			return not not url
		end )
		:distinctUntilChanged()
		:flatMapLatest( function( getXmpUrl )
			return catalogSpec:getBinary( getXmpUrl )
		end )
		:map( function( response, headers )

			if headers.status_code ~= '200' then
				return false
			end

			return tostring( response )

		end )
end

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

local function readLocalSidecarFile( rxTest, docStore, assetId )

	local sidecarContents

	rxTest:expectSingleValue( docStore:docValue( assetId )
		:map( function( docValue )

			local localXmpFilePath = WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue )

			if localXmpFilePath then
				local sidecarFile = io.open( localXmpFilePath, 'rb' )
				sidecarContents = sidecarFile:read( '*all' )
				sidecarFile:close()
			else
				return false, docValue
			end

			return #sidecarContents > 0

		end )
	, true )

	return sidecarContents

end

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

function WFCatDesktopBinaryUploaderTest.rxTestImportAndUploadOneFolder_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local _, docStore, catalog = setupImportTestSession( rxTest )

	local importArray = {
		{
			filePath = KSPathUtils.child( testDataFolder1, "IMG_7706.PNG" ),
			width = 640,
			height = 1136,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "IMG_7706.PNG",
			fileSize = 9609,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "IMG_7763.jpg" ),
			width = 3264,
			height = 2448,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "IMG_7763.jpg",
			fileSize = 2072173,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "IMG_7766.jpg" ),
			width = 1200,
			height = 1600,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "IMG_7766.jpg",
			fileSize = 703991,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "IMG_9876.CR2" ),
			width = 2048,
			height = 3456,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "IMG_9876.CR2",
			fileSize = 26243176,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "NikonPhotoworldSafariMiami281.NEF" ),
			width = 3872,
			height = 2592,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "NikonPhotoworldSafariMiami281.NEF",
			fileSize = 9844596,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "ir-trees.dng" ),
			width = 500,
			height = 334,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "ir-trees.dng",
			fileSize = 165598,
		},
		-- duplicate (don't import)
		{
			filePath = KSPathUtils.child( testDataFolder1, "ir-trees2.dng" ),
			width = 500,
			height = 334,
			xmpCameraRaw = '<xmp:camera stuff .. />',
			fileName = "ir-trees2.dng",
			fileSize = 165598,
		},
		{
			filePath = KSPathUtils.child( testDataFolder1, "IMG_7772.mp4" ),
			isVideo = true,
			width = 270,
			height = 480,
			xmpCameraRaw = '<xmp />',
			fileName = 'IMG_7772.mp4',
			fileSize = 211332,
		}
	}

	local importOptions = {
		batchSize = 1,
		numParallel = 1,
	}

	local addImageStream = rxTest:observe( WFCatImport.addImageArrayToCatalog( catalog, nil, importArray, nil, importOptions )
		:map( function( importResult )
			if importResult.err then
				importResult.filePath = KSPathUtils.leafName( importResult.filePath )
			end
			return importResult
		end )
		)
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectData( { err = 'matchesExistingFileWithinSession', filePath = 'ir-trees2.dng' } )
	addImageStream:expectData( ':ignore:' )
	addImageStream:expectCompleted()

	local filteredChangedDocs = countDocs( docStore )
		-- successfully uploaded docs will have an original link

	local fcd = rxTest:observe( filteredChangedDocs )

	fcd:expectDataWithinTime( 360, 7, 7, 0 )
		-- 6 photos and 1 video

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadWithSidecar_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local testFileName = "IMG_9876.CR2"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	local importSidecarPath = KSPathUtils.child( importFolder, 'IMG_9876.XMP' )
	generateSidecarFile( rxTest, importSidecarPath )

	local _, docStore, catalog = setupImportTestSession( rxTest )

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			additionalInfo = {
				XMPSideCarPath = importSidecarPath,
			},
			filePath = importFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } ),
		":ignore:" )

	local assetDocId
	local localXmpFileSize

	local function countDocsWithSidecarsFiles( docStore )

		return Observable.timer( 1, 1, WFRxScheduler.sharedScheduler() )

			:flatMapLatest( function()

				return docStore:changedDocs { fromChangeCounter = 0 }

			end )

			:map( function( changedDocs )

				local nAssetsWithSidecarPendingUpload = 0

				for _, docValue in ipairs( changedDocs ) do

					if docValue.type ~= "catalog" then

						assetDocId = docValue._id
						localXmpFileSize = WFCatFileSpec.getXmpSidecarFileSizeFromDocValue( docValue )

						local sidecarUploadPending = derefOpt( docValue, '_localOnly', 'xmpSidecarExists' )
									and not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/xmp', 'href' )
						nAssetsWithSidecarPendingUpload = sidecarUploadPending and 1 or 0

					end

				end

				return nAssetsWithSidecarPendingUpload

			end )

	end


	local fcd = rxTest:observe( countDocsWithSidecarsFiles( docStore ) )

	-- Verify the sidecar upload is pending.
	fcd:expectDataWithinTime( 240, 1 )

	-- Verify the sidecar upload is cleared.
	fcd:expectDataWithinTime( 240, 0 )

	local sidecarContents = readLocalSidecarFile( rxTest, docStore, assetDocId )

	-- make sure the sidecar on oz matches the local sidecar file
	local xmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalog:ozCatalogSpec(), assetDocId ) )
	xmpContentsObserver:expectDataWithinTime( 60, sidecarContents )

end

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

local function bulkDelete( catalog, docIds )

	-- very simple bulkDelete that does not delete album_assets automatically

	local overlays = {}

	for i, docId in ipairs( docIds ) do
		overlays[ #overlays + 1 ] = {
			_id = docId,
			_deleted = true,
			_localOnly = { deletedLocally = true },
		}
	end

	return WFRxBatchedBulkApplyOverlay( "bulkDeleteTest", catalog:docStore(), overlays )

end

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

local function deleteDoc( catalog, docId )

	return bulkDelete( catalog, { docId } )
end


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

local function testChunkedUpload( rxTest, debugForceRestart, negativeFileSize )

	rxTest:setTimeLimit( 360 )

	local bigFileName = "IMG_9876.CR2"
	local bigFilePath = KSPathUtils.child( testDataFolder1, bigFileName )
		-- file is 26MB

	local _, docStore, catalog = setupImportTestSession( rxTest, 20*MB, 5*MB, debugForceRestart, negativeFileSize )
		-- chunk files larger than 20MB, using 5MB chunks

	if negativeFileSize then

		-- Attempting to upload a asset document with a negative file size will result
		-- in the document being rejected. The error handler subsequently removes the file
		-- size from the doc, allowing it to upload. However, the negative file size
		-- annotation remains in the local only section.

		local errorHandler = WFCatErrorHandler( catalog,{
			deleteAssets = bulkDelete,
			deleteIndividualAlbum = deleteDoc },
			{ debugMode = true } )

		rxTest:addCloseTarget( errorHandler )

	end

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = bigFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = bigFileName,
			fileSize = negativeFileSize and -100 or WFFileUtils.fileAttributes( bigFilePath ).fileSize,
			} } ),
		":ignore:" )

	local filteredChangedDocs = countDocs( docStore )
		-- successfully uploaded docs will have an original link

	local fcd = rxTest:observe( filteredChangedDocs )

	fcd:expectDataWithinTime( 240, 1, 1, 0 )

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUpload_testNightly( rxTest )

	testChunkedUpload( rxTest )

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadWithNegativeFileSize_testNightly( rxTest )

	-- The following test verifies that the binary uploader will recalculate file
	-- size for a particular asset if the file size provided in the local only
	-- annotation is negative. This test verifies the fix for WF-1730.

	testChunkedUpload( rxTest, false, true )

end

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

function WFCatDesktopBinaryUploaderTest.DISABLE_rxTestChunkedUpload_forceRestart_testNightly( rxTest )

	-- This test was only used by the old chunked uploader, content range uploader
	-- doesn't have a way to force a restart.

	testChunkedUpload( rxTest, true )

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 WFCatSpecSpy = {
		originalCatalogSpec = catalogSpec,
		getBinary = getBinaryFn,
		putBinary = putBinaryFn,

	}

	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 WFCatDesktopBinaryUploaderTest.rxTestUploadWithZeroLengthFile_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, options, catalogSpecSpy, uploaderOptions )

	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 = "IMG_0000.jpg"
	local testFilePath = KSPathUtils.child( importFolder, testFileName )

	local file = io.open( testFilePath, 'wb' )
	file:close()

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine

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

	local uploadStatusObserver = rxTest:observe( uploader:errorStatus()
		:map( function( assetId, errorMessage )
			return assetId, KSJsonDecoder.decodeWebSafeJson( errorMessage )
		end ) )

	uploadStatusObserver:expectDataWithinTime( 240, ':ignore:', {
		code = 1005,
		description = "Input Validation Error",
		errors = {
			payload = {
				"must exist",
			},
		},
		subtype = "InputValidationError",
	} )

	-- Verify the corrupted master has been flagged as an invalid file.

	rxTest:expectSingleValue(
		docStore:docValue( assetId )
			:pluck( '_localOnly', 'localOriginalError' ),
		'InvalidLocalOriginal' )

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

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadWithZeroLengthFile_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 uploader = WFCatBinaryUploader(
			catalog, nil, options, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )
	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that is large enough to be chunked so the
	-- documents fileSize will be large enought to send it to the
	-- chunked uploader.

	local testFileName = "IMG_9876.CR2"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- Now change the file to have 0 bytes in it, speculation that the migrator
	-- may not be validating the file length matches the importSource.fileSize.

	local originalFilePath
	local file

	rxTest:waitFor(
		docStore:docValue( assetId )
			:tap( function( docValue )

				originalFilePath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )

				file = io.open( originalFilePath, 'wb' )
				file:close()

			end ) )

	-- start up sync engine

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

	local uploading = rxTest:observe( uploader:isDoingWork() )

	uploading:expectDataWithinTime( 60, true )

	uploading:expectDataWithinTime( 60, false )

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

	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_localOnly', 'localOriginalError' )
	):expectDataWithinTime( 40, 'InvalidLocalOriginal' )

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

end

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


function WFCatDesktopBinaryUploaderTest.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, options, catalogSpecSpy, uploaderOptions )

	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

	rxTest:waitFor( 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


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

function WFCatDesktopBinaryUploaderTest.rxTestUploadWithInvalidSha256_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, options, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file.

	local testFileName = "IMG_7763.jpg"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:map( function( result )
				assetId = result.assetId
				return type( assetId ), result
			end ),
		"string", ":ignore:" )

	-- Now corrupt the file before it's uploaded to simulate the http put
	-- corrupting the file, this will cause the SHA256 that was calculated
	-- during import to no longer match.

	local originalFilePath

	rxTest:waitFor(
		docStore:docValue( assetId )
			:tap( function( docValue )

				originalFilePath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )

				local file = io.open( originalFilePath, 'wb' )
				file:write("garbage")
				file:close()

			end ) )

	-- start up sync engine

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

	local uploadStatusObserver = rxTest:observe( uploader:errorStatus()
		:map( function( assetId, errorMessage )
			return assetId, KSJsonDecoder.decodeWebSafeJson( errorMessage )
		end ) )

	uploadStatusObserver:expectDataWithinTime( 240, ':ignore:', {
		code = 1005,
		description = "Input Validation Error",
		errors = {
			sha256 = {
				"does not match",
			},
		},
		subtype = "InputValidationError",
	} )

	puller:stopContinuousUpdates()
	pusher:stopContinuousUpdates()
	uploader:stopContinuousUploads()

	-- Verify the corrupted master has been flagged as an invalid file.

	rxTest:expectSingleValue(
		docStore:docValue( assetId )
			:pluck( '_localOnly', 'localOriginalError' ),
		'InvalidLocalOriginal' )

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

	uploader:DEBUG_resetDiagnosticLogErrors()

	-- Fix the corrupted file, and remove the error marker.
	-- This demonstrates if the file was not corrupted locally on disk
	-- but had a corruption during upload the binary uploader we try to
	-- upload the file again.

	WFFileUtils.delete( originalFilePath )
	WFFileUtils.copy( testFilePath, originalFilePath )

	rxTest:waitFor( docStore:applyOverlay {
		_id = assetId,
		_localOnly = {
			localOriginalError = { _delete = true },
		},
	} )

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

	rxTest:expectSingleValue(
		docStore:docValue( assetId )
			:pluck( 'importSource', 'status', 'error', 'all', 'reason', 'sha256'),
		nil )

	-- Verify the retry successfully uploaded doc, it will have an original link.

	local filteredChangedDocs = countDocs( docStore )

	local fcd = rxTest:observe( filteredChangedDocs )

	fcd:expectDataWithinTime( 240, 1, 1, 0 )

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

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadWithInvalidSha256_corruptUpload_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 chunkFileAtSize = 5*MB
	local chunkSize = 5*MB
	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpec, chunkFileAtSize, chunkSize )

	local tempFilePath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'temp' ),
					rxTest:testName() .. KSIdGenerator.uuidInLowercaseHex() )

	local file = io.open( tempFilePath, 'w+b' )
	file:write("garbage")
	file:close()

	rxTest:finally( function()
		WFFileUtils.delete( tempFilePath )
	end )
	local chunkedUploadCalls = 0

	local hackedChunkedUploader = {

		doChunkedUpload = function( self, masterUrl, filePath, options, docId )
			chunkedUploadCalls = chunkedUploadCalls + 1
			if chunkedUploadCalls <= 1 then
				-- substitute a corrupt file the first time, but the second time we try to upload
				-- use the correct file to upload
				return chunkedUploader:doChunkedUpload( masterUrl, tempFilePath, options )
			else
				return chunkedUploader:doChunkedUpload( masterUrl, filePath, options )
			end
		end,
		chunkFileSize = function( self )
			return chunkedUploader:chunkFileSize()
		end,
		minimumChunkSize = function( self )
			return chunkedUploader:minimumChunkSize()
		end,
		setChunkSize = function( self, size )
			return chunkedUploader:setChunkSize( size )
		end,
	}
	hackedChunkedUploader = Rx.Class( WFCatChunkedUploaderClassName, hackedChunkedUploader )()

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, hackedChunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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


	-- Import a file that will use the chunked uploader.

	local testFileName = "NikonPhotoworldSafariMiami281.NEF" -- file is 9MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- wait for master to be uploaded successfully
	local isMasterUploadedSuccessfully = rxTest:observe( docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			local firstRevision = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids', 1 )

			local sha256Error = KSTableUtils.derefOpt( docValue, 'importSource', 'status', 'error', 'all', 'reason', 'sha256' )

			local masterLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )

			local masterLinkIsForFirstRevision = false
			if firstRevision and masterLink then
				masterLinkIsForFirstRevision = not not string.find( masterLink, firstRevision )
			end

			local localFileSize = derefOpt( docValue, '_localOnly', 'files', 'original', 'master', 'size' )
			local ozFileSize = derefOpt( docValue, 'importSource', 'fileSize' )

			return sha256Error,
				ozFileSize == localFileSize,
				not not masterLink,
				masterLinkIsForFirstRevision

		end ) )

	isMasterUploadedSuccessfully:expectDataWithinTime( 340, nil, true, true, false )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadRetryDelay429_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 chunkedUploadPutCount = 0

	local retryCount = 0
	local retryDuration = 0
	local retryDelay = 10
	local errorEncountered

	local WFCatSpecSpy = {

		serverBaseUrl = catalogSpec.serverBaseUrl,
		catalogId = catalogSpec.catalogId,
		ozUserId = catalogSpec.ozUserId,
		catalogHref = catalogSpec.catalogHref,
		catalogUpdatesUrl = catalogSpec.catalogUpdatesUrl,
		get = catalogSpec.get,
		put = catalogSpec.put,
		getBinary = catalogSpec.getBinary,

		putBinary = function( self, url, data, options, progressReporter )

			chunkedUploadPutCount = chunkedUploadPutCount + 1
			if chunkedUploadPutCount == 3 then

				local optionsWithReturn429 = KSTableUtils.deepCopy( options )

				table.insert( optionsWithReturn429.requestHeaders, {
					header = 'X-Return-429-Status',
					value = 1,
				} )

				table.insert( optionsWithReturn429.requestHeaders, {
					header = 'X-Return-Retry-After',
					value = retryDelay,
				} )

				optionsWithReturn429.maxAttempts = 2

				local startTime = KSDate.timeNowForAnimation()

				local function logResponseOrError( responseData, responseHeaders )
					errorEncountered = responseHeaders.status_code
					retryDuration = KSDate.timeNowForAnimation() - startTime
					retryCount = responseHeaders.retryCount or 0
				end

				return catalogSpec:putBinary( url, data, optionsWithReturn429, progressReporter )
					:takeLast( 1 )
					:tap( logResponseOrError, logResponseOrError )

			else

				return catalogSpec:putBinary( url, data, options, progressReporter )

			end

		end,
	}
	local catalogSpecSpy = Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

	local chunkFileAtSize = 20*MB
	local chunkSize = 5*MB
	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpecSpy, chunkFileAtSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that will use the chunked uploader.

	local testFileName = "IMG_9876.CR2" -- file is 26MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- wait for master to be uploaded successfully
	local isMasterUploadedSuccessfully = rxTest:observe( docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			local masterLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			return masterLink ~= nil
		end ) )

	isMasterUploadedSuccessfully:expectDataWithinTime( 240, true )

	rxTest:expectEqual( '429', errorEncountered )

	-- maxAttempts set to 2 above
	rxTest:expectEqual( 2, 1 + retryCount )

	-- Make sure WFRxHttpClient imposed the delay on the retries and after the 429 failure.
	rxTest:expectEqual( true, retryDuration >= ( 1 + retryCount ) * retryDelay )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestStopDisposesActiveUploads_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 numPutsActiveObservable = Rx.ReplaySubject(1)

	local numPutsActive = 0
	local maxPutsActive = 0
	local numPutsCompleted = 0

	-- Start out delaying all puts by 30 seconds so we have time to queue up everything
	local putDelaySeconds = 30

	local WFCatSpecSpy = {

		serverBaseUrl = catalogSpec.serverBaseUrl,
		catalogId = catalogSpec.catalogId,
		ozUserId = catalogSpec.ozUserId,
		catalogHref = catalogSpec.catalogHref,
		catalogUpdatesUrl = catalogSpec.catalogUpdatesUrl,
		get = catalogSpec.get,
		put = catalogSpec.put,
		getBinary = catalogSpec.getBinary,

		putBinary = function( self, url, data, options, progressReporter )

			numPutsActive = numPutsActive + 1
			maxPutsActive = math.max( maxPutsActive, numPutsActive )
			numPutsActiveObservable:onNext( numPutsActive )

			local delay = putDelaySeconds
			if string.find( options.requestDescription, "Initiate Chunked Upload") then
				-- let the chunked upload initiate go through immediately so we can start the chunks
				delay = 0
			end

			return Observable.timer( delay, WFRxScheduler.sharedScheduler() )
				:flatMap( function()
					return catalogSpec:putBinary( url, data, options, progressReporter )
					:skip( 1 ) -- Skip the in-progress response.
					:tapOnCompleted( function()
						numPutsCompleted = numPutsCompleted + 1
					end )
					:finally( function()
						numPutsActive = numPutsActive - 1
						numPutsActiveObservable:onNext( numPutsActive )
					end )
				end )
				:startWith( 'requestInProgress' )

		end,

	}

	local catalogSpecSpy = Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

	local chunkFileAtSize = 5*MB
	local chunkSize = 5*MB
	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpecSpy, chunkFileAtSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that will use the chunked uploader.

	local testFileName = "IMG_9876.CR2" -- file is 26MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )
	local testFileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize

	local testFileName2 = "IMG_7763.jpg"
	local testFilePath2 = KSPathUtils.child( testDataFolder1, testFileName2 )
	local testFileSize2 = WFFileUtils.fileAttributes( testFilePath2 ).fileSize

	local assetId
	local assetId2

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
				filePath = testFilePath,
				width = 2048,
				height = 1024,
				fileName = testFileName,
				fileSize = testFileSize,
			},
			{
				filePath = testFilePath2,
				width = 2048,
				height = 1024,
				fileName = testFileName2,
				fileSize = testFileSize2,
			} } )
			:tap( function( result )
				if KSPathUtils.leafName( result.filePath ) == "IMG_9876.CR2" then
					assetId = result.assetId
				else
					assetId2 = result.assetId
				end
			end )
			:toArray(),
		":ignore:" )

	-- Start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()

	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )

	uploader:startContinuousUploads()

	-- verify the various puts have been issued
	local numPutsActiveObserved = rxTest:observe( numPutsActiveObservable )
	numPutsActiveObserved:expectDataWithinTime( 20, 5 ) -- 1 initiate + 6 chunks + 1 file = 7 but rate limited to 5 uploads

	observeBinariesInFlight:expectDataWithinTime( 10, 2 )

	uploader:stopContinuousUploads()

	-- verify numBinariesInFlight
	observeBinariesInFlight:expectDataWithinTime( 10, 0 )

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 10, false )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadTimeoutOnChunk_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 timeoutInjected = false
	local sendTimeOuts = Rx.ReplaySubject(1)

	local putCounter = 0

	local WFCatSpecSpy = {

		serverBaseUrl = catalogSpec.serverBaseUrl,
		catalogId = catalogSpec.catalogId,
		ozUserId = catalogSpec.ozUserId,
		catalogHref = catalogSpec.catalogHref,
		catalogUpdatesUrl = catalogSpec.catalogUpdatesUrl,
		get = catalogSpec.get,
		put = catalogSpec.put,
		getBinary = catalogSpec.getBinary,

		putBinary = function( self, url, data, options )

			putCounter = putCounter + 1

			if putCounter == 3 then

				timeoutInjected = true

				return Observable.just( "The operation timed out\r\ ", nil )
					:sample( sendTimeOuts )
					:delay( 1, WFRxScheduler.sharedScheduler() ) -- wait one more second to make sure the other chunk is processed
					:flatMap( function( ... )
						return Rx.Observable.throw( ... )
					end )
					:startWith( 'requestInProgress' )

			end

			local putRequest = catalogSpec:putBinary( url, data, options )
				:tapOnCompleted( function()
					sendTimeOuts:onNext( true ) -- send the timeouts now that we have completed a chunk
				end )

			return putRequest

		end,

	}

	local catalogSpecSpy = Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpecSpy )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions)

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that will use the chunked uploader.

	local testFileName = "IMG_9876.CR2" -- file is 26MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )
	local testFileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = testFileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- Start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- Verify a timeout was sent
	local observeSendTimeOuts = rxTest:observe( sendTimeOuts )
	observeSendTimeOuts:expectDataWithinTime( 240, true )

	-- Wait for master to be uploaded successfully
	local isMasterUploadedSuccessfully = rxTest:observe( docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			local masterLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			return masterLink ~= nil
		end ) )

	isMasterUploadedSuccessfully:expectDataWithinTime( 240, true )

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 240, false )

	rxTest:expectEqual( true, timeoutInjected )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadTimeoutOnRegularUpload_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 numTimeoutsToInject = 1
	local numTimeouts = 0
	local sendTimeOuts = Rx.ReplaySubject(1)

	local WFCatSpecSpy = {

		serverBaseUrl = catalogSpec.serverBaseUrl,
		catalogId = catalogSpec.catalogId,
		ozUserId = catalogSpec.ozUserId,
		catalogHref = catalogSpec.catalogHref,
		catalogUpdatesUrl = catalogSpec.catalogUpdatesUrl,
		get = catalogSpec.get,
		put = catalogSpec.put,
		getBinary = catalogSpec.getBinary,

		putBinary = function( self, url, data, options )

			if options.dataFormat == "filePath" and KSPathUtils.leafName( data ) == "IMG_7763.jpg" and numTimeouts < numTimeoutsToInject then

				numTimeouts = numTimeouts + 1

				return Observable.just( "The operation timed out\r\ ", nil )
					:sample( sendTimeOuts )
					:delay( 1, WFRxScheduler.sharedScheduler() ) -- wait one more second to make sure the other chunk is processed
					:flatMap( function( ... )
						return Rx.Observable.throw( ... )
					end )
					:startWith( 'requestInProgress' )

			end

			local putRequest = catalogSpec:putBinary( url, data, options )

			putRequest = putRequest
				:tap( function( responseData, responseHeader )

					if type( responseHeader ) == 'table' and responseHeader.status_code == '201' and options.length then

						sendTimeOuts:onNext( true ) -- send the timeouts now that we have completed a chunk

					end

				end )


			return putRequest

		end,

	}

	local catalogSpecSpy = Rx.Class( 'WFCatSpec', WFCatSpecSpy )()

	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpecSpy )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that will use the chunked uploader.

	local testFileName = "IMG_9876.CR2" -- file is 26MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )
	local testFileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize

	local testFileName2 = "IMG_7763.jpg"
	local testFilePath2 = KSPathUtils.child( testDataFolder1, testFileName2 )
	local testFileSize2 = WFFileUtils.fileAttributes( testFilePath2 ).fileSize

	local assetId
	local assetId2

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
				filePath = testFilePath,
				width = 2048,
				height = 1024,
				xmpCameraRaw = '<xmp />',
				fileName = testFileName,
				fileSize = testFileSize,
			},
			{
				filePath = testFilePath2,
				width = 2048,
				height = 1024,
				xmpCameraRaw = '<xmp />',
				fileName = testFileName2,
				fileSize = testFileSize2,
			} } )
			:tap( function( result )
				local fileName = KSPathUtils.leafName( result.filePath )
				if fileName == "IMG_9876.CR2" then
					assetId = result.assetId
				elseif fileName == "IMG_7763.jpg" then
					assetId2 = result.assetId
				else
					rxTest:expectEqual( "Unexpected fileName", result )
				end
			end )
			:toArray(),
		":ignore:" )

	-- Start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- Verify a timeout was sent
	local observeSendTimeOuts = rxTest:observe( sendTimeOuts )
	observeSendTimeOuts:expectDataWithinTime( 240, true )

	-- Wait for master to be uploaded successfully (the chunked upload)
	local isMasterUploadedSuccessfully = rxTest:observe( docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			local masterLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			return masterLink ~= nil
		end ) )
	isMasterUploadedSuccessfully:expectDataWithinTime( 240, true )

	-- check to see if the regular upload was successful
	local isMaster2UploadedSuccessfully = rxTest:observe( docStore:ongoingDocValue( assetId2 )
		:map( function( docValue )
			local masterLink = KSTableUtils.derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master', 'href' )
			return masterLink ~= nil
		end ) )
	isMaster2UploadedSuccessfully:expectDataWithinTime( 240, true )

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 240, false )

	rxTest:expectEqual( numTimeoutsToInject, numTimeouts )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadWithInvalidSha256_corruptLocal_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 chunkFileAtSize = 5*MB
	local chunkSize = 5*MB
	local chunkedUploader = WFCatChunkedUploader( docStore, catalogSpec, chunkFileAtSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that will use the chunked uploader.

	local testFileName = "NikonPhotoworldSafariMiami281.NEF" -- file is 9MB
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- Now that it's been imported, corrupt the file before it's uploaded.

	local originalFilePath

	rxTest:waitFor(
		docStore:docValue( assetId )
			:tap( function( docValue )

				originalFilePath = WFCatFileSpec.buildOriginalPathFromDocValue( docValue )

				local file = io.open( originalFilePath, 'r+b' )
				file:write("garbage")
				file:close()

			end ) )

	-- start up sync engine

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

	-- verify that the file is marked with a localOriginalError

	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_localOnly', 'localOriginalError' )
	):expectDataWithinTime( 340, 'InvalidLocalOriginal' )

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

end

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

local function addWillNotUploadMasterToDoc( catalog, assetId )

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

end

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

local function testUploadWithWillNotSendMasterFlag( rxTest, useChunkedUploader )

-- The scenario supposed happens when the migrator has an already existing asset
-- from lightroom that hasn't been downloaded into the catalog merged with an
-- asset imported from the migration.  Theory is the willNotUploadMaster flag
-- is added back on from the changes feed.

	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 testFileName = "IMG_7763.jpg"

	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, noRetryOnPutError )

	local chunkedUploader = nil

	if useChunkedUploader then

		testFileName = "IMG_9876.CR2" -- file is 26MB

		local chunkFileAtSize = 20*MB
		local chunkSize = 5*MB
		chunkedUploader = WFCatChunkedUploader( docStore, catalogSpecSpy, chunkFileAtSize, chunkSize )

	end

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, nil, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import the file.

	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId
	local expectedResult = {
		localId = 1,
		filePath = testFilePath,
		assetId = 'AssetIdNotSet',
		operation = "AddAsset",
	}

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:map( function( result )
				assetId = result.assetId
				if assetId then
					expectedResult.assetId = assetId
				end
				return result
			end ),
		expectedResult )

	-- Now pretend like the willNotUploadMaster flag got merged in from the
	-- changes feed.

	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )

	-- Verify the doc we imported does not have an original link yet.

	do
		local filteredChangedDocs = countDocs( docStore )
		local fcd = rxTest:observe( filteredChangedDocs )
		fcd:expectDataWithinTime( 240, 0, 1, 0 )
	end

	-- start up sync engine

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

	local uploadStatusObserver = rxTest:observe( uploader:errorStatus()
		:map( function( assetId, errorMessage )
			return assetId, KSJsonDecoder.decodeWebSafeJson( errorMessage )
		end ) )


	-- Oz doesn't generate an error if the willNotUploadMaster flag is set when using
	-- the chunked upload api, it also doesn't set importSource.status.  And it does
	-- update the changes feed with a master link when completed.

	if not useChunkedUploader then

		uploadStatusObserver:expectDataWithinTime( 120, ':ignore:', {
			code = 'UploadMasterWhenNotAllowedError',
			description = "Cannot upload, unset the flag to upload master",
			errors = {},
			subtype = "UploadMasterWhenNotAllowedError",
		} )

		-- Stop and restart the sync engine to avoid waiting for the retry timeout.
		-- To make test run in 35 secs instead of 75.

		puller:stopContinuousUpdates()
		pusher:stopContinuousUpdates()
		uploader:stopContinuousUploads()

		-- Restart the sync engine.

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

	end

	-- Verify we successfully uploaded the doc, it will have an original link.

	do
		local filteredChangedDocs = countDocs( docStore )
		local fcd = rxTest:observe( filteredChangedDocs )
		fcd:expectDataWithinTime( 240, 1, 1, 0 )
	end

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

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadWithWillNotSendMasterFlag_testNightly( rxTest )
	testUploadWithWillNotSendMasterFlag( rxTest, false )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestChunkedUploadWithWillNotSendMasterFlag_testNightly( rxTest )
	testUploadWithWillNotSendMasterFlag( rxTest, true )
end


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

local function hasRequiredLinks( docValue, otherLink )

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

	local requiredLinks = {
		'self',
		'/rels/master_create',
		'/rels/proxy_create',
		'/rels/rendition_create',
		'/rels/xmp_develop_create',

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

	if otherLink then
		table.insert( requiredLinks, 1, otherLink )
	end

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

	return "Has required Links in docValue"

end

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

local function hasRequiredLinksWithMaster( docValue )

	return hasRequiredLinks( docValue, '/rels/master' )

end

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

local function testCopyInsteadOfUpload( rxTest, testVideo )

	-- This test validates that we can copy an asset instead of uploading it
	-- if we find another asset with the same sha hash exists on oz.
	-- Lightroom CC prevents adding duplicates during import but has no
	-- such rules preventing duplicates from being uploaded once they
	-- exist.

	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 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, options, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = 'IMG_7763.JPG'
	local width = 3264
	local height = 2448

	if testVideo then
		testFileName = 'IMG_7772.mp4'
		width = 270
		height = 480
	end

	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	local assetId
	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			isVideo = testVideo,
			filePath = importFilePath,
			width = width,
			height = height,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master' ):map( nonNilToTrue )
	):expectDataWithinTime( 240, true )
	:dispose()

	-- Wait for the first import to upload so imported duplicate will see there
	-- is an asset on oz it can copy.
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 10, 0 )

	-- Import a duplicate of the asset
	local assetId2
	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {

			importIfDuplicate = true,

			isVideo = testVideo,
			filePath = importFilePath,
			width = width,
			height = height,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId2 = result.assetId
			end ),
		":ignore:" )

	rxTest:observe( docStore:ongoingDocValue( assetId2 )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/master' ):map( nonNilToTrue )
	):expectDataWithinTime( 240, true )
	:dispose()

	observeBinariesInFlight:expectDataWithinTime( 10, 0 )

	-- Make sure we saw the copy api being used
	rxTest:expectEqual( true, sawCopyRequest )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyInstead_image_testNightly( rxTest )

	-- Verifies copy api is used instead of upload api if another asset is found
	-- with same sha256 even if asset wasn't copied.

	testCopyInsteadOfUpload( rxTest )

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyInstead_video_testNightly( rxTest )

	-- Verifies copy api is used instead of upload api if another asset is found
	-- with same sha256 even if asset wasn't copied.

	testCopyInsteadOfUpload( rxTest, true )

end

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

local function testUploadCopyAfterAdd_testNightly( rxTest, useBigXmpDevelop, useXmpSidecar )

	-- This test validates the basic functionality of createCopiesOfAssets with
	-- actual http communication with oz.  More detailed checking is done via
	-- mocked http calls below.

	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 uploader = WFCatBinaryUploader(
			catalog, nil, options, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = "IMG_7763.JPG"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	local importSidecarPath
	if useXmpSidecar then
		importSidecarPath = KSPathUtils.child( importFolder, 'IMG_7763.XMP' )
		generateSidecarFile( rxTest, importSidecarPath )
	end

	local xmpCameraRaw = getCameraRaw( useBigXmpDevelop )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = importFilePath,
			additionalInfo = {
				XMPSideCarPath = importSidecarPath,
			},
			width = 3264,
			height = 2448,
			xmpCameraRaw = xmpCameraRaw,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

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

	local copiedAssetCopyId

	local obsCopiedDocHasPaths = rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
		:map( function( docValue )

			copiedAssetCopyId = derefOpt( docValue, 'copy', 'copy_id' )

			-- Check to see if all the files are copied ( original, xmpsidecar, xmpdevelop ).
			-- Proxy isn't there yet as we haven't started the sync engine, and the source asset didn't have one downloaded yet.
			rxTest:expectEqual( 'file', WFFileUtils.exists( WFCatFileSpec.buildOriginalPathFromDocValue( docValue ) ) )

			if useXmpSidecar then
				rxTest:expectEqual( 'file', WFFileUtils.exists( WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue ) ) )
			end

			if useBigXmpDevelop then
				rxTest:expectEqual( 'file', WFFileUtils.exists( derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' ) ) )
			end

			return true

		end ) )
	obsCopiedDocHasPaths:expectDataWithinTime( 60, true )
	obsCopiedDocHasPaths:dispose()
	obsCopiedDocHasPaths = nil

	-- Check that there is a copy id associated with the copy.
	rxTest:expectEqual( true, copiedAssetCopyId ~= nil )

	-- Make sure the source asset has a copy_id that matches the copied asset.
	rxTest:expectSingleValue( docStore:docValue( assetId ):pluck( 'copy', 'copy_id' ), copiedAssetCopyId )

	-- start up sync engine
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- Check that the document changes feed has updated all the expected _serverProvidedMetadata links in the original asset.
	rxTest:observe(
		Observable.combineLatest(
			catalog:docStore():ongoingDocValue( assetId ),
			downloadPayloadForAssetId( docStore, catalogSpec, assetId ) )
		:map( function( docValue, downloadedDocValue )

			local message = hasRequiredLinksWithMaster( docValue )

			if message ~= "Has required Links in docValue" then
				return message, docValue, downloadedDocValue
			end

			return message

		end )
	):expectDataWithinTime( 240, "Has required Links in docValue" )

	-- Check that the document changes feed has updated all the expected _serverProvidedMetadata links in the copied asset.
	rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
		:map( hasRequiredLinksWithMaster )
	):expectDataWithinTime( 240, "Has required Links in docValue" )


	if useXmpSidecar then

		-- Read in the sidecar contents, and get the xmp url
		local sidecarContents = readLocalSidecarFile( rxTest, docStore, assetId )

		-- make sure the sidecar on oz matches the local sidecar file
		local xmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalogSpec, assetId ) )
		xmpContentsObserver:expectDataWithinTime( 60, sidecarContents )
		xmpContentsObserver:dispose()

		-- make sure the copy's sidecar on oz matches the local sidecar file.
		local copiedXmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalogSpec, copiedAssetId ) )
		copiedXmpContentsObserver:expectDataWithinTime( 60, sidecarContents )
		copiedXmpContentsObserver:dispose()

	end

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

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyAfterAdd_Normal_testNightly( rxTest )
	testUploadCopyAfterAdd_testNightly( rxTest, false, false )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyAfterAdd_XmpBoth_testNightly( rxTest )
	testUploadCopyAfterAdd_testNightly( rxTest, true, true )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyAfterAdd_XmpDev_testNightly( rxTest )
	testUploadCopyAfterAdd_testNightly( rxTest, true, false )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopyAfterAdd_XmpSidecar_testNightly( rxTest )
	testUploadCopyAfterAdd_testNightly( rxTest, false, true )
end

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

local function removeOriginalPathFromDoc( catalog, assetId )

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

end

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

local function removeProxyPathFromDoc( catalog, assetId )

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

end

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

local function testUploadCopy( rxTest, testProxy, deleteSource, noLocalFile, addAnotherCopy, willNotUploadMasterOnCopy )

	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 sawResourceNotFoundError = false

	local function watchForResourceNotFoundErrorOnPut( self, url, data, options, progressReporter )
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
			:tapOnError( function( responseData, responseHeader )
				local responseError = KSJsonDecoder.decodeWebSafeJson( responseData )
				if responseError.subtype == "ResourceNotFoundError" then
					sawResourceNotFoundError = true
				end
			end )
	end

	local function watchForResourceNotFoundErrorOnGet( self, url, data, options, progressReporter )
		return self.originalCatalogSpec:getBinary( url, data, options, progressReporter )
			:tapOnError( function( responseData, responseHeader )
				local responseError = KSJsonDecoder.decodeWebSafeJson( responseData )
				if responseError.subtype == "ResourceNotFoundError" then
					sawResourceNotFoundError = true
				end
			end )
	end

	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, watchForResourceNotFoundErrorOnPut, watchForResourceNotFoundErrorOnGet )

	local uploader = WFCatBinaryUploader(
			catalog, nil, options, catalogSpecSpy, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	local conflictResolver = WFCatConflictResolver( catalog:docStore(), catalog:indexStore() )
	rxTest:addCloseTarget( conflictResolver )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = "IMG_7763.JPG"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	local importSidecarPath = KSPathUtils.child( importFolder, 'IMG_7763.XMP' )
	generateSidecarFile( rxTest, importSidecarPath )

	-- Create a large xmpCameraRaw field so xmp_develop file will be created.
	local xmpCameraRaw = getCameraRaw( true )

	local assetId = rxTest:getFirstValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = importFilePath,
			additionalInfo = {
				XMPSideCarPath = importSidecarPath,
			},
			width = 2048,
			height = 1024,
			xmpCameraRaw = xmpCameraRaw,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
		} } )
		:map( function( result )
			return result.assetId
		end ) )

	-- Read in the sidecar contents, and get the xmp url
	local sidecarContents = readLocalSidecarFile( rxTest, docStore, assetId )

	local updatesToSend = 1

	local otherCopiedAssetId
	if addAnotherCopy then

		-- Add a copy of the asset to test the case where we can find the original from a
		-- different asset if the originally copied asset is no longer available.

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

		updatesToSend = 2

	end

	-- start up sync engine

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

	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	-- manually push inital data for more control

	local sendNUpdates = rxTest:observe( pusher:_DEBUG_sendNUpdates( updatesToSend ) )
	sendNUpdates:expectCompletedWithinTime( 5 )
	sendNUpdates:dispose()

	local linkType = testProxy and '/rels/proxy_type/2560' or '/rels/master'

	-- Wait for upload of the original/proxy

	uploadSuccessStream:expectDataWithinTime( 180, assetId )

	-- Wait for the original/proxy to be uploaded so the copy will target the master link on oz.

	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', linkType ):map( nonNilToTrue )
	):expectDataWithinTime( 180, true )

	if otherCopiedAssetId then
		-- Wait for the extra copy to be uploaded so the copy can find the source
		rxTest:observe( docStore:ongoingDocValue( otherCopiedAssetId )
			:pluck( '_serverProvidedMetadata', 'links', linkType ):map( nonNilToTrue )
		):expectDataWithinTime( 120, true )

	end

	-- Verify the xmp file has been uploaded before as the hack to create a fake
	-- Lightroom classic asset by removing the master in the testProxy case below
	-- will cause the sidecar xmp not to upload.
	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp', 'href' )
		:map( function( maybeHref ) return not not maybeHref end )
	):expectDataWithinTime( 120, true )

	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 180, false )

	uploader:stopContinuousUploads()

	-- TODO [matthewj 2019-12-02]: This :sleep is to deal with the fact that the copy is being done locally *before* we get the confirmation
	-- from Oz of the uploaded sidecar. Because of that we have a stale xmp link that we're using in sourceLinkForCopy and so we copy the
	-- old xmp sidecar. This sleep ensures that we're copying the asset after we've pulled the updated xmp link. But that doesn't fix the
	-- race condition in production code - that requires fixing https://jira.corp.adobe.com/browse/WF-688
	rxTest:sleep( 10 )

	if testProxy then

		local revId

		-- Make this look like an asset that was imported by Lightroom Classic.
		rxTest:expectSingleValue( docStore:docValue( assetId )
			:map( function( docValue )
				revId = docValue._revId
				return type( revId )
			end )
		, 'string' )

		rxTest:waitFor( docStore:applyOverlay {

			label = 'oz', -- pretend like this came from oz

			_id = assetId,

			-- avoid conflict resolution by re-using the same revId since we are pretending this came from oz.
			_newRevId = revId,

			_serverProvidedMetadata = {
				links = {
					[ '/rels/master' ] = { _delete = true },
				},
			},
			importSource = {
				willNotUploadMaster = true,
			},
			_localOnly = {
				files = {
					original = {
						master = { _delete = true },
					},

					proxy = {
						[ '2560' ] = {
							relativePath = WFFileUtils.removeVolumeFromPath( importFilePath ),
							volumeGuid = WFFileUtils.getVolumeGuidForPath( importFilePath ),
							size = WFFileUtils.fileAttributes( importFilePath ).fileSize,
						}
					},

				},
			},
		} )

	end

	if noLocalFile then

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

		if testProxy then
			rxTest:waitFor( removeProxyPathFromDoc( catalog, assetId ) )
		end

	end

	local copiedAssetId
	rxTest:expectSingleValue( WFCatCopies.createCopiesOfAssets( catalog, { assetId } )
		:flatMap( function( copiedAssetIds )
			copiedAssetId = copiedAssetIds[ 1 ]
			if willNotUploadMasterOnCopy then
				return docStore:applyOverlay( {
					_id = copiedAssetId,
					importSource = {
						willNotUploadMaster = true
					}
				} )
				:map( function()
					return #copiedAssetIds
				end )
			else
				return Observable.just( #copiedAssetIds )
			end
		end ),
	1 )

	updatesToSend = 1

	if deleteSource then
		-- Delete the source asset.
		rxTest:expectOnlyCompleted( WFCatDelete.deleteAssets( catalog, { assetId } ) )
		updatesToSend = 2
	end

	-- manually push the copy and possibly a deletion

	rxTest:expectOnlyCompleted( pusher:_DEBUG_sendNUpdates( updatesToSend, 5, true ) )

	WFRxScheduler.sharedScheduler():scheduleFuture( ( deleteSource or willNotUploadMasterOnCopy ) and 10 or 2, function()
		-- Delay start of uploader to make sure the deletion/willNotUploadMaster is sent via catalog metadata first.
		uploader:startContinuousUploads()
	end )

	rxTest:waitFor( pusher:sendUpdatesContinuously() )

	if deleteSource and noLocalFile and not addAnotherCopy then

		-- Unfortunately we expect to fail in this case as there is no original to copy

		rxTest:observe( docStore:ongoingDocValue( copiedAssetId )
			:pluck( '_localOnly', 'localOriginalError' )
		):expectDataWithinTime( 120, 'CopyFailedSourceMissing' )

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

	else

		if not deleteSource then

			-- make sure the sidecar on oz matches the local sidecar file
			local xmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalog:ozCatalogSpec(), assetId ) )
			xmpContentsObserver:expectDataWithinTime( 120, sidecarContents )
			xmpContentsObserver:dispose()

		end

		-- Check that all the expected _serverProvidedMetadata proxies and renditions are on the copied asset.
		rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
			:map( hasRequiredLinks )
		):expectDataWithinTime( willNotUploadMasterOnCopy and 300 or 240, "Has required Links in docValue" )

		-- make sure the copied asset has the master link if it's not proxy only
		rxTest:expectSingleValue( docStore:docValue( copiedAssetId )
			:pluck( '_serverProvidedMetadata', 'links', linkType )
			:map( nonNilToTrue ), true )

		-- Make sure the copy isn't marked as invalid.
		rxTest:expectSingleValue( docStore:docValue( copiedAssetId ):pluck( '_localOnly', 'localOriginalError' ), nil )

		-- proxy only assets don't have sidecar files uploaded.
		if not testProxy then
			-- make sure the copied asset's sidecar on oz matches the local sidecar file
			local copiedXmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalog:ozCatalogSpec(), copiedAssetId ) )
			copiedXmpContentsObserver:expectDataWithinTime( 60, sidecarContents )
			copiedXmpContentsObserver:dispose()
		end

		local willUseSourceLinkToMakeCopy = ( not testProxy or noLocalFile )
		local shouldEncounterResourceNotFoundError = deleteSource and willUseSourceLinkToMakeCopy
		rxTest:expectEqual( shouldEncounterResourceNotFoundError, sawResourceNotFoundError )

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

		if willNotUploadMasterOnCopy then
			-- make sure we did get an UploadMasterWhenNotAllowedError
			rxTest:expectEqual( true, removedUploadMasterWhenNotAllowedErrorFromDiagnosticLog >= 1 )
		end
	end

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopy_master_src_loc_testNightly( rxTest )
-- Test uploading a copy of a master where the source asset still exists and there is a local proxy file present.
	testUploadCopy( rxTest, false, false, false, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_master_src_noLoc_testNightly( rxTest )
-- Test uploading a copy of a master where the source asset still exists but no local proxy file present.
	testUploadCopy( rxTest, false, false, true, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_master_noSrc_loc_testNightly( rxTest )
-- Test uploading a copy of a master where the source asset was deleted with local file present.
	testUploadCopy( rxTest, false, true, false, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_master_noSrc_noLoc_testNightly( rxTest )

-- Test uploading a copy of a master where the source asset was deleted and no local file is present.
	testUploadCopy( rxTest, false, true, true, false )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopy_willNotUploadMaster_testNightly( rxTest )

	rxTest:setRetryLimit( 3, 60 )
		-- This case is known to fail frequently.

-- Test uploading a copy of a master where the copy asset has a willNotUploadMaster flag set
	testUploadCopy( rxTest, false, false, false, false, true )

end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_master_otherExists_testNightly( rxTest )
-- Test uploading a copy of a master where the source asset that was copied is deleted
-- before upload and there was no local file during the copy but another asset with
-- the same sha hash exists.

	testUploadCopy( rxTest, false, true, true, true )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopy_proxy_src_loc_testNightly( rxTest )
-- Test uploading a copy of a proxy where the source asset still exists and there is a local proxy file present.
	testUploadCopy( rxTest, true, false, false, false )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopy_proxy_src_noLoc_testNightly( rxTest )
-- Test uploading a copy of a proxy where the source asset still exists but no local proxy file present.
	testUploadCopy( rxTest, true, false, true, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_proxy_noSrc_loc_testNightly( rxTest )
-- Test uploading a copy of a proxy where the source asset was deleted with local file present.
	testUploadCopy( rxTest, true, true, false, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_proxy_noSrc_noLoc_testNightly( rxTest )
-- Test uploading a copy of a proxy where the source asset was deleted and no local file is present.
	testUploadCopy( rxTest, true, true, true, false )
end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestUploadCopy_proxy_otherExists_testNightly( rxTest )
-- Test uploading a copy of a proxy where the source asset that was copied is deleted
-- before upload and there was no local file during the copy but another asset with
-- the same sha hash exists.
	testUploadCopy( rxTest, true, true, true, true )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadCopy_panoNoSidecarLoc_testNightly( rxTest )

	-- This test validates that xmpSidecar data is copied when it hasn't been
	-- downloaded locally.

	-- It also verifies panorama images have the proxy_type/panorama created.

	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 uploader = WFCatBinaryUploader(
			catalog, nil, options, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testImagesPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "assets" )
	local testFileName = "SonyNEX-5_Panorama.JPG"
	local testFilePath = KSPathUtils.child( testImagesPath, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	local importSidecarPath = KSPathUtils.child( importFolder, 'SonyNEX-5_Panorama.XMP' )
	generateSidecarFile( rxTest, importSidecarPath )

	-- Create a large xmpCameraRaw field so xmp_develop file will be created.
	local xmpCameraRaw = getCameraRaw( true )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = importFilePath,
			additionalInfo = {
				XMPSideCarPath = importSidecarPath,
			},
			width = 12416,
			height = 1856,
			xmpCameraRaw = xmpCameraRaw,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- Verify the source doc is created.
	do
		local filteredChangedDocs = countDocs( docStore )
		local fcd = rxTest:observe( filteredChangedDocs )
		fcd:expectDataWithinTime( 120, 0, 1, 0 )
	end

	-- Start up sync engine.
	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()
	uploader:startContinuousUploads()

	local uploading = rxTest:observe( uploader:isDoingWork() )

	-- Wait for the uploader to start and finish uploading the source asset.
	uploading:expectDataWithinTime( 15, true )

	-- Wait for the inflight uploads to complete.
	local observeBinariesInFlight = rxTest:observe( pollBinariesInFlight( uploader ) )
	observeBinariesInFlight:expectDataWithinTime( 120, 0 )

	-- Check that the uploader thinks it's done as well.
	uploading:expectDataWithinTime( 120, false )

	-- Check that the xmp file has been uploaded before deleting it
	rxTest:observe( docStore:ongoingDocValue( assetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/xmp', 'href' )
		:map( function( maybeHref ) return not not maybeHref end )
	):expectDataWithinTime( 120, true )

	-- Read in the sidecar contents, and get the xmp url
	local sidecarContents = readLocalSidecarFile( rxTest, docStore, assetId )

	-- make sure the sidecar on oz matches the local sidecar file
	local xmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalog:ozCatalogSpec(), assetId ) )
	xmpContentsObserver:expectDataWithinTime( 60, sidecarContents )
	xmpContentsObserver:dispose()

	-- Remove the local xmp sidecar file so oz has to copy it.
	rxTest:waitFor( docStore:applyOverlay {
		_id = assetId,
		_localOnly = {
			files = {
				original = {
					xmp = { _delete = true },
				},
			},
		},
	} )

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

	-- Wait for the uploader to start and finish uploading the copied asset.
	uploading:expectDataWithinTime( 60, true )
	uploading:expectDataWithinTime( 120, false )

	local copiedAssetCopyId
	rxTest:expectSingleValue( docStore:docValue( copiedAssetId )
		:map( function( docValue )

			copiedAssetCopyId = derefOpt( docValue, 'copy', 'copy_id' )

			-- Check to see if all the files are copied ( original, xmpsidecar, xmpdevelop ).
			-- Proxy isn't there yet as we haven't started the sync engine, and the source asset didn't have one downloaded yet.
			rxTest:expectEqual( 'file', WFFileUtils.exists( WFCatFileSpec.buildOriginalPathFromDocValue( docValue ) ) )
			rxTest:expectEqual( nil, WFCatFileSpec.getXmpSidecarPathFromDocValue( docValue ) )
			rxTest:expectEqual( 'file', WFFileUtils.exists( derefOpt( WFCatFileSpec.getXmpDevelopInfoFromDocValue( docValue ), 'path' ) ) )

			return true

		end )
	, true )

	-- Check that there is a copy id associated with the copy.
	rxTest:expectEqual( true, copiedAssetCopyId ~= nil )

	-- Make sure the source asset has a copy_id that matches the copied asset.
	rxTest:expectSingleValue( docStore:docValue( assetId ):pluck( 'copy', 'copy_id' ), copiedAssetCopyId )

	-- Verify that two docs are uploaded.
	do
		local filteredChangedDocs = countDocs( docStore )
		local fcd = rxTest:observe( filteredChangedDocs )
		fcd:expectDataWithinTime( 240, 2, 2, 0 )
	end

	-- Check that all the expected _serverProvidedMetadata links are on the copied asset.
	rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
		:map( hasRequiredLinks )
	):expectDataWithinTime( 240, "Has required Links in docValue" )

	-- make sure there is a panoramic proxy as well
	rxTest:observe( catalog:docStore():ongoingDocValue( copiedAssetId )
		:pluck( '_serverProvidedMetadata', 'links', '/rels/proxy_type/panorama' )
		:map( nonNilToTrue )
	):expectDataWithinTime( 10, true )


	-- make sure the copied asset's sidecar on oz matches the local sidecar file
	local copiedXmpContentsObserver = rxTest:observe( downloadXmpSidecarForAssetId( docStore, catalog:ozCatalogSpec(), copiedAssetId ) )
	copiedXmpContentsObserver:expectDataWithinTime( 60, sidecarContents )
	copiedXmpContentsObserver:dispose()

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

end

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

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

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

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

	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 catalog:docStore():setDocValue( baseAssetDoc )

end

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

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

	return catalog:docStore():applyOverlay {
			_id = assetId,
			_localOnly = {
				files = {
					original = {
						xmp = {
							relativePath = WFFileUtils.removeVolumeFromPath( xmpSidecarFilePath ),
							volumeGuid = WFFileUtils.getVolumeGuidForPath( xmpSidecarFilePath ),
							fileSize = xmpSidecarFileSize,
						},
					},
				},
				xmpSidecarExists = true,
			},
		}

end

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

local function addProxyPathToDocument( catalog, assetId, proxyFilePath, proxyFileSize )
	return catalog:docStore():applyOverlay {
		_id = assetId,
		_localOnly = {
			files = {
				proxy = {
					['2560'] = {
						relativePath = WFFileUtils.removeVolumeFromPath( proxyFilePath ),
						volumeGuid = WFFileUtils.getVolumeGuidForPath( proxyFilePath ),
						size = proxyFileSize,
					},
				},
			},
		}
	}
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/main_create/parts' ] = {
					href = hrefPrefaceForCreate .. '/main/parts?access=put&upload_status={upload_status}&file_size={file_size}&part_size={part_size}&content_type={content_type}',
					templated = true,
				},
				[ '/rels/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 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 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 )

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

	errorCode = errorCode or '201'
	local requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'X-Read-Meta',
				value = 'none',
			},
			{
				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",
			notifyWhenHttpRequestIsInProgress = true,
			requestCost = 2,
			requestPriority = 3,
			requestHeaders = requestHeaders,
		},

		headers = {
			status_code = errorCode,
		},
	}

end

local function generateCreateMasterResponse( catalogId, assetId, revisionId, filePath, errorCode )
	return generateCreateMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, filePath, errorCode )
end

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

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

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

	errorCode = errorCode or '202'

	local requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'X-Read-Meta',
				value = 'none',
			},
			{
				header = 'Content-Type',
				value = 'application/json',
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'X-Generate-Proxies',
				value = 'all',
			},
			{
				header = 'X-Async-Copy',
				value = "true",
			},
		}

	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 )
	return generateCopyMasterResponseImp( 'catalogs', catalogId, assetId, revisionId, catalogId, sourceAssetId, sourceRevisionId, errorCode )
end

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

local function generateUploadXmpFileResponse( catalogId, assetId, revisionId, xmpSidecarFilePath )

	local url = '/v2/catalogs/' .. catalogId .. '/assets/' .. assetId .. '/revisions/' .. revisionId .. '/xmp'

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

end


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

local function generateCopyXmpSidecarResponseImp( catalogsOrSpaces, catalogOrSpaceId, catalogId, assetId, revisionId, 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, catalogId, assetId, revisionId, sourceAssetId, sourceRevisionId, errorCode )
end

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

local function generateUploadXmpSidecarFileResponseImp( catalogsOrSpaces, catalogOrSpaceId, assetId, revisionId, filePath )

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

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

end

local function generateUploadXmpSidecarFileResponse( catalogId, assetId, revisionId, filePath )
	return generateUploadXmpSidecarFileResponseImp( 'catalogs', catalogId, assetId, revisionId, filePath )
end

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

local function setupMockedCatalogAndUploader( rxTest, catalogId, urlResponses )

	local userId = '87f526b44f9e41eca6cb5a550f84ceb6'

	local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local uploader = WFCatBinaryUploader( catalog, nil, options, nil, {
			fallbackToOzUploadForOriginalsOnly = true,
			clearFromRecentlyUploadedSeconds = 1,
			waitForOtherUploadSeconds = .5,
			waitForCopyFailedSeconds = .5,
			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

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

function WFCatDesktopBinaryUploaderTest.rxTestMockedUploadAddWithXmpSidecar( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 tempFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testSidecarName = string.format( "sidecar%s.xmp", KSIdGenerator.uuidInLowercaseHex() )
	local xmpSidecarFilePath = KSPathUtils.child( tempFolder, testSidecarName )
	WFCatAssetTestUtils.createXmpSidecarFile( xmpSidecarFilePath )

	local urlResponses = {
		generateUploadXmpSidecarFileResponse( catalogId, assetId, revisionId, xmpSidecarFilePath ),
		generateCreateMasterResponse( catalogId, assetId, revisionId, originalFilePath ),
	}

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

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

	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 now upload the master (with no X-Read-Meta header)
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	httpRequestsProcessed:expectDataWithinTime( 2, 2 )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestMockedUploadAddWithXmpSidecarLegacyFields( rxTest )

	-- This test validates that assets that were already created with the legacy xmpSidecarNeedsUpload field
	-- will still get properly uploaded afterwards and the flag should get cleared. If xmpSidecarNeedsUpload
	-- is true and there is no /rels/xmp link on Oz we can treat it like a regular xmp upload where we upload
	-- the xmp first with X-Read-Meta header and the master second, with X-Read-Meta: none - that is this
	-- test.
	-- If xmpSidecarNeedsUpload is true and there *is* a /rels/xmp link on Oz, the master is already up
	-- (and Oz extracted the metadata from it) and we now just need to upload the xmp sidecar to overwrite it,
	-- that is rxTestMockedUploadAddWithXmpSidecarLegacyFields_XmpAlreadyUp

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 tempFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testSidecarName = string.format( "sidecar%s.xmp", KSIdGenerator.uuidInLowercaseHex() )
	local xmpSidecarFilePath = KSPathUtils.child( tempFolder, testSidecarName )
	WFCatAssetTestUtils.createXmpSidecarFile( xmpSidecarFilePath )

	local urlResponses = {
		generateUploadXmpSidecarFileResponse( catalogId, assetId, revisionId, xmpSidecarFilePath ),
		generateCreateMasterResponse( catalogId, assetId, revisionId, originalFilePath ),
	}

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

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, nil, xmpSidecarFilePath ) )
	-- add the xmpSidecarNeedsUpload field as if it was a previously imported photo with sidecar that hadn't yet completed
	-- upload at the time of upgrade
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_localOnly = {
			xmpSidecarNeedsUpload = true
		}
	} )

	local hasXmpSidecarNeedsUpload = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_localOnly', 'xmpSidecarNeedsUpload' ) )

	hasXmpSidecarNeedsUpload:expectDataWithinTime( 2, true )

	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 now upload the master (with no X-Read-Meta header)
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	httpRequestsProcessed:expectDataWithinTime( 2, 2 )

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

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

	uploading:expectDataWithinTime( 5, false )

	-- make sure xmpSidecarNeedsUpload is removed
	hasXmpSidecarNeedsUpload:expectDataWithinTime( 2, nil )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestMockedUploadAddWithXmpSidecarLegacyFields_XmpAlreadyUp( rxTest )

	-- This test validates that assets that were already created with the legacy xmpSidecarNeedsUpload field
	-- will still get properly uploaded afterwards and the flag should get cleared. If xmpSidecarNeedsUpload
	-- is true and there is no /rels/xmp link on Oz we can treat it like a regular xmp upload where we upload
	-- the xmp first with X-Read-Meta header and the master second, with X-Read-Meta: none - that is
	-- rxTestMockedUploadAddWithXmpSidecarLegacyFields.
	-- If xmpSidecarNeedsUpload is true and there *is* a /rels/xmp link on Oz, the master is already up (and
	-- Oz extracted the metadata from it) and we now just need to upload the xmp sidecar to overwrite it -
	-- that is this test.

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 tempFolder = WFTestFileUtils.createTestFolder( rxTest )
	local testSidecarName = string.format( "sidecar%s.xmp", KSIdGenerator.uuidInLowercaseHex() )
	local xmpSidecarFilePath = KSPathUtils.child( tempFolder, testSidecarName )
	WFCatAssetTestUtils.createXmpSidecarFile( xmpSidecarFilePath )

	local urlResponses = {
		generateUploadXmpSidecarFileResponse( catalogId, assetId, revisionId, xmpSidecarFilePath ),
	}

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

	rxTest:waitFor( addDocumentWithOzMetadataUpdate( catalog, catalogId, assetId, revisionId, sha256, originalFilePath, nil, xmpSidecarFilePath ) )
	-- add the xmpSidecarNeedsUpload field as if it was a previously imported photo with sidecar that hadn't yet completed
	-- upload at the time of upgrade
	rxTest:waitFor( catalog:docStore():applyOverlay {
		_id = assetId,
		_localOnly = {
			xmpSidecarNeedsUpload = true
		}
	} )

	local hasXmpSidecarNeedsUpload = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:pluck( '_localOnly', 'xmpSidecarNeedsUpload' ) )

	hasXmpSidecarNeedsUpload:expectDataWithinTime( 2, true )

	-- make it look like the master was already uploaded and Oz extracted the XMP
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

	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 )

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

	uploading:expectDataWithinTime( 5, false )

	-- make sure xmpSidecarNeedsUpload is removed
	hasXmpSidecarNeedsUpload:expectDataWithinTime( 2, nil )

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestUploadProxyOnly_testNightly( 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( 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 testFileName = "IMG_7763.JPG"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )
	local testImagesPath = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'testdata' ), "assets" )
	local proxyFilePath = KSPathUtils.child( testImagesPath, "IMG_7763.DNG" )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )
	local importFilePath = KSPathUtils.child( importFolder, testFileName )
	WFFileUtils.copy( testFilePath, importFilePath )

	-- Create a large xmpCameraRaw field so xmp_develop file will be created.
	local xmpCameraRaw = getCameraRaw( true )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = importFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = xmpCameraRaw,
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- mark it as willNotUploadMaster and add proxy so it looks like a proxy-only asset
	rxTest:waitFor( addWillNotUploadMasterToDoc( catalog, assetId ) )
	rxTest:waitFor( addProxyPathToDocument( catalog, assetId, proxyFilePath, WFFileUtils.fileAttributes( proxyFilePath ).fileSize ) )

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

	-- start the metadata sync
	local pusher = WFCatMetadataPusher( catalog, options )
	rxTest:addCloseTarget( pusher )

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

	local uploader = WFCatBinaryUploader(
			catalog, nil, options, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

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

	local hasProxyAndRenditions = rxTest:observe( catalog:docStore():ongoingDocValue( assetId )
		:map( function( docValue )
			if not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/proxy_type/2560', 'href' ) then
				return string.format( 'missing proxy for %s\n\n%s', assetId, WFTableArchiving.tableToString( docValue ) )
			elseif not derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/rendition_type/1280', 'href') then
				return string.format( 'missing renditions for %s\n\n%s', assetId, WFTableArchiving.tableToString( docValue ) )
			else
				return true
			end
		end ) )

	hasProxyAndRenditions:expectDataWithinTime( 360, true )

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

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestMockedUploadCopy_sourceDeletedAfterUpload_masterLocal( rxTest )

	-- Verify uploading of a copied asset still works when the source asset is deleted
	-- and the master is local.

	-- Expected result is to try to copy the master and fail and then upload the master.

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 = {
		generateCopyMasterResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId, '404' ),
		generateCreateMasterResponse( catalogId, copiedAssetId, copiedRevisionId, copiedOriginalPath ),
	}

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

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

	-- add the master link so createCopiesOfAssets refers specially to this asset.
	rxTest:waitFor( addMasterAndXmpLinkToDoc( catalog, catalogId, assetId, revisionId ) )

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

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

	-- Delete the source asset.
	rxTest:expectOnlyCompleted( WFCatDelete.deleteAssets( catalog, { assetId } ) )

	-- update the expected url/response pairs
	urlResponses[ 1 ] = generateCopyMasterResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId, '404' )
	urlResponses[ 2 ] = generateCreateMasterResponse( catalogId, copiedAssetId, copiedRevisionId, copiedOriginalPath )

	-- 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( 5, 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, copiedAssetId, copiedRevisionId ) )

	uploading:expectDataWithinTime( 5, false )

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

end

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestMockedUploadCopy_sourceDeletedBeforeUpload_masterLocal( rxTest )

	-- Verify uploading of a copied asset still works when the source asset is deleted
	-- and the master is local.

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

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 = {
		generateCreateMasterResponse( catalogId, copiedAssetId, copiedRevisionId, copiedOriginalPath ),
	}

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

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

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

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

	-- Delete the source asset.
	rxTest:expectOnlyCompleted( WFCatDelete.deleteAssets( catalog, { assetId } ) )

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

	-- 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:expectData( 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 )
	errorsRecordedInDiagnosticLog = removeResourceNotFoundError( errorsRecordedInDiagnosticLog )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatDesktopBinaryUploaderTest.rxTestMockedUploadCopy_sourceDeleted_noMasterLocal_testNightly( rxTest )

-- Verify we generate an error when upload an asset that was copied from an asset that was deleted before
-- the upload could occur.

-- Test modified to make 30 copies to get into a state where we never think we
-- are trying to upload the first asset when we fail to make a copy.  NMB-13485

	rxTest:setTimeLimit( 60 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

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

	-- 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 = {
	}

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

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

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

	local copies = {}

	local numCopiesToMake = 30
	for i = 1, numCopiesToMake do

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

		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 )

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

		copies[ #copies + 1 ] = copiedAssetId

	end

	-- Delete the source asset directly to avoid cloud trash behavior
	rxTest:waitFor( catalog:docStore():applyOverlay {
			_id = assetId,
			_deleted = true
		}
	)

	uploader:startContinuousUploads()

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

	-- See if the copied asset is marked invalid.
	for i = 1, numCopiesToMake do
		local copiedAssetId = copies[ i ]
		rxTest:expectSingleValue( catalog:docStore():docValue( copiedAssetId )
			:pluck( '_localOnly', 'localOriginalError' )
		, 'CopyFailedSourceMissing' )
	end

	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 )
	errorsRecordedInDiagnosticLog = removeMarkingOriginalInvalidError( errorsRecordedInDiagnosticLog )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

function WFCatDesktopBinaryUploaderTest.rxTestMockedUploadCopy_xmpSidecarLocal( rxTest )

	-- Expects the rels/xmp to already exist on oz and to be copied via the copy master api.

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

	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 xmpSidecarFilePath = KSPathUtils.child( originalsTestFolder, 'IMG_7763.XMP' )
	local xmpSidecarFileSize = generateSidecarFile( rxTest, xmpSidecarFilePath )

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

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

	rxTest:waitFor( addXmpSidecarToDocument( catalog, assetId, xmpSidecarFilePath, xmpSidecarFileSize, nil ) )

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

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

	local _
	copiedRevisionId, _, _, _ = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )

	-- Update the expected url/response pairs with copiedAssetId and copiedRevisionId.
	catalogSpec:expectHttpRequest( generateCopyXmpSidecarResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId ) )
	catalogSpec:expectHttpRequest( generateCopyMasterResponse( 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 got an update with the xmp link in it so the uploader knows it can
	-- move on to the master
	rxTest:waitFor( addXmpLinkToDoc( catalog, catalogId, copiedAssetId, copiedRevisionId ) )

	httpRequestsProcessed:expectDataWithinTime( 2, 2 )

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

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

-- With cloud trash enabled this test has been replaced by the CopyDelete
-- tests in WFCatTrashDesktop.

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestMockedUploadCopy_xmpSidecarLocal_srcDeleted( rxTest )

	-- Uploads rels/xmp instead of copying it.

	rxTest:setTimeLimit( 10 )

	local catalogId = KSIdGenerator.uuidInLowercaseHex()

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

	local copiedAssetId = 'tempId'
	local copiedRevisionId = 'tempRevisionId'
	local copiedOriginalPath = 'tempOriginalPath'
	local copiedXmpSidecarFilePath = 'tempXmpSidecarPath'

	-- 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 xmpSidecarFilePath = KSPathUtils.child( originalsTestFolder, 'IMG_7763.XMP' )
	local xmpSidecarFileSize = generateSidecarFile( rxTest, xmpSidecarFilePath )

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

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

	rxTest:waitFor( addXmpSidecarToDocument( catalog, assetId, xmpSidecarFilePath, xmpSidecarFileSize, true ) )

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

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

	local proxyPath
	copiedRevisionId, copiedOriginalPath, proxyPath, copiedXmpSidecarFilePath = getCopiedAssetInfo( rxTest, catalog, copiedAssetId )
	rxTest:expectEqual( 'string', type( proxyPath ) )

	-- Delete the source asset.
	rxTest:expectOnlyCompleted( WFCatDelete.deleteAssets( catalog, { assetId } ) )

	-- Update the expected url/response pairs with copiedAssetId and copiedRevisionId.
	catalogSpec:expectHttpRequest( generateCopyMasterResponse( catalogId, copiedAssetId, copiedRevisionId, assetId, revisionId, '404' ) )
	catalogSpec:expectHttpRequest( generateCreateMasterResponse( catalogId, copiedAssetId, copiedRevisionId, copiedOriginalPath ) )
	catalogSpec:expectHttpRequest( generateUploadXmpFileResponse( catalogId, copiedAssetId, copiedRevisionId, copiedXmpSidecarFilePath ) )

	-- 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:expectData( 2 )

	-- 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 )
	errorsRecordedInDiagnosticLog = removeResourceNotFoundError( errorsRecordedInDiagnosticLog )
	rxTest:expectEqual( "", removeEmptyErrorHeader( errorsRecordedInDiagnosticLog ) )

end

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

local function testUploadPerformance( rxTest, numFilesToUpload, testFileName, chunkFileAtSize, chunkSize, putBinaryFn )

	local maxRuntimeMinutes = 20
	local maxRuntimeSeconds = maxRuntimeMinutes * 60

	rxTest:setTimeLimit( maxRuntimeSeconds )

	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 )

	if putBinaryFn then
		local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, putBinaryFn )
		catalogSpec = catalogSpecSpy
	end

	local chunkedUploader = nil

	if chunkFileAtSize then
		chunkedUploader = WFCatChunkedUploader( docStore, catalogSpec, chunkFileAtSize, chunkSize )
	end

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, options, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )

	uploader:DEBUG_resetDiagnosticLogErrors()

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

	-- Import a file that is large enough to be chunked so the
	-- documents fileSize will be large enought to send it to the
	-- chunked uploader.

	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local importFolder = WFTestFileUtils.createTestFolder( rxTest )

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

	local importArray = {}

	local initialFileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize
	local _, _, testFileExt = string.find( testFilePath, ".+%.([^.]+)" )

	local step1StartFileCopy = KSDate.timeNowForAnimation()

	for i = 1, numFilesToUpload do

		local importFileName = string.format( "IMG_%d.%s", i, testFileExt )
		local importFilePath = KSPathUtils.child( importFolder, importFileName )

		importFilePath = WFFileUtils.copyToUniquelyNamedFile( testFilePath, importFilePath )

		local file = io.open( importFilePath, 'w' )
		file:seek( 'set', initialFileSize - 1 )
		file:write(tostring(i))
		file:close()

		local importEntry = {
			filePath = importFilePath,
			width = 2048,
			height = 1024,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( importFilePath ).fileSize,
		}

		table.insert( importArray, importEntry )

	end

	local step2StartImport = KSDate.timeNowForAnimation()

	local importedAssetIds = {}
	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, importArray )
			:toArray()
			:map( function( importInfoArray )
				for _, importItem in ipairs( importInfoArray ) do
					if importItem.assetId then
						table.insert( importedAssetIds, importItem.assetId )
					end
				end
				return #importedAssetIds
			end ),
		numFilesToUpload )

	local step3StartMetadataUpload = KSDate.timeNowForAnimation()

	-- start up metadatasync

	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()

	local uploadingMetadata = rxTest:observe( pusher:isDoingWork() )
	uploadingMetadata:expectDataWithinTime( 60, true )
	uploadingMetadata:expectDataWithinTime( 700, false )

	local step4StartBinaryUpload = KSDate.timeNowForAnimation()

	uploader:startContinuousUploads()

	local uploadingBinaries = rxTest:observe( uploader:isDoingWork() )

	uploadingBinaries:expectDataWithinTime( 60, true )
	uploadingBinaries:expectDataWithinTime( 700, false )

	local step5EndBinaryUpload = KSDate.timeNowForAnimation()

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

	local uploaderStats = uploader:sendDiagnosticLogData()

	local unitTestStats = {
		duration1_FileCopy = step2StartImport - step1StartFileCopy,
		duration2_Import = step3StartMetadataUpload - step2StartImport,
		duration3_MetadataUpload = step4StartBinaryUpload - step3StartMetadataUpload,
		duration4_BinaryUpload = step5EndBinaryUpload - step4StartBinaryUpload,
	}

	rxTest:logf( "Status = %s", WFTableArchiving.debugTableToString( {
		uploaderStats = uploaderStats,
		unitTestStats = unitTestStats, } ) )

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

	-- Make sure all the binaries have a master link to prove they have been uploaded successfully.
	for _, assetId in ipairs( importedAssetIds ) do

		rxTest:observe( docStore:ongoingDocValue( assetId )
			:map( function( docValue )
				return WFCatFileSpec.getOriginalLinkFromDocValue( docValue ) ~= nil
			end )
		):expectDataWithinTime( 40, true )

	end

	return errorsRecordedInDiagnosticLog

end

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

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestPerfUploadRegular_testNightly( rxTest )
	local loggedErrors = testUploadPerformance( rxTest, 30, "IMG_9876.CR2", 50*MB, 5*MB )
	rxTest:expectEqual( "", removeEmptyErrorHeader( loggedErrors ) )
end

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

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestPerfUploadChunked20_testNightly( rxTest )
	local loggedErrors = testUploadPerformance( rxTest, 30, "IMG_9876.CR2", 20*MB, 20*MB )
	rxTest:expectEqual( "", removeEmptyErrorHeader( loggedErrors ) )
end

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

function WFCatDesktopBinaryUploaderTest.DISABLED_rxTestPerfUploadChunked5_testNightly( rxTest )
	local loggedErrors = testUploadPerformance( rxTest, 30, "IMG_9876.CR2", 5*MB, 5*MB )
	rxTest:expectEqual( "", removeEmptyErrorHeader( loggedErrors ) )
end

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

function WFCatDesktopBinaryUploaderTest.rxTestContentRangeUploader_normal_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog, catalogSpec, docStore

	rxTest:waitFor(
		catalogTestSession:catalog()
			:tap( function( _catalog )
				WFArguments.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 numChunkedUploads = 0
	local function countChunkedUploads( self, url, data, options, progressReporter )
		numChunkedUploads = numChunkedUploads + 1
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, countChunkedUploads )

	local chunkSize = 1000*1000
	local chunkedUploader = WFCatContentRangeUploader( docStore, catalogSpecSpy, chunkSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )
	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = "IMG_9876.CR2"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 5184,
			height = 3456,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine

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

	local uploading = rxTest:observe( uploader:isDoingWork() )

	uploading:expectDataWithinTime( 60, true )

	uploading:expectDataWithinTime( 400, false )

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

	rxTest:expectEqual( 27, numChunkedUploads )

	-- Check that the document changes feed has updated all the expected _serverProvidedMetadata links in the copied asset.
	local observeHasRequiredLinks = rxTest:observe( catalog:docStore():ongoingDocValue( assetId ):map( hasRequiredLinksWithMaster ))
	observeHasRequiredLinks:expectDataWithinTime( 240, "Has required Links in docValue" )
	observeHasRequiredLinks:dispose()

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestContentRangeUploader_resumes_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog, catalogSpec, docStore

	rxTest:waitFor(
		catalogTestSession:catalog()
			:tap( function( _catalog )
				WFArguments.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 numChunkedUploads = 0
	local function countChunkedUploads( self, url, data, options, progressReporter )
		numChunkedUploads = numChunkedUploads + 1
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, countChunkedUploads )

	local chunkSize = 1000*1000
	local chunkedUploader = WFCatContentRangeUploader( docStore, catalogSpecSpy, chunkSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )
	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = "IMG_9876.CR2"
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local masterFileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			width = 5184,
			height = 3456,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = masterFileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- start up sync engine

	rxTest:waitFor( pusher:sendUpdatesContinuously() )
	puller:getUpdatesContinuously()

	-- wait for the metadata update to get the mastercreate link
	local masterContentType
	local masterFilePath
	local masterCreateUrl
	local observeMasterCreateLinkPresent = rxTest:observe(
		docStore:ongoingDocValue( assetId )
		:map( function( docValue )
			local ozMasterCreateTemplate = derefOpt( docValue, '_serverProvidedMetadata', 'links', '/rels/master_create', 'href' )
			local revisionIds = derefOpt( docValue, '_serverProvidedMetadata', 'revision_ids' )
			local latestRevisionId = revisionIds and revisionIds[ #revisionIds ]

			if ozMasterCreateTemplate and latestRevisionId then
				masterCreateUrl = ozMasterCreateTemplate:replace( '{revision_id}', latestRevisionId )
			end

			masterFilePath = masterFilePath or WFCatFileSpec.buildOriginalPathFromDocValue( docValue )
			masterContentType = masterContentType or WFCatFileSpec.getContentTypeFromDocValue( docValue )

			return masterCreateUrl ~= nil
		end ) )
	observeMasterCreateLinkPresent:expectDataWithinTime( 60, true )
	observeMasterCreateLinkPresent:dispose()

	-- Manually upload a few chunks so that we can see if the chunkedUploader will resume uploading

	local putArgs = {
		requestHeaders = {
			{
				header = 'X-Generate-Renditions',
				value = 'all',
			},
			{
				header = 'Content-Type',
				value = masterContentType,
			},
			{
				header = 'Accept',
				value = 'application/json',
			},
			{
				header = 'X-Generate-Proxies',
				value = 'all',
			},
		},
		dataFormat = 'filePath',
		requestPriority = WFRxHttpClient.requestPriorityMedium,
		requestCost = WFRxHttpClient.requestCostUploadNegative,
	}

	local chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, -1, nil )
	rxTest:expectSingleValue( chunkedUploader:_putChunk( masterCreateUrl, masterFilePath, putArgs, chunkStart, chunkEnd, masterFileSize ), ':ignore:', ':ignore:' )

	-- Skip writing chunks 2 and 3 to make a couple holes.
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, chunkEnd, nil )
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, chunkEnd, nil )

	for x = 4, 10 do
		chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, chunkEnd, nil )
		rxTest:expectSingleValue( chunkedUploader:_putChunk( masterCreateUrl, masterFilePath, putArgs, chunkStart, chunkEnd, masterFileSize ), ':ignore:', ':ignore:' )
	end

	-- Skip chunk 11 and write chunk 12.
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, chunkEnd, nil )
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( masterFileSize, chunkEnd, nil )
	rxTest:expectSingleValue( chunkedUploader:_putChunk( masterCreateUrl, masterFilePath, putArgs, chunkStart, chunkEnd, masterFileSize ), ':ignore:', ':ignore:' )

	-- start up the binary uploader and wait for it to upload.
	uploader:startContinuousUploads()
	local uploading = rxTest:observe( uploader:isDoingWork() )
	uploading:expectDataWithinTime( 60, true )
	uploading:expectDataWithinTime( 400, false )

	 -- check that we didn't re-upload everything, + 1 as doChunkedUpload always sends first chunk to see what chunks are already uploaded.
	rxTest:expectEqual( 27 + 1, numChunkedUploads )

	-- Check that the document changes feed has updated all the expected _serverProvidedMetadata links in the copied asset.
	local observeHasRequiredLinks = rxTest:observe( catalog:docStore():ongoingDocValue( assetId ):map( hasRequiredLinksWithMaster ))
	observeHasRequiredLinks:expectDataWithinTime( 240, "Has required Links in docValue" )
	observeHasRequiredLinks:dispose()

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

end

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

function WFCatDesktopBinaryUploaderTest.rxTestContentRangeUploader_video_noContentType_testNightly( rxTest )

	rxTest:setTimeLimit( 360 )

	local catalogTestSession = WFCatTestSession( rxTest, 'NmbCatalog' )

	local catalog, catalogSpec, docStore

	rxTest:waitFor(
		catalogTestSession:catalog()
			:tap( function( _catalog )
				WFArguments.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 numChunkedUploads = 0
	local function countChunkedUploads( self, url, data, options, progressReporter )
		numChunkedUploads = numChunkedUploads + 1
		return self.originalCatalogSpec:putBinary( url, data, options, progressReporter )
	end
	local catalogSpecSpy = makeWFCatSpecSpy( catalogSpec, countChunkedUploads )

	-- IMG_7772.mp4 is 211,332 bytes so this should result in 5 50KB chunks (last chunk
	local chunkSize = 50*1024
	local chunkedUploader = WFCatContentRangeUploader( docStore, catalogSpecSpy, chunkSize, chunkSize )

	local taskDelays = {
		progressiveBackoff = true,
		errorDelay = 10,
		maxDelay = 10,
	}

	local uploader = WFCatBinaryUploader(
			catalog, chunkedUploader, taskDelays, nil, uploaderOptions )

	rxTest:addCloseTarget( uploader )
	uploader:DEBUG_resetDiagnosticLogErrors()

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

	local testFileName = 'IMG_7772.mp4'
	local width = 270
	local height = 480
	local testFilePath = KSPathUtils.child( testDataFolder1, testFileName )

	local assetId

	rxTest:expectSingleValue(
		WFCatImport.addImageArrayToCatalog( catalog, nil, { {
			filePath = testFilePath,
			isVideo = true,
			width = width,
			height = height,
			xmpCameraRaw = '<xmp />',
			fileName = testFileName,
			fileSize = WFFileUtils.fileAttributes( testFilePath ).fileSize,
			} } )
			:tap( function( result )
				assetId = result.assetId
			end ),
		":ignore:" )

	-- Remove the content type as we had a bug where we defaulted it
	-- wrong for videos.
	rxTest:waitFor( catalog:docStore():applyOverlay {
			_id = assetId,
			importSource = {
				contentType = { _delete = true }
			},
		}
	)

	-- start up sync engine

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

	local uploading = rxTest:observe( uploader:isDoingWork() )

	uploading:expectDataWithinTime( 60, true )

	uploading:expectDataWithinTime( 120, false )

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

	rxTest:expectEqual( 5, numChunkedUploads )


	local function hasRequiredVideoLinksWithMaster( docValue )

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

		local requiredLinks = {
			'self',
			'/rels/master_create',
			'/rels/rendition_create',
			'/rels/xmp_develop_create',

			'/rels/master',
			'/rels/rendition_type/360p',
			'/rels/rendition_type/720p',
			'/rels/rendition_type/2048',
			'/rels/rendition_type/hls',
			'/rels/rendition_type/thumbnail2x',
		}

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

		return "Has required Links in docValue"

	end

	-- Check that the document changes feed has updated all the expected _serverProvidedMetadata links in the copied asset.
	local observeHasRequiredLinks = rxTest:observe( catalog:docStore():ongoingDocValue( assetId ):map( hasRequiredVideoLinksWithMaster ))
	observeHasRequiredLinks:expectDataWithinTime( 120, "Has required Links in docValue" )
	observeHasRequiredLinks:dispose()

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

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( 'WFCatDesktopBinaryUploaderTest', WFCatDesktopBinaryUploaderTest )
