local e=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\106\101\115\116-\103\108\111\098\097\108\115")local k=e.expect local p=e.jest local f=e.it local S=e.describe local A=require("\046\046\047\083\105\110\103\108\101\069\118\101\110\116\077\097\110\097\103\101\114")local M=require("\046\047\119\097\105\116\070\111\114\069\118\101\110\116\115")S("\110\101\119",function()f("\115\104\111\117\108\100 \099\114\101\097\116\101 \097 \083\105\110\103\108\101\069\118\101\110\116\077\097\110\097\103\101\114",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local p=A.new(e);(k(p)).never.toBeNil()end)end)S("\099\111\110\110\101\099\116\069\118\101\110\116",function()f("\115\104\111\117\108\100 \099\111\110\110\101\099\116 \116\111 \101\118\101\110\116\115",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S=p.fn()f:connectEvent("\069\118\101\110\116",function(...)S(...)end)f:resume()e:Fire("\102\111\111")M();(k(S)).toBeCalledTimes(1);(k(S)).toBeCalledWith(e,"\102\111\111")e:Fire("\098\097\114")M();(k(S)).toBeCalledTimes(2);(k(S)).toBeCalledWith(e,"\098\097\114")f:connectEvent("\069\118\101\110\116")e:Fire("\098\097\122")M();(k(S)).toBeCalledTimes(2)end)f("\115\104\111\117\108\100 \100\114\111\112 \101\118\101\110\116\115 \117\110\116\105\108 \114\101\115\117\109\101\100 \105\110\105\116\105\097\108\108\121",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S=p.fn()f:connectEvent("\069\118\101\110\116",function(...)S(...)end)e:Fire("\102\111\111")M();(k(S)).never.toBeCalled()f:resume()e:Fire("\098\097\114")M();(k(S)).toBeCalledTimes(1);(k(S)).toBeCalledWith(e,"\098\097\114")end)f("\115\104\111\117\108\100 \105\110\118\111\107\101 \115\117\115\112\101\110\100\101\100 \101\118\101\110\116\115 \119\104\101\110 \114\101\115\117\109\101\100",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S=p.fn()f:connectEvent("\069\118\101\110\116",function(...)S(...)end)f:resume()e:Fire("\102\111\111")M();(k(S)).toBeCalledTimes(1);(k(S)).toBeCalledWith(e,"\102\111\111")f:suspend()e:Fire("\098\097\114")M();(k(S)).toBeCalledTimes(1)f:resume();(k(S)).toBeCalledTimes(2);(k(S)).toBeCalledWith(e,"\098\097\114")end)f("\115\104\111\117\108\100 \105\110\118\111\107\101 \101\118\101\110\116\115 \116\114\105\103\103\101\114\101\100 \100\117\114\105\110\103 \114\101\115\117\109\112\116\105\111\110 \105\110 \116\104\101 \099\111\114\114\101\099\116 \111\114\100\101\114",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S={}local O=p.fn(function(k,p)table.insert(S,p)if p==2 then e:Fire(3)elseif p==3 then e:Fire(4)end end)f:connectEvent("\069\118\101\110\116",function(...)O(...)end)f:suspend()e:Fire(1)e:Fire(2)M()f:resume()M()M();(k(O)).toBeCalledTimes(4);(k(S)).toEqual({1,2;3;4})end)f("\115\104\111\117\108\100 \110\111\116 \105\110\118\111\107\101 \101\118\101\110\116\115 \102\105\114\101\100 \100\117\114\105\110\103 \115\117\115\112\101\110\115\105\111\110 \098\117\116 \100\105\115\099\111\110\110\101\099\116\101\100 \098\101\102\111\114\101 \114\101\115\117\109\112\116\105\111\110",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S=p.fn()f:connectEvent("\069\118\101\110\116",function(...)S(...)end)f:suspend()e:Fire(1)M()f:connectEvent("\069\118\101\110\116")f:resume();(k(S)).never.toBeCalled()end)f("\115\104\111\117\108\100 \110\111\116 \121\105\101\108\100 \101\118\101\110\116\115 \116\104\114\111\117\103\104 \116\104\101 \083\105\110\103\108\101\069\118\101\110\116\077\097\110\097\103\101\114 \119\104\101\110 \114\101\115\117\109\105\110\103",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local p=A.new(e)p:connectEvent("\069\118\101\110\116",function()coroutine.yield()end)p:resume()local f=coroutine.create(function()e:Fire(5)M()end)assert(coroutine.resume(f))M();(k(coroutine.status(f))).toBe("\100\101\097\100")p:suspend()e:Fire(5)M()f=coroutine.create(function()p:resume()end)assert(coroutine.resume(f));(k(coroutine.status(f))).toBe("\100\101\097\100")end)f("\115\104\111\117\108\100 \110\111\116 \116\104\114\111\119 \101\114\114\111\114\115 \116\104\114\111\117\103\104 \083\105\110\103\108\101\069\118\101\110\116\077\097\110\097\103\101\114 \119\104\101\110 \114\101\115\117\109\105\110\103",function()local e="\069\114\114\111\114 \102\114\111\109 \083\105\110\103\108\101\069\118\101\110\116\077\097\110\097\103\101\114 \116\101\115\116"local k=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local p=A.new(k)p:connectEvent("\069\118\101\110\116",function()error(e)end)p:resume()p:suspend()k:Fire(5)end)f("\115\104\111\117\108\100 \110\111\116 \111\118\101\114\102\108\111\119 \119\105\116\104 \101\118\101\110\116\115 \105\102 \109\097\110\097\103\101\114\058\114\101\115\117\109\101\040\041 \105\115 \105\110\118\111\107\101\100 \119\104\101\110 \114\101\115\117\109\105\110\103 \097 \115\117\115\112\101\110\100\101\100 \101\118\101\110\116",function()local e=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")local f=A.new(e)local S=p.fn(function(k,p)if p==1 then f:suspend()e:Fire(2)f:resume()end end)f:connectEvent("\069\118\101\110\116",function(...)S(...)end)f:suspend()e:Fire(1)f:resume()M()M();(k(S)).toBeCalledTimes(2)end)end)S("\099\111\110\110\101\099\116\080\114\111\112\101\114\116\121\067\104\097\110\103\101",function()f("\115\104\111\117\108\100 \099\111\110\110\101\099\116 \116\111 \112\114\111\112\101\114\116\121 \099\104\097\110\103\101\115",function()local e=Instance.new("\070\111\108\100\101\114")local f=A.new(e)local S=p.fn()f:connectPropertyChange("\078\097\109\101",function(...)S(...)end)f:resume()e.Name="\102\111\111"M();(k(S)).toBeCalledTimes(1);(k(S)).toBeCalledWith(e)e.Name="\098\097\114"M();(k(S)).toBeCalledTimes(2);(k(S)).toBeCalledWith(e)f:connectPropertyChange("\078\097\109\101")e.Name="\098\097\122"M();(k(S)).toBeCalledTimes(2)end)f("\115\104\111\117\108\100 \116\104\114\111\119 \097\110 \101\114\114\111\114 \105\102 \116\104\101 \112\114\111\112\101\114\116\121 \105\115 \105\110\118\097\108\105\100",function()local e=Instance.new("\070\111\108\100\101\114")local p=A.new(e);(k(function()p:connectPropertyChange("\102\111\111",function() end)end)).toThrow()end)end)
-- ROBLOX upstream: https://github.com/Roblox/roact/blob/b2ba9cf4c219c2654e6572219a68d0bf1b541418/src/SingleEventManager.spec.lua
--[[
	* Copyright (c) Roblox Corporation. All rights reserved.
	* Licensed under the Apache License, Version 2.0 (the "License");
	* you may not use this file except in compliance with the License.
	* You may obtain a copy of the License at
	*
	*     http://www.apache.org/licenses/LICENSE-2.0
	*
	* Unless required by applicable law or agreed to in writing, software
	* distributed under the License is distributed on an "AS IS" BASIS,
	* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	* See the License for the specific language governing permissions and
	* limitations under the License.
]]

local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local it = JestGlobals.it
local describe = JestGlobals.describe
-- ROBLOX FIXME
-- local Logging = require("./Logging")

local SingleEventManager = require("../SingleEventManager")
local waitForEvents = require("./waitForEvents")

describe("new", function()
	it("should create a SingleEventManager", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)

		jestExpect(manager).never.toBeNil()
	end)
end)

describe("connectEvent", function()
	it("should connect to events", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)
		local eventSpy = jest.fn()

		manager:connectEvent("Event", function(...)
			eventSpy(...)
		end)
		manager:resume()

		instance:Fire("foo")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(1)
		jestExpect(eventSpy).toBeCalledWith(instance, "foo")

		instance:Fire("bar")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(2)
		jestExpect(eventSpy).toBeCalledWith(instance, "bar")

		manager:connectEvent("Event")

		instance:Fire("baz")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(2)
	end)

	it("should drop events until resumed initially", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)
		local eventSpy = jest.fn()

		manager:connectEvent("Event", function(...)
			eventSpy(...)
		end)

		instance:Fire("foo")
		waitForEvents()

		jestExpect(eventSpy).never.toBeCalled()

		manager:resume()

		instance:Fire("bar")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(1)
		jestExpect(eventSpy).toBeCalledWith(instance, "bar")
	end)

	it("should invoke suspended events when resumed", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)
		local eventSpy = jest.fn()

		manager:connectEvent("Event", function(...)
			eventSpy(...)
		end)
		manager:resume()

		instance:Fire("foo")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(1)
		jestExpect(eventSpy).toBeCalledWith(instance, "foo")

		manager:suspend()

		instance:Fire("bar")
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(1)

		manager:resume()
		jestExpect(eventSpy).toBeCalledTimes(2)
		jestExpect(eventSpy).toBeCalledWith(instance, "bar")
	end)

	it("should invoke events triggered during resumption in the correct order", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)

		local recordedValues = {}
		local eventSpy = jest.fn(function(_, value)
			table.insert(recordedValues, value)

			if value == 2 then
				instance:Fire(3)
			elseif value == 3 then
				instance:Fire(4)
			end
		end)

		manager:connectEvent("Event", function(...)
			eventSpy(...)
		end)
		manager:suspend()

		instance:Fire(1)
		instance:Fire(2)
		waitForEvents()

		manager:resume()
		waitForEvents()
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(4)
		jestExpect(recordedValues).toEqual({ 1, 2, 3, 4 })
	end)

	it(
		"should not invoke events fired during suspension but disconnected before resumption",
		function()
			local instance = Instance.new("BindableEvent")
			local manager = SingleEventManager.new(instance)
			local eventSpy = jest.fn()

			manager:connectEvent("Event", function(...)
				eventSpy(...)
			end)
			manager:suspend()

			instance:Fire(1)
			waitForEvents()

			manager:connectEvent("Event")

			manager:resume()
			jestExpect(eventSpy).never.toBeCalled()
		end
	)

	it("should not yield events through the SingleEventManager when resuming", function()
		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)

		manager:connectEvent("Event", function()
			coroutine.yield()
		end)

		manager:resume()

		local co = coroutine.create(function()
			instance:Fire(5)
			waitForEvents()
		end)

		assert(coroutine.resume(co))
		waitForEvents()

		jestExpect(coroutine.status(co)).toBe("dead")

		manager:suspend()
		instance:Fire(5)
		waitForEvents()

		co = coroutine.create(function()
			manager:resume()
		end)

		assert(coroutine.resume(co))
		jestExpect(coroutine.status(co)).toBe("dead")
	end)

	it("should not throw errors through SingleEventManager when resuming", function()
		local errorText = "Error from SingleEventManager test"

		local instance = Instance.new("BindableEvent")
		local manager = SingleEventManager.new(instance)

		manager:connectEvent("Event", function()
			error(errorText)
		end)

		manager:resume()

		-- If we call instance:Fire() here, the error message will leak to
		-- the console since the thread's resumption will be handled by
		-- Roblox's scheduler.

		manager:suspend()
		instance:Fire(5)

		-- local logInfo = Logging.capture(function()
		-- 	manager:resume()
		-- end)

		-- jestExpect(#logInfo.errors).to.equal(0)
		-- jestExpect(#logInfo.warnings).to.equal(1)
		-- jestExpect(#logInfo.infos).to.equal(0)

		-- jestExpect(logInfo.warnings[1]:find(errorText)).to.be.ok()
	end)

	it(
		"should not overflow with events if manager:resume() is invoked when resuming a suspended event",
		function()
			local instance = Instance.new("BindableEvent")
			local manager = SingleEventManager.new(instance)

			-- This connection emulates what happens if reconciliation is
			-- triggered again in response to reconciliation. Without
			-- appropriate guards, the inner resume() call will process the
			-- Fire(1) event again, causing a nasty stack overflow.
			local eventSpy = jest.fn(function(_, value)
				if value == 1 then
					manager:suspend()
					instance:Fire(2)
					manager:resume()
				end
			end)

			manager:connectEvent("Event", function(...)
				eventSpy(...)
			end)

			manager:suspend()
			instance:Fire(1)
			manager:resume()
			waitForEvents()
			waitForEvents()

			jestExpect(eventSpy).toBeCalledTimes(2)
		end
	)
end)

describe("connectPropertyChange", function()
	-- Since property changes utilize the same mechanisms as other events,
	-- the tests here are slimmed down to reduce redundancy.

	it("should connect to property changes", function()
		local instance = Instance.new("Folder")
		local manager = SingleEventManager.new(instance)
		local eventSpy = jest.fn()

		manager:connectPropertyChange("Name", function(...)
			eventSpy(...)
		end)
		manager:resume()

		instance.Name = "foo"
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(1)
		jestExpect(eventSpy).toBeCalledWith(instance)

		instance.Name = "bar"
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(2)
		jestExpect(eventSpy).toBeCalledWith(instance)

		manager:connectPropertyChange("Name")

		instance.Name = "baz"
		waitForEvents()

		jestExpect(eventSpy).toBeCalledTimes(2)
	end)

	it("should throw an error if the property is invalid", function()
		local instance = Instance.new("Folder")
		local manager = SingleEventManager.new(instance)

		jestExpect(function()
			manager:connectPropertyChange("foo", function() end)
		end).toThrow()
	end)
end)
