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

WFCatPixelFileIOWorkerTest.lua

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

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

NOTICE: All information contained herein is, and remains 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 are protected by all applicable
intellectual property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

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

	-- WF.core.utils
local KSIdGenerator = require 'KSIdGenerator'
local WFCrypto = require 'WFCrypto'
local WFFileUtils = require 'WFFileUtils'
local WFFunctionUtils = require 'WFFunctionUtils'
local KSPathUtils = require 'KSPathUtils'

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

	-- WF.oz_client.managed_catalog.pixel_provider
local WFCatPixelFileIOWorker = require 'WFCatPixelFileIOWorker'
local WFCatPixelPathGenerator = require 'WFCatPixelPathGenerator'

	-- Rx
local Rx = require "Rx"

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

local function makeDummyDataFile( path )

	assert( not WFFileUtils.exists( path ), 
			string.format( "%s exists, refusing to add dummy data to it.", path ) )

	local fh = assert( io.open( path, "w" ), 
						string.format( "Couldn't open %s in order to add fake data", path ) )

	fh:write( "wut" )

	fh:close()

	return path

end

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

local function addDummyDataFile( paths, longSide, basePath, assetId, digest )

	if not require 'WFArguments_uuid'.isUuid( digest ) then
		halt()
	end

	paths[ #paths + 1 ] = makeDummyDataFile(
							WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdge( 
 								basePath, assetId, digest, longSide ) )

	return paths

end

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

local function addDummyDataFilesForLongSides( paths, digest, longSides, basePath, assetId )

	return WFFunctionUtils.reduce(
			paths,
				addDummyDataFile,
					longSides, basePath, assetId, digest )

end

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

local function addDummyDataFilesForDigestsLongSides( basePath, assetId, digests, longSides )

	return WFFunctionUtils.reduce(
			{},
				addDummyDataFilesForLongSides,
					digests, longSides, basePath, assetId )

end

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

local function checkExists( path, rxTest, expected, desc )

	rxTest:logf( "%s: %s", desc, path )

	rxTest:expectEqual( not not WFFileUtils.exists( path ), expected )

end

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

local function lastNComponents( path, n )

	local partToStrip = path

	for _ = 1, n do

		partToStrip = KSPathUtils.parent( partToStrip )

	end

	local lastNWithSep = string.sub( path, #partToStrip + 1, #path )

	return string.sub( lastNWithSep, 2, #lastNWithSep )

end

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

local function makeSuccessfulDeleteTuple( path )

	return { true, lastNComponents( path, 3 ) }

end

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

local function replacePathWithLastThreeComponents( deleteTuple )

	return { deleteTuple[ 1 ], lastNComponents( deleteTuple[ 2 ], 3 ), 
				unpack( deleteTuple, 3 ) }

end

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

local function normalizeDeleteResults( deleteTuples )

	return WFFunctionUtils.map( replacePathWithLastThreeComponents,
									deleteTuples )

end

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

local function rotateHex( h )

	assert( ( h >= '0' and h <= '9' ) or 
				( h >= 'a' and h <= 'f' ) )

	if h == '9' then

		return 'a'

	end

	if h == 'f' then

		return '0'

	end

	return string.char( string.byte( h ) + 1 )

end

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

local function rotateUUIDCharN( uuid, n )

	local prefix = n > 1 and string.sub( uuid, 1, n - 1 ) or ""

	local rotatedHex = rotateHex( string.sub( uuid, n, n ) )

	local suffix = n < #uuid and string.sub( uuid, n + 1, #uuid ) or ""

	return prefix .. rotatedHex .. suffix


end

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

local function randomDigest()

	return ( WFCrypto.MD5.digest( KSIdGenerator.uuidInLowercaseHex() ) )

end

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

local function orderDeleteResults( rslt1, rslt2 )

	if type( rslt1 ) ~= type( rslt2 ) then

		return type( rslt1 ) < type( rslt2 ) 

	end

	if type( rslt1 ) ~= "table" then

		return tostring( rslt1 ) < tostring( rslt2 )

	end

	local exists1, fname1 = unpack( rslt1 )

	local exists2, fname2 = unpack( rslt2 )

	if exists1 ~= exists2 then

		return not not exists1

	end

	return tostring( fname1 ) < tostring( fname2 )

end

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

local function sortDeleteResults( rslts )

	if type( rslts ) ~= "table" then

		return rslts

	end

	table.sort( rslts, orderDeleteResults )

	return rslts

end

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

local function rxTestDeleteAssetId( rxTest )

	local wfCatalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )
	
	local basePath = wfCatalog:fileSpec():pixelsPath()

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()

	-- choose assetId2 so that it's in the same folder as assetId1
	local assetId2 = rotateUUIDCharN( assetId1, #assetId1 )

	local digests = { randomDigest(), randomDigest(), randomDigest(),
						randomDigest() }

	local longSides = { 2, 640, 2 ^16 - 2, "full" }

	-- make multiple preview files for assetId1
	local assetId1Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId1, digests,
													longSides )

	-- make a non-rendition path in the same folder
	local wutFilePath = makeDummyDataFile( 
							KSPathUtils.child( KSPathUtils.parent( assetId1Files[ 1 ] ),
											"wut.jpg" ) )

	-- make multiple preview files for assetId2
	local assetId2Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId2, digests,
													longSides )

	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, true, "assetId1 files exist at start" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, true, "assetId2 files exist at start" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist at start" )

	local cacheChecker = WFCatPixelFileIOWorker( basePath )

	local globalPurgeStreamWatcher = rxTest:observe( cacheChecker:purgeResultsStream()
														:map( normalizeDeleteResults )
														:map( sortDeleteResults ) )

	rxTest:log( "delete an assetId that ain't there" )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( rotateUUIDCharN( assetId2, #assetId2 ) ) ) )
		:expectData( {} )

	globalPurgeStreamWatcher:expectData( {} )

	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, true, "assetId1 files exist after deleting nonesuch assetId" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, true, "assetId2 files exist after deleting nonesuch assetId" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist after deleting nonesuch assetId" )

	rxTest:log( "delete assetId1" )

	local expectedSuccessfulDeletes = 
			sortDeleteResults( 
				WFFunctionUtils.map ( makeSuccessfulDeleteTuple, 
										assetId1Files ) )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( assetId1 ) )
					:map( normalizeDeleteResults )
					:map( sortDeleteResults ) )
		:expectData( expectedSuccessfulDeletes )

	globalPurgeStreamWatcher:expectData( expectedSuccessfulDeletes )

	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, false, "assetId1 files gone" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, true, "assetId2 files exist after assetId1 delete" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist after assetId1 delete" )

	rxTest:log( "delete assetId2" )

	expectedSuccessfulDeletes = 
			sortDeleteResults( 
				WFFunctionUtils.map ( makeSuccessfulDeleteTuple, 
										assetId2Files ) )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( assetId2 ) )
					:map( normalizeDeleteResults )
					:map( sortDeleteResults ) )
		:expectData( expectedSuccessfulDeletes )

	globalPurgeStreamWatcher:expectData( expectedSuccessfulDeletes )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, false, "assetId2 files gone" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist after assetId2 delete" )
	
	rxTest:finally( 
		function()

			WFFileUtils.delete( wutFilePath  )

		end )
	
end

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

local function makeExpectedScanResult( assetIds, digests, longSides )

	local expectedInfos = {}

	for _, assetId in ipairs( assetIds ) do

		for _, digest in ipairs( digests ) do

			for _, longSide in ipairs( longSides ) do 

				expectedInfos[ assetId ] = expectedInfos[ assetId ] or {}

				expectedInfos[ assetId ][ digest ] = 
					expectedInfos[ assetId ][ digest ] or {}

				expectedInfos[ assetId ][ digest ][ longSide ] = true

			end
		
		end

	end

	return expectedInfos

end

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

local function makeExpectedDeleteResult( pixelsPath, assetIds, digests, longSides )

	local expectedInfos = {}

	for _, assetId in ipairs( assetIds ) do

		for _, digest in ipairs( digests ) do

			for _, longSide in ipairs( longSides ) do 

				expectedInfos[ #expectedInfos + 1 ] = {
					true,
					WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdge(
									pixelsPath, assetId, digest, longSide )
				}

				expectedInfos[ #expectedInfos + 1 ] = {
					true,
					WFCatPixelPathGenerator.pathForPreviewDigestAndLongEdgeJpg(
									pixelsPath, assetId, digest, longSide )
				}

			end
		
		end

	end

	return expectedInfos

end

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

local function rxTestScan( rxTest )

	rxTest:setTimeLimit( 3600 )

	local wfCatalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )
	
	local basePath = wfCatalog:fileSpec():pixelsPath()

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()

	-- choose assetId2 so that it's in the same folder as assetId1
	local assetId2 = rotateUUIDCharN( assetId1, #assetId1 )

	local digests = { randomDigest(), randomDigest(), randomDigest(),
						randomDigest() }

	local longSides = { 2, 640, 2 ^16 - 2, "full" }

	-- make multiple preview files for assetId1
	local assetId1Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId1, digests,
													longSides )

	-- make a non-rendition path in the same folder
	local wutFilePath = makeDummyDataFile( 
							KSPathUtils.child( KSPathUtils.parent( assetId1Files[ 1 ] ),
											"wut.jpg" ) )

	-- make multiple preview files for assetId2
	local assetId2Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId2, digests,
													longSides )

	-- make assetId3 in a different folder from assetId
	local assetId3 = rotateUUIDCharN( assetId1, 1 )

	-- make multiple preview files for assetId2
	local assetId3Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId3, digests,
													longSides )

	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, true, "assetId1 files exist at start" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, true, "assetId2 files exist at start" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist at start" )

	WFFunctionUtils.map( checkExists,
							assetId3Files, rxTest, true, "assetId2 files exist at start" )

	local cacheChecker = WFCatPixelFileIOWorker( basePath )

	local assetId1FolderName = WFCatPixelPathGenerator.renditionFolderNameForAssetId( assetId1 )

	rxTest:log( "Doing scanRenditionFolderNamed and check" )

	local expectedScanResult = makeExpectedScanResult( 
								{ assetId1, assetId2 }, digests, longSides )


	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( assetId1FolderName ) ) )
		:expectData( expectedScanResult )

	rxTest:log( "Cleanup assetId1" )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( assetId1 ) )
					:map( function() return {} end ) )
		:expectData( {} )

	expectedScanResult = makeExpectedScanResult( 
								{ assetId2 }, digests, longSides )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( assetId1FolderName ) ) )
		:expectData( expectedScanResult )

	rxTest:log( "Cleanup assetId2" )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( assetId2 ) )
					:map( function() return {} end ) )
		:expectData( {} )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( assetId1FolderName ) ) )
		:expectData( {} )

	local assetId3FolderName = WFCatPixelPathGenerator.renditionFolderNameForAssetId( assetId3 )

	expectedScanResult = makeExpectedScanResult( 
								{ assetId3 }, digests, longSides )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( assetId3FolderName ) ) )
		:expectData( expectedScanResult )

	rxTest:log( "Cleanup assetId3" )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteAssetRenditions( assetId3 ) )
					:map( function() return {} end ) )
		:expectData( {} )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( assetId3FolderName ) ) )
		:expectData( {} )

	rxTest:log( "scan bogus folder" )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:scanRenditionFolderNamed( 'wut' ) ) )
		:expectData( {} )
	
	rxTest:finally( 
		function()

			WFFileUtils.delete( wutFilePath  )

		end )
	
end

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

local function rxTestDeleteRenditions( rxTest )

	rxTest:setTimeLimit( 3600 )

	local wfCatalog = WFCatMockCatalog.createFakeWFCatalog( rxTest )
	
	local basePath = wfCatalog:fileSpec():pixelsPath()

	local assetId1 = KSIdGenerator.uuidInLowercaseHex()

	-- choose assetId2 so that it's in the same folder as assetId1
	local assetId2 = rotateUUIDCharN( assetId1, #assetId1 )

	local digests = { randomDigest(), randomDigest(), randomDigest(),
						randomDigest() }

	local longSides = { 2, 640, 2 ^16 - 2, "full" }

	-- make multiple preview files for assetId1
	local assetId1Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId1, digests,
													longSides )

	-- make a non-rendition path in the same folder
	local wutFilePath = makeDummyDataFile( 
							KSPathUtils.child( KSPathUtils.parent( assetId1Files[ 1 ] ),
											"wut.jpg" ) )

	-- make multiple preview files for assetId2
	local assetId2Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId2, digests,
													longSides )

	-- make assetId3 in a different folder from assetId
	local assetId3 = rotateUUIDCharN( assetId1, 1 )

	-- make multiple preview files for assetId2
	local assetId3Files = 
			addDummyDataFilesForDigestsLongSides( basePath, assetId3, digests,
													longSides )

	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, true, "assetId1 files exist at start" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, true, "assetId2 files exist at start" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath exist at start" )

	WFFunctionUtils.map( checkExists,
							assetId3Files, rxTest, true, "assetId3 files exist at start" )

	local cacheChecker = WFCatPixelFileIOWorker( basePath )

	local assetId1FolderName = WFCatPixelPathGenerator.renditionFolderNameForAssetId( assetId1 )

	rxTest:log( "Doing scanRenditionFolderNamed and check" )

	local expectedScanResult = makeExpectedScanResult( 
								{ assetId1, assetId2 }, digests, longSides )
								
	local renditionInfos = cacheChecker:scanRenditionFolderNamed( assetId1FolderName )							
	
	rxTest
		:observe( Rx.Observable.just( renditionInfos ) )
		:expectData( expectedScanResult )

	local expectedDeleteResults = makeExpectedDeleteResult( 
								basePath, { assetId1, assetId2 }, digests, longSides )

	rxTest
		:observe( Rx.Observable.just( cacheChecker:deleteRenditions( renditionInfos ) )
			:map( sortDeleteResults )
		)
		:expectData( sortDeleteResults( expectedDeleteResults ) )
		
		
	WFFunctionUtils.map( checkExists,
							assetId1Files, rxTest, false, "assetId1 files do not exist at end" )

	WFFunctionUtils.map( checkExists,
							assetId2Files, rxTest, false, "assetId2 files do not exist at end" )

	WFFunctionUtils.map( checkExists,
							{ wutFilePath }, rxTest, true, "wutFilePath should exist at end" )

	WFFunctionUtils.map( checkExists,
							assetId3Files, rxTest, true, "assetId3 should exist at end" )
	
end


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

return WFRxTestCase.wrapSuite( 'WFCatPixelFileIOWorkerTest', {

	rxTestDeleteAssetId = rxTestDeleteAssetId,

	rxTestDeleteRenditions = rxTestDeleteRenditions,

	rxTestScan = rxTestScan,

} )
