--[[
Copyright 2025 ffrostfall

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
--!optimize 2
--!native
local task = require("@lune/task")

local tspawn = task.spawn
local cyield = coroutine.yield

local freeThread: thread? = nil

local function deletedSignalError()
	error("Cannot fire a deleted signal", 2)
end

local errorTable = {
	fire = deletedSignalError,
	connect = deletedSignalError,
	once = deletedSignalError,
	wait = deletedSignalError,
	disconnectAll = deletedSignalError,
}

local function run<T...>(func: (T...) -> (), ...: T...)
	local ref = freeThread
	freeThread = nil

	func(...)

	freeThread = ref
end

local function yieldLoop()
	while true do
		run(cyield())
	end
end

local signal = {}
signal.__index = signal
type InternalIdentity<T...> = setmetatable<{ (T...) -> () }, typeof(signal)>

--[=[
	@class signal

	The main class for this package. It is actually just an array with a metatable! Constructed with:
	```lua
	local signal = require(...)

	signal()
	```
]=]
local function constructor<T...>(): InternalIdentity<T...>
	return setmetatable({}, signal)
end

--[=[
	@method connect
	@within signal

	Connects a function to the signal. This will be called whenever the signal is fired.
	Connections can be "disconnected" (they won't be called when the signal is fired) by calling the function that they return.
	```lua
	local sig = signal()

	-- You can connect a function like this
	sig:connect(function(text)
		print(text)
	end)

	-- You can also directly connect the function like this.
	sig:connect(print)

	-- Will print twice, because two connections.
	sig:fire("Hello, world!")
	```

	You shouldn't connect the same function twice. This is because the function is used as a reference to disconnect when disconnecting later.
	While it *might* work for simple or specialized cases, you may find that it doesn't work as expected in some cases.

	Connections will be ran in order of connection.

	It's also worth noting that disconnecting is not an optimal operation, as it will shift the array. If you have a lot of connections to a single signal, you should avoid frequent disconnections.

	@param callback (T...) -> ()
	@return () -> ()
]=]
function signal.connect<T...>(self: InternalIdentity<T...>, callback: (T...) -> ())
	table.insert(self, callback)

	return function()
		local index = table.find(self, callback)

		if index then table.remove(self, index) end
	end
end

--[=[
	@method fire
	@within signal

	Firing a signal will run all connections in order, while using thread reusage.
	[You can find more on this optimization here](https://devforum.roblox.com/t/thread-reuse-how-it-works-why-it-works/1999166).

	You should note these things about thread reusage:
	- You cannot rely on the thread being different, or the same consistently.
	- The above means that certain "no-yield" implementations might not work correctly in connections.
	- If you have a lot of connections, you should avoid yielding in said connections. Yielding forces a new thread to be spawned, which isn't optimal, but it's still okay.

	@param data ...
	@return void
]=]
function signal.fire<T...>(self: InternalIdentity<T...>, ...: T...)
	for i = #(self :: any), 1, -1 do
		if not freeThread then
			freeThread = coroutine.create(yieldLoop)
			tspawn(freeThread :: thread)
		end

		-- Type states don't take into account assigning yet
		tspawn(freeThread :: thread, self[i], ...)
	end
end

--[=[
	@method once
	@within signal

	Connects to a signal, and disconnects after the first time it is fired.
	You can still disconnect the connection before it is fired.

	@param callback (T...) -> ()
	@return () -> ()
]=]
function signal.once<T...>(self: InternalIdentity<T...>, callback: (T...) -> ())
	local disconnect
	disconnect = self:connect(function(...)
		assert(disconnect ~= nil, "Luau")
		disconnect()

		callback(...)
	end)
end

--[=[
	@method wait
	@within signal

	Yields the current thread until the signal is fired, and returns the arguments passed.
	Will raise an error if the thread is resumed before the signal is fired.

	@return T...
]=]
function signal.wait<T...>(self: InternalIdentity<T...>): T...
	local running = coroutine.running()

	self:once(function(...)
		assert(
			coroutine.status(running) == "suspended",
			":wait() called, then another thread resumed the waiting thread. Please dont do that :("
		)

		tspawn(running, ...)
	end)

	return cyield()
end

--[=[
	@method disconnectAll
	@within signal

	Disconnects all connections to the signal. This is an efficient operation, utilizing `table.clear`.
]=]
function signal.disconnectAll<T...>(self: InternalIdentity<T...>)
	table.clear(self)
end

--[=[
	@method delete
	@within signal

	Disconnects all connections to the signal, and renders the signal unusable.
	While this technically isn't required or needed for memory concerns, you might want to use this when you're working with others.

	This will prevent and raise an error upon any attempts to use the signal, which is useful for debugging & catching leaks.
]=]
function signal.delete<T...>(self: InternalIdentity<T...>): ()
	self:disconnectAll()

	setmetatable(self, errorTable)
end

export type Identity<T...> = {
	fire: (self: Identity<T...>, T...) -> (),
	connect: (self: Identity<T...>, callback: (T...) -> ()) -> () -> (),
	once: (self: Identity<T...>, callback: (T...) -> ()) -> () -> (),
	wait: (self: Identity<T...>) -> T...,
	disconnectAll: (self: Identity<T...>) -> (),
	delete: (self: Identity<T...>) -> (),
}

return constructor :: <T...>() -> Identity<T...>


local b2 = require("@b2/")
local ecs = require("./ecs")
local runtest = require("@runtest/")

type Ent = b2.Ent

local style = runtest.util.style
local color = style.color
local b2i = b2.internal
local ecsi = ecs.internal
local pretty_print = style.from_epsilon
local indent_newlines = style.indent_newlines
-- local brightblue = color.blue_bright
-- local cyan = color.cyan
-- local grey = color.grey
local magenta = color.magenta

local INDEX = b2i.INDEX
local eindex_sparse = ecsi.eindex_sparse
local eindex_dense = ecsi.eindex_dense

local function non_entity(value: any)
	return `NaE{magenta "<"}{pretty_print(value)}{magenta ">"}`
end

local id_any
local id_entity
local id_pair

function id_entity(entity: number): string
	local id = b2i.ENT_LO(entity)
	local generation = b2i.ENT_HI(entity)

	if not eindex_sparse[id] then return `NaE{magenta "<"}{entity}{magenta ">"}` end

	if ecsi.eindex_alive_eh(entity) then
		return `Ent{magenta "<"}{pretty_print(id)}{magenta "@"}{pretty_print(generation)}{magenta ">"}`
	else
		return `Ent{magenta "<"}{pretty_print(id)}{magenta "@"}dead-{pretty_print(generation)}{magenta ">"}`
	end
end

function id_pair(pair: number): string
	local first = b2i.ENT_PAIR_HI(pair)
	local second = b2i.ENT_PAIR_LO(pair)
	local first_record = ecsi.eindex_try_any_record(first)
	local second_record = ecsi.eindex_try_any_record(second)
	local first_str: string
	local second_str: string
	if first_record then
		first_str = id_entity(eindex_dense[first_record.dense])
	else
		first_str = `NaE{magenta "<"}{pretty_print(first)}{magenta ">"}`
	end
	if second_record then
		second_str = id_entity(eindex_dense[second_record.dense])
	else
		second_str = `NaE{magenta "<"}{pretty_print(second)}{magenta ">"}`
	end

	return `Pair{magenta "<"}{first_str}{magenta ":"}{second_str}{magenta ">"}`
end

function id_any(entity: any): string
	if type(entity) ~= "number" then return non_entity(entity) end
	if b2i.ENT_PAIR_EH(entity) then
		return id_pair(entity)
	else
		return id_entity(entity)
	end
end

local function ent_name(entity: any): string
	if type(entity) ~= "number" then return `{magenta "$"}{non_entity(entity)}` end
	local name = ecs.get(entity :: any, b2.Name)
	if name then
		return name
	else
		if b2i.ENT_PAIR_EH(entity) then
			return `Pair{magenta "<"}{ent_name(b2i.ENT_PAIR_HI(entity))}{magenta ":"}{ent_name(b2i.ENT_PAIR_LO(entity))}{magenta ">"}`
		end
		return `{magenta "$"}{id_any(entity)}`
	end
end

local function ent_fullname(entity: any): string
	if type(entity) ~= "number" then return non_entity(entity) end
	local name = ecs.get(entity :: any, b2.Name)
	if name then
		return `{id_any(entity)}({name})`
	else
		return `{id_any(entity)}(NoName)`
	end
end

local function ent_data(entity: any): string
	if type(entity) ~= "number" then return `{non_entity(entity)} \{!NO RECORD!}` end
	local erecord = ecsi.eindex_try_matching_record(entity :: number)
	local output_str = `{id_any(entity)} \{`
	if erecord then
		local arch = erecord.arch
		local row = erecord.row
		if row then
			for id, column in arch.columns_map do
				output_str ..= indent_newlines(`\n[{ent_name(id)}] = {pretty_print(column[row])}`)
			end
		end
		output_str ..= "\n}"
	else
		output_str ..= "!NO RECORD!}"
	end
	return output_str
end

local function ent_location(entity: any): string
	if type(entity) ~= "number" then return `{non_entity(entity)} \{!NO RECORD!}` end
	local erecord = ecsi.eindex_try_matching_record(entity :: number)
	local output_str: string
	if erecord then
		local arch = erecord.arch
		local row = erecord.row
		output_str = `{id_any(entity)} row {row} of arch_{arch.location}`
	else
		output_str = `{id_any(entity)} \{!NO RECORD!}`
	end
	return output_str
end

local function component_record(component: number | Ent): string
	local id_record = ecsi.cindex[component :: number]
	if not id_record then return `{ent_name(component)} Record \{!NO RECORD!}` end
	local id_record_flags = INDEX(id_record, b2i.idr_flags)
	local flags_list = {}
	if bit32.btest(id_record_flags, b2i.IDFLAG_TAG) then
		table.insert(flags_list, "DATALESS")
	else
		table.insert(flags_list, "HAS DATA")
	end
	if bit32.btest(id_record_flags, b2i.IDFLAG_DELETE_ONCLEAR) then table.insert(flags_list, "DELETES ON CLEAR") end
	if bit32.btest(id_record_flags, b2i.IDFLAG_DELETE_ONDELETE) then table.insert(flags_list, "DELETES ON DELETE") end
	if bit32.btest(id_record_flags, b2i.IDFLAG_DELETE_ONCLEARTARGET) then
		table.insert(flags_list, "DELETES ON CLEAR TARGET")
	end
	if bit32.btest(id_record_flags, b2i.IDFLAG_DELETE_ONDELETETARGET) then
		table.insert(flags_list, "DELETES ON DELETE TARGET")
	end
	if bit32.btest(id_record_flags, b2i.IDFLAG_EXCLUSIVE) then table.insert(flags_list, "EXCLUSIVE RELATION") end
	if bit32.btest(id_record_flags, b2i.IDFLAG_HOOK_ONADD) then table.insert(flags_list, "HOOK_ONADD") end
	if bit32.btest(id_record_flags, b2i.IDFLAG_HOOK_ONCHANGE) then table.insert(flags_list, "HOOK_ONCHANGE") end
	if bit32.btest(id_record_flags, b2i.IDFLAG_HOOK_ONREM) then table.insert(flags_list, "HOOK_ONREM") end
	return `{ent_name(component)} Record {pretty_print(flags_list)}`
end

local function component_cache(component: number | Ent): string
	local id_record = ecsi.cindex[component :: number]
	if not id_record then return `{ent_name(component)} Cache \{!NO RECORD!}` end
	local id_record_dense_cached = INDEX(id_record, b2i.idr_dense_cached)
	local list = {}
	for _, arch in id_record_dense_cached do
		table.insert(list, `loc = {arch.location}, cols = {#arch.columns_dense}, rows = {#arch.ents}`)
	end
	return pretty_print(list)
end

return {
	entity = {
		id = id_any,
		name = ent_name,
		fullname = ent_fullname,
		data = ent_data,
		location = ent_location,
	},
	component = {
		record = component_record,
		cache = component_cache,
	},
}


--[[
simple util for creating specs
]]
local runtest = require("@runtest/")

type SpecRunner = runtest.SpecRunner
type SpecInterface = runtest.SpecInterface
type TestInterface = runtest.TestInterface
type LoadedSpec = runtest.LoadedSpec

local function serve(PASSED: SpecRunner)
	local spec = runtest.test.spec.init(PASSED)

	local scriptname = debug.info(2, "s")
	local test_prefix: string

	local function PREFIX(prefix: string?): ()
		prefix = prefix or scriptname
		if prefix == "" then
			test_prefix = ""
		else
			test_prefix = `[{prefix}] `
		end
	end
	local function TEST(label: string, test: (TestInterface) -> ()): ()
		spec.test(`{test_prefix}{label}`, test)
	end
	local function DONE(): LoadedSpec
		return spec.done()
	end

	return TEST, DONE, PREFIX
end

return serve


--!strict
--!native

--[[
	Pajamas plugin - event:

	A simple event module that provides an agnostic interface
	for a pajamas project to hook up to a serial or parallel
	loop event determined by the host runtime.
]]

export type Hook = {
	Connect: (Hook, boolean?) -> (),
	Disconnect: (Hook) -> ()
}

local RunService = game:GetService("RunService")

local Events = {}

Events.Loop =
	if game.Players.LocalPlayer then
		RunService.PreRender
	else
		RunService.PreSimulation

return function( Function, EventName: string )
	assert( Events[EventName], `Event named '{EventName}' does not exist` )
	local Signal = Events[EventName]
	local Connection: RBXScriptConnection
	local Hook = {}
	function Hook:Connect( Parallel: boolean? )
		if Parallel == true then
			Connection = Signal:ConnectParallel(Function)
		else
			Connection = Signal:Connect(Function)
		end
	end
	function Hook:Disconnect()
		if Connection.Connected then
			Connection:Disconnect()
		end
	end
	table.freeze( Hook )
	return Hook
end


--!strict
--!native

--[[
	Pajamas plugin - input:

	A simple keyboard input module providing an agnostic interface
	for a pajamas project to react to keyboard user intent.

	Note:
	No method for disconnection keyboard input yet.
	It is currently assumed that a pajamas project runs for as
	long as the host runtime is also running.
]]

local Input = {}
local UIS = game:GetService("UserInputService")

function Input:KeyDown( Listen_KeyCode: string, func )
	UIS.InputBegan:Connect(function( input: InputObject )
		if ( string.split( tostring( input.KeyCode ), `.` )[3] or `` ) == Listen_KeyCode then
			func()
		end
	end)
end

function Input:KeyUp( Listen_KeyCode: string, func )
	UIS.InputEnded:Connect(function( input: InputObject )
		if ( string.split( tostring( input.KeyCode ), `.` )[3] or `` ) == Listen_KeyCode then
			func()
		end
	end)
end

return Input


--!strict
--!native

--[[
	Pajamas plugin - task:

	Takes advantage of the Luau runtime's own task library.
	This plugin also considers if sync/desync support is present.
]]
assert( task, "This Luau runtime does not provide 'task'" )

local synchronize = task.synchronize or function() print("Warning: Missing sync/desync support in task") end
local function sync_func(): boolean
	synchronize()
	return true
end

local function is_synchronized(): boolean
	local sync_thread: thread = 			coroutine.create(sync_func)
	local _: boolean, sync_bool: boolean? = coroutine.resume(sync_thread)
	return									(not not sync_bool)
end

return table.freeze {
	cancel =		task.cancel,
	spawn =			task.spawn,
	defer = 		task.defer,
	wait = 			task.wait,
	synchronize = 	synchronize,
	desynchronize = task.desynchronize or synchronize,
	checksync = 	is_synchronized,
	checkdesync =	function(): boolean return not is_synchronized() end,
}


--!strict
--!native
-- Disable
local iswrapper = script.Parent:FindFirstChild("iswrapper")
if iswrapper then
	if require(iswrapper).Disable == true then
		script.Disabled = true
		return
	end
end
-- Wrapper
local Attributes: {[string]: any} = script:GetAttributes()
local Params: {[number]: any} = {}
local i: number = 1
for _, t in Attributes do
	Params[i] = Attributes[`Param_{i}`]
	i += 1
end
table.insert( Params, 1, script )
task.synchronize()
require(`../`)( table.unpack(Params) )


-- Crude validation for existing in the wrapper directory in
-- the face of datamodel properties not syncing outside of Roblox.
return table.freeze( { Disable = true } )


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

return function()
    test.suite("Arrays", function()
        test.case("contains", function()
            local a = { "one", "two", "three", "four", "five" }
            check.array.contains(a, "eleven") -- check for one value
            check.array.contains(a, { "four", "five", "six", "seven" }) -- check for multiple values
        end)

        test.case("equal changed", function()
            local a = { 1, 2, 3, 4, 5 }
            local b = { 1, 2, 10, 4, 5 }
            check.array.equal(a, b)
        end)

        test.case("equal extra", function()
            local a = { 1, 2, 3, 4, 5 }
            local b = { 1, 2, 3, 4, 5, 6, 7 }
            check.array.equal(a, b)
        end)

        test.case("equal fewer", function()
            local a = { 1, 2, 3, 4, 5 }
            local b = { 1, 2, 3 }
            check.array.equal(a, b)
        end)

        test.case("equal changed and more", function()
            local a = { 1, 2, 3, 4, 5 }
            local b = { 1, 20, 3, 4, 5, 6, 7 }
            check.array.equal(a, b)
        end)

        test.case("for arrays with tables, you should use check.table.equal", function()
            local a = { { a = 1 }, { a = 2 } }
            local b = { { a = 1 }, { a = 2 } }
            check.array.equal(a, b)
        end)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require

return function()
    test.suite("Passing", function()
        test.case("The only passing test", function()
            check.is_true(true)
        end)
    end)

    test.suite("Check and require", function()
        test.case("checks", function()
            -- checks dont halt the test
            check.equal(1, 100)
            check.equal(2, 100)
        end)

        test.case("require", function()
            -- requires do halt the test
            req.equal(1, 100)
            req.equal(2, 100) -- this will never run
        end)
    end)

    test.suite("Basic type equality", function()
        test.case("numbers", function()
            check.equal(10, 11)
        end)

        test.case("booleans", function()
            check.equal(false, true)
        end)

        test.case("strings", function()
            check.equal("foo", "bar")
        end)
    end)

    test.suite("Inequality", function()
        test.case("not equal", function()
            check.not_equal(10, 10)
        end)

        test.case("greater", function()
            check.greater(10, 10)
            check.greater_eq(9, 10)
        end)

        test.case("lesser", function()
            check.lesser(10, 10)
            check.lesser_eq(10, 9)
        end)
    end)

    test.suite("Boolean checks", function()
        test.case("is_true", function()
            check.is_true(false)
        end)

        test.case("is_false", function()
            check.is_false(true)
        end)

        test.case("truthy", function()
            check.truthy(nil)
        end)

        test.case("falsy", function()
            check.falsy(10)
        end)
    end)

    test.suite("Nil checks", function()
        test.case("not nil", function()
            local a = nil
            check.not_nil(a)
        end)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

return function()
    test.suite("Errors", function()
        test.case("throwing", function()
            error("some error") -- errors end a test
        end)

        test.case("should error", function()
            check.should_error(function()
                return 10
            end)
        end)

        test.case("should not error", function()
            check.should_not_error(function()
                error("some error")
            end)
        end)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

-- use test.focus variants of the case/suire functions to temporarily skip all but one test/suite
return function()
    -- can be used on suites to run only one suite
    test.focus.suite("Only run me [suite]", function()
        test.case("test 1", function()
            check.equal(1, 2)
        end)

        test.case("test 2", function()
            check.equal(1, 2)
        end)
    end)

    -- can be used on case to run only one case
    -- note: focus.case takes precedence over focus.suite, comment out this test to focus on the suite
    test.focus.case("Only run me [test]", function()
        check.equal(1, 2)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require
local msg = frktest.assert.msg

return function()
    test.suite("Messages", function()
        test.case("Print message if assertion fails", function()
            check.equal(10, 20, msg("the check failed and has a message"))
            local exp = 20
            req.equal(10, exp, msg(`messages can use interpolation: {exp}`))
        end)

        test.case("Message doesn't have to be in assertion", function()
            -- because of the way it's implemented, msg can be called before the check
            msg("message on different line")
            check.equal(1, 2)
        end)

        test.case("There can only be one message at a time", function()
            msg("this message will never print")
            check.equal(1, 2, msg("this message overwrote the one above"))
        end)

        test.case("Messages only affect the next assertion", function()
            -- messages are forgotten after each assertion, whether is passes or fails
            -- but only printed if the assertion fails
            msg("this message will never print")
            check.equal(1, 1) -- this assertions passes, but the message is still forgotten
            check.equal(1, 2) -- prints no custom message
        end)
    end)
end


-- parametrized testing, also known as 'data driven tests' or 'table driven tests'
-- quite popular in the golang community, is a style of testing where the data
-- and logic are separated so that you can run the same test logic on multiple
-- input/output pairs

local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

return function()
    test.suite("Parametrized tests", function()
        -- we can run code in the suite function!

        local test_data = {
            {
                name = "10 plus 10",
                data_in = {
                    a = 10,
                    b = 10,
                },
                exp = 21,
            },
            {
                name = "11 plus 10",
                data_in = {
                    a = 11,
                    b = 10,
                },
                exp = 20,
            },
        }

        -- for loop makes a test case for every element in test_data
        -- to add new cases we only need to add elements to the array
        for _, tt in test_data do
            test.case("case: " .. tt.name, function()
                local got = tt.data_in.a + tt.data_in.b
                check.equal(got, tt.exp)
            end)
        end
    end)
end


function load_tests()
    require("./the_test")()
end

function run_tests()
    local frktest = require("@frktest/frktest")
    local console_reporter = require("@frktest/reporters/console_reporter")

    console_reporter.init()
    frktest.run()
end

load_tests()
run_tests()


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check
local req = frktest.assert.require
local msg = frktest.assert.msg

return function()
    test.suite("A collection of tests", function()
        test.case("A test that passes", function()
            local exp = 4
            local got = 2 + 2
            check.equal(got, exp)
        end)

        test.case("A test that fails", function()
            local exp = 3
            local got = 2 + 2
            check.equal(got, exp)
        end)

        test.case("Halt early", function()
            local a = nil
            req.not_nil(a)
            check.falsy(a.b)
        end)

        test.case("Custom messages!", function()
            check.equal(1, 2, msg("my message prints in addition to the expansion"))
        end)

        test.case("Array assertions", function()
            local a = { "a", "b", "c" }
            local b = { "a", "z" }
            check.array.contains(a, { "z", "x" })
            check.array.equal(a, b)
        end)

        test.case("Table assertions", function()
            local a = { a = 1, b = 2, c = { a = 1, b = 2 } }
            local b = { a = 1, b = 22, c = { a = -1 } }
            check.table.contains(a, "b")
            check.table.equal(a, b)
        end)
    end)
end


function load_tests()
    -- tests execute in the order they are loaded
    require("../suites_cases")()
    require("../basic_assertions")()
    require("../messages")()
    require("../errors")()
    require("../parametrized")()
    require("../arrays")()
    require("../tables")()
    require("../skip")()
    -- require("./focus")() -- this file contains test.focus definitions, uncomment and only these definitions will run
end

function run_tests()
    local frktest = require("@frktest/frktest")
    local console_reporter = require("@frktest/reporters/console_reporter")

    console_reporter.init()
    local success = frktest.run()
    return success
end

load_tests()
local passed = run_tests()

-- return non-0 exit code on failure (runtime-dependent)
if not passed then
    -- use the version of your preferred runtime
    -- require("@lune/process").exit(1) -- lune
    -- zune.process.exit(1) -- zune

    -- this might do the trick in a version-agnostic way, but it's noisy
    -- error("tests failed")
end


function load_tests()
    require("../basic_assertions")()
end

function run_tests()
    -- same as run, but uses the entrypoint.luau entrypoint script, like a pesde project would
    local frktest = require("../../src/entrypoint")
    local console_reporter = frktest.reporters.console_reporter

    console_reporter.init()
    local success = frktest.run()
    return success
end

load_tests()
run_tests()


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

-- use test.skip to not run suites/cases
return function()
    -- can be used on suites to skip the entire suite
    test.skip.suite("This suite will not run", function()
        test.case("test 1", function()
            check.equal(1, 2)
        end)

        test.case("test 2", function()
            check.equal(1, 2)
        end)
    end)

    -- can be used on case to skip a specific case
    test.skip.case("This case will not run", function()
        check.equal(1, 2)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

return function()
    -- use suites to group test cases together
    test.suite("A collection of test cases", function()
        test.case("case 1", function()
            check.is_true(false)
        end)

        test.case("case 2", function()
            check.is_true(false)
        end)
    end)

    -- casses don't have to be in a suite
    -- global tests run before suites no matter where they are defined
    test.case("case in the global suite", function()
        check.is_true(false)
    end)
end


local frktest = require("@frktest/frktest")
local test = frktest.test
local check = frktest.assert.check

return function()
    test.suite("Tables", function()
        test.case("value equal", function()
            local a = { foo = 1 }
            local b = { foo = 1 }
            local c = a
            check.equal(a, b) -- fails, equal checks the address of tables
            check.not_equal(a, c) -- fails, variables pointing to the same table equal eachother
        end)

        test.case("contains", function()
            local a = { a = 1, b = 2, c = 3 }
            check.table.contains(a, "d") -- check for one key
            check.table.contains(a, { "b", "c", "d", "e" }) -- check for multiple keys
        end)

        test.case("table equal modified", function()
            local a = { a = 1, b = 2 }
            local b = { a = 1, b = 20 }
            check.table.equal(a, b)
        end)

        test.case("table equal added", function()
            local a = { a = 1, b = 2 }
            local b = { a = 1 }
            check.table.equal(a, b)
        end)

        test.case("table equal missing", function()
            local a = { a = 1 }
            local b = { a = 1, b = 2 }
            check.table.equal(a, b)
        end)

        test.case("arrays of tables fit here", function()
            local a = { { a = 1 }, { a = 3 } }
            local b = { { a = 1 }, { a = 22 }, { a = 3 } }
            check.table.equal(a, b)
        end)

        test.case("table-ness is checked at runtime", function()
            local not_table = "str"
            local a = { a = 1, b = 2 }

            -- passing non-tables will fail the assertion with helpful message
            check.table.contains(not_table, "a")
            check.table.equal(a, not_table)
            check.table.equal(not_table, a)
        end)

        test.case("metatables aren't considered", function()
            -- these 2 tables are equal, even though their MTs are different
            local a = setmetatable({ a = 10 }, { foo = function() end, b = 10 })
            local b = setmetatable({ a = 10 }, { bar = function() end, b = 20 })
            check.table.equal(a, b)
        end)
    end)
end


local types = require("./types")

type Failure = types.Failure

local _state = {}
_state.listeners = {}
_state.listeners.check_failed = {} :: { (Failure) -> nil }
_state.listeners.require_failed = {} :: { (Failure) -> nil }
_state.current_user_msg = nil :: string?

local test = require("./test")

function exec_listeners<T>(i: T, listeners: { (T) -> nil })
    for _, l in ipairs(listeners) do
        l(i)
    end
end

function make_assert<T...>(impl: (T...) -> Failure?): (T...) -> Failure?
    return function(...)
        local current_test = test.current_test()
        if current_test == nil then
            error("current test is nil, is the assertion called outside of a test function?")
        end

        local f = impl(...)
        if f == nil then
            _state.current_user_msg = nil
            return nil
        end

        -- check for user message
        if _state.current_user_msg ~= nil then
            f.user_msg = _state.current_user_msg
        end

        current_test.failed = true
        _state.current_user_msg = nil
        return f
    end
end

function make_check<T...>(impl: (T...) -> Failure?): (T...) -> nil
    local assertion = make_assert(impl)
    return function(...)
        local f = assertion(...)
        if f == nil then
            return
        end
        exec_listeners(f, _state.listeners.check_failed)
    end
end

function make_require<T...>(impl: (T...) -> Failure?): (T...) -> nil
    local assertion = make_assert(impl)
    return function(...)
        local f = assertion(...)
        if f == nil then
            return
        end
        exec_listeners(f, _state.listeners.require_failed)
        error({ -- halt test execution on require
            _is_frktest_require = true,
            failure = f,
        })
    end
end

local assert = {}

function assert.on_check_failed(f: (Failure) -> nil)
    table.insert(_state.listeners.check_failed, f)
end

function assert.on_require_failed(f: (Failure) -> nil)
    table.insert(_state.listeners.require_failed, f)
end

-- used to add custom messages on assertion failures
function assert.msg(s: string)
    _state.current_user_msg = s
end

-- assertion implementaitons
assert.check = {}
assert.require = {}

assert.check.array = {}
assert.require.array = {}
assert.check.table = {}
assert.require.table = {}

local assert_impl = require("./assert_impl")

-- value assertions
assert.check.equal = make_check(assert_impl.equal)
assert.require.equal = make_require(assert_impl.equal)

assert.check.not_equal = make_check(assert_impl.not_equal)
assert.require.not_equal = make_require(assert_impl.not_equal)

assert.check.greater = make_check(assert_impl.greater)
assert.require.greater = make_require(assert_impl.greater)

assert.check.greater_eq = make_check(assert_impl.greater_eq)
assert.require.greater_eq = make_require(assert_impl.greater_eq)

assert.check.lesser = make_check(assert_impl.lesser)
assert.require.lesser = make_require(assert_impl.lesser)

assert.check.lesser_eq = make_check(assert_impl.lesser_eq)
assert.require.lesser_eq = make_require(assert_impl.lesser_eq)

assert.check.is_true = make_check(assert_impl.is_true)
assert.require.is_true = make_require(assert_impl.is_true)

assert.check.is_false = make_check(assert_impl.is_false)
assert.require.is_false = make_require(assert_impl.is_false)

assert.check.truthy = make_check(assert_impl.truthy)
assert.require.truthy = make_require(assert_impl.truthy)

assert.check.falsy = make_check(assert_impl.falsy)
assert.require.falsy = make_require(assert_impl.falsy)

assert.check.not_nil = make_check(assert_impl.not_nil)
assert.require.not_nil = make_require(assert_impl.not_nil)

-- error/exception assertions
assert.check.should_error = make_check(assert_impl.should_error)
assert.require.should_errors = make_require(assert_impl.should_error)

assert.check.should_not_error = make_check(assert_impl.should_not_error)
assert.require.should_not_errors = make_require(assert_impl.should_not_error)

-- array assertions
assert.check.array.contains = make_check(assert_impl.array_contains)
assert.require.array.contains = make_require(assert_impl.array_contains)

assert.check.array.equal = make_check(assert_impl.array_equal)
assert.require.array.equal = make_require(assert_impl.array_equal)

-- table assertions
assert.check.table.contains = make_check(assert_impl.table_contains)
assert.require.table.contains = make_require(assert_impl.table_contains)

assert.check.table.equal = make_check(assert_impl.table_equal)
assert.require.table.equal = make_require(assert_impl.table_equal)

return assert


local types = require("./types")
local utils = require("./utils")

type Failure = types.Failure
type DiffEntry<T> = types.DiffEntry<T>
type TableDiff = types.TableDiff

local assert_impl = {}

function make_failure(message: string): Failure
    local file, line = debug.info(5, "sl")
    return {
        location = {
            file = file,
            line = line,
        },
        message = message,
    }
end

-- value assertions

function assert_impl.equal<T>(a: T, b: T): Failure?
    if a == b then
        return nil
    end
    return make_failure(`{a} == {b}`)
end

function assert_impl.not_equal<T>(a: T, b: T): Failure?
    if a ~= b then
        return nil
    end
    return make_failure(`{a} ~= {b}`)
end

function assert_impl.greater(a: number, b: number): Failure?
    if a > b then
        return nil
    end
    return make_failure(`{a} > {b}`)
end

function assert_impl.greater_eq(a: number, b: number): Failure?
    if a >= b then
        return nil
    end
    return make_failure(`{a} >= {b}`)
end

function assert_impl.lesser(a: number, b: number): Failure?
    if a < b then
        return nil
    end
    return make_failure(`{a} < {b}`)
end

function assert_impl.lesser_eq(a: number, b: number): Failure?
    if a <= b then
        return nil
    end
    return make_failure(`{a} <= {b}`)
end

function assert_impl.is_true<T>(a: boolean): Failure?
    if a == true then
        return nil
    end
    return make_failure(`{a} == true`)
end

function assert_impl.is_false<T>(a: boolean): Failure?
    if a == false then
        return nil
    end
    return make_failure(`{a} == false`)
end

function assert_impl.truthy<T>(a: any): Failure?
    if a then
        return nil
    end
    return make_failure(`if {a} - condition failed`)
end

function assert_impl.falsy<T>(a: any): Failure?
    if not a then
        return nil
    end
    return make_failure(`if not {a} - condition failed`)
end

function assert_impl.not_nil(a: any): Failure?
    if a ~= nil then
        return nil
    end
    return make_failure("value was nil")
end

-- error/exception assertions

function assert_impl.should_error<T...>(f: (T...) -> any?): Failure?
    local ok, _ = pcall(f)
    if not ok then
        return nil
    end
    return make_failure(`did not error`)
end

function assert_impl.should_not_error<T...>(f: (T...) -> any?): Failure?
    local ok, err = pcall(f)
    if ok then
        return nil
    end
    return make_failure(`errored: {err}`)
end

-- array assertions

function assert_impl.array_contains<T>(a: { T }, keys: T | { T }): Failure?
    if type(keys) ~= "table" then
        keys = { keys }
    end
    local keys_table = {}
    for _, k in ipairs(keys :: { T }) do
        keys_table[k] = false
    end

    for _, e in ipairs(a) do
        keys_table[e] = true
    end

    local missing = {}
    for k, found in pairs(keys_table) do
        if not found then
            if type(k) == "string" then
                k = '"' .. k .. '"'
            end
            table.insert(missing, k)
        end
    end

    if #missing == 0 then
        return nil
    end

    return make_failure(`missing values: {table.concat(missing, ", ")}`)
end

function assert_impl.array_equal<T>(a: { [number]: T }, b: { [number]: T }): Failure?
    local min_len = math.min(#a, #b)
    local diff = {} :: { [number]: DiffEntry<T> }

    for i = 1, min_len do
        if a[i] ~= b[i] then
            table.insert(diff, { k = i, v = { a = a[i], b = b[i] }, t = "~" })
        end
    end

    if #a ~= #b then
        local max_len = math.max(#a, #b)
        local longer: { [number]: T } = if #a > #b then a else b
        local type: "+" | "-" = if #a > #b then "+" else "-"
        for i = min_len + 1, max_len do
            table.insert(diff, { k = i, t = type, v = longer[i] })
        end
    end

    if #diff == 0 then
        return nil
    end

    local f = make_failure("Arrays were not equal")
    f.complex_info = {
        type = "ArrayDiff",
        data = diff,
    }

    return f
end

-- table assertions

function assert_impl.table_contains(t: any, keys: any | { [number]: any }): Failure?
    -- ensure input is a tables
    if typeof(t) ~= "table" then
        return make_failure(`type of arg 1 is {typeof(t)}, expected a table`)
    end

    if type(keys) ~= "table" then
        keys = { keys }
    end

    local missing = {}
    for _, k in ipairs(keys) do
        if t[k] == nil then
            table.insert(missing, k)
        end
    end

    if #missing == 0 then
        return nil
    end

    return make_failure(`missing keys: {table.concat(missing, ", ")}`)
end

local ltdiff = require("./lib/ltdiff")
function assert_impl.table_equal(a: any, b: any): Failure?
    -- ensure inputs are tables
    if typeof(a) ~= "table" then
        return make_failure(`type of arg 1 is {typeof(a)}, expected a table`)
    end
    if typeof(b) ~= "table" then
        return make_failure(`type of arg 2 is {typeof(b)}, expected a table`)
    end

    -- convert ltdiff into our diff format
    local function ltdiff_to_frkdiff(lt_diff: ltdiff.Diff): (boolean, TableDiff)
        local has_diff = false
        local frk_diff: TableDiff = { diff = {} }

        if lt_diff.add ~= nil then
            has_diff = true
            for _, v in ipairs(lt_diff.add) do
                table.insert(frk_diff.diff, { k = v.k, t = "-", v = v.v })
            end
        end

        if lt_diff.del ~= nil then
            has_diff = true
            for _, v in ipairs(lt_diff.del) do
                table.insert(frk_diff.diff, { k = v.k, t = "+", v = v.v })
            end
        end

        if lt_diff.mod ~= nil then
            has_diff = true
            for k, v in pairs(lt_diff.mod) do
                table.insert(frk_diff.diff, { k = k, t = "~", v = { a = v.a, b = v.b } })
            end
        end

        if lt_diff.sub ~= nil then
            has_diff = true
            frk_diff.sub = {}
            for k, v in pairs(lt_diff.sub) do
                assert(frk_diff.sub)
                local sub_has_diff
                sub_has_diff, frk_diff.sub[k] = ltdiff_to_frkdiff(v)
                has_diff = has_diff or sub_has_diff
            end
        end

        return has_diff, frk_diff
    end
    local lt_diff, _warning = ltdiff.diff(a, b)

    local has_diff, diff = ltdiff_to_frkdiff(lt_diff)
    if not has_diff then
        return nil
    end

    local f = make_failure("Tables were not equal")
    f.complex_info = {
        type = "TableDiff",
        data = diff,
    }

    return f
end

return assert_impl


-- i'm not a fan of having a single entrypoint, but this seems to be idiomatic for pesde/wally
-- with this, reporters will be acessible to folks who want to consume the lib through a single entrypoint
local frktest = require("./frktest")

local console_reporter = require("./reporters/console_reporter")
type ConsoleReporter = typeof(console_reporter)

local reporters = {
    console_reporter = console_reporter,
}

setmetatable(reporters, {
    __index = function(_, k)
        -- for backwards-compatibility
        if k == "lune_console_reporter" then
            warn("[DEPRECATED] lune_console_reporter is deprecated, use console_reporter instead\n")
            return console_reporter
        end
        return nil :: any
    end,
})

type T = typeof(frktest) & { reporters: typeof(reporters) }

(frktest :: any)._reporters = reporters; -- kept for backwards compatibility
(frktest :: any).reporters = reporters

return frktest :: T


local frktest = {}

local test = require("./test")
local assert = require("./assert")

-- make sure to initalize a reporter before running, otherwise you will get no output
-- see `src/reporters` for builtin reporters available
-- returns false if any test failed, true otherwise
frktest.run = function(): boolean
    test.load_suites()
    return test.run_tests()
end

frktest.test = test
frktest.assert = assert

return frktest


-- usage:
--     local reporter = require("@frktest/reporters/console_reporter")
--     reporter.init()
--     -- run tests

local types = require("../types")
local utils = require("../utils")
local inspect = require("../lib/inspect")
local color = require("./runtimes/color")

type Failure = types.Failure
type DiffEntry<T> = types.DiffEntry<T>
type TableDiff = types.TableDiff
type TestError = types.TestError

local reporter = {}

local _state = {}
_state.indent = 0
_state.current_suite = nil :: types.Suite?
_state.current_test = nil :: types.Test?
_state.suite_printed = false
_state.test_printed = false

function print_indent(s: string, offset: number?)
    if offset == nil then
        offset = 0
    end
    local indent = _state.indent + offset :: number
    return print(utils.make_indent(indent) .. s)
end

function print_test_info_once()
    if _state.current_suite ~= nil and _state.suite_printed == false then
        _state.suite_printed = true
        print_indent(`Suite: {color("purple", _state.current_suite.name)}`, -2)
        print("")
    end
    if _state.current_test ~= nil and _state.test_printed == false then
        _state.test_printed = true
        print_indent(`Test: {color("purple", _state.current_test.name)}`, -1)
        print("")
    end
end

function make_array_diff_string<T>(diff: { [number]: DiffEntry<T> }): string
    local make_line = function<T>(e: DiffEntry<T>): string
        local colors = {
            ["+"] = "green",
            ["-"] = "red",
            ["~"] = "yellow",
        }
        local value_s
        if e.t == "~" then
            value_s = `{(e.v :: any).a} == {(e.v :: any).b}`
        else
            value_s = `{e.v}`
        end
        return color(colors[e.t], `{e.t} [{e.k}]: {value_s}`)
    end
    local message = {}
    for _, d in ipairs(diff) do
        table.insert(message, make_line(d))
    end
    return table.concat(message, "\n")
end

function make_table_diff_string(diff: TableDiff): string
    local make_line = function<T>(i: number, e: DiffEntry<any>): string
        local colors = {
            ["+"] = "green",
            ["-"] = "red",
            ["~"] = "yellow",
        }
        local value_s = ""
        local inspect_opts = { newline = "", indent = "" }
        if e.t == "~" then
            value_s = `{inspect((e.v :: any).a, inspect_opts)} == {inspect((e.v :: any).b, inspect_opts)}`
        else
            value_s = inspect(e.v, inspect_opts)
        end
        return color(colors[e.t], `{utils.make_indent(i)}{e.t} [{e.k}]: {value_s}`)
    end
    local message = {}

    local function add_to_diff_message(diff: TableDiff, i: number)
        for _, v in ipairs(diff.diff) do
            table.insert(message, make_line(i, v))
        end
        if diff.sub ~= nil then
            for k, v in pairs(diff.sub) do
                table.insert(message, `{utils.make_indent(i)}  [{k}]:`)
                add_to_diff_message(v, i + 1)
            end
        end
    end
    add_to_diff_message(diff, 0)

    return table.concat(message, "\n")
end

function handle_complex_info(type: string, data: any): string?
    if type == "ArrayDiff" then
        return make_array_diff_string(data :: { [number]: DiffEntry<any> })
    elseif type == "TableDiff" then
        return make_table_diff_string(data :: TableDiff)
    end
    return nil
end

function reporter.print_failure(type: "CHECK" | "REQUIRE", f: Failure)
    print_test_info_once()
    print_indent(`{color("red", `{type} FAILED:`)} {f.location.file}:{f.location.line}`)
    print_indent(`{color("yellow", f.message)}`, 1)
    if f.complex_info ~= nil then
        local complex_msg = handle_complex_info(f.complex_info.type, f.complex_info.data)
        if complex_msg ~= nil then
            print(utils.indent_multiline(complex_msg, _state.indent + 1))
        end
    end
    if f.user_msg ~= nil then
        print_indent("with message:")
        print_indent(color("yellow", f.user_msg), 1)
    end
    print("")
end

function reporter.print_test_error(e: TestError)
    print_test_info_once()
    print_indent(`{color("red", "ERROR:")} {e.location.file}:{e.location.line}`)
    print_indent(`{color("yellow", e.error)}`, 1)
    print_indent("trace:")
    print(`{color("yellow", utils.indent_multiline(utils.trim(e.strace), _state.indent + 1))}`)
    print("")
end

function reporter.print_counts(tests: number, suites: number)
    print(`Running - {tests} tests | {suites} suites...`)
    print("")
end

function reporter.print_summary(ran: number, passed: number, failed: number, skipped: number)
    print("--------------------")
    local summary = `Tests Ran: {ran}`
    if passed > 0 then
        summary = summary .. " | " .. color("green", `Passed: {passed}`)
    end
    if failed > 0 then
        summary = summary .. " | " .. color("red", `Failed: {failed}`)
    end
    if skipped > 0 then
        summary = summary .. " | " .. color("yellow", `Skipped: {skipped}`)
    end
    print(summary)
    local status = if failed == 0 then color("green", "OK") else color("red", "FAIL")
    status = if ran == 0 then color("yellow", "SKIPPED") else status
    print(`status: {status}`)
end

function reporter.init()
    -- register testing events to keep track of current testing state
    local test = require("../test")
    test.on_suite_enter(function(s)
        _state.suite_printed = false
        _state.current_suite = s
        _state.indent += 1
    end)
    test.on_suite_leave(function(_)
        _state.current_suite = nil
        _state.indent -= 1
    end)
    test.on_test_enter(function(t)
        _state.test_printed = false
        _state.current_test = t
        _state.indent += 1
    end)
    test.on_test_leave(function(_)
        _state.current_test = nil
        _state.indent -= 1
    end)

    test.on_exec_begin(function(c)
        reporter.print_counts(c.tests, c.suites)
    end)
    test.on_exec_end(function(r)
        reporter.print_summary(r.ran, r.passed, r.failed, r.skipped)
    end)

    test.on_test_error(function(e)
        reporter.print_test_error(e)
    end)

    local assert = require("../assert")
    assert.on_check_failed(function(f)
        reporter.print_failure("CHECK", f)
    end)
    assert.on_require_failed(function(f)
        reporter.print_failure("REQUIRE", f)
    end)
end

return reporter


-- file is kept here for backwards compatibility
warn("[DEPRECATED] lune_console_reporter is deprecated, use console_reporter instead\n")
return require("./console_reporter")


local runtime = require("./get_runtime")()
type Color = "red" | "green" | "yellow" | "purple"

type ColorFn = (Color, string) -> string

-- default, fallback to plain print
local color: ColorFn = function(_, s: string)
    return s
end

if runtime == "lune" then
    local stdio = require("@lune/stdio")
    color = function(c, s: string)
        return `{stdio.color(c)}{s}{stdio.color("reset")}`
    end
elseif runtime == "zune" then
    local stdio = zune.stdio
    color = function(c, s: string)
        if c == "purple" then
            c = "magenta" :: any
        end
        return `{stdio.color(c)}{s}{stdio.reset()}`
    end
elseif runtime == "lute" then
    local richterm = require("../../lib/lute_batteries/richterm")

    color = function(c: string, s: string)
        if c == "red" then
            return richterm.red(s)
        elseif c == "green" then
            return richterm.green(s)
        elseif c == "yellow" then
            return richterm.yellow(s)
        elseif c == "purple" then
            return richterm.magenta(s)
        end
        return s
    end
end

return color :: ColorFn


export type Runtime = "lune" | "zune" | "lute" | "unknown"

return function(): Runtime
    if type(_VERSION) == "string" and string.sub(_VERSION, 1, 4) == "Lune" then
        return "lune"
    elseif type(_VERSION) == "string" and string.sub(_VERSION, 1, 4) == "Zune" then
        return "zune"
    else
        -- test for runtimes that require a pcall last
        -- lute
        local ok, _ = pcall(function()
            return (require)("@lute/process")
        end)
        if ok then
            return "lute"
        end

        return "unknown"
    end
end


local types = require("./types")
local utils = require("./utils")

local test = {}

type Test = types.Test
type Suite = types.Suite
type ExecCounts = types.ExecCounts
type ExecResult = types.ExecResult
type TestError = types.TestError

local _state = {}
_state.suites = {} :: { Suite }
_state.global_suite = {} :: { Test }
_state.current_load_suite = nil :: Suite?

_state.current_exec_suite = nil :: Suite?
_state.current_exec_test = nil :: Test?

_state.focus_suite = nil :: Suite?
_state.focus_test = nil :: Test?

_state.listeners = {}
_state.listeners.suite_enter = {} :: { (Suite) -> nil }
_state.listeners.suite_leave = {} :: { (Suite) -> nil }
_state.listeners.test_enter = {} :: { (Test) -> nil }
_state.listeners.test_leave = {} :: { (Test) -> nil }
_state.listeners.test_skipped = {} :: { (Test) -> nil }
_state.listeners.exec_begin = {} :: { (ExecCounts) -> nil }
_state.listeners.exec_end = {} :: { (ExecResult) -> nil }
_state.listeners.test_error = {} :: { (TestError) -> nil }

function get_current_load_suite_tests(): { Test }
    local suite_test = _state.global_suite
    if _state.current_load_suite ~= nil then
        suite_test = _state.current_load_suite.tests
    end
    return suite_test
end

function test.suite(name: string, fn: () -> nil)
    table.insert(_state.suites, {
        name = name,
        fn = fn,
        tests = {},
        skip = false,
    })
end

function test.case(name: string, fn: () -> nil)
    local suite_tests = get_current_load_suite_tests()
    table.insert(suite_tests, {
        name = name,
        fn = fn,
        skip = false,
        failed = false,
    })
end

-- to focus on one suite/case
test.focus = {}
function test.focus.case(name: string, fn: () -> nil)
    if _state.focus_test ~= nil then
        error(`focus.test called more than once, current focus test: '{_state.focus_test.name}'`)
    end

    test.case(name, fn)

    local suite_tests = get_current_load_suite_tests()
    _state.focus_test = suite_tests[#suite_tests]
end

function test.focus.suite(name: string, fn: () -> nil)
    if _state.focus_suite ~= nil then
        error(`focus.suite called more than once, current focus suite: '{_state.focus_suite.name}'`)
    end

    test.suite(name, fn)
    _state.focus_suite = _state.suites[#_state.suites]
end

-- to skip suites/cases
test.skip = {}
function test.skip.case(name: string, fn: () -> nil)
    test.case(name, fn)

    local suite_tests = get_current_load_suite_tests()
    suite_tests[#suite_tests].skip = true
end

function test.skip.suite(name: string, fn: () -> nil)
    test.suite(name, fn)
    _state.suites[#_state.suites].skip = true
end

function test.load_suites()
    for _, suite_i in ipairs(_state.suites) do
        _state.current_load_suite = suite_i
        assert(_state.current_load_suite ~= nil)
        _state.current_load_suite.tests = {} -- empty list so we dont have duplicate tests
        suite_i.fn()
        _state.current_load_suite = nil
    end
end

function exec_listeners<T>(i: T, listeners: { (T) -> nil })
    for _, l in ipairs(listeners) do
        l(i)
    end
end

function count_tests(): number
    local count = #_state.global_suite
    for _, s in ipairs(_state.suites) do
        count += #s.tests
    end
    return count
end

-- set true to remove xpcall
-- used to debug the framework itself
test._hard_errors = false

-- returns true if all tests succeeded, false otherwise
function test.run_tests(): boolean
    local tests_ran = 0
    local tests_passed = 0
    local tests_failed = 0
    local tests_skipped = 0

    if _state.focus_suite ~= nil and _state.focus_test ~= nil then
        warn(
            `both focus.case, and focus.suite have been set, the case will be run exclusively, other tests in suite '{_state.focus_suite.name}' will be skipped\n`
        )
    end

    local run_test = function(t: Test, suite_skip: boolean?)
        if t.skip or suite_skip == true then
            tests_skipped += 1
            exec_listeners(t, _state.listeners.test_skipped)
            return
        end

        t.failed = false
        _state.current_exec_test = t
        exec_listeners(t, _state.listeners.test_enter)

        if test._hard_errors then
            t.fn()
        else
            local _, _ = xpcall(function()
                t.fn()
            end, function(err)
                if type(err) == "table" and err["_is_frktest_require"] == true then
                    return nil -- no extra work to do listeners will handle the failure
                end
                local strace = debug.traceback("", 2)
                local file, line = debug.info(3, "sl")
                exec_listeners({
                    location = {
                        file = file,
                        line = line,
                    },
                    strace = strace,
                    error = err,
                }, _state.listeners.test_error)

                t.failed = true
                return nil
            end)
        end

        exec_listeners(t, _state.listeners.test_leave)
        _state.current_exec_test = nil

        tests_ran += 1
        if t.failed == false then
            tests_passed += 1
        else
            tests_failed += 1
        end
    end

    local run_suite = function(s: Suite)
        _state.current_exec_suite = s
        exec_listeners(s, _state.listeners.suite_enter)
        for _, t in ipairs(s.tests) do
            run_test(t, s.skip)
        end
        exec_listeners(s, _state.listeners.suite_leave)
        _state.current_exec_suite = nil
    end

    exec_listeners({
        tests = count_tests(),
        suites = #_state.suites,
    }, _state.listeners.exec_begin)

    if _state.focus_test ~= nil then
        run_test(_state.focus_test)
    elseif _state.focus_suite ~= nil then
        run_suite(_state.focus_suite)
    else
        for _, t in ipairs(_state.global_suite) do
            run_test(t)
        end

        for _, s in ipairs(_state.suites) do
            run_suite(s)
        end
    end

    exec_listeners({
        ran = tests_ran,
        passed = tests_passed,
        failed = tests_failed,
        skipped = tests_skipped,
    }, _state.listeners.exec_end)

    return tests_failed == 0
end

function test.on_suite_enter(f: (Suite) -> nil)
    table.insert(_state.listeners.suite_enter, f)
end

function test.on_suite_leave(f: (Suite) -> nil)
    table.insert(_state.listeners.suite_leave, f)
end

function test.on_test_enter(f: (Test) -> nil)
    table.insert(_state.listeners.test_enter, f)
end

function test.on_test_leave(f: (Test) -> nil)
    table.insert(_state.listeners.test_leave, f)
end

function test.on_test_skipped(f: (Test) -> nil)
    table.insert(_state.listeners.test_skipped, f)
end

function test.on_exec_begin(f: (ExecCounts) -> nil)
    table.insert(_state.listeners.exec_begin, f)
end

function test.on_exec_end(f: (ExecResult) -> nil)
    table.insert(_state.listeners.exec_end, f)
end

function test.on_test_error(f: (TestError) -> nil)
    table.insert(_state.listeners.test_error, f)
end

function test.reset()
    _state.tests = {}
    _state.global_suite = {}
    _state.current_load_suite = nil

    _state.current_exec_suite = nil
    _state.current_exec_test = nil
end

-- returns suite object if currently executing a suite, nil otherwise
function test.current_suite(): Suite?
    return _state.current_exec_suite
end

-- returns test object if currently executing a test, nil otherwise
function test.current_test(): Test?
    return _state.current_exec_test
end

return test


type Location = {
    file: string,
    line: string,
}

export type Failure = {
    location: Location,
    message: string,
    user_msg: string?,
    complex_info: {
        type: string,
        data: any,
    }?,
}

export type DiffEntry<T> = {
    k: any,
    t: "+" | "-" | "~",
    v: T | { a: T, b: T }, -- latter for t=="~"
}

export type TableDiff = {
    diff: { [number]: DiffEntry<any> },
    sub: { [any]: TableDiff }?,
}

export type TestError = {
    location: Location,
    strace: string,
    error: any,
}

export type Test = {
    -- config state
    name: string,
    fn: () -> nil,
    skip: boolean,
    -- result state
    failed: boolean,
}

export type Suite = {
    name: string,
    fn: () -> nil,
    tests: { Test },
    skip: boolean,
}

export type ExecCounts = {
    suites: number,
    tests: number,
}

export type ExecResult = {
    ran: number,
    passed: number,
    failed: number,
    skipped: number,
}

return {}


local utils = {}

function utils.trim(s: string): string
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end

function utils.make_indent(i: number): string
    local spaces = "    "
    local s = ""
    for j = 0, i - 1 do
        s = s .. spaces
    end
    return s
end

function utils.multiline_to_array(s: string): { string }
    local res = {}
    for line in string.gmatch(s .. "\n", "(.-)\n") do
        table.insert(res, line)
    end
    return res
end

function utils.indent_multiline(s: string, i: number): string
    local lines = utils.multiline_to_array(s)
    for k, s in ipairs(utils.multiline_to_array(s)) do
        lines[k] = utils.make_indent(i) .. s
    end
    return table.concat(lines, "\n")
end

return utils


--> lib: Builder pattern class to spawn child processes

local stdio = require("@lune/stdio")
local process = require("@lune/process")

local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>

local CommandBuilder = {}

export type CommandBuilder = typeof(setmetatable({} :: CommandBuilderFields, { __index = CommandBuilder }))
type CommandBuilderFields = {
	program: string,
	args: { string },
	stdioStrategy: Option<IoStrategyMapping>,
}
export type StdioStrategy = "pipe" | "forward" | "none"
export type IoStrategyMapping = {
	stdout: Option<StdioStrategy>,
	stderr: Option<StdioStrategy>,
}

-- FIXME: remove unknown usage
local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
	stdout = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
	stderr = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
}
function CommandBuilder.new(program: string)
	return setmetatable(
		{
			program = program,
			args = {},
			stdioStrategy = Option.None :: Option<IoStrategyMapping>,
		} :: CommandBuilderFields,
		{
			__index = CommandBuilder,
		}
	)
end

function CommandBuilder.withArg(self: CommandBuilder, arg: string): CommandBuilder
	table.insert(self.args, arg)
	return self
end

function CommandBuilder.withArgs(self: CommandBuilder, args: { string }): CommandBuilder
	for _, arg in args do
		self:withArg(arg)
	end

	return self
end

function CommandBuilder.withStdioStrategy(
	self: CommandBuilder,
	strategy: StdioStrategy | IoStrategyMapping
): CommandBuilder
	-- FIXME: remove unknown usage
	self.stdioStrategy = Option.Some(if typeof(strategy) == "string"
		then {
			stdout = Option.Some(strategy) :: Option<unknown>,
			stderr = Option.Some(strategy) :: Option<unknown>,
		}
		else strategy) :: Option<IoStrategyMapping>
	return self
end

local function intoSpawnOptionsStdioKind(strategy: StdioStrategy): process.SpawnOptionsStdioKind
	if strategy == "pipe" then
		return "default"
	end

	if strategy == "forward" then
		return "forward"
	end

	if strategy == "none" then
		return "none"
	end

	error(`Non-strategy provided: {strategy}`)
end

function CommandBuilder.exec(self: CommandBuilder): process.SpawnResult
	print("$", self.program, table.concat(self.args, " "))
	local child = process.spawn(self.program, self.args, {
		shell = true,
		stdio = self
			.stdioStrategy
			-- FIXME: remove unknown usage
			:orOpt(Option.Some(DEFAULT_STDIO_STRATEGY) :: Option<unknown>)
			:map(function(mappings: IoStrategyMapping)
				local translatedMappings: process.SpawnOptionsStdio = {}
				for field, value in mappings do
					translatedMappings[field] = intoSpawnOptionsStdioKind((value :: Option<StdioStrategy>):unwrap())
				end

				return translatedMappings
			end)
			:unwrap(),
	})

	if not child.ok then
		print(`\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`, child.code)
	end

	return child
end

return CommandBuilder


--> Run stylua to check for formatting errors

local process = require("@lune/process")

local CommandBuilder = require("./exec")

process.exit(CommandBuilder.new("stylua"):withArg("."):withArgs(process.args):withStdioStrategy("forward"):exec().code)


--> Run tests using frktest runner

local fs = require("@lune/fs")
local process = require("@lune/process")

local frktest = require("@pkg/frktest")
local reporter = require("../tests/_reporter")

-- HACK: Cast require to allow for dynamic paths in strict mode
-- A more proper solution would be to use luau.load instead, but
-- frktest requires its global state to be modified by test suites
local require = require :: (
	path: string
) -> (
	test: typeof(setmetatable(
		{} :: {
			case: (name: string, fn: () -> nil) -> (),
			suite: (name: string, fn: () -> ()) -> (),
		},
		{ __index = frktest.test }
	))
) -> ()

local allowed_tests = process.args
for _, test in fs.readDir("tests") do
	-- If we are given any arguments, we only run those tests, otherwise,
	-- we run all the tests

	-- So, to include only a certain set of test files, you can provide either
	-- the full path to the test file (with or without the extension) or the test
	-- file name
	local withoutExt = string.sub(test, 1, -6)
	local is_allowed = #process.args == 0
		or table.find(allowed_tests, `tests/{test}`)
		or table.find(allowed_tests, withoutExt)
		or table.find(allowed_tests, `tests/{withoutExt}`)

	local constructors = {
		case = frktest.test.case,
		suite = frktest.test.suite,
	}

	if not is_allowed then
		constructors.case = frktest.test.skip.case
		constructors.suite = frktest.test.skip.suite
	end

	-- Ignore files starting with underscores, eg: _reporter.luau
	if string.sub(test, 1, 1) == "_" then
		continue
	end

	require("../tests/" .. test)(setmetatable(constructors, { __index = frktest.test }))
end

reporter.init()
process.exit(tonumber(frktest.run()))


--> Run luau-lsp analysis to check for type errors

local process = require("@lune/process")

local CommandBuilder = require("./exec")

process.exit(
	CommandBuilder.new("luau-lsp")
		:withArg("analyze")
		:withArgs({ "--settings", ".vscode/settings.json" })
		:withArgs({ "--ignore", "'**/.pesde/**'" })
		:withArg(".")
		:withStdioStrategy("forward")
		:exec().code
)


local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>

local Result = require("../luau_packages/result")
type Result<T, E> = Result.Result<T, E>

local function stringStartsWith(str: string, sub: string)
	return string.sub(str, 1, #sub) == sub
end

local function hasLeadingZeros(num: string)
	return string.match(num, "^(0+)") ~= nil
end

local function doLeadingZeroesCheck(type: "major" | "minor" | "patch", component: string)
	-- We only do this check if each component has more than 1 digit as an optimization
	if #component > 1 and hasLeadingZeros(component) then
		return Result.Err({
			msg = Option.Some(`Found disallowed leading zeros in {string.upper(type)} component`),
			kind = {
				id = "LeadingZerosPresent",
				component = type,
			} :: ParseError,
		} :: SemverError)
	end

	return Result.Ok(nil)
end

type InvalidTypes = "char" | "symbol"
local function getInvalidTypeName(invalidComponent: string): InvalidTypes
	local typePatterns: { [InvalidTypes]: string } = {
		char = "%a",
		symbol = "%p",
	}

	local presentType: InvalidTypes

	for type: InvalidTypes, pattern in typePatterns do
		if string.match(invalidComponent, pattern) then
			presentType = type
			break
		end
	end

	return presentType
end

--[=[
	@class Semver

	A Luau library to parse and compare [Semantic Versioning](https://semver.org) compatible version numbers.

	Semantic Versioning (Semver) is a versioning scheme for software that uses a three-part version number format, `MAJOR.MINOR.PATCH`.
	Additionally, Semver defines rules on when to increment each of these parts:

	- `MAJOR` version increments indicate incompatible API changes.
	- `MINOR` version increments indicate the addition of functionality in a backward-compatible manner.
	- `PATCH` version increments indicate backward-compatible bug fixes.

	Semver also allows for optional pre-release and build metadata tags.

	For a detailed set of guidelines that Semver version numbers follow, visit the previously linked website containing the full
	Semver specification.
]=]
local Semver = {}

export type SemverImpl = typeof(setmetatable({} :: Version, Semver))

--[=[
	@type PreleaseType "alpha" | "beta" | "rc"
	@within Semver

	A type representing the different prerelease stages
]=]
export type PreleaseType = "alpha" | "beta" | "rc"

--[=[
	@interface PrereleaseVersion
	@within Semver

	@field type PreleaseType -- The type of prerelease stage
	@field ordinal Option<number> -- Optional ordinal number for the prerelease
]=]
export type PrereleaseVersion = {
	type: PreleaseType,
	ordinal: Option<number>,
}

--[=[
	@interface Version
	@within Semver

	@field major number -- Major version number
	@field minor number -- Minor version number
	@field patch number -- Patch version number
	@field buildMetadata Option<string> -- Optional build metadata
	@field prerelease Option<PrereleaseVersion> -- Optional prerelease information
]=]
export type Version = {
	major: number,
	minor: number,
	patch: number,
	buildMetadata: Option<string>,
	prerelease: Option<PrereleaseVersion>,
}

--[=[
	@type SemverResult<T, E = SemverError> = Result<T, E>
	@within Semver
]=]
export type SemverResult<T, E = SemverError> = Result<T, E>

--[=[
	@interface SemverError
	@within Semver

	@field msg Option<string> -- Optional error message
	@field kind SemverErrorKind -- The kind of error that occurred
]=]
export type SemverError = {
	msg: Option<string>,
	kind: SemverErrorKind,
}

--[=[
	@type SemverErrorKind ParseError | {}
	@within Semver

	All the possible error types that can be returned
]=]
export type SemverErrorKind = ParseError | {}

--[=[
	@type ParseError { id: string, component: string?, expected: string?, got: string?, components: string?, type: string? }
	@within Semver

	All the possible error types that can occur during semver parsing:

	* `MandatoryComponentMissing`: When required version components are missing
	* `InvalidComponentType`: When a version component has an invalid type
	* `LeadingZerosPresent`: When a version component has leading zeros
	* `InvalidIdentifierCharacter`: When prerelease or metadata contains invalid characters
	* `InvalidPrereleaseType`: When prerelease type is not alpha, beta, or rc
	* `InvalidPrereleaseOrdinalType`: When prerelease ordinal is not a number
]=]
export type ParseError =
	| { id: "MandatoryComponentMissing", components: { string } }
	| {
		id: "InvalidComponentType",
		component: "major" | "minor" | "patch",
		expected: "number",
		got: "char" | "symbol" | "unknown",
	}
	| { id: "LeadingZerosPresent", component: "major" | "minor" | "patch" }
	| { id: "InvalidIdentifierCharacter", component: "prerelease" | "metadata" }
	| { id: "InvalidPrereleaseType", component: "prerelease", type: string }
	| { id: "InvalidPrereleaseOrdinalType", expected: "number", got: "char" | "symbol" }

--[=[
	@prop PRERELEASE_LEX_ORDER table
	@within Semver
	@private

	Lexicographical ordering for prerelease types
]=]
local PRERELEASE_LEX_ORDER: { [PreleaseType]: number } = table.freeze({
	alpha = 1,
	beta = 2,
	rc = 3,
})

--[=[
	@within Semver
	@function new

	Creates a new Semver instance from a [Version]

	@param ver Version -- The version to create a Semver instance from
	@return SemverImpl -- The new Semver instance
]=]
function Semver.new(ver: Version): SemverImpl
	return setmetatable(ver, Semver)
end

--[=[
	@within Semver
	@function parse

	Parses a version string into a Semver instance

	@param ver string -- The version string to parse
	@return SemverResult<SemverImpl> -- Result containing either the parsed Semver or an error
]=]
function Semver.parse(ver: string): SemverResult<SemverImpl>
	local components = string.split(if stringStartsWith(ver, "v") then string.sub(ver, 2) else ver, ".")
	if #components < 3 then
		return Result.Err({
			msg = Option.Some(`Expected MAJOR.MINOR.PATCH format, missing {#components} / 3 components`),
			kind = {
				id = "MandatoryComponentMissing",
				components = components,
			} :: ParseError,
		} :: SemverError)
	end

	local patchStr, ext = string.match(components[3], "(%d+)([-+]?.*)")
	if patchStr == nil then
		return Result.Err({
			msg = Option.Some(`Expected patch to be only a number`),
			kind = {
				id = "InvalidComponentType",
				expected = "number",
				got = getInvalidTypeName(components[3]),
				component = "patch",
			} :: ParseError,
		} :: SemverError)
	end

	local major, minor, patch = tonumber(components[1]), tonumber(components[2]), tonumber(patchStr)
	if major == nil or minor == nil or patch == nil then
		local invalidComponentType: "major" | "minor" | "patch", invalidComponent: string

		if major == nil then
			invalidComponentType, invalidComponent = "major", components[1]
		elseif minor == nil then
			invalidComponentType, invalidComponent = "minor", components[2]
		elseif patch == nil then
			invalidComponentType, invalidComponent = "patch", patchStr :: string
		end

		local presentType: InvalidTypes = getInvalidTypeName(invalidComponent)
		return Result.Err({
			msg = Option.Some(
				`Expected {string.upper(invalidComponentType)} to be only a number, but got {presentType}`
			),
			kind = {
				id = "InvalidComponentType",
				expected = "number",
				got = presentType,
				component = invalidComponentType,
			} :: ParseError,
		} :: SemverError)
	end

	-- All components were valid numbers, but we check to see if they had any leading zeros
	-- Leading zeros are invalid in semver
	local majorLeadingZeros = doLeadingZeroesCheck("major", components[1])
	if majorLeadingZeros:isErr() then
		return majorLeadingZeros
	end

	local minorLeadingZeros = doLeadingZeroesCheck("minor", components[2])
	if minorLeadingZeros:isErr() then
		return minorLeadingZeros
	end

	local patchLeadingZeros = doLeadingZeroesCheck("patch", patchStr)
	if patchLeadingZeros:isErr() then
		return patchLeadingZeros
	end

	local parsed: Version = {
		major = major :: number,
		minor = minor :: number,
		patch = patch :: number,
		buildMetadata = Option.None :: Option<string>,
		prerelease = Option.None :: Option<PrereleaseVersion>,
	}

	if ext ~= nil then
		if stringStartsWith(ext, "-") then
			-- Prerelease information
			local prereleaseType = string.sub(ext, 2)
			if prereleaseType ~= "alpha" and prereleaseType ~= "beta" and prereleaseType ~= "rc" then
				return Result.Err({
					msg = Option.Some(`Expected prerelease type to be alpha | beta | rc, but got {prereleaseType}`),
					kind = {
						id = "InvalidPrereleaseType",
						component = "prerelease",
						type = prereleaseType,
					} :: ParseError,
				} :: SemverError)
			end

			local function badPrereleaseType(prerelease: string): SemverResult<SemverImpl>
				local invalidType: InvalidTypes = getInvalidTypeName(prerelease)
				return Result.Err({
					msg = Option.Some(`Expected PRERELEASE to only be a number, but got {invalidType}`),
					kind = {
						id = "InvalidPrereleaseOrdinalType",
						expected = "number",
						got = invalidType,
					} :: ParseError,
				} :: SemverError)
			end

			local prereleaseOrdinal = Option.None :: Option<number>

			-- TODO: Cleanup this recovery logic of recomputing a bit more, maybe remove it entirely
			if components[4] ~= nil then
				local ordinalNum = tonumber(components[4])
				if ordinalNum == nil then
					-- If it wasn't a valid number for the ordinal, that must mean one of two things:
					-- a) The PRERELEASE component was bad
					-- b) The component has build metadata after prerelease info
					-- Here, we handle both those cases

					local potentialOrdinalNumber, potentialBuildMetadata = string.match(components[4], "(.*)+(.*)")
					if potentialOrdinalNumber == nil then
						return badPrereleaseType(components[4])
					end

					if potentialBuildMetadata ~= nil then
						local fullBuildMetadata = potentialBuildMetadata
						for i = 5, #components do
							fullBuildMetadata ..= "." .. components[i]
						end

						parsed.buildMetadata = Option.Some(fullBuildMetadata :: string) :: Option<string>
					end

					if potentialOrdinalNumber ~= nil then
						ordinalNum = tonumber(potentialOrdinalNumber)
						if ordinalNum == nil then
							return badPrereleaseType(components[4])
						end
					end
				elseif #components > 4 then
					-- The ordinal component was bad, we should error
					local badPrerelease = ""
					for i = 4, #components do
						badPrerelease ..= "." .. components[i]
					end

					return badPrereleaseType(badPrerelease)
				end

				prereleaseOrdinal = Option.Some(ordinalNum :: number) :: Option<number>
			end

			parsed.prerelease = Option.Some({
				type = prereleaseType,
				ordinal = prereleaseOrdinal,
			} :: PrereleaseVersion) :: Option<PrereleaseVersion>
		end

		if stringStartsWith(ext, "+") then
			-- Build metadata information; we remove the leading `+` symbol and add
			-- the rest of the components to the build metadata
			local fullBuildMetadata = string.sub(ext, 2)
			for i = 4, #components do
				fullBuildMetadata ..= "." .. components[i]
			end

			parsed.buildMetadata = Option.Some(fullBuildMetadata) :: Option<string>
		end
	end

	return Result.Ok(Semver.new(parsed))
end

local function prereleaseEq(leftPrerelease: PrereleaseVersion?, rightPrerelease: PrereleaseVersion?): boolean
	if leftPrerelease == nil and rightPrerelease == nil then
		return true
	end

	if leftPrerelease == nil or rightPrerelease == nil then
		return false
	end

	return leftPrerelease.type == rightPrerelease.type and leftPrerelease.ordinal == rightPrerelease.ordinal
end

local function prereleaseLt(leftPrerelease: PrereleaseVersion?, rightPrerelase: PrereleaseVersion?): boolean
	if not prereleaseEq(leftPrerelease, rightPrerelase) then
		if leftPrerelease ~= nil and rightPrerelase ~= nil then
			if leftPrerelease.type == rightPrerelase.type then
				return leftPrerelease.ordinal:unwrapOr(0) < rightPrerelase.ordinal:unwrapOr(0)
			end

			return PRERELEASE_LEX_ORDER[leftPrerelease.type] < PRERELEASE_LEX_ORDER[rightPrerelase.type]
		end

		return not (leftPrerelease == nil and rightPrerelase ~= nil)
			and (leftPrerelease ~= nil and rightPrerelase == nil)
	end

	return false
end

local function prereleaseLe(leftPrerelease: PrereleaseVersion?, rightPrerelase: PrereleaseVersion?): boolean
	return prereleaseLt(leftPrerelease, rightPrerelase) or prereleaseEq(leftPrerelease, rightPrerelase)
end

function Semver.__eq(left: SemverImpl, right: SemverImpl): boolean
	return left.major == right.major
		and left.minor == right.minor
		and left.patch == right.patch
		and prereleaseEq(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
end

function Semver.__lt(left: SemverImpl, right: SemverImpl): boolean
	return if left.major ~= right.major
		then left.major < right.major
		elseif left.minor ~= right.minor then left.minor < right.minor
		elseif left.patch ~= right.patch then left.patch < right.patch
		else prereleaseLt(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
end

function Semver.__le(left: SemverImpl, right: SemverImpl): boolean
	return if left.major ~= right.major
		then left.major <= right.major
		elseif left.minor ~= right.minor then left.minor <= right.minor
		elseif left.patch ~= right.patch then left.patch <= right.patch
		else prereleaseLe(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
end

function Semver.__tostring(self: SemverImpl): string
	local base = `{self.major}.{self.minor}.{self.patch}`
	local prerelease = self.prerelease
		:map(function(prerelease: PrereleaseVersion)
			return `-{prerelease.type}{prerelease.ordinal
				:map(function(ord: number)
					return `.{ord}`
				end)
				:unwrapOr("")}`
		end)
		:unwrapOr("")
	local build = self.buildMetadata
		:map(function(buildMetadata: string)
			return `+{buildMetadata}`
		end)
		:unwrapOr("")

	return base .. prerelease .. build
end

return Semver


local frktest = require("@pkg/frktest")
local check = frktest.assert.check

local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>

local Semver = require("../lib")

return function(test: typeof(frktest.test))
	test.suite("Semver comparison tests", function()
		test.case("Basic version comparisons", function()
			local v1 = Semver.parse("1.2.3"):unwrap()
			local v2 = Semver.parse("1.2.4"):unwrap()
			local v3 = Semver.parse("1.3.0"):unwrap()
			local v4 = Semver.parse("2.0.0"):unwrap()
			local v5 = Semver.parse("2.1.0"):unwrap()
			local v6 = Semver.parse("3.0.0"):unwrap()

			check.is_true(v1 < v2)
			check.is_true(v2 < v3)
			check.is_true(v3 < v4)
			check.is_true(v4 < v5)
			check.is_true(v5 < v6)
			check.is_true(v1 <= v2)
			check.is_true(v2 > v1)
			check.is_true(v4 >= v3)
			check.is_true(v6 > v1)
		end)

		test.case("Equal version comparisons", function()
			local v1 = Semver.parse("1.2.3"):unwrap()
			local v2 = Semver.parse("1.2.3"):unwrap()
			local v3 = Semver.parse("1.2.3"):unwrap()
			local v4 = Semver.parse("1.2.3"):unwrap()

			check.is_true(v1 == v2)
			check.is_true(v2 == v3)
			check.is_true(v3 == v4)
			check.is_true(v1 <= v2)
			check.is_true(v1 >= v2)
			check.is_false(v1 < v2)
			check.is_false(v1 > v2)
			check.is_false(v3 > v4)
		end)

		test.case("Prerelease version comparisons", function()
			local v1 = Semver.parse("1.2.3-alpha.1"):unwrap()
			local v2 = Semver.parse("1.2.3-alpha.2"):unwrap()
			local v3 = Semver.parse("1.2.3-beta.1"):unwrap()
			local v4 = Semver.parse("1.2.3"):unwrap()
			local v5 = Semver.parse("1.2.3-rc.1"):unwrap()
			local v6 = Semver.parse("1.2.3-rc.2"):unwrap()

			check.is_true(v1 < v2)
			check.is_true(v2 < v3)
			check.is_true(v3 < v4)
			check.is_true(v3 < v5)
			check.is_true(v5 < v6)
			check.is_true(v6 < v4)
			check.is_false(v4 < v1)
			check.is_true(v4 > v3)
		end)

		test.case("Build metadata comparisons", function()
			local v1 = Semver.parse("1.2.3+build.1"):unwrap()
			local v2 = Semver.parse("1.2.3+build.2"):unwrap()
			local v3 = Semver.parse("1.2.3+build.123"):unwrap()
			local v4 = Semver.parse("1.2.3+20230615"):unwrap()
			local v5 = Semver.parse("1.2.3+exp.sha.5114f85"):unwrap()

			-- Build metadata should be ignored in comparisons
			check.is_true(v1 == v2)
			check.is_true(v2 == v3)
			check.is_true(v3 == v4)
			check.is_true(v4 == v5)
			check.is_false(v1 < v2)
			check.is_false(v2 > v3)
			check.is_false(v4 > v5)
		end)

		test.case("Complex version comparisons", function()
			local v1 = Semver.parse("2.0.0-alpha.1+build.123"):unwrap()
			local v2 = Semver.parse("2.0.0-beta.1+build.123"):unwrap()
			local v3 = Semver.parse("2.0.0+build.123"):unwrap()
			local v4 = Semver.parse("2.1.0-alpha.1"):unwrap()
			local v5 = Semver.parse("2.1.0"):unwrap()
			local v6 = Semver.parse("2.1.0-rc.1+build.999"):unwrap()
			local v7 = Semver.parse("2.1.1-alpha.1+sha.xyz"):unwrap()

			check.is_true(v1 < v2)
			check.is_true(v2 < v3)
			check.is_true(v3 < v4)
			check.is_true(v4 < v5)
			check.is_true(v4 < v6)
			check.is_true(v6 < v5)
			check.is_true(v5 < v7)
			check.is_false(v5 < v1)
			check.is_false(v7 < v6)
		end)
	end)
end


local frktest = require("@pkg/frktest")
local check = frktest.assert.check

local Semver = require("../lib")

return function(test: typeof(frktest.test))
	test.suite("Invalid semver parsing tests", function()
		test.case("Rejects missing components", function()
			local res = Semver.parse("1.2")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "MandatoryComponentMissing",
					components = { "1", "2" },
				},
			})
		end)

		test.case("Rejects invalid component types", function()
			-- Test invalid major
			local res = Semver.parse("a.2.3")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidComponentType",
					component = "major",
					got = "char",
				},
			})

			-- Test invalid minor with symbols
			res = Semver.parse("1.$.3")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidComponentType",
					component = "minor",
					got = "symbol",
				},
			})

			-- Test invalid patch
			res = Semver.parse("1.2.x")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidComponentType",
					component = "patch",
					got = "char",
				},
			})
		end)

		test.case("Rejects leading zeros", function()
			-- Test leading zeros in major
			local res = Semver.parse("01.2.3")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "LeadingZerosPresent",
					component = "major",
				},
			})

			-- Test leading zeros in minor
			res = Semver.parse("1.02.3")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "LeadingZerosPresent",
					component = "minor",
				},
			})

			-- Test leading zeros in patch
			res = Semver.parse("1.2.03")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "LeadingZerosPresent",
					component = "patch",
				},
			})
		end)

		test.case("Rejects invalid prerelease types", function()
			local res = Semver.parse("1.2.3-gamma.1")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidPrereleaseType",
					type = "gamma",
				},
			})
		end)

		test.case("Rejects invalid prerelease ordinals", function()
			-- Test with character ordinal
			local res = Semver.parse("1.2.3-beta.abc")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidPrereleaseOrdinalType",
					expected = "number",
					got = "char",
				},
			})

			-- Test with symbol ordinal
			res = Semver.parse("1.2.3-beta.$")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidPrereleaseOrdinalType",
					expected = "number",
					got = "symbol",
				},
			})

			-- Test with extra symbols in ordinal
			res = Semver.parse("1.2.3-beta.3.4.5")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidPrereleaseOrdinalType",
					expected = "number",
					got = "symbol",
				},
			})

			-- Test with extra symbols in ordinal and build metadata
			res = Semver.parse("1.2.3-beta.3.4.5+build.1732213169")
			check.is_true(res:isErr())
			check.table.contains(res:unwrapErr(), {
				kind = {
					id = "InvalidPrereleaseOrdinalType",
					expected = "number",
					got = "symbol",
				},
			})
		end)
	end)
end


local frktest = require("@pkg/frktest")
local check = frktest.assert.check

local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>

local Semver = require("../lib")

return function(test: typeof(frktest.test))
	test.suite("Basic tests", function()
		test.case("Semver creates valid version objects", function()
			local res = Semver.parse("1.2.3-beta.1")
			check.is_true(res:isOk())

			local version = res:unwrap()
			check.equal(version.major, 1)
			check.equal(version.minor, 2)
			check.equal(version.patch, 3)
		end)

		test.case("Semver handles prerelease versions", function()
			local res = Semver.parse("1.2.3-beta.1")
			check.is_true(res:isOk())

			local version = res:unwrap()
			check.equal(version.major, 1)
			check.equal(version.minor, 2)
			check.equal(version.patch, 3)

			check.table.equal(
				version.prerelease,
				Option.Some({
					type = "beta",
					ordinal = Option.Some(1),
				})
			)
		end)

		test.case("Semver handles build metadata", function()
			local res = Semver.parse("1.2.3+build.123")
			check.is_true(res:isOk())

			local version = res:unwrap()

			check.equal(version.major, 1)
			check.equal(version.minor, 2)
			check.equal(version.patch, 3)
			check.table.equal(version.buildMetadata, Option.Some("build.123"))
		end)

		test.case("Semver handles prerelease versions with build metadata", function()
			local res = Semver.parse("1.2.3-beta.1+build.123")
			check.is_true(res:isOk())

			local version = res:unwrap()
			check.equal(version.major, 1)
			check.equal(version.minor, 2)
			check.equal(version.patch, 3)

			check.table.equal(
				version.prerelease,
				Option.Some({
					type = "beta",
					ordinal = Option.Some(1),
				})
			)

			check.table.equal(version.buildMetadata, Option.Some("build.123"))
		end)
	end)
end


local frktest = require("@pkg/frktest")
local check = frktest.assert.check

local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>

local Semver = require("../lib")

return function(test: typeof(frktest.test))
	test.suite("Stringification tests", function()
		test.case("A version constructed with new() when stringified should match expected string", function()
			local versionMap: { [string]: Semver.Version } = {
				["1.2.3"] = { major = 1, minor = 2, patch = 3, prerelease = Option.None, buildMetadata = Option.None },
				["1.0.0-alpha"] = {
					major = 1,
					minor = 0,
					patch = 0,
					prerelease = Option.Some({ type = "alpha" :: Semver.PreleaseType, ordinal = Option.None }),
					buildMetadata = Option.None,
				},
				["2.3.4-beta.1"] = {
					major = 2,
					minor = 3,
					patch = 4,
					prerelease = Option.Some({
						type = "beta" :: Semver.PreleaseType,
						ordinal = Option.Some(1),
					}),
					buildMetadata = Option.None,
				},
				["3.0.0-rc.1+build.123"] = {
					major = 3,
					minor = 0,
					patch = 0,
					prerelease = Option.Some({
						type = "rc" :: Semver.PreleaseType,
						ordinal = Option.Some(1),
					}),
					buildMetadata = Option.Some("build.123"),
				},
				["4.5.6+sha.xyz"] = {
					major = 4,
					minor = 5,
					patch = 6,
					prerelease = Option.None,
					buildMetadata = Option.Some("sha.xyz"),
				},
			}

			-- FIXME: unknown usage here is because these types are too complex for
			-- Luau to typecheck properly, we cast it manually since the above
			-- map is typed properly anyway
			for expectedString, version: unknown in versionMap do
				local constructed = Semver.new(version :: Semver.Version)
				local stringified = tostring(constructed)

				check.equal(stringified, expectedString)
			end
		end)

		test.case("A parsed version when stringified should not change in roundtrip", function()
			local versions = {
				"1.2.3",
				"1.0.0-alpha",
				"2.3.4-beta.1",
				"3.0.0-rc.1+build.123",
				"4.5.6+sha.xyz",
				"5.0.0-alpha.1+build.999",
				"6.7.8-beta.2+exp.sha.5114f85",
				"7.0.0-alpha.1",
				"9.9.9+20230615",
			}

			for _, version in versions do
				local parsed = Semver.parse(version):unwrap()
				local stringified = tostring(parsed)

				check.equal(stringified, version)
			end
		end)
	end)
end


local luau_spec = require("../../luau/spec")

local boolean_constant = {}
boolean_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_BOOLEAN
boolean_constant.__index = boolean_constant

function boolean_constant.new(value: boolean)
    return setmetatable({ value = value }, boolean_constant)
end

function boolean_constant:__tostring()
    return tostring(self.value)
end

export type Class = typeof(boolean_constant.new(nil :: never))

return boolean_constant


local luau_spec = require("../../luau/spec")

local closure_constant = {}
closure_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_CLOSURE
closure_constant.__index = closure_constant

function closure_constant.new(value: number)
    return setmetatable({ value = value }, closure_constant)
end

function closure_constant:__tostring()
    return `closure({self.value})`
end

export type Class = typeof(closure_constant.new(nil :: never))

return closure_constant


local luau_spec = require("../../luau/spec")
local string_constant_class = require("string_constant")

local import_constant = {}
import_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_IMPORT
import_constant.__index = import_constant

function import_constant.new(value: { string_constant_class.Class })
    return setmetatable({ value = value }, import_constant)
end

function import_constant:__tostring()
    local result: { string } = {}

    for _, constant in self.value do
        table.insert(result, tostring(constant.value))
    end

    return table.concat(result, ".")
end

export type Class = typeof(import_constant.new(nil :: never))

return import_constant


local luau_spec = require("../../luau/spec")

local nil_constant = {}
nil_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_NIL
nil_constant.__index = nil_constant

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

function nil_constant:__tostring()
    return "nil"
end

export type Class = typeof(nil_constant.new())

return nil_constant


local luau_spec = require("../../luau/spec")

local number_constant = {}
number_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_NUMBER
number_constant.__index = number_constant

function number_constant.new(value: number)
    return setmetatable({ value = value }, number_constant)
end

function number_constant:__tostring()
    return tostring(self.value)
end

export type Class = typeof(number_constant.new(nil :: never))

return number_constant


local luau_spec = require("../../luau/spec")

local string_constant = {}
string_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_STRING
string_constant.__index = string_constant

function string_constant.new(value: string)
    return setmetatable({ value = value }, string_constant)
end

function string_constant:__tostring()
    return `"{self.value}"`
end

export type Class = typeof(string_constant.new(nil :: never))

return string_constant


local luau_spec = require("../../luau/spec")

local table_constant = {}
table_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_TABLE
table_constant.__index = table_constant

function table_constant.new(value: { any })
    return setmetatable({ value = value }, table_constant)
end

function table_constant:__tostring()
    return `\{ {table.concat(self.value, ", ")} \}`
end

export type Class = typeof(table_constant.new(nil :: never))

return table_constant


local luau_spec = require("../../luau/spec")

local vector_constant = {}
vector_constant.__constant_type = luau_spec.LuauBytecodeConstants.LBC_CONSTANT_VECTOR
vector_constant.__index = vector_constant

function vector_constant.new(x: number, y: number, z: number, w: number)
    return setmetatable({ x= x, y = y, z = z, w = w }, vector_constant)
end

function vector_constant:__tostring()
    return `Vector({self.x}, {self.y}, {self.z}, {self.w})`
end

export type Class = typeof(vector_constant.new(nil :: never))

return vector_constant


export type DebugLocal = {
    name: string,     -- var_int (string_table)
    start_pc: number, -- var_int
    end_pc: number,   -- var_int,
    reg: number,      -- byte
}

export type DebugUpval = {
    name: string, -- var_int (string_table)
}

export type DebugLocalsTable = {
    debug_locals_size: number,
    debug_locals: { DebugLocal }
}

export type DebugUpvalsTable = {
    debug_upvals_size: number,
    debug_upvals: { DebugUpval }
}

local binary_reader_class = require("../utilities/binary_reader")
local string_table_class = require("string_table")

local debug_info = {}
debug_info.__index = debug_info

function debug_info.new(binary_reader: binary_reader_class.Class, string_table: string_table_class.Class)
    local self = setmetatable({}, debug_info)

    local debug_locals_size: number = binary_reader:read_var_int()

    local debug_locals: { DebugLocal } = table.create(debug_locals_size)
    for debug_local_index: number = 1, debug_locals_size do
        debug_locals[debug_local_index] = {
            name = string_table.strings[binary_reader:read_var_int()],
            start_pc = binary_reader:read_var_int(),
            end_pc = binary_reader:read_var_int(),
            reg = binary_reader:read_u8()
        }
    end

    local debug_upvals_size: number = binary_reader:read_var_int()

    local debug_upvals: { DebugUpval } = table.create(debug_upvals_size)
    for debug_upval_index: number = 1, debug_upvals_size do
        debug_upvals[debug_upval_index] = {
            name = string_table.strings[binary_reader:read_var_int()],
        }
    end

    self.debug_locals_table = {
        debug_locals_size = debug_upvals_size,
        debug_locals = debug_locals
    }

    self.debug_upvals_table = {
        debug_upvals_size = debug_upvals_size,
        debug_upvals = debug_upvals
    }

    return self
end

export type Class = typeof(debug_info.new(nil :: never))

return debug_info


export type Registers = {
    A: number,
    B: number,
    C: number,
    D: number,
    E: number,
}

local luau_spec = require("../luau/spec")
local binary_reader_class = require("../utilities/binary_reader")

local instruction = {}
instruction.__index = instruction

local function arshift(x: number, disp: number): number
    local result = bit32.rshift(x, disp)
    return if (result >= 2 ^ 15) then result - 2 ^ 16 else result
end

function instruction.new(binary_reader: binary_reader_class.Class, value: number, current_aux_offset: number)
    local self = setmetatable({}, instruction)
    self.aux_offset = current_aux_offset

    local opcode = bit32.band(value, 0xFF)
    assert(opcode > 0 and opcode < luau_spec.LuauOpcode.LOP__COUNT.index, `Invalid opcode {opcode}`)

    for _, opcode_information in luau_spec.LuauOpcode do
        if (opcode_information.index == opcode) then
            self.opcode_information = opcode_information
        end
    end

    if (self.opcode_information.has_aux) then self.aux = binary_reader:read_u32() end

    self.registers = {
        A = bit32.band(bit32.arshift(value, 8), 0xFF),
        B = bit32.band(bit32.arshift(value, 16), 0xFF),
        C = bit32.band(bit32.arshift(value, 24), 0xFF),
        D = arshift(value, 16),
        E = arshift(value, 8),
    }

    return self
end

export type Class = typeof(instruction.new(nil :: never)) & {line: number?}

return instruction


local binary_reader_class = require("../utilities/binary_reader")
local instruction_class = require("../classes/instruction")

export type FunctionInstructionsTable = {
    instructions_size: number,
    instructions: { instruction_class.Class }
}

local line_info = {}
line_info.__index = line_info

function line_info.new(binary_reader: binary_reader_class.Class, instructions_table: FunctionInstructionsTable)
    local self = setmetatable({}, line_info)

    self.log_span = binary_reader:read_u8()
    self.line_deltas = {}
    self.line_baselines = {}

    for i = 1, instructions_table.instructions_size do
        local delta = if (self.line_deltas[i - 1]) then
            self.line_deltas[i - 1] + binary_reader:read_u8()
        else
            binary_reader:read_u8()

        self.line_deltas[i] = delta
    end

    local baseline_count = bit32.rshift(instructions_table.instructions_size - 1, self.log_span) + 1
    for i = 1, baseline_count do
        local baseline = if (self.line_baselines[i - 1]) then
            self.line_baselines[i - 1] + binary_reader:read_i32()
        else
            binary_reader:read_i32()

        self.line_baselines[i] = baseline
    end

    return self
end

function line_info:get_line(instruction_index: number): number
    local chunk = bit32.rshift(instruction_index - 1, self.log_span) + 1

    local baseline = self.line_baselines[chunk]
    local delta = self.line_deltas[instruction_index]

    return baseline + delta
end

export type Class = typeof(line_info.new(nil :: never))

return line_info


local binary_reader_class = require("../utilities/binary_reader")

local string_table = {}
string_table.__index = string_table

function string_table.new(binary_reader: binary_reader_class.Class)
    local self = setmetatable({}, string_table)

    self.string_count = binary_reader:read_var_int()
    self.strings = table.create(self.string_count)

    for index: number = 1, self.string_count do
        self.strings[index] = binary_reader:read_string()
    end

    return self
end

export type Class = typeof(string_table.new(nil :: never))

return string_table