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

WFAsyncFileUtilsTest.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 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.

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

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

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

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

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

local WFAsyncFileUtilsTest = {}

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

local child = KSPathUtils.child

local srcFolder = child( KSPathUtils.getStandardFilePath( 'testdata' ),
	'wffileutils' )

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

local function createDstPath( rxTest )

	local dstPath = child( KSPathUtils.getStandardFilePath( 'carouselDocuments' ),
		'WFAsyncFileUtilsTestFolder-' .. KSIdGenerator.uuidInLowercaseHex() )

	pcall( WFFileUtils.mkdir, dstPath )

	WFTestFileUtils.addTestFolder( dstPath )

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

	return dstPath

end

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

function WFAsyncFileUtilsTest.rxTestCopyToUniquelyNamedFile( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local dst = child( dstFolder, 'file.txt' )
	
	assert( not WFFileUtils.exists( dst ), "File already exists at destination" )
	
	local src = child( srcFolder, 'file.txt' )

	-- First time we copy to dest folder, name shouldn't change.

	rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ), dst )

	assert( WFFileUtils.exists( dst ) == 'file', "Didn't create copy of file" )
	
	-- If we copy a file on top of itself, it shouldn't change.	

	rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( dst, dst ), dst )

	assert( WFFileUtils.exists( dst ) == 'file',
		"Copying to same path caused file to be erased" )
	
	-- Make a second copy. This time, the file name should change since there's
	-- already a file at that location.

	local dst2

	rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst )
		:map( function( newDst )

			dst2 = newDst

			assert( KSPathUtils.parent( newDst ) == dstFolder,
				"Didn't create copy in same folder" )

			assert( WFFileUtils.exists( newDst ) == 'file',
				"Didn't create copy of file as expected" )

			return newDst ~= dst

		end ), true )

	-- Make a third copy. This time, name shouldn't match eithe rof the first two names.
	
	rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst )
		:map( function( newDst )

			assert( KSPathUtils.parent( newDst ) == dstFolder,
				"Didn't create copy in same folder" )

			assert( WFFileUtils.exists( newDst ) == 'file',
				"Didn't create copy of file as expected" )

			return newDst ~= dst and newDst ~= dst2

		end ), true )

end

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

function WFAsyncFileUtilsTest.rxTestCopyToUniquelyNamedFileSendsErrorOnMissingFile( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = child( srcFolder, 'MissingFile.txt' )
	local dst = child( dstFolder, 'file.txt' )

	rxTest:expectOnlyError( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ),
		"Unable to copy file.", ":ignore:" )
		
end

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

function WFAsyncFileUtilsTest.rxTestCopyToUniquelyNamedFileWhenSourceIsFolder( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = srcFolder
	local dst = child( dstFolder, 'newFolder' )

	if WFPlatformInfo.isWindows or WFPlatformInfo.isAndroid then

		rxTest:expectOnlyError( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ),
			"Unable to copy file.", ":ignore:" )

	else

		rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ), dst )
		assert( WFFileUtils.exists( dst ) == 'directory', "Didn't create copy of directory" )

		local fileInDst = child( dst, 'file.txt' )
		assert( WFFileUtils.exists( fileInDst ) == 'file', "Didn't copy contents of directory" )

	end

end

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

local function backupFile( rxTest, srcFile )
	local backupPath = createDstPath( rxTest )
	local backupFile = child( backupPath, 'FileToMove.txt' )
	WFFileUtils.copy( srcFile, backupFile )
	
	return backupFile
end

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

function WFAsyncFileUtilsTest.rxTestMoveToUniquelyNamedFile( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local dst = child( dstFolder, 'file.txt' )
	
	assert( not WFFileUtils.exists( dst ), "File already exists at destination" )
	
	local srcFile = child( srcFolder, 'file.txt' )
	
	-- First make a backup of the source work with that file
	local src = backupFile( rxTest, srcFile )


	-- First time we copy to dest folder, name shouldn't change.

	rxTest:expectSingleValue( WFAsyncFileUtils.moveToUniquelyNamedFile( src, dst ), dst )

	assert( WFFileUtils.exists( dst ) == 'file', "Didn't move the file" )
	assert( WFFileUtils.exists( src ) == false, "Source file still exists" )
	
	-- If we copy a file on top of itself, it shouldn't change.	

	rxTest:expectSingleValue( WFAsyncFileUtils.moveToUniquelyNamedFile( dst, dst ), dst )

	assert( WFFileUtils.exists( dst ) == 'file',
		"Copying to same path caused file to be erased" )
	
	-- Make a second copy. This time, the file name should change since there's
	-- already a file at that location.
	local src2 = backupFile( rxTest, srcFile )
	local dst2

	rxTest:expectSingleValue( WFAsyncFileUtils.moveToUniquelyNamedFile( src2, dst )
		:map( function( newDst )

			dst2 = newDst

			assert( KSPathUtils.parent( newDst ) == dstFolder,
				"Didn't move to same folder" )

			assert( WFFileUtils.exists( newDst ) == 'file',
				"Didn't move the file as expected" )
				
			assert( WFFileUtils.exists( src2 ) == false,
				"Source file still exists" )

			return newDst ~= dst

		end ), true )

	-- Make a third copy. This time, name shouldn't match either of the first two names.
	local src3 = backupFile( rxTest, srcFile )
	
	rxTest:expectSingleValue( WFAsyncFileUtils.moveToUniquelyNamedFile( src3, dst )
		:map( function( newDst )

			assert( KSPathUtils.parent( newDst ) == dstFolder,
				"Didn't move to same folder" )

			assert( WFFileUtils.exists( newDst ) == 'file',
				"Didn't move the file as expected" )

			assert( WFFileUtils.exists( src3 ) == false,
				"Source file still exists" )

			return newDst ~= dst and newDst ~= dst2

		end ), true )

end

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

function WFAsyncFileUtilsTest.rxTestMoveToUniquelyNamedFileSendsErrorOnMissingFile( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = child( srcFolder, 'MissingFile.txt' )
	local dst = child( dstFolder, 'file.txt' )

	rxTest:expectOnlyError( WFAsyncFileUtils.moveToUniquelyNamedFile( src, dst ),
		"Unable to move file.", ":ignore:" )
		
end

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

function WFAsyncFileUtilsTest.rxTestMoveFile( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local dstFolder = createDstPath( rxTest )
	
	local srcFile = child( srcFolder, 'file.txt' )
	
	-- First make a backup of the source work with that file
	local src = backupFile( rxTest, srcFile )
	local dst = child( dstFolder, 'file.txt' )
	
	rxTest:expectSingleValue( WFAsyncFileUtils.moveFile( src, dst ), dst )

end

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

function WFAsyncFileUtilsTest.rxTestMoveFileErrorOnMissingFile( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local dstFolder = createDstPath( rxTest )
	
	local src = child( srcFolder, 'MissingFile.txt' )
	local dst = child( dstFolder, 'file.txt' )
	
	rxTest:expectOnlyError( WFAsyncFileUtils.moveFile( src, dst ), "Unable to move file." , ":ignore:" )
	
end

--------------------------------------------------------------------------------
 
function WFAsyncFileUtilsTest.rxTestMoveFileErrorAlreadyExists( rxTest )

	rxTest:setTimeLimit( 10 )
	
	local dstFolder = createDstPath( rxTest )
	local src = backupFile( rxTest, child( srcFolder, 'file.txt' ) )
	local dst = child( dstFolder, 'file.txt' )
	
	WFFileUtils.copy( src, dst )
	
	rxTest:expectOnlyError( WFAsyncFileUtils.moveFile( src, dst ), "Unable to move file.", "File exists at destination" )
	
end

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

local function makeFilePathWithLongestLegalName( inFolder )

	local testFilePath
	local leafNameLength = 257
		-- Start with a length that is too long for any operating system.

	repeat

		leafNameLength = leafNameLength - 1

		if leafNameLength == 0 then
			error( "unable to create test file" )
		end

		local testFileName = string.rep( 'a', leafNameLength ) .. '.jpg'
		testFilePath = child( inFolder, testFileName )

		local testFile = io.open( testFilePath, 'w' )
		if testFile then
			testFile:close()
			WFFileUtils.delete( testFilePath )
		end

	until testFile

	return testFilePath

end

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

function WFAsyncFileUtilsTest.rxTestCopyToUniquelyNamedFileSendsErrorOnFailure( rxTest )

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = child( srcFolder, 'file.txt' )
	local dst = makeFilePathWithLongestLegalName( dstFolder )

	-- First copy to dest folder.

	rxTest:expectSingleValue( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ), dst )

	-- Second copy should fail as it's too long to be made unique.

	assert( WFFileUtils.exists( dst ) == 'file', "Didn't create copy of file" )

	rxTest:expectOnlyError( WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst ),
		"Unable to copy file.", ":ignore:" )

end

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

function WFAsyncFileUtilsTest.rxTestCopyToUniquelyNamedFileWithLateSubscription( rxTest )

	local sleepTime = 1
	if WFPlatformInfo.isWindows or WFPlatformInfo.isAndroid then
		sleepTime = 3
	end

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = child( srcFolder, 'file.txt' )
	local dst = child( dstFolder, 'file.txt' )

	local copyObservable = WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst )

	rxTest:sleep( sleepTime )

	assert( not WFFileUtils.exists( dst ), "Copy doesn't occur until subscription" )

	rxTest:expectSingleValue( copyObservable, dst )

	assert( WFFileUtils.exists( dst ) )

end

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

function WFAsyncFileUtilsTest.DISABLED_rxTestCopyToUniquelyNamedFileWithDisposedSubscription( rxTest )

	local sleepTime = 1
	if WFPlatformInfo.isWindows or WFPlatformInfo.isAndroid then
		sleepTime = 3
	end

	rxTest:setTimeLimit( 10 )

	local dstFolder = createDstPath( rxTest )

	local src = child( srcFolder, 'file.txt' )
	local dst = child( dstFolder, 'file.txt' )

	local copyObservable = WFAsyncFileUtils.copyToUniquelyNamedFile( src, dst )

	-- Subscription will start the copy
	local copyCompleted = false
	local subscription = copyObservable:subscribe( function( finalPath )
		copyCompleted = true
	end )

	-- Drop the subscription before we have a chance to be called back.
	subscription:dispose()

	-- Delay a little to make sure the file has a chance to be copied.
	rxTest:sleep( sleepTime )

	assert( not copyCompleted, "Shouldn't have had a chance to call the subscriber function" )

	-- Known issue that the file was copied but the actual filename wasn't sent,
	-- leaving an unreferenced file.

	assert( WFFileUtils.exists( dst ), "Known issue that file is still copied." )
		-- It would be preferable to have file not exist.  This assert documents
		-- the current expected behavior.

end

--------------------------------------------------------------------------------
--
-- WFAsyncFileUtils.calculatePathSize not yet implemented on Android
if not WFPlatformInfo.isAndroid then 

function WFAsyncFileUtilsTest.rxTestCalculatePathSize( rxTest )

	local dstPath = createDstPath( rxTest )
	
	local sizeObservable = WFAsyncFileUtils.calculatePathSize( dstPath )
	
	-- Expected size of empty directory to be 0
	rxTest:expectSingleValue( sizeObservable, 0 )
	
	-- add a file to the dstPath
	WFFileUtils.copyToUniquelyNamedFile( child( srcFolder, 'file.txt' ),
											child( dstPath, 'file.txt' ) )
	
	-- re calculate directory size
	sizeObservable = WFAsyncFileUtils.calculatePathSize( dstPath )
	
	-- Expected size of directory to be 5
	rxTest:expectSingleValue( sizeObservable, 5 )
	
	-- add a sub directory to dstPath
	local subDir = child( dstPath, 'subDir' )
	WFFileUtils.mkdir( subDir )
	
	-- add a file to sub directory
	-- add a file to the dstPath
	local srcFile2 = WFFileUtils.copyToUniquelyNamedFile( child( srcFolder, 'file.txt' ),
														  child( subDir, 'file.txt' ) )
	
	-- re calculate directory size
	sizeObservable = WFAsyncFileUtils.calculatePathSize( dstPath )
	
	-- Expected size of directory and subdirectories to be 10
	rxTest:expectSingleValue( sizeObservable, 10 )
	
	-- calculate directory size of non-existent dir ( 0 )
	local nonexistentDir = child( dstPath, 'dontcreate' )
	
	sizeObservable = WFAsyncFileUtils.calculatePathSize( nonexistentDir )
	
	-- Expected size of non-existent directory to be 0
	rxTest:expectSingleValue( sizeObservable, 0 )

	-- calculate directory size of file instead of dir (file size)
	sizeObservable = WFAsyncFileUtils.calculatePathSize( srcFile2 )
	
	-- Expected size of file to be 5
	rxTest:expectSingleValue( sizeObservable, 5 )

end

end

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

function WFAsyncFileUtilsTest.rxTestReadFile( rxTest )
	
	local dstFolder = createDstPath( rxTest )
	
	local file = child( dstFolder, 'testReadFile.txt' )
	
	local fileContent = "THIS IS THE WFAsyncFileUtilsTest.rxTestReadFile"
	local data = KSData.make( fileContent )
	
	local ok, message = pcall( KSData.saveToFile, data, file )
	
	data:close()
	
	assert( ok, string.format( "failed to write file for rxTestReadFile: %s", message ) )
	
	local readObservable = WFAsyncFileUtils.readFile( file )
		:map( function( data )
			local result
			if data then
				result = tostring( data )
				data:close()
			end
			
			return result
		end )
	
	local observer = rxTest:observe( readObservable )
	
	observer:expectData( fileContent )
	
	observer:expectCompleted()
	
	local missingFile = child( dstFolder, 'testReadFileMissing.txt' )
		
	readObservable = WFAsyncFileUtils.readFile( missingFile )
		:map( function( data )
			local result
			if data then
				result = tostring( data )
				data:close()
			end
			
			return result
		end )
	
	local missingObserver = rxTest:observe( readObservable )
	
	missingObserver:expectData( nil )
	
	missingObserver:expectCompleted()
end

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

function WFAsyncFileUtilsTest.rxTestSaveToFile( rxTest )

	local dstFolder = createDstPath( rxTest )
	
	local file = child( dstFolder, 'testSaveToFile.txt' )
	
	local fileContent = "THIS IS THE WFAsyncFileUtilsTest.rxTestSaveToFile"
	local data = KSData.make( fileContent )
	
	local writeObservable = WFAsyncFileUtils.saveToFile( data, file )

	local observer = rxTest:observe( writeObservable )
	
	observer:expectData( true )
	
	observer:expectCompleted()
	
	local data2 = KSData.memoryMappedContentsOfFile( file )
	
	assert( tostring( data2 ) == fileContent, "file content did not match expected content" )
	
	data:close()
	data2:close()
end

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

if WFPlatformInfo.isWindows then

-- SPAM [jandrew 2021-01-11]: This test verifies that the implementation
-- of `WFFileUtils_saveToFile`, which is is called by WFAsyncFileUtils,
-- supports writing very, very large files. 

-- We need to attempt to save more than 4gb of content. Why 4gb?
-- On Windows, we used to use the Win32 function WriteFile to write
-- files to disk. WriteFile takes a DWORD as the third parameter to
-- indicate the number of bytes to write to disk.

-- DWORD is an unsigned integer data type, with a maximum size of 4294967295 (4GB).
-- In other words, the Windows code used to only work with files up to 4GB in size.

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

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 makeFourGbFile( filePath )

	local fileSize = 4*1024*1024*1024 + 100 -- 4Gb + a small buffer

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

end

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

function WFAsyncFileUtilsTest.rxTestSaveToFile_UNSTABLE_testNightly( rxTest )

	rxTest:setTimeLimit( 500 )
	
	local dstFolder = createDstPath( rxTest )
	
	local success, fileSystemFreeSpace = pcall( WFFileUtils.freeSpaceAtPath, dstFolder )
	
	local eightGb = 8*1024*1024*1024 + 1024 -- 8Gb + a small buffer
	
	if success and fileSystemFreeSpace <= eightGb then
	
		-- This test requires at least 8Gb of free space. If the build machine does 
		-- not have enough space, don't bother running the test.
		
		return
		
	end
	
	-- Create a 4 GB file and save it to disk.
	
	local filePath1 = child( dstFolder, 'rxTestSaveToFile_testNightly1.txt' )
	
	makeFourGbFile( filePath1 )
	
	local data = KSData.memoryMappedContentsOfFile( filePath1 )
	
	local filePath2 = child( dstFolder, 'rxTestSaveToFile_testNightly2.txt' )
	
	local writeObservable = WFAsyncFileUtils.saveToFile( data, filePath2 )

	local observer = rxTest:observe( writeObservable )
	
	observer:expectDataWithinTime( 300, true )
	
	observer:expectCompleted()
	
	data:close()
	
	local fileAttr = WFFileUtils.fileAttributes( filePath2 )
	
	local fourGbSize = 1024 * 1024 * 1024 * 4
	
	assert( fileAttr.fileSize >= fourGbSize, "file content was smaller than we expected" )

end

end

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

function WFAsyncFileUtilsTest.rxTestSaveToUniquelyNamedFile( rxTest )

	local dstFolder = createDstPath( rxTest )
	
	local file = child( dstFolder, 'testSaveToFile.txt' )
	
	local fileContent = "THIS IS THE WFAsyncFileUtilsTest.rxTestSaveToFile"
	local data = KSData.make( fileContent )
	
	local writeObservable = WFAsyncFileUtils.saveToUniquelyNamedFile( data, file )

	local observer = rxTest:observe( writeObservable )
	
	observer:expectData( file )
	
	observer:expectCompleted()
	
	
	local data2 = KSData.memoryMappedContentsOfFile( file )
	
	assert( tostring( data2 ) == fileContent, "file content did not match expected content" )
	
	
	-- Try writing again. Data should be saved with a different name.
	
	writeObservable = WFAsyncFileUtils.saveToUniquelyNamedFile( data, file )

	observer = rxTest:observe( writeObservable )
	
	local file2 = child( dstFolder, 'testSaveToFile-2.txt' )
	
	observer:expectData( file2 )
	
	observer:expectCompleted()
	
	data:close()
	data2:close()
end

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

return WFRxTestCase.wrapSuite( WFAsyncFileUtilsTest )
