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

WFCloseSupportTest.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2010 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 WFCloseSupportTest = {}

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

function WFCloseSupportTest.testCloseSupport()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	-- FIXME [scouten 2011-12-16]: Used only in KSReactiveBindingTest.
	-- Remove these tests once that dependency is removed.

	assert( WFCloseSupport.closeMethod, "No closeMethod" )
	
	local methodTable = { }
	
	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	assert( methodTable.addCloseTarget, "No method added for adding close targets" )
	assert( methodTable.addCloseAction, "No method added for adding close actionss" )
	assert( methodTable.close, "No close method added" )
	
	local x = setmetatable( { }, { __index = methodTable } )
	
	local closed = 0
	
	local fn1 = function() closed = closed + 1 end
	local fn2 = function() closed = closed + 2 end
	
	x:addCloseAction( fn1 )
	x:addCloseAction( fn2 )
	
	x:close()
	
	assert( closed == 3, "We should have called both close functions" )
	
	-- Now we try it without keeping them in local variables
		
	x = setmetatable( { }, { __index = methodTable } )

	closed = 0
	
	x:addCloseAction( function() closed = closed + 1 end )
	x:addCloseAction( function() closed = closed + 2 end )
	
	collectgarbage()
	collectgarbage()
	collectgarbage()
	
	-- But we might as well test removing nil as a target
	
	x:close()
	
	assert( closed == 3, "Close actions should survive GC" )
		
	x = setmetatable( { }, { __index = methodTable } )

	closed = 0
	
	x:addCloseAction( function() closed = closed + 1 end )
	x:addCloseAction( function() closed = closed + 2 end )
	x:addCloseAction( function() error "Boom!" end )
	
	x:close()
	
	assert( closed == 3, "We should have survived an error while calling close functions" )
	
	methodTable = { close = function( self ) self.closed = true end }
	
	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	local y
	
	x = setmetatable( { }, { __index = methodTable } )
	y = setmetatable( { }, { __index = methodTable } )
	
	x:addCloseTarget( y )
	
	x:close()
	
	assert( x.closed == true, "x should have been marked as closed" )
	assert( y.closed == true, "y should have been marked as closed" )
	
	-- Try again but with a cycle

	x = setmetatable( { }, { __index = methodTable } )
	y = setmetatable( { }, { __index = methodTable } )
	local z = setmetatable( { }, { __index = methodTable } )
	
	x:addCloseTarget( y )
	y:addCloseTarget( x )
	y:addCloseTarget( z )
	
	x:close()
	
	assert( x.closed == true, "x should have been marked as closed" )
	assert( y.closed == true, "y should have been marked as closed" )
	assert( z.closed == true, "z should have been marked as closed" )

end

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

function WFCloseSupportTest.testChainedClose()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local methodTable = { close = function( self ) self.closed = true end }
	
	WFCloseSupport.addCloseSupportMethods( methodTable )

	local w = setmetatable( { }, { __index = methodTable } )
	local x = setmetatable( { }, { __index = methodTable } )
	local y = setmetatable( { }, { __index = methodTable } )
	local z = setmetatable( { }, { __index = methodTable } )
	
	w:addCloseTarget( x )
	x:addCloseTarget( y )
	y:addCloseTarget( z )
	
	w:close()
	
	assert( w.closed, "Chained closure appears to have failed (w)." )
	assert( x.closed, "Chained closure appears to have failed (x)." )
	assert( y.closed, "Chained closure appears to have failed (y)." )
	assert( z.closed, "Chained closure appears to have failed (z)." )

end

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

function WFCloseSupportTest.testCloseRemovesFromSources()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local enqueueCount = 0
	
	WFCloseSupport._debug_setEnqueueHook( function() enqueueCount = enqueueCount + 1 end )
	
	local methodTable = { close = function( self ) end }
	
	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	local x = setmetatable( { }, { __index = methodTable } )
	local y = setmetatable( { }, { __index = methodTable } )
	local z = setmetatable( { }, { __index = methodTable } )
	
	x:addCloseTarget( y )
	x:addCloseTarget( z )
	
	y:close()
	
	assert( enqueueCount == 0, "We should not have enqueued any closes yet." )
	
	x:close()
	
	assert( enqueueCount == 1, "We should have done one enqueued close." )

	WFCloseSupport._debug_setEnqueueHook( nil )

end

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

function WFCloseSupportTest.testMustClose()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local didClose = false

	local methodTable = { close = function( self ) didClose = true end }

	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	local obj = setmetatable( { }, { __index = methodTable } )
	
	WFCloseSupport.mustClose( obj )
	
	obj = nil
	
	collectgarbage()
	collectgarbage()
	collectgarbage()
	
	assert( WFCloseSupport.processGCQueue(), "GC Queue should not have been empty" )

	collectgarbage()
	collectgarbage()
	collectgarbage()
	
	assert( didClose, "Must close should have resulted in the close method getting run on GC." )
	
	local obj2 = setmetatable( { }, { __index = methodTable } )
	
	didClose = false
	
	local closer = WFCloseSupport.mustClose( obj2 )
	
	closer:close()
	
	assert( didClose, "Explicitly closing the closer should have closed the object" )

end

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

function WFCloseSupportTest.testBounceClosure()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local methodTable = { close = function( self ) self._didClose = true end }
	
	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	local function makeObject()
		return setmetatable( { }, { __index = methodTable } )
	end
	
	local function makeObjects()
		local obj1, obj2 = makeObject(), makeObject()
		obj1:addCloseTarget( obj2 )
		return obj1, obj2
	end
	
	-- Confirm that everything works right without use of bounceClosure
	
	local linkage1, linkage2 = makeObjects()
	
	linkage1:close()
	
	assert( linkage1._didClose, "Linkage test: first object failed to close" )
	assert( linkage2._didClose, "Linkage test: second object failed to close" )

	-- Confirm that everything works right when we bounce closure from the second
	
	local second1, second2 = makeObjects()
	
	second2:bounceClosure()
	
	second1:close()
	
	assert( second1._didClose, "Bounce second test: first object failed to close" )
	assert( not second2._didClose, "Bounce second test: second object closed when it shouldn't have" )

	-- Confirm that everything works right when we bounce closure from the first
	
	local first1, first2 = makeObjects()
	
	first1:bounceClosure()
	
	first1:close()
	
	assert( not first1._didClose, "Bounce first test: first object closed when it shouldn't have" )
	assert( not first2._didClose, "Bounce first test: second object closed when it shouldn't have" )

end

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

function WFCloseSupportTest.testMemoize()

	local WFCloseSupport = assert( require "WFCloseSupport" )

	local MyClass = Rx.Class 'MyClass' {
		WFCloseSupport.addCloseSupportMethods,
		_value = false,
		
		__init = function( self, value )
			self._value = value
		end,
		
		getValue = function( self )
			return self._value
		end,
	}
	
	local nCalls = 0
	local function makeObj( n )
		nCalls = nCalls + 1
		return MyClass( n )
	end
	makeObj = WFCloseSupport.memoizeFn( makeObj )
	
	local obj = makeObj( 42 )
	assert( nCalls == 1, "Should have been called once" )
	assert( obj:getValue() == 42, "obj value should be 42" )
	local obj2 = makeObj( 42 )
	assert( obj == obj2, "obj and obj2 should be equal" )
	obj:close()
	local obj3 = makeObj( 42 )
	assert( obj ~= obj3, "New obj should be made when closed" )
	assert( obj2:getValue() == 42, "obj value should be 42" )
	assert( obj3:getValue() )

end

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

function WFCloseSupportTest.testWillClose()

	-- not sure if this is meant to be a public API but this will hit a couple previously unhit code paths

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	-- check on nil item

	assert( not WFCloseSupport.willClose( nil ), "should fail with nil" )
	
	-- check on already closed item

	local methodTable = { close = function( self ) self.closed = true end }

	WFCloseSupport.addCloseSupportMethods( methodTable )

	local x = setmetatable( { }, { __index = methodTable } )
	
	x:close()
	
	assert( x.closed )

	assert( not WFCloseSupport.willClose( x ), "should fail, x already closed" )

end

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

function WFCloseSupportTest.testHasBeenOrWillBeClosed()

	local WFCloseSupport = assert( require "WFCloseSupport" )

	-- test with nil

	assert( WFCloseSupport.hasBeenOrWillBeClosed( nil ), "consider closing nil successful" )

	-- test with false. (Rx.Class members often start out as false, not nil.)

	assert( WFCloseSupport.hasBeenOrWillBeClosed( false ), "consider closing false successful" )

	-- test typical cases

	local methodTable = { close = function( self ) self.closed = true end }

	WFCloseSupport.addCloseSupportMethods( methodTable )

	local x = setmetatable( { }, { __index = methodTable } )
	
	assert( not WFCloseSupport.hasBeenOrWillBeClosed( x ), "hasn't been requested to be closed yet, return false" )

	x:close()
	
	assert( x.closed == true, "x should have been marked as closed" )
	
	assert( WFCloseSupport.hasBeenOrWillBeClosed( x ), "return true, x already closed" )


	-- test case where closure is bounced
	
	local methodTable2 = { }
	
	WFCloseSupport.addCloseSupportMethods( methodTable2 )
	local x2 = setmetatable( { }, { __index = methodTable2 } )
	
	assert( not WFCloseSupport.hasBeenOrWillBeClosed( x2 ), "hasn't been requested to be closed yet, return false" )

	WFCloseSupport.enqueueTargetForClose( x2 )

	x2:bounceClosure()

	assert( not WFCloseSupport.hasBeenOrWillBeClosed( x2 ), "return false becaue closure was bounced" )


	-- test case where closure is closed then bounced
		
	local methodTable3 = { }
	
	WFCloseSupport.addCloseSupportMethods( methodTable3 )
	local x3 = setmetatable( { }, { __index = methodTable3 } )
	
	assert( not WFCloseSupport.hasBeenOrWillBeClosed( x3 ), "hasn't been requested to be closed yet, return false" )

	x3:close()
	
	x3:bounceClosure() -- intentionally after close

	-- FIXME [kcarlson 2013-05-28]: Think this is a corner case that probably doesn't matter
	-- however we should be returning true as x3 was already closed before the bounce could prevent it
--	assert( WFCloseSupport.hasBeenOrWillBeClosed( x3 ), "return true as it was closed before bounce could prevent it" )

end

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

function WFCloseSupportTest.testFetchCached()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local methodTable = { close = function( self ) self.closed = true end }

	WFCloseSupport.addCloseSupportMethods( methodTable )

	local testCache = { }

	local x = setmetatable( { }, { __index = methodTable } )
	
	local xid = 'xID'
	testCache[ xid ] = x
	
	assert( WFCloseSupport.fetchCached( testCache, xid ) == x, "found xid as expected" )
	
	x:close()
	
	assert( x.closed )
	
	assert( WFCloseSupport.fetchCached( testCache, xid ) == nil, "didn't find xid because it was closed" )

end

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

function WFCloseSupportTest.testQueuedCloseFunction()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local closed = false
	
	local function closeFn()
			closed = true
		end
	
	WFCloseSupport.enqueueTargetForClose( closeFn )
	
	WFCloseSupport.runCloseQueue( )
	
	assert( closed )
	
end

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

function WFCloseSupportTest.testAddCloseTarget()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local methodTable = { close = function( self ) self.closed = true end }
	WFCloseSupport.addCloseSupportMethods( methodTable )
	local x = setmetatable( { }, { __index = methodTable } )
	
	assert( x:addCloseTarget( nil ) == x )

	local x1 = setmetatable( { }, { __index = methodTable } )
	assert( x:addCloseTarget( x1 ) == x, "should return self if successful" )

	assert( not pcall( function() x:addCloseTarget( x1 ) end ), "should fail can only add to close target once" )
	
	x:close()
	
	local x2 = setmetatable( { }, { __index = methodTable } )
	assert( not pcall( function() x:addCloseTarget( x2 ) end ), "it's an error to add to already closed object" )

	local y = setmetatable( { }, { __index = methodTable } )
	assert( y:addCloseTarget( x ) == y, "should return self if target already closed" )

	assert( not pcall( function() y:addCloseTarget( "hi" ) end ), "error if target type support close" )
	
	local uncloseableClass = Rx.Class 'uncloseable' {
		__init = function( self ) end,
	}
	
	local uncloseable = uncloseableClass()
	
	assert( not pcall( function() y:addCloseTarget( uncloseable ) end ),
		"error if target type doesn't have a close method" )
	
end

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

function WFCloseSupportTest.testSetOwned()

	local WFCloseSupport = assert( require "WFCloseSupport" )

	local methodTable = { close = function( self ) self.closed = true end }
	WFCloseSupport.addCloseSupportMethods( methodTable )

	local compositeObject = setmetatable( { }, { __index = methodTable } )

	local obj1 = setmetatable( { }, { __index = methodTable } )
	local obj2 = setmetatable( { }, { __index = methodTable } )
	local obj3 = setmetatable( { }, { __index = methodTable } )
	local obj4 = setmetatable( { }, { __index = methodTable } )
	local obj5 = false
	local obj6 = setmetatable( { }, { __index = methodTable } )

	-- works with one closeable sub-object
	compositeObject:setOwned { _obj1 = obj1 }
	assert( compositeObject._obj1:isClosable() )

	-- works with multiple sub-objects
	compositeObject:setOwned { _obj2 = obj2, _obj3 = obj3 }
	assert( compositeObject._obj2:isClosable() )
	assert( compositeObject._obj3:isClosable() )

	-- works with multiple sub-objects where one is already
	-- owned and isClosable
	compositeObject:setOwned { _obj3 = obj3, _obj4 = obj4 }
	assert( compositeObject._obj3:isClosable() )
	assert( compositeObject._obj4:isClosable() )

	-- works with multiple sub-objects where one is disabled by
	-- setting the object to false
	compositeObject:setOwned { _obj5 = obj5, _obj6 = obj6 }
	assert( not compositeObject._obj5 )
	assert( compositeObject._obj6:isClosable() )

	compositeObject:close()

	-- verify sub-objects are closed
	assert( compositeObject._obj1.closed )
	assert( compositeObject._obj2.closed )
	assert( compositeObject._obj3.closed )
	assert( compositeObject._obj4.closed )
	assert( not compositeObject._obj5 )
	assert( compositeObject._obj6.closed )

end

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

function WFCloseSupportTest.testAddCloseAction()

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	local function closeFn( self ) self.closed = true end
	
	local methodTable = {  }
	WFCloseSupport.addCloseSupportMethods( methodTable )
	local x = setmetatable( { }, { __index = methodTable } )
	
	assert( not pcall(x.addCloseAction, x, nil ), "nil is not allowed" )

	assert( x:addCloseAction( closeFn ) == x, "should succeed and return self"  )
	
	WFCloseSupport.enqueueTargetForClose( closeFn )
	assert( not pcall( x.addCloseAction, x, closeFn ), "should fail because closeFn already queued"  )

	x:close()

	assert( not pcall( x.addCloseAction, x, closeFn ), "should fail because x already closed"  )
	
end

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

local testObjClosedByCloserDuringGCWasRun = false
local testObjClosedByCloserDuringGCCloseCount = 0

-- FIXME [jandrew 2/4/2015]: This test fails on Android.
local WFPlatformInfo = require 'WFPlatformInfo'
if not WFPlatformInfo.isAndroid then

function WFCloseSupportTest.testObjClosedByCloserDuringGC( test )

	-- note that code coverage does not track __gc functions

	local WFCloseSupport = assert( require "WFCloseSupport" )
	
	testObjClosedByCloserDuringGCWasRun = true
	testObjClosedByCloserDuringGCCloseCount = 0

	local methodTable = { close = function( self )
			testObjClosedByCloserDuringGCCloseCount = testObjClosedByCloserDuringGCCloseCount + 1
		end }

	WFCloseSupport.addCloseSupportMethods( methodTable )
	
	local obj1 = setmetatable( { }, { __index = methodTable } )
	
	WFCloseSupport.makeCloser( obj1, 'obj1', debug.traceback())
	
	obj1 = nil
	
	collectgarbage()
	collectgarbage()
	collectgarbage()
	
	local obj2 = setmetatable( { }, { __index = methodTable } )
	
	WFCloseSupport.makeCloser( obj2 ) -- run with no label to execute another code path
	
	obj2 = nil
	
	collectgarbage()
	collectgarbage()
	collectgarbage()
	
	local WFAsyncTestCase = require 'WFAsyncTestCase'
	local KSPromises = require 'KSPromises'
	local WFIdle = require 'WFIdle'
	
	local lazyLazyIsCountRightP, resolve = KSPromises.promiseP()
	
	WFIdle.onIdleDo( function()
		WFIdle.onIdleDo( function()
			if not testObjClosedByCloserDuringGCWasRun or testObjClosedByCloserDuringGCCloseCount == 2 then
				resolve( 'data', 'GCCloseCount is right' )
			elseif testObjClosedByCloserDuringGCWasRun then
				resolve( 'error', 'Obj was closed by closer but count is wrong (' .. tostring( testObjClosedByCloserDuringGCCloseCount ) .. ")"  )
			else
				resolve( 'error', 'Obj was NOT closed by closer!'  )
			end
		end )
	end )
	
	WFAsyncTestCase( test )
		:waitFor( lazyLazyIsCountRightP )
		:continueWhenValueMatches( 'data', 'GCCloseCount is right' )
	
end

end

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

-- FIXME [mhamburg 2011-03-16]: Test explict close network construction.

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

return WFCloseSupportTest
