--!strict
-- Tests for Phase 3.3 Collections

local TestFramework = require("../framework")
local base = require("../../src/PyLua/objects/base")
local builtins = require("../../src/PyLua/objects/builtins")
local collections = require("../../src/PyLua/objects/collections")
local PyLua = require("../../src/PyLua")

local suite: TestFramework.TestSuite = {
	name = "PyLua Collections",
	tests = {
		{
			name = "Create and append list",
			test = function()
				local lst = collections.list({ builtins.Int(1), builtins.Int(2) })
				TestFramework.assertEqual(lst.__type, "list", "Type list")
				local appendMethod = base.getattr(lst, "append")
				base.call(appendMethod, { builtins.Int(3) })
				TestFramework.assertEqual(#lst.__value, 3, "List length 3")
			end,
		},
		{
			name = "List pop and concat",
			test = function()
				local a = collections.list({ builtins.Int(1), builtins.Int(2) })
				local b = collections.list({ builtins.Int(3) })
				local add = base.getattr(a, "__add__")
				local combined = base.call(add, { b })
				TestFramework.assertEqual(#combined.__value, 3, "Concat length 3")
				local popMethod = base.getattr(combined, "pop")
				local last = base.call(popMethod, {})
				TestFramework.assertEqual(last.__value, 3, "Popped 3")
				TestFramework.assertEqual(#combined.__value, 2, "Length now 2")
			end,
		},
		{
			name = "List getitem setitem contains",
			test = function()
				local lst = collections.list({ builtins.Int(1), builtins.Int(2), builtins.Int(3) })
				local getItem = base.getattr(lst, "__getitem__")
				local first = base.call(getItem, { builtins.Int(0) })
				TestFramework.assertEqual(first.__value, 1, "Index 0 should be first element")
				local neg = base.call(getItem, { builtins.Int(-1) })
				TestFramework.assertEqual(neg.__value, 3, "Negative index should wrap")
				local setItem = base.getattr(lst, "__setitem__")
				base.call(setItem, { builtins.Int(1), builtins.Int(42) })
				local updated = base.call(getItem, { builtins.Int(1) })
				TestFramework.assertEqual(updated.__value, 42, "Assignment should update value")
				local contains = base.getattr(lst, "__contains__")
				local hasVal = base.call(contains, { builtins.Int(42) })
				TestFramework.assertTrue(hasVal.__value, "List should report containment")
				local missing = base.call(contains, { builtins.Int(99) })
				TestFramework.assertFalse(missing.__value, "Missing value should be false")
			end,
		},
		{
			name = "List matrix multiplication",
			test = function()
				local left = collections.list({
					collections.list({ builtins.Int(1), builtins.Int(2) }),
					collections.list({ builtins.Int(3), builtins.Int(4) }),
				})
				local right = collections.list({
					collections.list({ builtins.Int(5), builtins.Int(6) }),
					collections.list({ builtins.Int(7), builtins.Int(8) }),
				})
				local matmul = base.getattr(left, "__matmul__")
				local result = base.call(matmul, { right })
				TestFramework.assertEqual(result.__type, "list", "Result should be list")
				TestFramework.assertEqual(#result.__value, 2, "Matrix should have two rows")
				TestFramework.assertEqual(result.__value[1].__value[1].__value, 19, "[0][0] should equal 19")
				TestFramework.assertEqual(result.__value[2].__value[2].__value, 50, "[1][1] should equal 50")
			end,
		},
		{
			name = "Dict item methods",
			test = function()
				local d = collections.dict(nil)
				local setItem = base.getattr(d, "__setitem__")
				local setArgsA: { base.PyObject } = {} :: any
				setArgsA[1] = builtins.Str("a")
				setArgsA[2] = builtins.Int(10)
				base.call(setItem, setArgsA)
				local setArgsB: { base.PyObject } = {} :: any
				setArgsB[1] = builtins.Str("b")
				setArgsB[2] = builtins.Int(20)
				base.call(setItem, setArgsB)
				local getItem = base.getattr(d, "__getitem__")
				local getArgs: { base.PyObject } = {} :: any
				getArgs[1] = builtins.Str("a")
				local aVal = base.call(getItem, getArgs)
				TestFramework.assertEqual(aVal.__value, 10, "__getitem__ should fetch value")
				local contains = base.getattr(d, "__contains__")
				local containsArgs: { base.PyObject } = {} :: any
				containsArgs[1] = builtins.Str("a")
				local hasA = base.call(contains, containsArgs)
				TestFramework.assertTrue(hasA.__value, "dict should contain key 'a'")
				local keys = base.call(base.getattr(d, "keys"), {})
				TestFramework.assertEqual(keys.__type, "list", "keys() should return list")
				TestFramework.assertEqual(#keys.__value, 2, "keys() should include two entries")
				local values = base.call(base.getattr(d, "values"), {})
				TestFramework.assertEqual(values.__value[1].__type, "int", "values() elements should be PyObjects")
				local items = base.call(base.getattr(d, "items"), {})
				TestFramework.assertEqual(items.__type, "list", "items() should return list of tuples")
				TestFramework.assertEqual(items.__value[1].__type, "tuple", "items should contain tuples")
			end,
		},
		{
			name = "Tuple creation and length",
			test = function()
				local t = collections.tuple({ builtins.Int(1), builtins.Int(2), builtins.Int(3) })
				local lenMethod = base.getattr(t, "__len__")
				local ln = base.call(lenMethod, {})
				TestFramework.assertEqual(ln.__value, 3, "Tuple len 3")
			end,
		},
		{
			name = "Dict set and get",
			test = function()
				local d = collections.dict(nil)
				local setM = base.getattr(d, "set")
				local setArgs: { base.PyObject } = {} :: any
				setArgs[1] = builtins.Str("a")
				setArgs[2] = builtins.Int(10)
				base.call(setM, setArgs)
				local getM = base.getattr(d, "get")
				local getArgs: { base.PyObject } = {} :: any
				getArgs[1] = builtins.Str("a")
				local value = base.call(getM, getArgs)
				TestFramework.assertEqual(value.__value, 10, "dict['a']=10")
			end,
		},
		{
			name = "Set add and length",
			test = function()
				local s = collections.set({ builtins.Int(1) })
				local addM = base.getattr(s, "add")
				base.call(addM, { builtins.Int(2) })
				base.call(addM, { builtins.Int(2) }) -- duplicate
				local lenM = base.getattr(s, "__len__")
				local ln = base.call(lenM, {})
				TestFramework.assertEqual(ln.__value, 2, "Set size 2")
			end,
		},
		{
			name = "List iteration",
			test = function()
				local lst = collections.list({ builtins.Int(1), builtins.Int(2), builtins.Int(3) })
				local iterM = base.getattr(lst, "__iter__")
				local iter = base.call(iterM, {})
				local nextM = base.getattr(iter, "__next__")
				local sum = 0
				for _ = 1, 3 do
					local v = base.call(nextM, {})
					sum += v.__value
				end
				TestFramework.assertEqual(sum, 6, "Sum 1+2+3=6")
				TestFramework.assertError(function(): any
					return base.call(nextM, {})
				end, "Expect StopIteration")
			end,
		},
		{
			name = "Dict get default",
			test = function()
				local d = collections.dict(nil)
				local getM = base.getattr(d, "get")
				local missArgs: { base.PyObject } = {} :: any
				missArgs[1] = builtins.Str("missing")
				missArgs[2] = builtins.Int(99)
				local defaultValue = base.call(getM, missArgs)
				TestFramework.assertEqual(defaultValue.__value, 99, "Default returned")
			end,
		},
		{
			name = "Collection truthiness",
			test = function()
				local emptyList = collections.list(nil)
				TestFramework.assertFalse(base.truthy(emptyList), "Empty list falsy")
				local nonEmptyList = collections.list({ builtins.Int(1) })
				TestFramework.assertTrue(base.truthy(nonEmptyList), "Non-empty list truthy")
				local emptyDict = collections.dict(nil)
				TestFramework.assertFalse(base.truthy(emptyDict), "Empty dict falsy")
				local d = collections.dict(nil)
				local setM = base.getattr(d, "set")
				local args: { base.PyObject } = {} :: any
				args[1] = builtins.Str("k")
				args[2] = builtins.Int(1)
				base.call(setM, args)
				TestFramework.assertTrue(base.truthy(d), "Non-empty dict truthy")
			end,
		},
		{
			name = "Empty dict unpacking",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {}
d2 = {**d1}
result = d2
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")
				-- Check it's empty
				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 0, "result should be empty")
			end,
		},

		{
			name = "Single dict unpacking",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {'a': 1, 'b': 2}
d2 = {**d1}
result = d2
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")

				-- Verify values are copied
				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 2, "result should have 2 entries")
			end,
		},

		{
			name = "Multiple dict unpacking",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {'a': 1, 'b': 2}
d2 = {'c': 3, 'd': 4}
d3 = {**d1, **d2}
result = d3
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")

				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 4, "result should have 4 entries")
			end,
		},

		{
			name = "Dict unpacking with additional keys",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {'a': 1, 'b': 2}
d2 = {'x': 10, **d1, 'y': 20}
result = d2
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")

				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 4, "result should have 4 entries (x, a, b, y)")
			end,
		},

		{
			name = "Dict unpacking with overwrite",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {'a': 1, 'b': 2}
d2 = {'a': 10, 'c': 3}
d3 = {**d1, **d2}
result = d3
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")

				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 3, "result should have 3 entries (a, b, c)")

				-- Get the value for key 'a'
				local dictType = base.getTypeObject("dict")
				local methods = dictType.methods
				if not methods then
					error("Dict type has no methods")
				end
				local getMethod = methods["get"]
				if not getMethod then
					error("Dict type has no get method")
				end
				local keyA = base.newPyObject("str", "a")
				local valueA = getMethod(result, { keyA })
				TestFramework.assert(valueA ~= nil, "value for 'a' should exist")
				TestFramework.assertEqual(valueA.__value, 10, "value for 'a' should be 10 (overwritten)")
			end,
		},

		{
			name = "Dict unpacking mixed with regular keys",
			test = function()
				local python = PyLua.new()
				python:execute([[
d1 = {'a': 1}
d2 = {'b': 2}
d3 = {'x': 10, **d1, 'y': 20, **d2, 'z': 30}
result = d3
]])
				local result = python:getGlobalPy("result")
				TestFramework.assert(result ~= nil, "result should exist")
				TestFramework.assert(result.__type == "dict", "result should be a dict")

				local storage = result.__value
				local count = 0
				for _ in pairs(storage) do
					count = count + 1
				end
				TestFramework.assertEqual(count, 5, "result should have 5 entries")
			end,
		},
	},
}

TestFramework.runSuite(suite)

return {}


--!nocheck
-- PyLua Parser Tests - Phase 2.1 Expression Parser
-- Test the expression parsing functionality

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")

-- Test suite for PyLua Parser
local parserTests: TestFramework.TestSuite = {
	name = "PyLua Parser Tests",
	tests = {
		{
			name = "Parse Number Literal",
			test = function()
				local tokens = Lexer.tokenize("42")
				local ast = Parser.parse(tokens)

				TestFramework.assertEqual(ast.type, "Module", "Should parse as Module")
				TestFramework.assertEqual(#ast.body, 1, "Should have one statement")
				TestFramework.assertEqual(ast.body[1].type, "Expr", "Should be expression statement")

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, 42, "Value should be 42")
			end,
		},

		{
			name = "Parse Float Literal",
			test = function()
				local tokens = Lexer.tokenize("3.14")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, 3.14, "Value should be 3.14")
			end,
		},

		{
			name = "Parse String Literal",
			test = function()
				local tokens = Lexer.tokenize('"hello"')
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, "hello", "Value should be 'hello'")
			end,
		},

		{
			name = "Parse u-prefixed string normalizes to str",
			test = function()
				local tokens = Lexer.tokenize("u'hello'")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, "hello", "Value should be 'hello'")
				TestFramework.assertEqual(expr.kind, nil, "u prefix should be normalized away")
			end,
		},

		{
			name = "Parse b-prefixed string marks bytes kind",
			test = function()
				local tokens = Lexer.tokenize("b'AB'")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, "AB", "Value should be 'AB'")
				TestFramework.assertEqual(expr.kind, "bytes", "Kind should be bytes")
			end,
		},

		{
			name = "Parse string literal subscript",
			test = function()
				local tokens = Lexer.tokenize('"hi"[0]')
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Subscript", "Should parse subscript on literal")
				TestFramework.assertEqual(expr.value.type, "Constant", "Base should be constant string")
				TestFramework.assertEqual(expr.value.value, "hi", "String literal should remain 'hi'")
				TestFramework.assertEqual(expr.slice.type, "Constant", "Index should be constant")
				TestFramework.assertEqual(expr.slice.value, 0, "Index value should be 0")
			end,
		},

		{
			name = "Parse Simple F-string",
			test = function()
				local tokens = Lexer.tokenize('f"Hello {name}!"')
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "F-string should become concatenation chain")
				TestFramework.assertEqual(expr.op, "Add", "Top-level op should be Add")
				TestFramework.assertEqual(expr.left.type, "BinOp", "Left branch should combine literal and expression")
				TestFramework.assertEqual(expr.left.op, "Add", "Left branch should also be Add")
				TestFramework.assertEqual(expr.left.left.type, "Constant", "First literal segment should be Constant")
				TestFramework.assertEqual(expr.left.left.value, "Hello ", "Literal should include trailing space")
				TestFramework.assertEqual(expr.left.right.type, "Call", "Expression segment should be wrapped in str()")
				TestFramework.assertEqual(expr.left.right.func.id, "str", "Conversion should use str()")
				TestFramework.assertEqual(expr.left.right.args[1].type, "Name", "Argument should be Name")
				TestFramework.assertEqual(expr.left.right.args[1].id, "name", "Name should be 'name'")
				TestFramework.assertEqual(expr.right.type, "Constant", "Trailing literal should be Constant")
				TestFramework.assertEqual(expr.right.value, "!", "Trailing literal should preserve punctuation")
			end,
		},

		{
			name = "Parse F-string With Format Spec",
			test = function()
				local tokens = Lexer.tokenize('f"{value:.2f}"')
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Call", "Format spec should produce Call")
				TestFramework.assertEqual(expr.func.type, "Name", "Function should be Name")
				TestFramework.assertEqual(expr.func.id, "format", "Should call built-in format()")
				TestFramework.assertEqual(#expr.args, 2, "format() should receive value and spec")
				TestFramework.assertEqual(expr.args[1].type, "Name", "First argument should be value Name")
				TestFramework.assertEqual(expr.args[1].id, "value", "Value argument should be 'value'")
				TestFramework.assertEqual(expr.args[2].type, "Constant", "Second argument should be Constant spec")
				TestFramework.assertEqual(expr.args[2].value, ".2f", "Format spec literal should match")
			end,
		},

		{
			name = "Parse Boolean Literals",
			test = function()
				local tokens = Lexer.tokenize("True")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, true, "Value should be true")
			end,
		},

		{
			name = "Parse None Literal",
			test = function()
				local tokens = Lexer.tokenize("None")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Constant", "Should be Constant node")
				TestFramework.assertEqual(expr.value, nil, "Value should be nil")
			end,
		},

		{
			name = "Parse Variable Name",
			test = function()
				local tokens = Lexer.tokenize("x")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Name", "Should be Name node")
				TestFramework.assertEqual(expr.id, "x", "Name should be 'x'")
				TestFramework.assertEqual(expr.ctx, "Load", "Context should be Load")
			end,
		},

		{
			name = "Parse Simple Addition",
			test = function()
				local tokens = Lexer.tokenize("1 + 2")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "Add", "Operator should be Add")
				TestFramework.assertEqual(expr.left.type, "Constant", "Left should be Constant")
				TestFramework.assertEqual(expr.left.value, 1, "Left value should be 1")
				TestFramework.assertEqual(expr.right.type, "Constant", "Right should be Constant")
				TestFramework.assertEqual(expr.right.value, 2, "Right value should be 2")
			end,
		},

		{
			name = "Parse Multiple Operations",
			test = function()
				local tokens = Lexer.tokenize("1 + 2 * 3")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "Add", "Top level should be Add")
				TestFramework.assertEqual(expr.left.value, 1, "Left should be 1")
				TestFramework.assertEqual(expr.right.type, "BinOp", "Right should be BinOp")
				TestFramework.assertEqual(expr.right.op, "Mult", "Right op should be Mult")
			end,
		},

		{
			name = "Parse Floor Division",
			test = function()
				local tokens = Lexer.tokenize("7 // 3")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "FloorDiv", "Operator should be FloorDiv")
			end,
		},

		{
			name = "Parse Matrix Multiply",
			test = function()
				local tokens = Lexer.tokenize("A @ B")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "MatMult", "Operator should be MatMult")
			end,
		},

		{
			name = "Parse Unary Minus",
			test = function()
				local tokens = Lexer.tokenize("-42")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "UnaryOp", "Should be UnaryOp node")
				TestFramework.assertEqual(expr.op, "USub", "Operator should be USub")
				TestFramework.assertEqual(expr.operand.type, "Constant", "Operand should be Constant")
				TestFramework.assertEqual(expr.operand.value, 42, "Operand value should be 42")
			end,
		},

		{
			name = "Parse Parenthesized Expression",
			test = function()
				local tokens = Lexer.tokenize("(1 + 2) * 3")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "Mult", "Top level should be Mult")
				TestFramework.assertEqual(expr.left.type, "BinOp", "Left should be BinOp")
				TestFramework.assertEqual(expr.left.op, "Add", "Left op should be Add")
			end,
		},

		{
			name = "Parse Comparison",
			test = function()
				local tokens = Lexer.tokenize("x < 5")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Compare", "Should be Compare node")
				TestFramework.assertEqual(#expr.ops, 1, "Should have one operator")
				TestFramework.assertEqual(expr.ops[1], "Lt", "Operator should be Lt")
				TestFramework.assertEqual(expr.left.type, "Name", "Left should be Name")
				TestFramework.assertEqual(expr.left.id, "x", "Left name should be x")
			end,
		},

		{
			name = "Parse Complex Expression",
			test = function()
				local tokens = Lexer.tokenize("x + y * 2 - 1")
				local ast = Parser.parse(tokens)

				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Should be BinOp node")
				TestFramework.assertEqual(expr.op, "Sub", "Top level should be Sub")
			end,
		},

		-- ========== STATEMENT PARSER TESTS ==========

		{
			name = "Parse Assignment Statement",
			test = function()
				local tokens = Lexer.tokenize("x = 42")
				local ast = Parser.parse(tokens)

				TestFramework.assertEqual(ast.type, "Module", "Should parse as Module")
				TestFramework.assertEqual(#ast.body, 1, "Should have one statement")

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "Assign", "Should be Assign statement")
				TestFramework.assertEqual(#stmt.targets, 1, "Should have one target")
				TestFramework.assertEqual(stmt.targets[1].type, "Name", "Target should be Name")
				TestFramework.assertEqual(stmt.targets[1].id, "x", "Target name should be 'x'")
				TestFramework.assertEqual(stmt.targets[1].ctx, "Store", "Target context should be Store")
				TestFramework.assertEqual(stmt.value.type, "Constant", "Value should be Constant")
				TestFramework.assertEqual(stmt.value.value, 42, "Value should be 42")
			end,
		},

		{
			name = "Parse Expression Statement",
			test = function()
				local tokens = Lexer.tokenize("print(42)")
				local ast = Parser.parse(tokens)

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "Expr", "Should be expression statement")
				TestFramework.assertEqual(stmt.value.type, "Call", "Should be function call")
				TestFramework.assertEqual(stmt.value.func.type, "Name", "Function should be Name")
				TestFramework.assertEqual(stmt.value.func.id, "print", "Function name should be 'print'")
			end,
		},

		{
			name = "Parse If Statement",
			test = function()
				local tokens = Lexer.tokenize("if x < 5:\n    y = 10")
				local ast = Parser.parse(tokens)

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "If", "Should be If statement")
				TestFramework.assertEqual(stmt.test.type, "Compare", "Test should be comparison")
				TestFramework.assertEqual(stmt.test.ops[1], "Lt", "Should be less than")
				TestFramework.assertEqual(#stmt.body, 1, "Should have one statement in body")
				TestFramework.assertEqual(stmt.body[1].type, "Assign", "Body should contain assignment")
			end,
		},

		{
			name = "Parse While Statement",
			test = function()
				local tokens = Lexer.tokenize("while x > 0:\n    x = x - 1")
				local ast = Parser.parse(tokens)

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "While", "Should be While statement")
				TestFramework.assertEqual(stmt.test.type, "Compare", "Test should be comparison")
				TestFramework.assertEqual(stmt.test.ops[1], "Gt", "Should be greater than")
				TestFramework.assertEqual(#stmt.body, 1, "Should have one statement in body")
			end,
		},

		{
			name = "Parse For Statement",
			test = function()
				local tokens = Lexer.tokenize("for i in range(10):\n    print(i)")
				local ast = Parser.parse(tokens)

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "For", "Should be For statement")
				TestFramework.assertEqual(stmt.target.type, "Name", "Target should be Name")
				TestFramework.assertEqual(stmt.target.id, "i", "Target should be 'i'")
				TestFramework.assertEqual(stmt.target.ctx, "Store", "Target context should be Store")
				TestFramework.assertEqual(stmt.iter.type, "Call", "Iterator should be function call")
				TestFramework.assertEqual(stmt.iter.func.id, "range", "Iterator function should be 'range'")
				TestFramework.assertEqual(#stmt.body, 1, "Should have one statement in body")
			end,
		},

		{
			name = "Parse Function Definition",
			test = function()
				local tokens = Lexer.tokenize("def hello():\n    return 42")
				local ast = Parser.parse(tokens)

				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "FunctionDef", "Should be FunctionDef statement")
				TestFramework.assertEqual(stmt.name, "hello", "Function name should be 'hello'")
				TestFramework.assertEqual(#stmt.args.args, 0, "Should have no arguments")
				TestFramework.assertEqual(#stmt.body, 1, "Should have one statement in body")
			end,
		},

		{
			name = "Parse Multiple Statements",
			test = function()
				local tokens = Lexer.tokenize("x = 1\ny = 2\nz = x + y")
				local ast = Parser.parse(tokens)

				TestFramework.assertEqual(#ast.body, 3, "Should have three statements")
				TestFramework.assertEqual(ast.body[1].type, "Assign", "First should be assignment")
				TestFramework.assertEqual(ast.body[2].type, "Assign", "Second should be assignment")
				TestFramework.assertEqual(ast.body[3].type, "Assign", "Third should be assignment")

				TestFramework.assertEqual(ast.body[1].targets[1].id, "x", "First assigns to x")
				TestFramework.assertEqual(ast.body[2].targets[1].id, "y", "Second assigns to y")
				TestFramework.assertEqual(ast.body[3].targets[1].id, "z", "Third assigns to z")
			end,
		},

		{
			name = "Parse Mixed Statements",
			test = function()
				local tokens = Lexer.tokenize("x = 42\nif x > 0:\n    print(x)")
				local ast = Parser.parse(tokens)

				TestFramework.assertEqual(#ast.body, 2, "Should have two statements")
				TestFramework.assertEqual(ast.body[1].type, "Assign", "First should be assignment")
				TestFramework.assertEqual(ast.body[2].type, "If", "Second should be if statement")
			end,
		},
		-- ========== ADVANCED PARSING (2.3) TESTS ==========
		{
			name = "Parse List Literal",
			test = function()
				local tokens = Lexer.tokenize("[1, 2, x]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "List", "Should be List node")
				TestFramework.assertEqual(#expr.elts, 3, "List should have 3 elements")
				TestFramework.assertEqual(expr.elts[1].value, 1, "First element 1")
			end,
		},
		{
			name = "Parse List Comprehension",
			test = function()
				local tokens = Lexer.tokenize("[x for x in data]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "ListComp", "Should be ListComp node")
				TestFramework.assertEqual(expr.elt.type, "Name", "Element should be Name")
				TestFramework.assertEqual(#expr.generators, 1, "Should have one generator")
				local gen = expr.generators[1]
				TestFramework.assertEqual(gen.target.type, "Name", "Target should be Name")
				TestFramework.assertEqual(gen.target.ctx, "Store", "Target context should be Store")
				TestFramework.assertEqual(gen.iter.type, "Name", "Iter should be Name")
				TestFramework.assertEqual(#gen.ifs, 0, "No filters")
			end,
		},
		{
			name = "Parse List Comprehension With Filter",
			test = function()
				local tokens = Lexer.tokenize("[x for x in data if x]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "ListComp", "Should be ListComp node")
				local gen = expr.generators[1]
				TestFramework.assertEqual(#gen.ifs, 1, "Should have one filter")
				TestFramework.assertEqual(gen.ifs[1].type, "Name", "Filter should be Name expression")
			end,
		},
		{
			name = "Parse Dict Literal",
			test = function()
				local tokens = Lexer.tokenize('{"a": 1, "b": x}')
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Dict", "Should be Dict node")
				TestFramework.assertEqual(#expr.keys, 2, "Two keys")
				TestFramework.assertEqual(expr.values[1].value, 1, "First value 1")
			end,
		},
		{
			name = "Parse Empty Dict Literal",
			test = function()
				local tokens = Lexer.tokenize("{}")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Dict", "Should be Dict node")
				TestFramework.assertEqual(#expr.keys, 0, "No keys")
			end,
		},
		{
			name = "Parse Set Literal",
			test = function()
				local tokens = Lexer.tokenize("{1, 2, 3}")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Set", "Should be Set node")
				TestFramework.assertEqual(#expr.elts, 3, "3 elements")
			end,
		},
		{
			name = "Parse Tuple Literal",
			test = function()
				local tokens = Lexer.tokenize("(1, 2, 3)")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Tuple", "Should be Tuple node")
				TestFramework.assertEqual(#expr.elts, 3, "3 elements")
			end,
		},
		{
			name = "Parse Indexing",
			test = function()
				local tokens = Lexer.tokenize("x[0]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Subscript", "Should be Subscript node")
				TestFramework.assertEqual(expr.value.type, "Name", "Base name")
			end,
		},
		{
			name = "Parse Attribute Access",
			test = function()
				local tokens = Lexer.tokenize("obj.field")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Attribute", "Should be Attribute node")
				TestFramework.assertEqual(expr.attr, "field", "Attr name")
			end,
		},
		{
			name = "Parse Call With Args And Keywords",
			test = function()
				local tokens = Lexer.tokenize("func(1, 2, x=3, y=foo)")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Call", "Should be Call")
				TestFramework.assertEqual(#expr.args, 2, "Two positional args")
				TestFramework.assertEqual(#expr.keywords, 2, "Two keyword args")
				TestFramework.assertEqual(expr.keywords[1].arg, "x", "First kw name x")
			end,
		},
		{
			name = "Parse Lambda Expression",
			test = function()
				local tokens = Lexer.tokenize("lambda x, y=1: x + y")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Lambda", "Should be Lambda node")
				TestFramework.assertEqual(#expr.args.args, 2, "Lambda should have two parameters")
				TestFramework.assertEqual(#expr.args.defaults, 1, "Lambda should capture defaults")
				TestFramework.assertEqual(expr.body.type, "BinOp", "Lambda body should be BinOp")
			end,
		},
		{
			name = "Parse Lambda As Binary Operand",
			test = function()
				local tokens = Lexer.tokenize("a + lambda x: x")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BinOp", "Top level should be BinOp")
				TestFramework.assertEqual(expr.op, "Add", "Operator should be Add")
				TestFramework.assertEqual(expr.right.type, "Lambda", "Right side should be Lambda expression")
			end,
		},
		{
			name = "Parse Nested Lambda Body",
			test = function()
				local tokens = Lexer.tokenize("lambda x: lambda y: x + y")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Lambda", "Outer expression should be Lambda")
				TestFramework.assertEqual(expr.body.type, "Lambda", "Body should be Lambda for nested form")
			end,
		},
		{
			name = "Parse Chained Postfix",
			test = function()
				local tokens = Lexer.tokenize("obj.method(1).attr[2]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Subscript", "Ends with subscript")
				TestFramework.assertEqual(expr.value.type, "Attribute" or "Call", "Previous node exists")
			end,
		},
		-- Phase 2.4 enhancements
		{
			name = "Parse Boolean And/Or Chain",
			test = function()
				local tokens = Lexer.tokenize("a and b or c")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "BoolOp", "Top should be BoolOp")
			end,
		},
		{
			name = "Parse Chained Comparisons",
			test = function()
				local tokens = Lexer.tokenize("a < b < c")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Compare", "Compare node")
				TestFramework.assertEqual(#expr.ops, 2, "Two ops")
			end,
		},
		{
			name = "Parse Augmented Assignment",
			test = function()
				local tokens = Lexer.tokenize("x += 1")
				local ast = Parser.parse(tokens)
				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "AugAssign", "AugAssign node")
				TestFramework.assertEqual(stmt.op, "Add", "Add op")
			end,
		},
		{
			name = "Parse Starred List Literal",
			test = function()
				local tokens = Lexer.tokenize("[*a, b]")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "List", "List node")
				TestFramework.assertEqual(expr.elts[1].type, "Starred", "First element starred")
			end,
		},
		{
			name = "Parse Dict Unpacking",
			test = function()
				local tokens = Lexer.tokenize('{**d1, "k": v}')
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Dict", "Dict node")
				TestFramework.assertEqual(expr.keys[1].type, "DictUnpack", "First key is DictUnpack sentinel")
				TestFramework.assertEqual(expr.values[1].type, "Name", "Value for unpack entry is expression")
			end,
		},
		{
			name = "Parse Starred Call Args",
			test = function()
				local tokens = Lexer.tokenize("f(*a, **k)")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Call", "Call node")
				TestFramework.assertEqual(expr.args[1].type, "Starred", "Starred arg")
				TestFramework.assertEqual(expr.keywords[1].arg, nil, "Keyword unpack")
			end,
		},
		{
			name = "Parse Compound Comparison Keywords",
			test = function()
				local tokens = Lexer.tokenize("a is not b")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Compare", "Compare node")
				TestFramework.assertEqual(expr.ops[1], "IsNot", "IsNot op")
			end,
		},
		{
			name = "Parse Not In Comparison",
			test = function()
				local tokens = Lexer.tokenize("x not in y")
				local ast = Parser.parse(tokens)
				local expr = ast.body[1].value
				TestFramework.assertEqual(expr.type, "Compare", "Compare node")
				TestFramework.assertEqual(expr.ops[1], "NotIn", "NotIn op")
			end,
		},
		{
			name = "Parse Attribute Assignment Target",
			test = function()
				local tokens = Lexer.tokenize("obj.attr = 1")
				local ast = Parser.parse(tokens)
				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "Assign", "Assign node")
				TestFramework.assertEqual(stmt.targets[1].type, "Attribute", "Attribute target")
				TestFramework.assertEqual(stmt.targets[1].ctx, "Store", "Store context")
			end,
		},
		{
			name = "Parse Subscript Assignment Target",
			test = function()
				local tokens = Lexer.tokenize("arr[0] = x")
				local ast = Parser.parse(tokens)
				local stmt = ast.body[1]
				TestFramework.assertEqual(stmt.type, "Assign", "Assign node")
				TestFramework.assertEqual(stmt.targets[1].type, "Subscript", "Subscript target")
				TestFramework.assertEqual(stmt.targets[1].ctx, "Store", "Store context")
			end,
		},
	},
}

TestFramework.runSuite(parserTests)

return {}


--!strict
-- PyLua Virtual Machine Tests
-- Tests for Phase 4 - VM execution

local TestFramework = require("../framework")
local Interpreter = require("../../src/PyLua/vm/interpreter")
local Frame = require("../../src/PyLua/vm/frame")
local Stack = require("../../src/PyLua/vm/stack")
local Instructions = require("../../src/PyLua/bytecode/instructions")
local Collections = require("../../src/PyLua/objects/collections")
local Base = require("../../src/PyLua/objects/base")

-- VM Test Suite
local vmTests: TestFramework.TestSuite = {
	name = "PyLua Virtual Machine Tests",
	tests = {
		{
			name = "Stack Operations",
			test = function()
				local stack = Stack.new()

				-- Test basic push/pop
				Stack.push(stack, 42)
				Stack.push(stack, "hello")
				TestFramework.assertEqual(Stack.size(stack), 2, "Stack should have 2 items")
				TestFramework.assertEqual(Stack.pop(stack), "hello", "Should pop 'hello'")
				TestFramework.assertEqual(Stack.pop(stack), 42, "Should pop 42")
				TestFramework.assertTrue(Stack.isEmpty(stack), "Stack should be empty")

				-- Test peek
				Stack.push(stack, 100)
				TestFramework.assertEqual(Stack.peek(stack), 100, "Should peek 100")
				TestFramework.assertEqual(Stack.size(stack), 1, "Stack should still have 1 item")

				-- Test rotation operations
				Stack.push(stack, 200)
				Stack.push(stack, 300)
				Stack.rotTwo(stack)
				TestFramework.assertEqual(Stack.pop(stack), 200, "After ROT_TWO, should pop 200")
				TestFramework.assertEqual(Stack.pop(stack), 300, "After ROT_TWO, should pop 300")

				-- Test popN
				Stack.push(stack, 1)
				Stack.push(stack, 2)
				Stack.push(stack, 3)
				local values = Stack.popN(stack, 3)
				TestFramework.assertEqual(#values, 3, "Should pop 3 values")
				TestFramework.assertEqual(values[1], 3, "First value should be 3 (top)")
				TestFramework.assertEqual(values[2], 2, "Second value should be 2")
				TestFramework.assertEqual(values[3], 1, "Third value should be 1 (bottom)")
			end,
		},

		{
			name = "Frame Operations",
			test = function()
				local code = {
					Instructions.make("LOAD_CONST", 42),
					Instructions.make("RETURN_VALUE"),
				}

				local frame = Frame.new(code, {}, {})

				-- Test basic frame properties
				TestFramework.assertEqual(frame.pc, 1, "Frame should start at PC=1")
				TestFramework.assertTrue(Frame.hasNext(frame), "Frame should have next instruction")

				-- Test instruction retrieval
				local inst = Frame.getCurrentInstruction(frame)
				TestFramework.assert(inst ~= nil, "Should get current instruction")
				if inst then
					TestFramework.assertEqual(inst.opcode, "LOAD_CONST", "Should get LOAD_CONST")
				end

				-- Test advancement
				Frame.advance(frame)
				TestFramework.assertEqual(frame.pc, 2, "PC should advance to 2")

				-- Test variable operations
				Frame.setVariable(frame, "test", 123)
				TestFramework.assertEqual(Frame.getVariable(frame, "test"), 123, "Should store and retrieve variable")

				-- Test stack operations through frame
				Frame.push(frame, "test_value")
				TestFramework.assertEqual(Frame.stackSize(frame), 1, "Frame stack should have 1 item")
				TestFramework.assertEqual(Frame.pop(frame), "test_value", "Should pop test_value")
			end,
		},

		{
			name = "Basic VM Execution",
			test = function()
				-- Test simple arithmetic: 5 + 3
				local code = {
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("LOAD_CONST", 3),
					Instructions.make("BINARY_ADD"),
					Instructions.make("RETURN_VALUE"),
				}

				local result = Interpreter.execute(code)
				TestFramework.assertEqual(result, 8, "5 + 3 should equal 8")
			end,
		},

		{
			name = "Arithmetic Operations",
			test = function()
				-- Test addition
				local addCode = {
					Instructions.make("LOAD_CONST", 10),
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("BINARY_ADD"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(addCode), 15, "10 + 5 = 15")

				-- Test subtraction
				local subCode = {
					Instructions.make("LOAD_CONST", 10),
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("BINARY_SUBTRACT"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(subCode), 5, "10 - 5 = 5")

				-- Test multiplication
				local mulCode = {
					Instructions.make("LOAD_CONST", 10),
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("BINARY_MULTIPLY"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(mulCode), 50, "10 * 5 = 50")

				local floorCode = {
					Instructions.make("LOAD_CONST", 7),
					Instructions.make("LOAD_CONST", 2),
					Instructions.make("BINARY_FLOOR_DIVIDE"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(floorCode), 3, "7 // 2 = 3")
			end,
		},

		{
			name = "Matrix multiplication opcode",
			test = function()
				local left = Collections.newList({
					Collections.newList({ Base.newInt(1), Base.newInt(2) }),
					Collections.newList({ Base.newInt(3), Base.newInt(4) }),
				})
				local right = Collections.newList({
					Collections.newList({ Base.newInt(5), Base.newInt(6) }),
					Collections.newList({ Base.newInt(7), Base.newInt(8) }),
				})
				local globals = { left = left, right = right }
				local names = { "left", "right" }
				local code = {
					Instructions.make("LOAD_NAME", 0),
					Instructions.make("LOAD_NAME", 1),
					Instructions.make("BINARY_MATRIX_MULTIPLY"),
					Instructions.make("RETURN_VALUE"),
				}
				local result = Interpreter.execute(code, globals, {}, nil, names)
				TestFramework.assertType(result, "table", "Result should be PyObject list")
				TestFramework.assertEqual(result.__value[1].__value[1].__value, 19, "Matrix entry [0][0] should equal 19")
			end,
		},

		{
			name = "Comparison Operations",
			test = function()
				-- Test less than (op code 0)
				local ltCode = {
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("LOAD_CONST", 10),
					Instructions.make("COMPARE_OP", 0),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(ltCode), true, "5 < 10 should be true")

				-- Test equality (op code 2)
				local eqCode = {
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("LOAD_CONST", 5),
					Instructions.make("COMPARE_OP", 2),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(eqCode), true, "5 == 5 should be true")

				-- Identity should detect identical objects
				local identityConstants = { Collections.newList({}) }
				local identitySameCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("DUP_TOP"),
					Instructions.make("COMPARE_OP", 6),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertTrue(
					Interpreter.execute(identitySameCode, {}, {}, identityConstants),
					"Object should be identical to itself"
				)

				local identityDifferentConstants = {
					Collections.newList({}),
					Collections.newList({}),
				}
				local identityDifferentCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("COMPARE_OP", 6),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertFalse(
					Interpreter.execute(identityDifferentCode, {}, {}, identityDifferentConstants),
					"Distinct objects should not be identical"
				)

				local identityNotCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("COMPARE_OP", 7),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertTrue(
					Interpreter.execute(identityNotCode, {}, {}, identityDifferentConstants),
					"Distinct objects should satisfy is not"
				)

				-- Membership should fall back to iterator protocol when __contains__ is missing
				local baseAny = Base :: any
				local typeOk, iterOnlyType = pcall(function()
					return baseAny.getTypeObject("iter_only")
				end)
				if not typeOk then
					iterOnlyType = Base.registerType("iter_only", {})
				end
				iterOnlyType.methods = iterOnlyType.methods or {}
				iterOnlyType.methods["__iter__"] = function(selfObj: Base.PyObject)
					local listVersion = Collections.newList(selfObj.__value :: { Base.PyObject })
					local iterMethod = Base.getattr(listVersion, "__iter__")
					return Base.call(iterMethod, {})
				end

				local iterContainer = Base.newPyObject(
					"iter_only",
					{
						Base.newInt(1),
						Base.newInt(2),
						Base.newInt(3),
					}
				)

				local containsConstants = { Base.newInt(2), iterContainer }
				local containsCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("COMPARE_OP", 8),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertTrue(
					Interpreter.execute(containsCode, {}, {}, containsConstants),
					"Iterator fallback should find element"
				)

				local missingConstants = { Base.newInt(9), iterContainer }
				local missingCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("COMPARE_OP", 8),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertFalse(
					Interpreter.execute(missingCode, {}, {}, missingConstants),
					"Iterator fallback should report missing elements"
				)

				local notInCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("COMPARE_OP", 9),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertTrue(
					Interpreter.execute(notInCode, {}, {}, missingConstants),
					"not in should negate iterator membership"
				)

				local type2Ok, opaqueType = pcall(function()
					return baseAny.getTypeObject("opaque_no_iter")
				end)
				if not type2Ok then
					opaqueType = Base.registerType("opaque_no_iter", {})
				end
				opaqueType.methods = opaqueType.methods or {}

				local opaqueInstance = Base.newPyObject("opaque_no_iter", {})
				local opaqueConstants = { Base.newInt(1), opaqueInstance }
				TestFramework.assertError(function()
					Interpreter.execute(containsCode, {}, {}, opaqueConstants)
					return nil
				end, "Membership on non-iterable should raise TypeError")
			end,
		},

		{
			name = "Collection Building",
			test = function()
				-- Test BUILD_LIST
				local listCode = {
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("LOAD_CONST", 2),
					Instructions.make("LOAD_CONST", 3),
					Instructions.make("BUILD_LIST", 3),
					Instructions.make("RETURN_VALUE"),
				}

				local listResult = Interpreter.execute(listCode)
				TestFramework.assertType(listResult, "table", "Should build a list")
				TestFramework.assertEqual(listResult.__type, "list", "Should be a PyObject list")
				TestFramework.assertEqual(#listResult.__value, 3, "List should have 3 elements")
				-- Check that elements are proper PyObjects
				TestFramework.assertEqual(
					listResult.__value[1].__value,
					1,
					"First element should be 1 (bottom of stack)"
				)
				TestFramework.assertEqual(listResult.__value[2].__value, 2, "Second element should be 2")
				TestFramework.assertEqual(listResult.__value[3].__value, 3, "Third element should be 3 (top of stack)")

				-- Test BUILD_MAP with constants
				local mapConstants: { any } = { "key1", "value1", "key2", "value2" }
				local mapCode = {
					Instructions.make("LOAD_CONST", 0), -- "key1"
					Instructions.make("LOAD_CONST", 1), -- "value1"
					Instructions.make("LOAD_CONST", 2), -- "key2"
					Instructions.make("LOAD_CONST", 3), -- "value2"
					Instructions.make("BUILD_MAP", 2),
					Instructions.make("RETURN_VALUE"),
				}

				local mapResult = Interpreter.execute(mapCode, {}, {}, mapConstants)
				TestFramework.assertType(mapResult, "table", "Should build a map")
				TestFramework.assertEqual(mapResult.__type, "dict", "Should be a PyObject dict")
				-- Storage is hashed; iterate entries and inspect entry.key/value PyObjects
				local foundKey1 = false
				local foundKey2 = false
				for _, entry in pairs(mapResult.__value) do
					if entry.key.__value == "key1" then
						TestFramework.assertEqual(entry.value.__value, "value1", "Should have key1 -> value1")
						foundKey1 = true
					elseif entry.key.__value == "key2" then
						TestFramework.assertEqual(entry.value.__value, "value2", "Should have key2 -> value2")
						foundKey2 = true
					end
				end
				TestFramework.assertTrue(foundKey1, "Should have key1")
				TestFramework.assertTrue(foundKey2, "Should have key2")
			end,
		},

		{
			name = "Control Flow",
			test = function()
				-- Test conditional jump (POP_JUMP_IF_FALSE)
				-- Load true, jump if false (should not jump), load 42, return
				local constants: { any } = { true, 42, 99 }
				local jumpCode = {
					Instructions.make("LOAD_CONST", 0), -- true
					Instructions.make("POP_JUMP_IF_FALSE", 5), -- Jump to instruction 5 if false
					Instructions.make("LOAD_CONST", 1), -- 42 - This should execute
					Instructions.make("JUMP_FORWARD", 1), -- Skip next instruction
					Instructions.make("LOAD_CONST", 2), -- 99 - This should be skipped
					Instructions.make("RETURN_VALUE"),
				}

				local jumpResult = Interpreter.execute(jumpCode, {}, {}, constants)
				TestFramework.assertEqual(jumpResult, 42, "Should execute conditional path correctly")
			end,
		},

		{
			name = "Boolean Constants",
			test = function()
				local constants: { any } = { false, true }
				local falseCode = {
					Instructions.make("LOAD_CONST", 0),
					Instructions.make("RETURN_VALUE"),
				}

				local falseResult = Interpreter.execute(falseCode, {}, {}, constants)
				TestFramework.assertType(falseResult, "boolean", "False constant should unwrap to boolean")
				TestFramework.assertFalse(falseResult, "LOAD_CONST false should produce false result")

				local trueCode = {
					Instructions.make("LOAD_CONST", 1),
					Instructions.make("RETURN_VALUE"),
				}

				local trueResult = Interpreter.execute(trueCode, {}, {}, constants)
				TestFramework.assertType(trueResult, "boolean", "True constant should unwrap to boolean")
				TestFramework.assertTrue(trueResult, "LOAD_CONST true should produce true result")
			end,
		},

		{
			name = "Unary Operations",
			test = function()
				-- Test unary negative
				local negCode = {
					Instructions.make("LOAD_CONST", 42),
					Instructions.make("UNARY_NEGATIVE"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(negCode), -42, "Unary negative should work")

				-- Test unary positive (identity)
				local posCode = {
					Instructions.make("LOAD_CONST", 42),
					Instructions.make("UNARY_POSITIVE"),
					Instructions.make("RETURN_VALUE"),
				}
				TestFramework.assertEqual(Interpreter.execute(posCode), 42, "Unary positive should be identity")
			end,
		},
	},
}

TestFramework.runSuite(vmTests)

return {}


--!nocheck
--selene: allow(incorrect_standard_library_use)
local build_single_example = require("../../build-examples/build_single_example")

build_single_example("01-hello-lingua", "bin")


-- When Rust panics, it doesn't exit helpfully by default. This function gives
-- the Rust side an external function it can call with details of the panic,
-- so the error can be emitted on the Luau side instead.

return function(extern_fns)
	local current_panic = nil
	function extern_fns.panic_reporter(
		len_or_byte: number
	): ()
		if current_panic == nil then
			current_panic = {
				len = len_or_byte,
				bytes = ""
			}
		else
			current_panic.bytes ..= string.char(len_or_byte)
			if #current_panic.bytes == current_panic.len then
				error(current_panic.bytes, 0)
			end
		end
	end
end


--!nocheck
--selene: allow(incorrect_standard_library_use)
local build_single_example = require("../../build-examples/build_single_example")

build_single_example("02-simple-library", "lib")


--!nocheck
-- See src/lib.rs for an introduction to this example.

local library = require(script.library)

local prices = {
	apple = 10,
	orange = 5,
	banana = 25
}

local fridge = {
	"apple",
	"apple",
	"banana",
	"orange",
	"apple",
	"orange",
	"orange",
	"banana",
	"banana"
}
local fridge_value = library.calculate_fridge_value(prices, fridge)
print(`The value of my fridge is {fridge_value}`)

local invalid_fridge = {
	"garden chair",
	"basket",
	"clothes pegs",
	"tennis ball"
}
-- This will error!
library.calculate_fridge_value(prices, invalid_fridge)


local lingua = require(script.Parent.Parent.lingua)
local library_wasm_loader = require(script.Parent.Parent.target_luau.example_wasm)

local add_panic_handler = require(script.Parent.add_panic_handler)

local wasm_env = { func_list = {} }
add_panic_handler(wasm_env.func_list)
local finish_lingua_init = lingua.init(wasm_env)
local library_wasm_module = library_wasm_loader({env = wasm_env})
local lingua_api = finish_lingua_init(library_wasm_module)

local library = {}

function library.calculate_fridge_value(
	prices: {[string]: number},
	fridge: {string}
): number
	local result = lingua_api.receive_from_rust(
		library_wasm_module.func_list.calculate_fridge_value(
			lingua_api.send_to_rust(prices),
			lingua_api.send_to_rust(fridge)
		)
	)
	return result.Ok or error(result.Err)
end

return library


-- Lune-style imports make both Luau LSP and Selene very unhappy...
--!nocheck
--!nolint LocalShadow
--selene: allow(incorrect_standard_library_use)
local process = require("@lune/process")
--selene: allow(incorrect_standard_library_use)
local stdio = require("@lune/stdio")
--selene: allow(incorrect_standard_library_use)
local fs = require("@lune/fs")

local RED = stdio.color("red")
local BLUE = stdio.color("blue")
local YELLOW = stdio.color("yellow")
local MAGENTA = stdio.color("magenta")
local RESET = stdio.color("reset")

local function get_example_dir(
	example_name: string
): string
	return `../{example_name}`
end

local function build_single_example(
	example_name: string,
	_example_type: "lib" | "bin"
): ()
	local working_dir = get_example_dir(example_name)
	print(`{YELLOW}[Build Example]{RESET} Building example: {BLUE}{example_name}{RESET}`)

	-- Compile Rust source code to WASM
	do
		print(`{BLUE}[Build Example > WASM]{RESET} Compiling WASM module with cargo...`)
		local result = process.spawn(
			"cargo",
			{
				"build",
				"--release",
				"--target-dir",
				"target"
			},
			{
				cwd = working_dir
			}
		)
		if not result.ok then
			print(result.stdout)
			print(result.stderr)
			print(`{BLUE}[Build Example > WASM]{RESET} WASM compile failed!`)
			return
		end
		print(`{BLUE}[Build Example > WASM]{RESET} WASM compile successful!`)
	end

	-- Convert WASM to Luau with Wasynth
	do
		print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpiling WASM to Luau with Wasynth...`)
		local result = process.spawn(
			"wasm2luau",
			{
				"target/wasm32-unknown-unknown/release/example_wasm.wasm"
			},
			{
				cwd = working_dir
			}
		)
		if not result.ok then
			print(result.stdout)
			print(result.stderr)
			print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation failed!`)
			return
		end

		print(`{MAGENTA}[Build Example > Wasynth]{RESET} Saving transpilation to file...`)

		pcall(
			fs.writeDir,
			working_dir .. "/target-luau"
		)
		local ok, result = pcall(
			fs.writeFile,
			working_dir .. "/target-luau/example_wasm.luau",
			"--!nocheck\n" .. result.stdout
		)
		if not ok then
			print(tostring(result))
			print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation failed!`)
			return
		end

		print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation succesful!`)
	end

	-- Build Rojo project
	do
		print(`{RED}[Build Example > Rojo]{RESET} Building Roblox project with Rojo...`)
		local result = process.spawn(
			"rojo",
			{
				"build",
				"example.project.json",
				"--output",
				"target-luau/build.rbxl"
			},
			{
				cwd = working_dir
			}
		)
		if not result.ok then
			print(result.stdout)
			print(result.stderr)
			print(`{RED}[Build Example > Rojo]{RESET} Rojo build failed!`)
			return
		end
		print(`{RED}[Build Example > Rojo]{RESET} Rojo build successful!`)
	end

	print(`{YELLOW}[Build Example]{RESET} Build finished!`)
	print(`{YELLOW}[Build Example]{RESET} The example project has been set up inside {BLUE}target-luau/build.rbxl{RESET} for you.`)
	print(`{YELLOW}[Build Example]{RESET} You can continuously serve this example by running {BLUE}rojo serve example.project.json{RESET}.`)
end

return build_single_example


--!strict
--Licensed under BSD3 from Lingua, (c) Daniel P H Fox 2024

--------------------------------------------------------------------------------
-- TYPE DEFINITIONS ------------------------------------------------------------
--------------------------------------------------------------------------------

-- Type aliases for convenience and documentation
type ptr_mut_u8 = number
type u32 = number
type u8 = number
type table = {
	[unknown]: unknown
}

-- When data is sent from Rust, this opaque handle is generated to concisely
-- refer to that data. This handle is received across the FFI boundary by
-- obtaining it through an `extern fn` as a `u32`.
export type DataFromRustHandle = u32

-- When data is sent from Luau, this opaque handle is generated to concisely
-- refer to that data. This handle must always be sent to the Rust side; this is
-- done by sending it through an `extern fn` as a `u32`.
export type DataFromLuauHandle = u32

-- Passed to Wasynth when initialising a WebAssembly module.
-- Lingua injects some of its own members into this environment, but otherwise
-- does not care about the rest of the contents.
type WasmEnvironment = {
	func_list: {
		[string]: (...any) -> (...any)
	}
}

-- Returned by Wasynth when initialising a WebAssembly module.
-- Lingua expects certain members to be present - in particular, some extern fn
-- definitions from the Rust side.
type WasmModule = {
	func_list: {
		-- The Luau side calls this function to initiate sending a JSON string.
		-- It's called with the handle that it generated and the length of the
		-- string that it would like to transfer. The Rust side is expected to
		-- reserve space for the string and return a pointer to this reserved
		-- space, with a null pointer representing a failure to allocate space.
		lingua_send_json_to_rust_alloc: (
			luau_handle: DataFromLuauHandle,
			len: u32
		) -> ptr_mut_u8,
		-- The Luau side is expected to call this function once it has finished
		-- writing to space previously allocated for the transfer of JSON data.
		-- This signals to the Rust side that it is safe to access the data.
		lingua_send_json_to_rust: (
			luau_handle: DataFromLuauHandle
		) -> u8
	},
	rt: {
		store: {
			string: (
				memory: WasmMemory,
				ptr: ptr_mut_u8,
				str: string
			) -> ()
		},
		load: {
			string: (
				memory: WasmMemory,
				ptr: ptr_mut_u8,
				len: u32
			) -> string
		}
	},
	memory_list: {
		memory: WasmMemory
	}
}

-- An entry in the `memory_list` dictionary returned by Wasynth.
-- Lingua doesn't care about its specific type - it's treated opaquely.
type WasmMemory = unknown

-- The internal state used by Lingua's Luau-side API.
-- This is not shared between WASM modules.
type ApiState = {
	-- This module provides the low level FFI that the API functions use.
	module: WasmModule,
	-- When the Rust side sends JSON strings, they're stored here, indexed by
	-- which handle the Rust side decided to use.
	just_received_json: {
		[u32]: string
	},
	-- To uniquely identify JSON strings the Luau side sends to the Rust side,
	-- this handle is incremented and wraps at the 32-bit limit. Handles
	-- generated on the Luau side may collide with handles generated on the Rust
	-- side because no synchronisation is done.
	next_luau_handle: DataFromLuauHandle
}

-- The public API surface exposed by Lingua after initialisation.
-- The API state is expected to be encapsulated as upvalues in closures.
export type Api = {
	-- Sends some data to the Rust side. An opaque `DataFromLuauHandle` is
	-- returned; you are always expected to send this handle to the Rust side.
	send_to_rust: (
		data: table
	) -> DataFromLuauHandle,

	-- Receives some data from the Rust side. You need to generate and send a
	-- `DataFromRustHandle` yourself from within Rust.
	receive_from_rust: (
		rust_handle: DataFromRustHandle
	) -> table
}

--------------------------------------------------------------------------------
-- CONSTANTS -------------------------------------------------------------------
--------------------------------------------------------------------------------

-- The return values of Lingua FFI functions are used to indicate whether the
-- FFI call was successful. Note that this has nothing to do with the specific
-- operation - it's specifically used to communicate low-level failures.
-- Returned data or operation-specific errors should be sent some other way.
-- The only exception are memory allocations, which must return a pointer for
-- pragmatic reasons - in this case, the null pointer represents failure.
local RETURN_CODES = {
	LUAU = {
		-- The FFI call was successfully handled without an unexpected error.
		SUCCESS = 0,
		-- A Luau error occurred outside of a protected block in an FFI call.
		UNCAUGHT_ERROR_AT_FFI_BOUNDARY = 1,
		-- An FFI call was received before Lingua's Luau API knew about the
		-- module that was sending the data.
		LUAU_API_NOT_READY = 2
	},
	RUST = {
		-- The FFI call was successfully handled without panicking.
		SUCCESS = 0,
		-- A Rust panic unwound up to the FFI boundary.
		PANIC_AT_FFI_BOUNDARY = 1
	}
}

--------------------------------------------------------------------------------
-- UTILITIES -------------------------------------------------------------------
--------------------------------------------------------------------------------

-- Serialise a table into a JSON string. Right now, this uses Roblox API under
-- the hood, but there's no reason a pure Luau solution can't be used if you're
-- using a standalone runtime like Lune.
local function encode_json(
	t: table
): string
	return game:GetService("HttpService"):JSONEncode(t)
end

-- Deserialise a JSON string into a table. Right now, this uses Roblox API under
-- the hood, but there's no reason a pure Luau solution can't be used if you're
-- using a standalone runtime like Lune.
local function decode_json(
	str: string
): table
	return game:GetService("HttpService"):JSONDecode(str)
end

-- Increment a u32 number. If the next number would be outside of the range that
-- u32 numbers can represent, wrap around to 0 again.
local function next_u32(
	current: u32
): u32
	return (current + 1) % (2 ^ 32)
end

-- This is used in FFI functions to ensure that errors do not spill across the
-- FFI boundary. Errors print and return `UNCAUGHT_ERROR_AT_FFI_BOUNDARY`.
-- Otherwise, the `SUCCESS` code is returned.
local function ffi_error_boundary<Args...>(
	f: (Args...) -> (),
	...: Args...
): u8
	local ok = xpcall(
		f,
		function(err)
			print(`[lingua] error at ffi boundary: {err}`)
		end,
		...
	)
	return if ok then
		RETURN_CODES.LUAU.SUCCESS
	else
		RETURN_CODES.LUAU.UNCAUGHT_ERROR_AT_FFI_BOUNDARY
end

--------------------------------------------------------------------------------
-- FOREIGN FUNCTION INTERFACE --------------------------------------------------
--------------------------------------------------------------------------------

local extern_fn = {}

-- The Rust side calls this function when it sends a JSON string. It's called
-- with the handle that it generated, the pointer to the string, and the length
-- of that string. To minimise the chance of errors at the FFI boundary, the
-- string is saved without decoding the data or invoking any user callbacks.
function extern_fn.send_json_to_luau(
	api_state: ApiState,
	rust_handle: DataFromRustHandle,
	ptr: ptr_mut_u8,
	len: u32
): ()
	assert(
		api_state.just_received_json[rust_handle] == nil,
		`[lingua] rust handle {rust_handle} is already in use - ensure you're reading all data sent to the luau side`
	)
	local str = api_state.module.rt.load.string(
		api_state.module.memory_list.memory,
		ptr,
		len
	)
	api_state.just_received_json[rust_handle] = str
end

--------------------------------------------------------------------------------
-- LUAU SIDE API ---------------------------------------------------------------
--------------------------------------------------------------------------------

local api_fn = {}

-- See the `Api` type for user-facing documentation.
function api_fn.send_to_rust(
	api_state: ApiState,
	data: table
): DataFromLuauHandle
	local str = encode_json(data)
	local luau_handle = api_state.next_luau_handle
	api_state.next_luau_handle = next_u32(luau_handle)
	local len = string.len(str)
	local ptr = api_state.module.func_list.lingua_send_json_to_rust_alloc(luau_handle, len)
	assert(
		ptr ~= 0,
		"[lingua] rust side failed to allocate memory for data to be sent"
	)
	api_state.module.rt.store.string(
		api_state.module.memory_list.memory,
		ptr,
		str
	)
	local status = api_state.module.func_list.lingua_send_json_to_rust(luau_handle)
	if status ~= RETURN_CODES.RUST.SUCCESS then
		local codename = "(unknown)"
		for name, code in pairs(RETURN_CODES.RUST) do
			if status == code then
				codename = name
				break
			end
		end
		error(`[lingua] rust side returned error code {status}: {codename}`)
	end
	return luau_handle
end

-- See the `Api` type for user-facing documentation.
function api_fn.receive_from_rust(
	api_state: ApiState,
	rust_handle: DataFromRustHandle
): table
	local str = api_state.just_received_json[rust_handle]
	assert(
		str ~= nil,
		`[lingua] handle {rust_handle} has no data - handles should be generated on the sending side and are single use`
	)
	return decode_json(str)
end

--------------------------------------------------------------------------------
-- INITIALISATION --------------------------------------------------------------
--------------------------------------------------------------------------------

local lingua = {}

-- Lingua needs to be initialised in two stages; firstly, it needs to register
-- its own extern functions into the WASM environment, and secondly, it needs to
-- obtain a reference to the WASM module so that it can invoke functions that
-- exist on the Rust side. Once both have been received, you will receive an
-- `Api` that you can use with the provided module.
function lingua.init(
	env: WasmEnvironment
)
	local api_state: ApiState? = nil
	for fn_name, fn in pairs(extern_fn) do
		env.func_list["lingua_"..fn_name] = function(...)
			if api_state == nil then
				print("[lingua] api not initialised yet on luau side - cannot handle incoming data")
				return RETURN_CODES.LUAU.LUAU_API_NOT_READY
			end
			return ffi_error_boundary(fn, api_state, ...)
		end
	end
	return function(
		module: WasmModule
	): Api
		assert(api_state == nil, "[lingua] only one module allowed per environment")
		api_state = {
			module = module,
			just_received_json = {},
			next_luau_handle = 0
		}
		local api = {}
		for fn_name, fn in pairs(api_fn) do
			api[fn_name] = function(...)
				return fn(api_state, ...)
			end
		end
		return api :: any
	end
end

return lingua


-- Implementation of this benchmark: https://hono.dev/docs/concepts/benchmarks

local Router = require("@lynx/router")

local router = Router.new()

router:add("GET", "/user", function() end)
router:add("GET", "/user/comments", function() end)
router:add("GET", "/user/avatar", function() end)
router:add("GET", "/user/lookup/username/:username", function() end)
router:add("GET", "/user/lookup/email/:address", function() end)
router:add("GET", "/event/:id", function() end)
router:add("GET", "/event/:id/comments", function() end)
router:add("POST", "/event/:id/comment", function() end)
router:add("GET", "/map/:location/events", function() end)
router:add("GET", "/status", function() end)
router:add("GET", "/very/deeply/nested/route/hello/there", function() end)
router:add("GET", "/static/*", function() end)

local routes = {
	{
		name = "short static",
		method = "GET",
		path = "/user",
	},
	{
		name = "static with same radix",
		method = "GET",
		path = "/user/comments",
	},
	{
		name = "dynamic route",
		method = "GET",
		path = "/user/lookup/username/hey",
	},
	{
		name = "mixed static dynamic",
		method = "GET",
		path = "/event/abcd1234/comments",
	},
	{
		name = "post",
		method = "POST",
		path = "/event/abcd1234/comment",
	},
	{
		name = "long static",
		method = "GET",
		path = "/very/deeply/nested/route/hello/there",
	},
	{
		name = "wildcard",
		method = "GET",
		path = "/static/index.html",
	},
}

local ITERATIONS = 1000000

for _, route in routes do
	local total = 0
	for _ = 1, ITERATIONS do
		local start = os.clock()
		Router.match(router, route.method, route.path)
		local finish = os.clock()
		total += finish - start
	end

	local average = total / ITERATIONS
	local averageNs = average * 1e9

	print(route.name, math.ceil(averageNs), "ns")
end

local totalTogether = 0

for _ = 1, ITERATIONS do
	for _, route in routes do
		local start = os.clock()
		Router.match(router, route.method, route.path)
		local finish = os.clock()
		totalTogether += finish - start
	end
end

local average = totalTogether / ITERATIONS
local averageNs = average * 1e9

print("all together", math.ceil(averageNs), "ns")


local serde = require("@lune/serde")
local Context = require("../context")
type Context = Context.Context

local ENCODINGS: { { name: string, encode: serde.CompressDecompressFormat } } = {
	{
		name = "br",
		encode = "brotli",
	},
	{
		name = "gzip",
		encode = "gzip",
	},
}

local function compress()
	return function(c: Context, next: () -> ())
		next()
		local acceptEncoding = c.req.headers["accept-encoding"]
		local encoding
		for _, type in ENCODINGS do
			if acceptEncoding:find(type.name) then
				encoding = type
				break
			end
		end
		if not encoding or not c.res.body then
			return
		end
		c:header("Content-Length", nil)
		c:header("Content-Encoding", encoding.name)
		c.res.body = serde.compress(encoding.encode, c.res.body)
	end
end

return compress


local datetime = require("@lune/datetime")
local process = require("@lune/process")
local Context = require("../context")
type Context = Context.Context
local Types = require("@lynx/types")
type Middleware = Types.Middleware

local color = if process.env.NO_COLOR then false else true

local function dim(string: string): string
	if not color then
		return string
	end
	return `\x1b[2m{string}\x1b[0m`
end

local function statusColor(code: number?): string
	if not code then
		return "[???]"
	end
	local c = `[{code}]`
	if not color then
		return c
	end
	if code >= 500 then
		return `\x1b[31m{c}\x1b[0m` -- red
	elseif code >= 400 then
		return `\x1b[33m{c}\x1b[0m` -- yellow
	elseif code >= 300 then
		return `\x1b[36m{c}\x1b[0m` -- cyan
	end
	return `\x1b[32m{c}\x1b[0m` -- green
end

local function logger(func: (string) -> ()?)
	local fn: (string) -> () = func or print
	return function(c: Context, next: () -> ())
		local start = os.clock()
		next()
		local time = datetime.now():formatLocalTime("%H:%M:%S")
		local status = c.res.status
		local method = c.req.method
		local path = c.req.path
		local elapsedMs = (os.clock() - start) * 1000
		local elapsedFormatted = if elapsedMs > 1000
			then math.round(elapsedMs / 1000) .. "s"
			else math.round(elapsedMs) .. "ms"

		fn(`{dim(time)} {statusColor(status)} {method} {path} {dim(elapsedFormatted)}`)
	end
end

return logger :: Middleware


local Node = require("./node")
type Node = Node.Node

local Types = require("./types")
type Handler = Types.Handler
type Middleware = Types.Middleware

local Router = {}
Router.__index = Router

type RouterData = {
	node: Node,
}

export type Router = typeof(setmetatable({} :: RouterData, Router))

function Router.new(): Router
	local self = {}
	self.node = Node.new()
	return setmetatable(self, Router)
end

function Router.add(self: Router, method: string, path: string, handler: Handler | Middleware)
	self.node:insert(method, path, handler)
end

function Router.match(self: Router, method: string, path: string)
	local parts = string.split(string.sub(path, 2), "/")

	local handlers = Node.search(self.node, method, parts, 1)

	if handlers then
		table.sort(handlers, function(a, b)
			return a.score < b.score
		end)
	end

	return handlers
end

return Router


local fs = require("@lune/fs")
local Context = require("./context")
type Context = Context.Context

-- From https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
local mimeTypes = {
	aac = "audio/aac",
	abw = "application/x-abiword",
	apng = "image/apng",
	arc = "application/x-freearc",
	avif = "image/avif",
	avi = "video/x-msvideo",
	azw = "application/vnd.amazon.ebook",
	bin = "application/octet-stream",
	bmp = "image/bmp",
	bz = "application/x-bzip",
	bz2 = "application/x-bzip2",
	cda = "application/x-cdf",
	csh = "application/x-csh",
	css = "text/css",
	csv = "text/csv",
	doc = "application/msword",
	docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	eot = "application/vnd.ms-fontobject",
	epub = "application/epub+zip",
	gz = "application/gzip",
	gif = "image/gif",
	htm = "text/html",
	html = "text/html",
	ico = "image/vnd.microsoft.icon",
	ics = "text/calendar",
	jar = "application/java-archive",
	jpeg = "image/jpeg",
	jpg = "image/jpeg",
	js = "text/javascript",
	json = "application/json",
	jsonld = "application/ld+json",
	mid = "audio/midi, audio/x-midi",
	midi = "audio/midi, audio/x-midi",
	mjs = "text/javascript",
	mp3 = "audio/mpeg",
	mp4 = "video/mp4",
	mpeg = "video/mpeg",
	mpkg = "application/vnd.apple.installer+xml",
	odp = "application/vnd.oasis.opendocument.presentation",
	ods = "application/vnd.oasis.opendocument.spreadsheet",
	odt = "application/vnd.oasis.opendocument.text",
	oga = "audio/ogg",
	ogv = "video/ogg",
	ogx = "application/ogg",
	opus = "audio/ogg",
	otf = "font/otf",
	png = "image/png",
	pdf = "application/pdf",
	php = "application/x-httpd-php",
	ppt = "application/vnd.ms-powerpoint",
	pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
	rar = "application/vnd.rar",
	rtf = "application/rtf",
	sh = "application/x-sh",
	svg = "image/svg+xml",
	tar = "application/x-tar",
	tif = "image/tiff",
	tiff = "image/tiff",
	ts = "video/mp2t",
	ttf = "font/ttf",
	txt = "text/plain",
	vsd = "application/vnd.visio",
	wav = "audio/wav",
	weba = "audio/webm",
	webm = "video/webm",
	webp = "image/webp",
	woff = "font/woff",
	woff2 = "font/woff2",
	xhtml = "application/xhtml+xml",
	xls = "application/vnd.ms-excel",
	xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
	xml = "application/xml",
	xul = "application/vnd.mozilla.xul+xml",
	zip = "application/zip",
	["3gp"] = "video/3gpp",
	["3g2"] = "video/3gpp2",
	["7z"] = "application/x-7z-compressed",
}

local function getMimeType(file: string): string
	local extension = file:match("^.+(%..+)$")
	if extension then
		extension = extension:sub(2):lower() -- Remove the dot and convert to lowercase
		local mimeType = mimeTypes[extension]
		if mimeType and mimeType:find("^text") ~= nil then
			mimeType ..= "; charset=utf-8"
		end
		return mimeType or "application/octet-stream"
	end
	return "application/octet-stream" -- Default MIME type
end

local function static(path: string, directory: string)
	local cleanPath = path:gsub("/$", "")
	return function(c: Context)
		local fullPath = c.req.path
		local relativePath = fullPath:sub(#cleanPath + 1)
		local filePath = directory .. relativePath
		local success, result = pcall(fs.readFile, filePath)

		if not success then
			local indexFilePath = filePath:gsub("/$", "") .. "/index.html"
			local indexSuccess, indexResult = pcall(fs.readFile, indexFilePath)
			if indexSuccess then
				c:header("Content-Type", getMimeType(indexFilePath))
				return c:body(indexResult)
			end
			return c:notFound()
		end

		c:header("Content-Type", getMimeType(filePath))
		return c:body(result)
	end
end

return static


--!optimize 2
--!native
--------------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- API:                                                                       --
--   local Signal = require(THIS MODULE)                                      --
--   local sig = Signal.new()                                                 --
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
--   sig:Fire(arg1, arg2, ...)                                                --
--   connection:Disconnect()                                                  --
--   sig:DisconnectAll()                                                      --
--   local arg1, arg2, ... = sig:Wait()                                       --
--                                                                            --
-- Licence:                                                                   --
--   Licenced under the MIT licence.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--------------------------------------------------------------------------------
--!nocheck

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread()
	-- Note: We cannot use the initial set of arguments passed to
	-- runEventHandlerInFreeThread for a call to the handler, because those
	-- arguments would stay on the stack for the duration of the thread's
	-- existence, temporarily leaking references. Without access to raw bytecode
	-- there's no way for us to clear the "..." references from the stack.
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection.new(signal, fn)
	return setmetatable({
		_connected = true,
		_signal = signal,
		_fn = fn,
		_next = false,
	}, Connection)
end

function Connection:Disconnect()
	self._connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_, key)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

export type Connection = {
	Disconnect: (self: Connection) -> (),
}

export type Signal<T...> = {
	Connect: (self: Signal<T...>, callback: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, callback: (T...) -> ()) -> Connection,
	Fire: (self: Signal<T...>, T...) -> (),
	Wait: (self: Signal<T...>) -> (),
}

-- Signal class
local Signal = {}
Signal.__index = Signal

function Signal.new<T...>(): Signal<T...>
	return setmetatable({
		_handlerListHead = false,
	}, Signal) :: any
end

function Signal:Connect(fn)
	local connection = Connection.new(self, fn)
	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end
	return connection
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
function Signal:DisconnectAll()
	self._handlerListHead = false
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item._connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
				-- Get the freeRunnerThread to the first yield
				coroutine.resume(freeRunnerThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

-- Implement Signal:Wait() in terms of a temporary connection using
-- a Signal:Connect() which disconnects itself.
function Signal:Wait()
	local waitingCoroutine = coroutine.running()
	local cn
	cn = self:Connect(function(...)
		cn:Disconnect()
		task.spawn(waitingCoroutine, ...)
	end)
	return coroutine.yield()
end

-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once(fn)
	local cn
	cn = self:Connect(function(...)
		if cn._connected then
			cn:Disconnect()
		end
		fn(...)
	end)
	return cn
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_, key)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return Signal


--!optimize 2
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local goodsignal = require(ReplicatedStorage.benches.libs.goodsignal)
local luausignal = require(ReplicatedStorage.signal)

return {
	Functions = {
		["luausignal constructor"] = function()
			local _s

			for i = 1, 500 do
				_s = luausignal()
			end
		end,

		["goodsignal constructor"] = function()
			local _s

			for i = 1, 500 do
				_s = goodsignal.new()
			end
		end,

		["luausignal connection"] = function()
			local sig = luausignal()
			local f = function() end

			for i = 1, 500 do
				sig:connect(f)
			end
		end,

		["goodsignal connection"] = function()
			local sig = goodsignal.new()
			local f = function() end

			for i = 1, 500 do
				sig:Connect(f)
			end
		end,

		["luausignal disconnection"] = function(Profiler: any)
			local sig = luausignal()
			local f = function() end
			local disconnects = {}

			for i = 1, 500 do
				disconnects[i] = sig:connect(f)
			end

			Profiler.Begin("disconnection")
			for i = 1, 500 do
				disconnects[i]()
			end
			Profiler.End()
		end,

		["goodsignal disconnection"] = function(Profiler: any)
			local sig = goodsignal.new()
			local f = function() end
			local disconnects = {}

			for i = 1, 500 do
				disconnects[i] = sig:Connect(f)
			end

			Profiler.Begin("disconnection")
			for i = 1, 500 do
				disconnects[i]:Disconnect()
			end
			Profiler.End()
		end,

		["luausignal firing (zero connections)"] = function()
			local sig = luausignal()

			for i = 1, 500 do
				sig:fire()
			end
		end,

		["goodsignal firing (zero connections)"] = function()
			local sig = goodsignal.new()

			for i = 1, 500 do
				sig:Fire()
			end
		end,

		["luausignal firing (five connections)"] = function()
			local sig = luausignal()
			for i = 1, 5 do
				sig:connect(function() end)
			end

			for i = 1, 500 do
				sig:fire()
			end
		end,

		["goodsignal firing (five connections)"] = function()
			local sig = goodsignal.new()
			for i = 1, 5 do
				sig:Connect(function() end)
			end

			for i = 1, 500 do
				sig:Fire()
			end
		end,
	},
}


local process = require("@lune/process")
local signal_spec = require("@tests/signal.spec")

-- if signal_spec then
-- 	process.exit(0)
-- else
-- 	process.exit(1)
-- end


--!optimize 2
--!native
local task = task or require("@lune/task")

local tspawn = task.spawn

local free_thread: thread? = nil

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

local error_tbl = {
	fire = deleted_signal_err,
	connect = deleted_signal_err,
	once = deleted_signal_err,
	wait = deleted_signal_err,
	disconnectAll = deleted_signal_err,
}

local function yield_loop()
	while true do
		local sig: InternalIdentity<any>, arg: any = coroutine.yield()

		local ref = free_thread
		free_thread = nil

		while sig._head ~= 0 do
			sig._head -= 1
			sig[sig._head + 1](arg)
		end

		free_thread = ref
	end
end

local signal = {}
signal.__index = signal
type InternalIdentity<T> = setmetatable<{ _head: number, [number]: (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({
		_head = 0,
	}, 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)

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

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

	return disconnecter
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>, arg: T)
	self._head = #self
	while self._head ~= 0 do
		if not free_thread then
			free_thread = tspawn(yield_loop)
		end

		-- Type states don't take into account assigning yet
		task.spawn(free_thread, self, arg)
	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(arg)
		assert(disconnect ~= nil, "Luau")
		disconnect()

		callback(arg)
	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(arg)
		assert(
			coroutine.status(running) == "suspended",
			":wait() called, then another thread resumed the waiting thread. Please dont do that :("
		)

		tspawn(running, arg)
	end)

	return coroutine.yield()
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, error_tbl)
end

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

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