--!nonstrict
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local root = script.Parent.Parent

local Phase = require(root.Phase)
local Scheduler = require(root.Scheduler)
local Pipeline = require(root.Pipeline)

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("scheduler", function()
	describe("insert", function()
		test("phase", function()
			local myPhase = Phase.new("myPhase")
			local myScheduler = Scheduler.new():insert(myPhase)

			local orderedPhases =
				myScheduler._defaultDependencyGraph:getOrderedList()
			local mainIndex =
				table.find(orderedPhases, myScheduler._defaultPhase)

			expect(orderedPhases[mainIndex - 1]).toBe(myPhase)
		end)

		test("phase with event", function()
			local myPhase = Phase.new("myPhase")

			local myScheduler = Scheduler.new()
				:insert(myPhase, RunService, "Heartbeat")

			local identifier = `{RunService}@Heartbeat`
			local dependencyGraph =
				myScheduler._eventDependencyGraphs[identifier]

			expect(dependencyGraph).toBeTruthy()
			expect(myScheduler._connectedEvents[identifier]).toBeTruthy()

			local orderedList = dependencyGraph:getOrderedList()
			expect(orderedList[#orderedList]).toBe(myPhase)
		end)

		test("pipeline", function()
			local myPhase = Phase.new()
			local otherPhase = Phase.new()

			local myPipeline =
				Pipeline.new("myPipeline"):insert(myPhase):insert(otherPhase)

			local myScheduler = Scheduler.new():insert(myPipeline)

			local orderedDependencies =
				myScheduler._defaultDependencyGraph:getOrderedList()
			local mainIndex =
				table.find(orderedDependencies, myScheduler._defaultPhase)

			expect(orderedDependencies[mainIndex - 1]).toBe(myPipeline)

			local orderedPhases = myPipeline.dependencyGraph:getOrderedList()
			expect(orderedPhases[1]).toBe(myPhase)
			expect(orderedPhases[2]).toBe(otherPhase)
		end)

		test("pipeline with event", function()
			local myPhase = Phase.new()
			local otherPhase = Phase.new()

			local myPipeline =
				Pipeline.new("myPipeline"):insert(myPhase):insert(otherPhase)

			local myScheduler = Scheduler.new()
				:insert(myPipeline, RunService, "Heartbeat")

			local identifier = `{RunService}@Heartbeat`
			local dependencyGraph =
				myScheduler._eventDependencyGraphs[identifier]

			expect(dependencyGraph).toBeTruthy()
			expect(myScheduler._connectedEvents[identifier]).toBeTruthy()

			local orderedList = dependencyGraph:getOrderedList()
			expect(orderedList[1]).toBe(myPipeline)

			local orderedPhases = myPipeline.dependencyGraph:getOrderedList()
			expect(orderedPhases[1]).toBe(myPhase)
			expect(orderedPhases[2]).toBe(otherPhase)
		end)
	end)

	describe("insertAfter", function()
		test("phase", function()
			local phaseOne = Phase.new("phaseOne")
			local phaseTwo = Phase.new("phaseTwo")
			local phaseThree = Phase.new("phaseThree")

			local myScheduler = Scheduler.new()
				:insert(phaseOne)
				:insert(phaseTwo)
				:insertAfter(phaseThree, phaseOne)

			local orderedPhases =
				myScheduler._defaultDependencyGraph:getOrderedList()
			local mainIndex =
				table.find(orderedPhases, myScheduler._defaultPhase)

			expect(orderedPhases[mainIndex - 3]).toBe(phaseOne)
			expect(orderedPhases[mainIndex - 2]).toBe(phaseTwo)
			expect(orderedPhases[mainIndex - 1]).toBe(phaseThree)
		end)

		test("pipeline", function()
			local phaseOne = Phase.new("phaseOne")
			local phaseTwo = Phase.new("phaseTwo")
			local phaseThree = Phase.new("phaseThree")
			local phaseFour = Phase.new("phaseFour")

			local pipelineOne = Pipeline.new():insert(phaseOne):insert(phaseTwo)
			local pipelineTwo = Pipeline.new():insert(phaseFour)
			local pipelineThree = Pipeline.new():insert(phaseThree)

			local myScheduler = Scheduler.new()
				:insert(pipelineOne)
				:insert(pipelineTwo)
				:insertAfter(pipelineThree, pipelineOne)

			local orderedDependencies =
				myScheduler._defaultDependencyGraph:getOrderedList()
			local mainIndex =
				table.find(orderedDependencies, myScheduler._defaultPhase)

			expect(orderedDependencies[mainIndex - 3]).toBe(pipelineOne)
			expect(orderedDependencies[mainIndex - 2]).toBe(pipelineTwo)
			expect(orderedDependencies[mainIndex - 1]).toBe(pipelineThree)
		end)
	end)

	test("phase run condition", function()
		expect.assertions(1)

		local bool = true

		local system = function(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:insert(myPhase)
			:addSystem(system, myPhase)
			:addRunCondition(myPhase, function()
				return bool
			end)

		myScheduler:runAll()

		bool = false
		myScheduler:runAll() -- Expect system to not run
	end)

	test("pipeline run condition", function()
		expect.assertions(1)

		local bool = true

		local system = function(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")

		local myPipeline = Pipeline.new():insert(myPhase)

		local myScheduler = Scheduler.new(1, 2, 3)
			:insert(myPipeline)
			:addSystem(system, myPhase)
			:addRunCondition(myPipeline, function()
				return bool
			end)

		myScheduler:run(myPipeline)

		bool = false
		myScheduler:run(myPipeline) -- Expect system to not run
	end)

	test("getDeltaTime", function()
		local deltaTime

		local myScheduler = Scheduler.new()

		local system = function()
			deltaTime = myScheduler:getDeltaTime()
		end

		myScheduler:addSystem(system)
		myScheduler:run(system)
		expect(deltaTime).toEqual(0)
		myScheduler:run(system)
		expect(deltaTime).never.toEqual(0)
	end)

	test("custom system name", function()
		expect.assertions(5)

		local myScheduler = Scheduler.new()

		local function mySystem() end

		myScheduler:addSystem({
			name = "CustomSystemName",
			system = mySystem,
		})

		expect(myScheduler._systemInfo[mySystem].name).toBe("CustomSystemName")

		local function anotherSystem() end
		myScheduler:addSystem(anotherSystem)

		expect(myScheduler._systemInfo[anotherSystem].name).toBe(
			"anotherSystem"
		)

		local function replacementSystem() end
		myScheduler:replaceSystem(mySystem, {
			name = "ReplacedSystemName",
			system = replacementSystem,
		})

		expect(myScheduler._systemInfo[replacementSystem].name).toBe(
			"ReplacedSystemName"
		)

		local function finalSystem() end
		myScheduler:replaceSystem(replacementSystem, finalSystem)

		expect(myScheduler._systemInfo[finalSystem].name).toBe("finalSystem")

		expect(myScheduler._systemInfo[replacementSystem]).toBeNil()
	end)

	describe("startup systems", function()
		test("should run once", function()
			expect.assertions(1)

			local myScheduler = Scheduler.new(1, 2, 3):addSystem(function(...)
				expect({ ... }).toEqual({ 1, 2, 3 })
			end, Phase.Startup)

			myScheduler:runAll()
			myScheduler:runAll()
		end)

		test("run before events once", function()
			expect.assertions(1)

			local myPhase = Phase.new("myPhase")
			local bindableEvent = Instance.new("BindableEvent")

			Scheduler.new(1, 2, 3)
				:addSystem(function(...)
					expect({ ... }).toEqual({ 1, 2, 3 })
				end, Phase.Startup)
				:insert(myPhase, bindableEvent, bindableEvent.Event)

			bindableEvent:Fire()
			task.wait()
			bindableEvent:Fire()
			task.wait()
		end)

		test("each run exactly once", function(_, done)
			expect.assertions(3)

			local myPhase = Phase.new("myPhase")
			local bindableEvent = Instance.new("BindableEvent")

			local n = 0
			local ranOnce = false

			Scheduler.new()
				:addSystem(function()
					n += 1
					expect(n).toEqual(1)
				end, Phase.PreStartup)
				:addSystem(function()
					n += 1
					expect(n).toEqual(2)
				end, Phase.Startup)
				:addSystem(function()
					n += 1
					expect(n).toEqual(3)
				end, Phase.PostStartup)
				:insert(myPhase, bindableEvent, bindableEvent.Event)
				:addSystem(function()
					if ranOnce then
						done()
					end

					ranOnce = true
				end, myPhase)

			bindableEvent:Fire()
			task.wait()
			bindableEvent:Fire()
			task.wait()
		end)
	end)
end)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local root = script.Parent.Parent

local Scheduler = require(root.Scheduler)
local conditions = require(root.conditions)

local isNot = conditions.isNot
local runOnce = conditions.runOnce
local timePassed = conditions.timePassed
local onEvent = conditions.onEvent

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("conditions", function()
	test("isNot", function()
		expect.assertions(1)
		local function system()
			expect("Planckton!!!").toEqual(expect.anything())
		end

		local bool = false

		local function condition()
			return bool
		end

		local myScheduler = Scheduler.new()
			:addSystem(system)
			:addRunCondition(system, isNot(condition))

		myScheduler:runAll() -- Expect to run

		bool = true
		myScheduler:runAll() -- Expect to not run
	end)

	test("runOnce", function()
		expect.assertions(1)
		local function system()
			expect(true).toBe(true)
		end

		local myScheduler =
			Scheduler.new():addSystem(system):addRunCondition(system, runOnce())

		myScheduler:runAll() -- Expect to run
		myScheduler:runAll() -- Expect to not run
	end)

	test("timePassed", function()
		expect.assertions(3)
		local didRun = false
		local firstRun = false

		local function system()
			if firstRun then
				didRun = true
			end

			firstRun = true
		end

		local myScheduler = Scheduler.new()
			:addSystem(system)
			:addRunCondition(system, timePassed(1))

		myScheduler:runAll()
		expect(didRun).toBe(false)
		task.wait(0.5)

		myScheduler:runAll()
		expect(didRun).toBe(false)
		task.wait(0.5)

		myScheduler:runAll()
		expect(didRun).toBe(true)
	end)

	test("onEvent", function()
		expect.assertions(4)
		local didRun = false
		local bindableEvent = Instance.new("BindableEvent")

		local function system()
			didRun = true
		end

		local myScheduler = Scheduler.new():addSystem(system):addRunCondition(
			system,
			onEvent(bindableEvent, bindableEvent.Event)
		)

		myScheduler:runAll()
		expect(didRun).toBe(false)
		bindableEvent:Fire()
		task.wait()

		myScheduler:runAll()
		expect(didRun).toBe(true)
		didRun = false

		myScheduler:runAll()
		expect(didRun).toBe(false)
		bindableEvent:Fire()
		task.wait()

		myScheduler:runAll()
		expect(didRun).toBe(true)
	end)

	test("onEvent in SystemTable", function()
		expect.assertions(4)
		local didRun = false
		local bindableEvent = Instance.new("BindableEvent")

		local function system()
			didRun = true
		end

		local myScheduler = Scheduler.new():addSystem({
			system = system,
			runConditions = { onEvent(bindableEvent, bindableEvent.Event) },
		})

		myScheduler:runAll()
		expect(didRun).toBe(false)
		bindableEvent:Fire()
		task.wait()

		myScheduler:runAll()
		expect(didRun).toBe(true)
		didRun = false

		myScheduler:runAll()
		expect(didRun).toBe(false)
		bindableEvent:Fire()
		task.wait()

		myScheduler:runAll()
		expect(didRun).toBe(true)
	end)
end)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local root = script.Parent.Parent

local Phase = require(root.Phase)
local Scheduler = require(root.Scheduler)

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("topoRuntime", function()
	test("useDeltaTime", function()
		local matterHooks =
			require(ReplicatedStorage.Packages.PlanckMatterHooks)
		local useDeltaTime = matterHooks.useDeltaTime
		expect.assertions(1)

		local plugin = matterHooks.Plugin.new()

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:addPlugin(plugin)
			:insert(myPhase)
			:addSystem(function()
				expect(useDeltaTime()).toBeTruthy()
			end, myPhase)

		myScheduler:runAll()
	end)

	test("useThrottle", function()
		local matterHooks =
			require(ReplicatedStorage.Packages.PlanckMatterHooks)
		local useThrottle = matterHooks.useThrottle
		expect.assertions(1)

		local plugin = matterHooks.Plugin.new()

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:addPlugin(plugin)
			:insert(myPhase)
			:addSystem(function()
				if useThrottle(1) then
					expect(true).toBe(true)
				end
			end, myPhase)

		myScheduler:runAll()
	end)
end)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local root = script.Parent.Parent

local Phase = require(root.Phase)
local Scheduler = require(root.Scheduler)

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("systems", function()
	test("add", function()
		expect.assertions(1)

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:insert(myPhase)
			:addSystem(function(...)
				expect({ ... }).toEqual({ 1, 2, 3 })
			end, myPhase)

		myScheduler:runAll()
	end)

	test("edit", function()
		expect.assertions(2)

		local function systemA(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler =
			Scheduler.new(1, 2, 3):insert(myPhase):addSystem(systemA, myPhase)

		myScheduler:runAll()

		local otherPhase = Phase.new()
		myScheduler:insert(otherPhase)

		myScheduler:editSystem(systemA, otherPhase)
		myScheduler:run(otherPhase)

		-- Expect nothing to run here
		myScheduler:run(myPhase)
	end)

	test("remove", function()
		expect.assertions(1)

		local function systemA(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler =
			Scheduler.new(1, 2, 3):insert(myPhase):addSystem(systemA, myPhase)

		myScheduler:runAll()

		myScheduler:removeSystem(systemA)
		myScheduler:runAll() -- Expect nothing here
	end)

	test("replace", function()
		expect.assertions(3)

		local function systemA(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local function systemB(...)
			-- FUTURE: Use Jest.spyOn
			expect(true).toBe(true)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler =
			Scheduler.new(1, 2, 3):insert(myPhase):addSystem(systemA, myPhase)

		myScheduler:runAll()

		myScheduler:replaceSystem(systemA, systemB)
		myScheduler:runAll()
	end)

	test("system table", function()
		expect.assertions(1)

		local myPhase = Phase.new("myPhase")

		local system = {
			system = function(...)
				expect({ ... }).toEqual({ 1, 2, 3 })
			end,
			phase = myPhase,
		}

		local myScheduler =
			Scheduler.new(1, 2, 3):insert(myPhase):addSystem(system)

		myScheduler:run(myPhase)
	end)

	test("run condition", function()
		expect.assertions(1)

		local bool = true

		local system = function(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:insert(myPhase)
			:addSystem(system, myPhase)
			:addRunCondition(system, function()
				return bool
			end)

		myScheduler:runAll()

		bool = false
		myScheduler:runAll() -- Expect system to not run
	end)

	test("multiple run conditions", function()
		expect.assertions(1)

		local firstCondition = true
		local secondCondition = true

		local system = function(...)
			expect({ ... }).toEqual({ 1, 2, 3 })
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler = Scheduler.new(1, 2, 3)
			:insert(myPhase)
			:addSystem(system, myPhase)
			:addRunCondition(system, function()
				return firstCondition
			end)
			:addRunCondition(system, function()
				return secondCondition
			end)

		firstCondition = false
		myScheduler:runAll()

		firstCondition = true
		secondCondition = false
		myScheduler:runAll()

		firstCondition = false
		secondCondition = false
		myScheduler:runAll()

		firstCondition = true
		secondCondition = true
		myScheduler:runAll() -- Expect to run
	end)

	test("yield", function()
		expect.assertions(1)

		local system = function()
			wait(5)
		end

		local myPhase = Phase.new("myPhase")
		local myScheduler =
			Scheduler.new(1, 2, 3):insert(myPhase):addSystem(system, myPhase)

		local err;

		(myScheduler :: any):_addHook(
			(myScheduler :: any).Hooks.SystemError,
			function(args)
				err = args.error
			end
		)

		myScheduler:runAll()
		expect(err).toEqual(expect.stringContaining("System yielded"))
	end)
end)


local utils = require(script.Parent.utils)
local getConnectFunction = utils.getConnectFunction

type GenericTable = utils.GenericTable
type SignalLike<U...> = utils.SignalLike<U...>
type ConnectionLike = utils.ConnectionLike
type ConnectFn<T, U...> = utils.ConnectFn<T, U...>
type Callback<U...> = utils.Callback<U...>

type Condition = () -> boolean

--- @class Conditions
---
--- Conditions can be used in systems or as Run Conditions.

--- @within Conditions
--- @param time number -- Time in seconds
--- @return hasTimePassed () -> boolean
---
--- A Throttle condition which checks whether the amount of
--- time given has passed or not.
local function timePassed(time: number): Condition
	local storedTime

	return function()
		if storedTime == nil or os.clock() - storedTime >= time then
			storedTime = os.clock()
			return true
		end

		return false
	end
end

--- @within Conditions
--- @return hasRanOnce () -> boolean
---
--- Checks whether the condition has been called once before
local function runOnce(): Condition
	local hasRan = false

	return function()
		if not hasRan then
			hasRan = true
			return true
		end

		return false
	end
end

type CollectEvents<U...> = () -> () -> (number, U...)

local conditionToCleanupFn = {}

local function cleanupCondition(condition: Condition)
	local cleanup = conditionToCleanupFn[condition]
	if cleanup then
		cleanup()
		conditionToCleanupFn[condition] = nil
	end
end

type GetDisconnectFn = () -> () -> ()

type OnEventFn =
	-- RBXScriptSignal & nil
	(<U...>(
		RBXScriptSignal<U...>
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))
	-- Instance & RBXScriptSignal
	-- Instance & string
	& (<U...>(
		Instance,
		RBXScriptSignal<U...> | string
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))
	-- SignalLike & nil
	& (<U...>(
		SignalLike<U...>
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))
	-- table & string
	& (<U...>(
		GenericTable,
		string
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))
	-- table & connectable method
	& (<T, U...>(
		GenericTable,
		(GenericTable, Callback<U...>, ...any) -> T?
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))
	-- connectable function
	& (<T, U...>(
		(Callback<U...>, ...any) -> T?
	) -> (Condition, CollectEvents<U...>, GetDisconnectFn))

--- @within Conditions
--- @param instance RBXScriptSignal | Instance | SignalLike<...any> | GenericTable | ((Callback<U...>,...any) -> ...any)
--- @param event (RBXScriptSignal | string | (...any) -> ())?
--- @return hasNewEvent () -> boolean
--- @return collectEvents () -> () -> (number, U...)
--- @return getDisconnectFn () -> () -> ()
---
--- Checks for any new events and allows for the collection of
--- those events.\
--- \
--- Read [OnEvent](../docs/design/conditions#on-event) for more information.
local function onEvent<U...>(
	instance,
	event
): (Condition, CollectEvents<U...>, () -> () -> ())
	local connect = getConnectFunction(instance, event)
	assert(connect, "Event passed to .onEvent is not valid")

	local newEvent = false
	local queue = {}

	local connection: ConnectionLike?

	local function disconnect()
		if not connection then
			return
		end

		utils.disconnectEvent(connection)
		connection = nil
	end

	local function callback(...)
		newEvent = true
		table.insert(queue, { ... })
	end

	connection = connect(callback)

	local function hasNewEvent()
		if newEvent then
			newEvent = false
			return true
		end

		table.clear(queue)
		return false
	end

	local function collectEvents()
		local n = 0
		return function(): (number, U...)
			n += 1

			local args = table.remove(queue, 1)

			if args then
				return n, table.unpack(args)
			end

			return nil :: any
		end
	end

	local function getDisconnectFn()
		return disconnect
	end

	conditionToCleanupFn[hasNewEvent] = disconnect

	return hasNewEvent, collectEvents, getDisconnectFn
end

--- @within Conditions
--- @param condition () -> boolean
--- @return inverseCondition () -> boolean
---
--- Inverses a given condition.

-- selene: allow(unused_variable)
local function isNot(condition: Condition, ...: any): Condition
	return function()
		return not condition()
	end
end

return {
	timePassed = timePassed,
	runOnce = runOnce,
	onEvent = onEvent :: OnEventFn,
	isNot = isNot,

	cleanupCondition = cleanupCondition,
}


-- Super-duper experimental Plugin Hooks API

local function systemAdd(scheduler, systemInfo)
	local hooks = scheduler._hooks[scheduler.Hooks.SystemAdd]
	local info = {
		scheduler = scheduler,
		system = systemInfo,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function systemRemove(scheduler, systemInfo)
	local hooks = scheduler._hooks[scheduler.Hooks.SystemRemove]
	local info = {
		scheduler = scheduler,
		system = systemInfo,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function systemCleanup(scheduler, systemInfo, cleanupError)
	local hooks = scheduler._hooks[scheduler.Hooks.SystemCleanup]
	local info = {
		scheduler = scheduler,
		system = systemInfo,
		error = cleanupError,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function systemReplace(scheduler, oldSystemInfo, newSystemInfo)
	local hooks = scheduler._hooks[scheduler.Hooks.SystemReplace]
	local info = {
		scheduler = scheduler,
		new = newSystemInfo,
		old = oldSystemInfo,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function systemTriedRun(scheduler, systemInfo)
	local hooks = scheduler._hooks[scheduler.Hooks.SystemTriedRun]
	local info = {
		scheduler = scheduler,
		system = systemInfo,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function systemCall(scheduler, hookName, systemInfo, nextFn)
	local hooks = scheduler._hooks[scheduler.Hooks[hookName]]

	if hooks then
		for _, hook in hooks do
			nextFn = hook({
				scheduler = nil,
				system = systemInfo,
				nextFn = nextFn,
			})

			if not nextFn then
				local source, line = debug.info(hook, "sl")
				warn(
					`{source}:{line}: Expected 'SystemCall' hook to return a function`
				)
			end
		end
	end

	nextFn()
end

local function systemError(scheduler, systemInfo, err)
	local hooks = scheduler._hooks[scheduler.Hooks["SystemError"]]

	if hooks then
		for _, hook in hooks do
			hook({
				scheduler = scheduler,
				system = systemInfo,
				error = err,
			})
		end
	end
end

type PhaseAdd = {
	scheduler: any,
	phase: any,
}

local function phaseAdd(scheduler, phase)
	local hooks = scheduler._hooks[scheduler.Hooks.PhaseAdd]
	local info = {
		scheduler = scheduler,
		phase = phase,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local function phaseBegan(scheduler, phase)
	local hooks = scheduler._hooks[scheduler.Hooks.PhaseBegan]
	local info = {
		scheduler = scheduler,
		phase = phase,
	}

	for _, hook in hooks do
		local success, err = pcall(hook, info)
		if not success then
			warn("Unexpected error in hook:", err)
		end
	end
end

local Hooks = {
	SystemAdd = "SystemAdd",
	SystemRemove = "SystemRemove",
	SystemReplace = "SystemReplace",
	SystemCleanup = "SystemCleanup",
	SystemError = "SystemError",
	SystemTriedRun = "SystemTriedRun",

	OuterSystemCall = "OuterSystemCall",
	InnerSystemCall = "InnerSystemCall",
	SystemCall = "SystemCall",

	PhaseAdd = "PhaseAdd",
	PhaseBegan = "PhaseBegan",
}

return {
	Hooks = Hooks,

	systemAdd = systemAdd,
	systemRemove = systemRemove,
	systemReplace = systemReplace,
	systemCleanup = systemCleanup,
	systemTriedRun = systemTriedRun,
	systemCall = systemCall,
	systemError = systemError,
	phaseAdd = phaseAdd,
	phaseBegan = phaseBegan,
}


local Phase = require(script.Phase) :: any
local Pipeline = require(script.Pipeline) :: any
local Scheduler = require(script.Scheduler) :: any

local conditions = require(script.conditions)
local utils = require(script.utils)

type GenericTable = utils.GenericTable
type SignalLike<U...> = utils.SignalLike<U...>
type ConnectionLike = utils.ConnectionLike
type ConnectFn<T, U...> = utils.ConnectFn<T, U...>
type Callback<U...> = utils.Callback<U...>

export type CleanupFn<U...> = (U...) -> ()

export type SystemFn<U...> = (U...) -> ()

export type InitializerResult<U...> = {
	system: SystemFn<U...>?,
	cleanup: CleanupFn<U...>?,
}

export type InitializerSystemFn<U...> =
	| ((U...) -> SystemFn<U...>)
	| ((U...) -> (SystemFn<U...>, CleanupFn<U...>))
	| ((U...) -> InitializerResult<U...>)

export type SystemTable<U...> = {
	system: SystemFn<U...> | InitializerSystemFn<U...>,
	phase: Phase?,
	name: string?,
	runConditions: { (U...) -> boolean }?,
	[any]: any,
}

export type System<U...> =
	SystemFn<U...>
	| InitializerSystemFn<U...>
	| SystemTable<U...>

export type Phase = {
	PreStartup: Phase,
	Startup: Phase,
	PostStartup: Phase,

	new: (debugName: string?) -> Phase,
}

export type Pipeline = {
	Startup: Pipeline,

	insert: (self: Pipeline, phase: Phase) -> Pipeline,
	insertAfter: (self: Pipeline, phase: Phase, after: Phase) -> Pipeline,
	insertBefore: (self: Pipeline, phase: Phase, before: Phase) -> Pipeline,
	new: (debugName: string?) -> Pipeline,
}

type Plugin<U...> =
	{
		build: (self: Plugin<U...>, scheduler: Scheduler<U...>) -> (),
		[any]: any,
	}
	| {
		build: (self: Plugin<U...>, scheduler: Scheduler<U...>) -> (),
		cleanup: (self: Plugin<U...>) -> (),
		[any]: any,
	}

export type Scheduler<U...> = {
	addPlugin: (self: Scheduler<U...>, plugin: Plugin<U...>) -> Scheduler<U...>,

	addSystem: (
		self: Scheduler<U...>,
		system: System<U...>,
		phase: Phase?
	) -> Scheduler<U...>,

	addSystems: (
		self: Scheduler<U...>,
		systems: { System<U...> },
		phase: Phase?
	) -> Scheduler<U...>,

	editSystem: (
		self: Scheduler<U...>,
		system: System<U...>,
		new: Phase
	) -> Scheduler<U...>,

	replaceSystem: (
		self: Scheduler<U...>,
		system: System<U...>,
		new: System<U...>
	) -> Scheduler<U...>,

	removeSystem: (
		self: Scheduler<U...>,
		system: System<U...>
	) -> Scheduler<U...>,

	addRunCondition: ((
		self: Scheduler<U...>,
		system: System<U...>,
		fn: (U...) -> boolean,
		...any
	) -> Scheduler<U...>) & ((
		self: Scheduler<U...>,
		phase: Phase,
		fn: (U...) -> boolean,
		...any
	) -> Scheduler<U...>) & ((
		self: Scheduler<U...>,
		pipeline: Pipeline,
		fn: (U...) -> boolean,
		...any
	) -> Scheduler<U...>),

	run: ((self: Scheduler<U...>, system: System<U...>) -> Scheduler<U...>)
		& ((self: Scheduler<U...>, phase: Phase) -> Scheduler<U...>)
		& ((self: Scheduler<U...>, pipeline: Pipeline) -> Scheduler<U...>),

	runAll: (self: Scheduler<U...>) -> Scheduler<U...>,

	getDeltaTime: (self: Scheduler<U...>) -> number,

	insert: ((
		self: Scheduler<U...>,
		dependency: Phase | Pipeline
	) -> Scheduler<U...>)
		-- RBXScriptSignal & nil
		& ((
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			signal: RBXScriptSignal<U...>
		) -> Scheduler<U...>)
		-- Instance & RBXScriptSignal
		-- Instance & string
		& ((
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			instance: Instance,
			event: RBXScriptSignal<U...> | string
		) -> Scheduler<U...>)
		-- SignalLike & nil
		& ((
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			signal: SignalLike<U...>
		) -> Scheduler<U...>)
		-- table & string
		& ((
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			table: GenericTable,
			event: string
		) -> Scheduler<U...>)
		-- table & connectable method
		& (<T>(
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			instance: GenericTable,
			connectMethod: (GenericTable, Callback<U...>, ...any) -> T
		) -> Scheduler<U...>)
		-- connectable function
		& (<T>(
			self: Scheduler<U...>,
			dependency: Phase | Pipeline,
			connectFn: (Callback<U...>, ...any) -> T
		) -> Scheduler<U...>),

	insertAfter: ((
		self: Scheduler<U...>,
		phase: Phase,
		after: Phase | Pipeline
	) -> Scheduler<U...>) & ((
		self: Scheduler<U...>,
		pipeline: Pipeline,
		after: Phase | Pipeline
	) -> Scheduler<U...>),

	insertBefore: ((
		self: Scheduler<U...>,
		phase: Phase,
		before: Phase | Pipeline
	) -> Scheduler<U...>) & ((
		self: Scheduler<U...>,
		pipeline: Pipeline,
		before: Phase | Pipeline
	) -> Scheduler<U...>),

	cleanup: (self: Scheduler<U...>) -> (),

	new: (U...) -> Scheduler<U...>,
}

return {
	Phase = Phase :: Phase,
	Pipeline = Pipeline :: Pipeline,
	Scheduler = Scheduler :: {
		new: <U...>(U...) -> Scheduler<U...>,
	},

	isNot = conditions.isNot,
	runOnce = conditions.runOnce,
	timePassed = conditions.timePassed,
	onEvent = conditions.onEvent,
}


local Jabby = require(script.Parent.Jabby)

type SystemInfo = {
	name: string,
	phase: any,
	system: (...any) -> ...any,
}

type HookArgs = {
	scheduler: any,
	system: SystemInfo,
	nextFn: (...any) -> ...any,
}

type SystemsAddRemove = {
	scheduler: any,
	system: SystemInfo,
}

type SystemsReplace = {
	scheduler: any,
	new: SystemInfo,
	old: SystemInfo,
}

local Plugin = {}
Plugin.__index = Plugin

local n = 0

function Plugin:build(scheduler: any)
	n += 1

	local identifier = n > 1 and ` #{n}` or ""
	local name = `Planck{identifier}`

	local jabbyScheduler = Jabby.scheduler.create()
	local startupScheduler = Jabby.scheduler.create()

	local systemToId = {}

	local STARTUP_PHASES = { "PreStartup", "Startup", "PostStartup" }

	local function addSystem(
		systemFn: (...any) -> ...any,
		systemInfo: { name: string, phase: any }
	)
		local phase = tostring(systemInfo.phase)
		if table.find(STARTUP_PHASES, phase) then
			local id = startupScheduler:register_system({
				name = systemInfo.name,
				phase = phase,
			})
			startupScheduler:set_system_data(id, {
				paused = true
			})
			systemToId[systemFn] = id
		else
			local id = jabbyScheduler:register_system({
				name = systemInfo.name,
				phase = phase,
			})
			jabbyScheduler:set_system_data(id, {
				paused = true
			})
			systemToId[systemFn] = id
		end
	end

	for systemFn, systemInfo in scheduler._systemInfo do
		addSystem(systemFn, systemInfo)
	end

	scheduler:_addHook(
		scheduler.Hooks.SystemAdd,
		function(info: SystemsAddRemove)
			addSystem(info.system.system, info.system)
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemRemove,
		function(info: SystemsAddRemove)
			jabbyScheduler:remove_system(systemToId[info.system.system])
			systemToId[info.system.system] = nil
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemReplace,
		function(info: SystemsReplace)
			local newSystem = info.new.system
			local oldSystem = info.old.system

			jabbyScheduler:remove_system(systemToId[oldSystem])
			systemToId[oldSystem] = nil

			local id = jabbyScheduler:register_system({
				name = info.new.name,
				phase = tostring(info.new.phase),
			})

			systemToId[newSystem] = id
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemTriedRun,
		function(info: SystemsAddRemove)
			local id = systemToId[info.system.system]
			local phase = tostring(info.system.phase)

			if table.find(STARTUP_PHASES, phase) then
				startupScheduler:set_system_data(id, {
					paused = true
				})
			else
				jabbyScheduler:set_system_data(id, {
					paused = true
				})
			end
		end
	)

	scheduler:_addHook(scheduler.Hooks.SystemCall, function(args: HookArgs)
		local id = systemToId[args.system.system]

		return function()
			local phase = tostring(args.system.phase)
			if table.find(STARTUP_PHASES, phase) then
				startupScheduler:set_system_data(id, {
					paused = false
				})
				startupScheduler:run(id, function()
					args.nextFn()
				end)
			else
			jabbyScheduler:set_system_data(id, {
				paused = false
			})
				jabbyScheduler:run(id, function()
					args.nextFn()
				end)
			end
		end
	end)

	Jabby.register({
		applet = Jabby.applets.scheduler,
		name = name .. " (Startup)",
		configuration = {
			scheduler = startupScheduler,
		},
	})

	Jabby.register({
		applet = Jabby.applets.scheduler,
		name = name,
		configuration = {
			scheduler = jabbyScheduler,
		},
	})
end

function Plugin.new()
	return setmetatable({}, Plugin)
end

type SchedulerLike<U...> = {
	addPlugin: (
		self: SchedulerLike<U...>,
		plugin: Plugin<U...>
	) -> SchedulerLike<U...>,
	new: (U...) -> SchedulerLike<U...>,
	[any]: any,
}

type Plugin<U...> = {
	build: (self: Plugin<U...>, scheduler: SchedulerLike<U...>) -> (),
	cleanup: ((self: Plugin<U...>) -> ())?,
	new: () -> Plugin<U...>,
}

return (Plugin :: any) :: Plugin<...any>


--!nonstrict

local Queue = {}
Queue.__index = Queue

function Queue.new()
	return setmetatable({
		_head = nil,
		_tail = nil,
	}, Queue)
end

function Queue:pushBack(value)
	local entry = {
		value = value,
		next = nil,
	}

	if self._tail ~= nil then
		self._tail.next = entry
	end

	self._tail = entry

	if self._head == nil then
		self._head = entry
	end
end

function Queue:popFront()
	if self._head == nil then
		return nil
	end

	local value = self._head.value
	self._head = self._head.next

	return value
end

return Queue


local ReplicatedStorage = game:GetService("ReplicatedStorage")

type SystemInfo = {
	name: string,
	phase: any,
	system: (...any) -> ...any,
}

type HookArgs = {
	scheduler: any,
	system: SystemInfo,
	nextFn: (...any) -> ...any,
}

type PhaseBeganArgs = {
	scheduler: any,
	phase: any,
}

type SystemsAddRemove = {
	scheduler: any,
	system: SystemInfo,
}

type SystemsReplace = {
	scheduler: any,
	new: SystemInfo,
	old: SystemInfo,
}

type ConnectionObject = {
	Disconnect: (() -> ())?,
	Destroy: (() -> ())?,
	disconnect: (() -> ())?,
	destroy: (() -> ())?,
} | () -> ()

type CustomEvent = {
	Connect: (...any) -> ConnectionObject,
	[any]: any,
} | {
	on: (...any) -> ConnectionObject,
	[any]: any,
} | {
	connect: (...any) -> ConnectionObject,
	[any]: any,
}

local topoRuntime
local export = {} :: any

local hooks = {
	"useDeltaTime",
	"useEvent",
	"useThrottle",
	"log",
}

local function setHooks(package)
	topoRuntime = require(package:FindFirstChild("topoRuntime", true)) :: any

	for _, hook in hooks do
		local module = package:FindFirstChild(hook, true)
		export[hook] = require(module) :: any
	end
end

-- When using Matter, we must use the topoRuntime in the library
if ReplicatedStorage:FindFirstChild("Packages") then
	for _, package in ReplicatedStorage.Packages._Index:GetChildren() do
		if
			string.find(package.Name, "matter%-ecs_matter@")
			or string.find(package.Name, "evaera_matter@")
		then
			setHooks(package)
			break
		end
	end
end

if not topoRuntime then
	setHooks(script)
end

export.useHookState = topoRuntime.useHookState

local Plugin = {}
Plugin.__index = Plugin

function Plugin:build(scheduler: any)
	if not scheduler._systemState then
		scheduler._systemState = {}
	end

	if not scheduler._systemLogs then
		scheduler._systemLogs = {}
	end

	local phaseDetails = {}

	for phase, _ in scheduler._phaseToSystems do
		if not phaseDetails[phase] then
			phaseDetails[phase] = {
				lastTime = os.clock(),
				generation = false,
			}
		end

		local details = phaseDetails[phase]

		details.currentTime = os.clock()
		details.deltaTime = details.currentTime - details.lastTime
		details.lastTime = details.currentTime

		details.generation = not details.generation
	end

	scheduler:_addHook(scheduler.Hooks.PhaseAdd, function(args: PhaseBeganArgs)
		local phase = args.phase

		if not phaseDetails[phase] then
			phaseDetails[phase] = {
				currentTime = os.clock(),
				deltaTime = 0,
				lastTime = os.clock(),
				generation = false,
			}
		end

		local details = phaseDetails[phase]

		details.currentTime = os.clock()
		details.deltaTime = details.currentTime - details.lastTime
		details.lastTime = details.currentTime

		details.generation = not details.generation
	end)

	scheduler:_addHook(
		scheduler.Hooks.PhaseBegan,
		function(args: PhaseBeganArgs)
			local phase = args.phase

			if not phaseDetails[phase] then
				phaseDetails[phase] = {
					currentTime = os.clock(),
					deltaTime = 0,
					lastTime = os.clock(),
					generation = false,
				}
			end

			local details = phaseDetails[phase]

			details.currentTime = os.clock()
			details.deltaTime = details.currentTime - details.lastTime
			details.lastTime = details.currentTime

			details.generation = not details.generation
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemAdd,
		function(info: SystemsAddRemove)
			local systemFn = info.system.system

			if not scheduler._systemState[systemFn] then
				scheduler._systemState[systemFn] = {}
			end

			if not scheduler._systemLogs[systemFn] then
				scheduler._systemLogs[systemFn] = {}
			end
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemRemove,
		function(info: SystemsAddRemove)
			local systemFn = info.system.system

			scheduler._systemState[systemFn] = nil
			scheduler._systemLogs[systemFn] = nil
		end
	)

	scheduler:_addHook(
		scheduler.Hooks.SystemReplace,
		function(info: SystemsReplace)
			local newSystem = info.new.system
			local oldSystem = info.old.system

			scheduler._systemState[newSystem] =
				scheduler._systemState[oldSystem]
			scheduler._systemLogs[newSystem] = scheduler._systemLogs[oldSystem]
			scheduler._systemState[oldSystem] = nil
			scheduler._systemLogs[oldSystem] = nil
		end
	)

	scheduler:_addHook(scheduler.Hooks.OuterSystemCall, function(args: HookArgs)
		local systemFn = args.system.system
		local phase = args.system.phase

		local details = phaseDetails[phase]

		return function()
			topoRuntime.start({
				system = scheduler._systemState[systemFn],
				frame = {
					generation = details.generation,
					deltaTime = details.deltaTime,
					logs = scheduler._systemLogs[systemFn],
				},
				currentSystem = systemFn,
			}, function()
				local thread = coroutine.create(function()
					args.nextFn()
				end)

				local _startTime = os.clock()
				local _success, _err = coroutine.resume(thread)
			end)
		end
	end)
end

function Plugin.new(module)
	if module then
		setHooks(module)
	end

	return setmetatable({}, Plugin)
end

export.Plugin = Plugin

type SchedulerLike<U...> = {
	addPlugin: (
		self: SchedulerLike<U...>,
		plugin: Plugin<U...>
	) -> SchedulerLike<U...>,
	new: (U...) -> SchedulerLike<U...>,
	[any]: any,
}

type Plugin<U...> = {
	getLoop: (self: Plugin<U...>) -> {
		[any]: any,
	},
	build: (self: Plugin<U...>, scheduler: SchedulerLike<U...>) -> (),
	cleanup: ((self: Plugin<U...>) -> ())?,
	new: (module: ModuleScript?) -> Plugin<U...>,
}

type Library = {
	useHookState: (
		discriminator: any?,
		cleanupCallback: ((storage: { [any]: any }) -> boolean)?
	) -> { [any]: any },
	useDeltaTime: () -> number,
	useEvent: (
		instance: Instance | { [string]: CustomEvent } | CustomEvent,
		event: string | RBXScriptSignal | CustomEvent
	) -> () -> (number, ...any),
	useThrottle: (seconds: number, discriminator: any?) -> boolean,
	log: (...any) -> (),

	Plugin: Plugin<...any>,
}

return export :: Library


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Planck = require(ReplicatedStorage.Packages.Planck)
local PlanckRunService = require(ReplicatedStorage.Packages.PlanckRunService)

local Phase = Planck.Phase
local Scheduler = Planck.Scheduler

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test

describe("Planck RunService", function()
	test("default phase works", function(_, done)
		expect.assertions(1)

		local function system()
			expect(true).toBe(true)
			done()
		end

		local runServicePlugin = PlanckRunService.Plugin.new()
		Scheduler.new()
			:addPlugin(runServicePlugin)
			:addSystem(system)
			:addRunCondition(system, Planck.runOnce())
	end)

	test("with startup phases", function(_, done)
		expect.assertions(2)

		local function system()
			expect(true).toBe(true)
			done()
		end

		local runServicePlugin = PlanckRunService.Plugin.new()
		Scheduler.new()
			:addPlugin(runServicePlugin)
			:addSystem(function()
				expect(true).toBe(true)
			end, Phase.Startup)
			:addSystem(system)
			:addRunCondition(system, Planck.runOnce())
	end)
end)


local RunService = game:GetService("RunService")

local Planck = require(script.Parent.Planck)

local Phase = Planck.Phase
type Phase = Planck.Phase

local Pipeline = Planck.Pipeline
type Pipeline = Planck.Pipeline

type System<U...> = Planck.System<U...>
type Scheduler<U...> = Planck.Scheduler<U...>

type SystemInfo<U...> = {
	name: string,
	phase: Phase,
	system: System<U...>,
}

type HookArgs<U...> = {
	scheduler: Scheduler<U...>,
	system: SystemInfo<U...>,
	nextFn: System<U...>,
}

type SystemsAddRemove<U...> = {
	scheduler: Scheduler<U...>,
	system: SystemInfo<U...>,
}

type SystemsReplace<U...> = {
	scheduler: Scheduler<U...>,
	new: SystemInfo<U...>,
	old: SystemInfo<U...>,
}

export type Phases = {
	PreRender: Phase,
	PreAnimation: Phase,
	PreSimulation: Phase,
	PostSimulation: Phase,

	First: Phase,
	PreUpdate: Phase,
	Update: Phase,
	PostUpdate: Phase,
	Last: Phase,
}

export type Pipelines = {
	Heartbeat: Pipeline,
	PreRender: Pipeline,
	PreAnimation: Pipeline,
	PreSimulation: Pipeline,
	PostSimulation: Pipeline,
}

local Phases = {} :: Phases

Phases.PreRender = Phase.new("PreRender")
Phases.PreAnimation = Phase.new("PreAnimation")
Phases.PreSimulation = Phase.new("PreSimulation")
Phases.PostSimulation = Phase.new("PostSimulation")

Phases.First = Phase.new("First")
Phases.PreUpdate = Phase.new("PreUpdate")
Phases.Update = Phase.new("Update")
Phases.PostUpdate = Phase.new("PostUpdate")
Phases.Last = Phase.new("Last")

local Pipelines: Pipelines = {} :: Pipelines

Pipelines.Heartbeat = Pipeline.new("HeartbeatPipeline")
	:insert(Phases.First)
	:insert(Phases.PreUpdate)
	:insert(Phases.Update)
	:insert(Phases.PostUpdate)
	:insert(Phases.Last)

Pipelines.PreRender = Pipeline.new("PreRenderPipeline"):insert(Phases.PreRender)

Pipelines.PreAnimation = Pipeline.new("PreAnimationPipeline")
	:insert(Phases.PreAnimation)

Pipelines.PreSimulation = Pipeline.new("PreSimulationPipeline")
	:insert(Phases.PreSimulation)

Pipelines.PostSimulation = Pipeline.new("PostSimulationPipeline")
	:insert(Phases.PostSimulation)

local Plugin = {}
Plugin.__index = Plugin

function Plugin:build<U...>(scheduler: Scheduler<U...>)
	for event, pipeline in pairs(Pipelines) do
		scheduler:insert(pipeline, RunService, event)
	end

	local _scheduler = scheduler :: any
	_scheduler._defaultDependencyGraph = (Pipelines.Heartbeat :: any).dependencyGraph
	_scheduler._defaultPhase = Phases.Update
end

function Plugin.new()
	return setmetatable({}, Plugin)
end

type Plugin<U...> = {
	build: (self: Plugin<U...>, scheduler: Scheduler<U...>) -> (),
	cleanup: ((self: Plugin<U...>) -> ())?,
	new: () -> Plugin<U...>,
}

return {
	Plugin = (Plugin :: any) :: Plugin<...any>,
	Phases = Phases,
	Pipelines = Pipelines,
}


_G.NOCOLOR = _G.NOCOLOR

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packages = ReplicatedStorage.Packages
local DevPackages = ReplicatedStorage.DevPackages

local Jest = require(DevPackages.Jest)

local runCLIOptions = {
	verbose = false,
	ci = false,
}

local projects = {
	Packages,
}

Jest.runCLI(script, runCLIOptions, projects):await()