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

WFCatContentRangeUploaderTest.lua

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

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

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

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

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

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

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

	-- WF.core.utils
local KSTableUtils = require 'KSTableUtils'
local KSIdGenerator = require 'KSIdGenerator'

	-- WF.oz_client.managed_catalog
local WFCatContentRangeUploader = require 'WFCatContentRangeUploader'
local WFCatMockCatalog = require 'WFCatMockCatalog'
local WFCatSpecMock = require 'WFCatSpecMock'
local WFCatTestSession = require 'WFCatTestSession'

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

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

local MB = 1024 * 1024

local OZ_CHUNK_FILE_SIZE_RECOMMENDED = 20 * MB
local OZ_MIN_CHUNK_SIZE = 5 * MB

local testFileSize = 26243176

local kUserId = '87f526b44f9e41eca6cb5a550f84ceb6'
local kCatalogId = KSIdGenerator.uuidInLowercaseHex()
local kDocId = 'e6885574883a485097a885018b5cca83'
local kChunkSize = OZ_MIN_CHUNK_SIZE
local kChunkFilesAtSize = OZ_CHUNK_FILE_SIZE_RECOMMENDED
local kChunkSize20MB = 20 * MB

local kDefaultPutArgs = {
	requestHeaders = {
		{
			header = 'X-Generate-Renditions',
			value = 'all',
		},
		{
			header = 'X-Generate-Proxies',
			value = 'all',
		},
		{
			header = 'Accept',
			value = 'application/json',
		},
		{
			header = 'Content-Type',
			value = 'application/octet-stream',
		},
	},
	dataFormat = 'filePath',
}

local kMasterUrl = '/v2/catalogs/' .. kCatalogId .. '/assets/' .. kDocId .. '/revisions/46ae2ddf47c5435fbb1d4cfe09ef7f6d/master'

local function setDocValueToImportState( rxTest, catalog )

	-- Set some stuff in the doc so it's not empty.
	-- It typically will have more fields but good enough for this test.
	rxTest:expectSingleValue( catalog:docStore():setDocValue {
		type = 'asset',
		subtype = 'image',
		_id = kDocId,
		importSource = {
			filePath = '/path/',
			originalWidth = 1,
			originalHeight = 2,
			
		},
		captureDate = '2016',
	}, ':ignore:' )

end

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

-- Work around limitation of lua's seek function where it only handles 32 bit
-- values on Windows.

local function SeekWriteBigFile( f, offset )

	local maxInt32 = 2^31

	f:seek( 'set', math.min( offset, maxInt32-1 ) )
	f:write( 'A' )
	local remainingOffset = offset - maxInt32

	while remainingOffset > 0 do
		f:seek( 'end', math.min( remainingOffset, maxInt32-1 ) )
		f:write( 'A' )
		remainingOffset = remainingOffset - maxInt32
	end

end

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

local function makeFileOfSize( rxTest, fileSize )

	local tempSubFolder = KSPathUtils.child( KSPathUtils.getStandardFilePath( 'temp' ),
		rxTest:testName() )

	WFFileUtils.mkdir( tempSubFolder )

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

	local filePath = KSPathUtils.child( tempSubFolder, "IMG_9876.CR2" )

	local f = io.open( filePath, "w" )
	if f then
		SeekWriteBigFile( f, fileSize - 1 )
		f:close()
	end

	rxTest:expectEqual( fileSize, WFFileUtils.fileAttributes( filePath ).fileSize )

	return filePath

end

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

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

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

	chunkSize = chunkSize or kChunkSize
	chunkLength = chunkLength or chunkSize

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

	local contentRangeStr = string.format( "bytes %s-%s/%s", startOffset, endOffset, fileSize)
	
	contentRangeStr = contentRangeStrOverride or contentRangeStr
	
	local options = KSTableUtils.deepCopy( kDefaultPutArgs )
	options.length = chunkLength
	options.offset = startOffset + 1
	table.insert( options.requestHeaders, {
		header = 'Content-Range',
		value = contentRangeStr,
	} )

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

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

	local headers = nil
	local fail = nil
	if resultHeaders then

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

	else
		headers = { status_code = '201' }
	end

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

		response = response,

		fail = fail,
		headers = headers,
	}

end

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

local function generateChunkResponseWithDelay( delay, filePath, chunkNumber, resultHeaders, parts, chunkSize, chunkLength )

	local result = generateChunkResponse( filePath, chunkNumber, resultHeaders, parts, chunkSize, chunkLength )

	result.responseDelayBySeconds = delay

	return result

end

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

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

	chunkSize = chunkSize or kChunkSize

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

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

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

end

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

local function contentRangeForChunk( chunkNumber, endOffset )
	local lastByte = endOffset or chunkNumber * kChunkSize - 1
	return { first = (chunkNumber - 1 ) * kChunkSize, last = lastByte }
end

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

local WFCatContentRangeUploaderTest = {}

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Success( rxTest )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	local urlResponses = {
		generateChunkResponse( filePath, 1 ),
		generateChunkResponse( filePath, 2 ),
		generateChunkResponse( filePath, 3 ),
		generateChunkResponse( filePath, 4 ),
		generateChunkResponse( filePath, 5 ),
		generateLastChunkResponse( filePath, 6 ),
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

end


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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Using20MBChunkSize( rxTest )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	local urlResponsesChunkSize20MB = {
		generateChunkResponse( filePath, 1, nil, nil, kChunkSize20MB ),
		generateLastChunkResponse( filePath, 2, kChunkSize20MB ),
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponsesChunkSize20MB )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkSize20MB, kChunkSize20MB )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_ChunkFileSize( rxTest )

	rxTest:setTimeLimit( 10 )

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, {} )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, 100 * MB, 1 )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectEqual( 100 * MB, chunkedUploader:chunkFileSize() )

end

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

local function testContentRangeUploader_Chunk_Error_ChunkX( rxTest, filePath, chunkError, chunkNumToFail )

	local urlResponses = {}

	-- request/responses for the first call to doChunkedUpload:

	for x = 1, chunkNumToFail - 1 do
		table.insert( urlResponses, generateChunkResponse( filePath, x ) )
	end

	if chunkNumToFail == 6 then
		table.insert( urlResponses, generateChunkResponse( filePath, chunkNumToFail, { status_code = chunkError }, nil, nil, testFileSize - 5 * kChunkSize ) )
	else
		table.insert( urlResponses, generateChunkResponse( filePath, chunkNumToFail, { status_code = chunkError } ) )
	end

	-- The following is for the second call to doChunkedUpload:
	local alreadyUploadedParts = {
		contentRangeForChunk( 1 ),
	}

	for x = 1, chunkNumToFail - 1 do
		table.insert( alreadyUploadedParts, contentRangeForChunk( x ) )
	end

	table.insert( urlResponses, generateChunkResponse( filePath, 1, nil, alreadyUploadedParts ) )

	for x = chunkNumToFail, 6 do
		if x == 1 then
			-- already added a request/response for chunk 1 above
		elseif x == 6 then
			table.insert( urlResponses, generateLastChunkResponse( filePath, x ) )
		else
			table.insert( urlResponses, generateChunkResponse( filePath, x ) )
		end
	end

	-- Verify doChunkedUpload encounters an error on the specified chunk

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectOnlyError( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:',
		{	-- headers (trimmed)
			status_code = chunkError,
		}
	)

	-- Verify it can continue from where it left off to complete.
	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_AllChunks( rxTest )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	-- handles error on any chunk

	for x = 1, 6 do
		testContentRangeUploader_Chunk_Error_ChunkX( rxTest, filePath, '500', x )
	end

end

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

local function testContentRangeUploader_ChunkError_Resumes( rxTest, chunk3Error )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	local urlResponses = {
		generateChunkResponse( filePath, 1 ),
		generateChunkResponse( filePath, 2 ),
		generateChunkResponse( filePath, 3 ),
		generateChunkResponse( filePath, 4, { status_code = chunk3Error } ),

		-- The following is for the second call to doChunkedUpload:
		generateChunkResponse( filePath, 1, nil, {
				contentRangeForChunk( 1 ),
				contentRangeForChunk( 2 ),
				contentRangeForChunk( 3 ),
			} ),
		generateChunkResponse( filePath, 4 ),
		generateChunkResponse( filePath, 5 ),
		generateLastChunkResponse( filePath, 6 ),
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectOnlyError( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:',
		{	-- headers (trimmed)
			status_code = chunk3Error,
		}
	)

	-- Verify we can continue from where we left off to complete.
	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_400( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '400' )
		-- GUID for revision id is invalid
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_401( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '401' )
		-- authorization token is invalid
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_401( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '403' )
		-- client upgrade required
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_404( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '404' )
		-- assetid doesn't exist on oz
end
--------------------------------------------------------------------------------

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_429( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '429' )
		-- too many requests
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_500( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '500' )
		-- internal server error
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_Chunk_Error_503( rxTest )
	testContentRangeUploader_ChunkError_Resumes( rxTest, '503' )
		-- service is unavailable
end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_DeletedDuringUpload( rxTest )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	-- simiulate file was deleted during upload as each time we upload a chunk the
	-- file will be accessed.
	local chunk3Response = generateChunkResponse( filePath, 3 )
	chunk3Response.fail = { 'fileio', 'Missing File' }

	local urlResponses = {
		generateChunkResponse( filePath, 1 ),
		generateChunkResponse( filePath, 2 ),
		chunk3Response,
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

	local chunkedUploaderObserver = rxTest:observe( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ) )

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

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

	chunkedUploaderObserver:expectErrorWithinTime( 3, 'fileio', 'Missing File' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_MissingOriginal( rxTest )

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	local urlResponses = {
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

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

	rxTest:expectOnlyError( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		'payload must exist', ':ignore:' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_DisposedDuringUpload( rxTest )

	-- Dispose the upload to simulate pause or close

	rxTest:setTimeLimit( 10 )

	local filePath = makeFileOfSize( rxTest, testFileSize )

	local urlResponses = {
		generateChunkResponse( filePath, 1 ),
		generateChunkResponse( filePath, 2 ),
		generateChunkResponseWithDelay( 1, filePath, 3 ), -- cause a delay so we can dispose the upload while in progress
		generateChunkResponseWithDelay( 1, filePath, 4 ), -- cause a delay so we can dispose the upload while in progress
		generateChunkResponse( filePath, 5 ),
		generateLastChunkResponse( filePath, 6 ),

		-- The following is for the second call to doChunkedUpload:
		generateChunkResponse( filePath, 1, nil, {
				contentRangeForChunk( 1 ),
				contentRangeForChunk( 2 ),
				contentRangeForChunk( 5 ),
				contentRangeForChunk( 6, testFileSize - 1 ),
			} ),

		generateChunkResponse( filePath, 3 ),
		generateChunkResponse( filePath, 4 ),
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, kChunkFilesAtSize, kChunkSize )
	setDocValueToImportState( rxTest, catalog )

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

	local subscription = chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ):subscribe()

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

	-- Dispose the upload to simulate pause or close
	subscription:dispose()

	httpRequestsProcessed:expectDataWithinTime( 1, 4 )

	-- now issue the same upload, and it should continue where it left off
	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

	httpRequestsProcessed:expectDataWithinTime( 1, 7 )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_calculateNextChunk( rxTest )

	local catalogId = '1111f02c0177438aa3552161cc3f1111'
	local userId = '2222f02c0177438aa3552161cc3f2222'

	local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, {} )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )

	local chunkSize = 1000*1000
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, chunkSize, chunkSize )

	local existingParts = {
		{ first=0, last=999999 },
		{ first=3000000, last=3999999 },
		{ first=4000000, last=4999999 },
		{ first=5000000, last=5999999 },
		{ first=6000000, last=6999999 },
		{ first=7000000, last=7999999 },
		{ first=8000000, last=8999999 },
		{ first=9000000, last=9999999 },
		{ first=11000000, last=11999999 }
	}

	local fileSize = 26243176
	local chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, -1, existingParts )
	rxTest:expectEqual( 1000000, chunkStart )
	rxTest:expectEqual( 1999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 2000000, chunkStart )
	rxTest:expectEqual( 2999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 10000000, chunkStart )
	rxTest:expectEqual( 10999999, chunkEnd )

	for x = 12, 25 do
		chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
		rxTest:expectEqual( x * 1000000, chunkStart )
		rxTest:expectEqual( x * 1000000 + 999999, chunkEnd )
	end
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 26000000, chunkStart )
	rxTest:expectEqual( fileSize-1, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( nil, chunkStart )
	rxTest:expectEqual( nil, chunkEnd )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_calculateNextChunk_existingMissingFirst( rxTest )

	-- This probably doesn't happen but just verifies we don't run into problems
	-- if Oz responds back with a list of segments that doesn't include the first
	-- chunk that we send up.

	local catalogId = '1111f02c0177438aa3552161cc3f1111'
	local userId = '2222f02c0177438aa3552161cc3f2222'

	local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, {} )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )

	local chunkSize = 1000*1000
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, chunkSize, chunkSize )

	local existingParts = {
		{ first=3000000, last=3999999 },
		{ first=4000000, last=4999999 },
		{ first=5000000, last=5999999 },
		{ first=6000000, last=6999999 },
		{ first=7000000, last=7999999 },
		{ first=8000000, last=8999999 },
		{ first=9000000, last=9999999 },
		{ first=11000000, last=11999999 }
	}

	local fileSize = 26243176
	local chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, -1, nil )
	rxTest:expectEqual( 0, chunkStart )
	rxTest:expectEqual( 999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 1000000, chunkStart )
	rxTest:expectEqual( 1999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 2000000, chunkStart )
	rxTest:expectEqual( 2999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 10000000, chunkStart )
	rxTest:expectEqual( 10999999, chunkEnd )

	for x = 12, 25 do
		chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
		rxTest:expectEqual( x * 1000000, chunkStart )
		rxTest:expectEqual( x * 1000000 + 999999, chunkEnd )
	end
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 26000000, chunkStart )
	rxTest:expectEqual( fileSize-1, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( nil, chunkStart )
	rxTest:expectEqual( nil, chunkEnd )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_calculateNextChunk_differentChunkSize( rxTest )

	local catalogId = '1111f02c0177438aa3552161cc3f1111'
	local userId = '2222f02c0177438aa3552161cc3f2222'

	local catalogSpec = WFCatSpecMock( catalogId, userId, rxTest, {} )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )

	local chunkSize = 1000*1000
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, chunkSize, chunkSize )

	local fileSize = 26243176

	local existingParts = {
		{ first=0, last=1500000 },
		{ first=3000000, last=fileSize-1 },
	}

	local chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, -1, nil )
	rxTest:expectEqual( 0, chunkStart )
	rxTest:expectEqual( 999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 1500001, chunkStart )
	rxTest:expectEqual( 2500000, chunkEnd )

	-- Verify we don't send more than we have to (the range is reduced smaller than chunkSize here)
	-- This is because the start is in an area that hasn't been uploaded but the end is within a chunk.
	-- This could only happen if the chunk size were to change and it hits some code that previously
	-- was not hit via unit tests.
	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( 2500001, chunkStart )
	rxTest:expectEqual( 2999999, chunkEnd )

	chunkStart, chunkEnd = chunkedUploader:_calculateNextChunk( fileSize, chunkEnd, existingParts )
	rxTest:expectEqual( nil, chunkStart )
	rxTest:expectEqual( nil, chunkEnd )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_BigFile2GB_testNightly( rxTest )
	-- Test takes .18 seconds on mac but 28 seconds on Windows VM so moved to testNightly

	-- 2GB+ File Using 200MB Chunks
	
	-- Oz requires files larger than 200MB to be chunked so the max chunk size
	-- has been capped at 200MB as well in the chunked and content range uploader.

	rxTest:setTimeLimit( 120 )

	local fileSize = 2161521032 -- File size that failed from customer

	local success, fileSystemFreeSpace = pcall( WFFileUtils.freeSpaceAtPath, KSPathUtils.getStandardFilePath( 'temp' ) )
	
	if success and fileSystemFreeSpace <= fileSize then
	
		-- This test requires at least 2Gb of free space. If the build machine does 
		-- not have enough space, don't bother running the test.
		
		return
		
	end

	local filePath = makeFileOfSize( rxTest, fileSize )

	local MB = 1024*1024
	local chunkSize = 200*MB

	local urlResponses = {
		generateChunkResponse( filePath, 1, nil, nil, chunkSize, nil, 'bytes 0-209715199/2161521032' ),
		generateChunkResponse( filePath, 2, nil, nil, chunkSize, nil, 'bytes 209715200-419430399/2161521032' ),
		generateChunkResponse( filePath, 3, nil, nil, chunkSize, nil, 'bytes 419430400-629145599/2161521032' ),
		generateChunkResponse( filePath, 4, nil, nil, chunkSize, nil, 'bytes 629145600-838860799/2161521032' ),
		generateChunkResponse( filePath, 5, nil, nil, chunkSize, nil, 'bytes 838860800-1048575999/2161521032' ),
		generateChunkResponse( filePath, 6, nil, nil, chunkSize, nil, 'bytes 1048576000-1258291199/2161521032' ),
		generateChunkResponse( filePath, 7, nil, nil, chunkSize, nil, 'bytes 1258291200-1468006399/2161521032' ),
		generateChunkResponse( filePath, 8, nil, nil, chunkSize, nil, 'bytes 1468006400-1677721599/2161521032' ),
		generateChunkResponse( filePath, 9, nil, nil, chunkSize, nil, 'bytes 1677721600-1887436799/2161521032' ),
		generateChunkResponse( filePath, 10, nil, nil, chunkSize, nil,'bytes 1887436800-2097151999/2161521032' ),
		generateLastChunkResponse( filePath, 11, chunkSize,			  'bytes 2097152000-2161521031/2161521032' )
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, chunkSize, chunkSize )
	setDocValueToImportState( rxTest, catalog )

	rxTest:waitFor( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ) )

end

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

function WFCatContentRangeUploaderTest.DISABLED_rxTestContentRangeUploader_BigFile4GB_testNightly( rxTest )
-- Disabled because test doesn't run on Windows VM (possibly too long, it takes 13 seconds on a SurfaceBook Pro)

	-- 4GB+ File Using 200MB Chunks
	
	-- Oz requires files larger than 200MB to be chunked so the max chunk size
	-- has been capped at 200MB as well in the chunked and content range uploader.

	rxTest:setTimeLimit( 120 )

	local MB = 1024*1024
	local fileSize = 4*1024*MB + 100
	local chunkSize = 200*MB

--	for i = 0, 22 do
--		print("bytes "..tostring(i*chunkSize) .. '-' .. tostring((i+1)*chunkSize-1 ) .. '/' .. tostring(fileSize))
--	end

	local filePath = makeFileOfSize( rxTest, fileSize)

	local urlResponses = {
		generateChunkResponse( filePath, 1, nil, nil, chunkSize, nil, 'bytes 0-209715199/4294967396' ),
		generateChunkResponse( filePath, 2, nil, nil, chunkSize, nil, 'bytes 209715200-419430399/4294967396' ),
		generateChunkResponse( filePath, 3, nil, nil, chunkSize, nil, 'bytes 419430400-629145599/4294967396' ),
		generateChunkResponse( filePath, 4, nil, nil, chunkSize, nil, 'bytes 629145600-838860799/4294967396' ),
		generateChunkResponse( filePath, 5, nil, nil, chunkSize, nil, 'bytes 838860800-1048575999/4294967396' ),
		generateChunkResponse( filePath, 6, nil, nil, chunkSize, nil, 'bytes 1048576000-1258291199/4294967396' ),
		generateChunkResponse( filePath, 7, nil, nil, chunkSize, nil, 'bytes 1258291200-1468006399/4294967396' ),
		generateChunkResponse( filePath, 8, nil, nil, chunkSize, nil, 'bytes 1468006400-1677721599/4294967396' ),
		generateChunkResponse( filePath, 9, nil, nil, chunkSize, nil, 'bytes 1677721600-1887436799/4294967396' ),
		generateChunkResponse( filePath, 10, nil, nil, chunkSize, nil, 'bytes 1887436800-2097151999/4294967396' ),
		generateChunkResponse( filePath, 11, nil, nil, chunkSize, nil, 'bytes 2097152000-2306867199/4294967396' ),
		generateChunkResponse( filePath, 12, nil, nil, chunkSize, nil, 'bytes 2306867200-2516582399/4294967396' ),
		generateChunkResponse( filePath, 13, nil, nil, chunkSize, nil, 'bytes 2516582400-2726297599/4294967396' ),
		generateChunkResponse( filePath, 14, nil, nil, chunkSize, nil, 'bytes 2726297600-2936012799/4294967396' ),
		generateChunkResponse( filePath, 15, nil, nil, chunkSize, nil, 'bytes 2936012800-3145727999/4294967396' ),
		generateChunkResponse( filePath, 16, nil, nil, chunkSize, nil, 'bytes 3145728000-3355443199/4294967396' ),
		generateChunkResponse( filePath, 17, nil, nil, chunkSize, nil, 'bytes 3355443200-3565158399/4294967396' ),
		generateChunkResponse( filePath, 18, nil, nil, chunkSize, nil, 'bytes 3565158400-3774873599/4294967396' ),
		generateChunkResponse( filePath, 19, nil, nil, chunkSize, nil, 'bytes 3774873600-3984588799/4294967396' ),
		generateChunkResponse( filePath, 20, nil, nil, chunkSize, nil, 'bytes 3984588800-4194303999/4294967396' ),
		generateLastChunkResponse( filePath, 21, chunkSize,			  'bytes 4194304000-4294967395/4294967396' ),
	}

	local catalogSpec = WFCatSpecMock( kCatalogId, kUserId, rxTest, urlResponses )
	local catalog = WFCatMockCatalog.createWFCatalogForTestWithSpec( rxTest, catalogSpec )
	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, chunkSize, chunkSize )
	setDocValueToImportState( rxTest, catalog )

	rxTest:expectSingleValue( chunkedUploader:doChunkedUpload( kMasterUrl, filePath, kDefaultPutArgs ),
		':ignore:', ':ignore:' )

end

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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_NotifyWhenHttpRequestIsInProgress_testNightly( rxTest )

	-- The following test verifies that the content range uploader correctly supports notifyWhenHttpRequestIsInProgress
	-- handling. This test executes against a real server, and not a mock HTTP server, so that it receives
	-- the proper `requestInProgress` message prior to receiving the response from doChunkedUpload.
	
	rxTest:setTimeLimit( 60 )

	local filePath = makeFileOfSize( rxTest, 1024 )

	local catalogTestSession = WFCatTestSession( rxTest )

	local catalog, catalogSpec, docStore

	rxTest:expectSingleValue(

		catalogTestSession:catalog()
			:tap( function( _catalog )
				catalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				docStore = catalog:docStore()
			end ),

		':ignore:' )

	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, 512, 256 )
	setDocValueToImportState( rxTest, catalog )

	local putArgs = KSTableUtils.deepCopy( kDefaultPutArgs )
	putArgs.notifyWhenHttpRequestIsInProgress = true

	local serverAddr = "http://sj1010120180138.corp.adobe.com:8888"
	local realServerUrl = string.format( '%s/AgHttpClientTest3.post.cgi', serverAddr )

	local requestInProgress = false

	local function uploadHasStartedHandler()
		
		if not requestInProgress then
		
			requestInProgress = true
			
		end
	
	end

	rxTest:waitFor( chunkedUploader:doChunkedUpload( realServerUrl, filePath, putArgs, uploadHasStartedHandler )
	
		:catch( function()
			return Observable.just( true )
				-- SPAM [jandrew 2021-11-04]: Unstable test fix: https://jira.corp.adobe.com/browse/WF-1790
				-- Because we're using communicating with a server that doesn't deliver a JSON response,
				-- doChunkedUpload throws an error after sending out the progress event when trying to
				-- parse the response. If the error is processed before the test ends, the test fails.
				-- Catching the error should stabilize the test.
		end ) )
		
	rxTest:expectEqual( true, requestInProgress )

end


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

function WFCatContentRangeUploaderTest.rxTestContentRangeUploader_WithProgress_testNightly( rxTest )

	-- The following test verifies that the content range uploader correctly supports progress events.
	
	rxTest:setTimeLimit( 60 )

	local filePath = makeFileOfSize( rxTest, 1024 )

	local catalogTestSession = WFCatTestSession( rxTest )

	local catalog, catalogSpec, docStore

	rxTest:expectSingleValue(

		catalogTestSession:catalog()
			:tap( function( _catalog )
				catalog = _catalog
				catalogSpec = _catalog:ozCatalogSpec()
				docStore = catalog:docStore()
			end ),

		':ignore:' )

	local chunkedUploader = WFCatContentRangeUploader( catalog:docStore(), catalogSpec, 512, 256 )
	setDocValueToImportState( rxTest, catalog )

	local putArgs = KSTableUtils.deepCopy( kDefaultPutArgs )
	putArgs.includeProgress = true

	local serverAddr = "http://sj1010120180138.corp.adobe.com:8888"
	local realServerUrl = string.format( '%s/AgHttpClientTest3.post.cgi', serverAddr )

	local gotProgress = false

	rxTest:waitFor( chunkedUploader:doChunkedUpload( realServerUrl, filePath, putArgs )
	
		:map( function( _, _, progress )

			if progress then
				gotProgress = true
			end
			
			return true

		end )
		
		:catch( function()
			return Observable.just( true )
				-- SPAM [jandrew 2021-11-04]: Unstable test fix: https://jira.corp.adobe.com/browse/WF-1790
				-- Because we're using communicating with a server that doesn't deliver a JSON response,
				-- doChunkedUpload throws an error after sending out the progress event when trying to
				-- parse the response. If the error is processed before the test ends, the test fails.
				-- Catching the error should stabilize the test.
		end ) )

	rxTest:expectEqual( true, gotProgress )

end

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

-- SPAM [jandrew 2021-09-01]: WFCatContentRangeUploader progress event support is
-- covered more by WFCatBinaryUploaderTest.rxTestUploadAdd_WithProgress_testNightly

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

return WFRxTestCase.wrapSuite( 'WFCatContentRangeUploaderTest', WFCatContentRangeUploaderTest )
