local Base = require("../../objects/base")

local Helpers = {}

export type PyObject = Base.PyObject

local function isPyObject(value: any): boolean
	return type(value) == "table" and value.__type ~= nil and value.__typeobj ~= nil
end

local function coercePyObject(value: any): PyObject
	if isPyObject(value) then
		return value :: PyObject
	end
	return Base.ensurePyObject(value)
end

local function coerceSingletonCandidate(value: any): any
	if isPyObject(value) then
		return value
	end
	local valueType = typeof(value)
	if valueType == "boolean" then
		return Base.newBool(value)
	elseif value == nil then
		return Base.newNone()
	end
	return value
end

local function pyEquals(lhs: PyObject, rhs: PyObject): boolean
	if lhs == rhs then
		return true
	end
	local ok, method = pcall(function()
		return Base.getattr(lhs, "__eq__")
	end)
	if ok and method then
		local result = Base.call(method, { rhs })
		local resultObj = coercePyObject(result)
		return Base.truthy(resultObj)
	end
	if lhs.__type == rhs.__type then
		local lval = lhs.__value
		local rval = rhs.__value
		if lval ~= nil and rval ~= nil then
			return lval == rval
		end
	end
	return false
end

local function identityCompare(left: any, right: any): boolean
	if left == right then
		return true
	end
	local coercedLeft = coerceSingletonCandidate(left)
	local coercedRight = coerceSingletonCandidate(right)
	local leftIsPy = isPyObject(coercedLeft)
	local rightIsPy = isPyObject(coercedRight)
	if leftIsPy and rightIsPy then
		return coercedLeft == coercedRight
	end
	if leftIsPy or rightIsPy then
		return false
	end
	return coercedLeft == coercedRight
end

local function iterMembershipContains(container: PyObject, needle: PyObject): boolean
	local okIter, iterMethod = pcall(function()
		return Base.getattr(container, "__iter__")
	end)
	if not okIter or not iterMethod then
		error("TypeError: argument of type '" .. tostring(container.__type) .. "' is not iterable", 2)
	end
	local iterator = Base.call(iterMethod, {})
	local okNext, nextMethod = pcall(function()
		return Base.getattr(iterator, "__next__")
	end)
	if not okNext or not nextMethod then
		error("TypeError: argument of type '" .. tostring(container.__type) .. "' is not iterable", 2)
	end
	while true do
		local okItem, itemOrErr = pcall(function()
			return Base.call(nextMethod, {})
		end)
		if okItem then
			local itemObj = coercePyObject(itemOrErr)
			if pyEquals(itemObj, needle) then
				return true
			end
		else
			if type(itemOrErr) == "table" and (itemOrErr :: any).__type == "StopIteration" then
				return false
			end
			local errMessage = tostring(itemOrErr)
			if string.find(errMessage, "StopIteration", 1, true) ~= nil then
				return false
			end
			error(itemOrErr, 0)
		end
	end
end

local function membershipContains(needle: PyObject, container: PyObject): boolean
	local okContains, containsMethod = pcall(function()
		return Base.getattr(container, "__contains__")
	end)
	if okContains and containsMethod then
		local result = Base.call(containsMethod, { needle })
		local resultObj = coercePyObject(result)
		return Base.truthy(resultObj)
	end
	return iterMembershipContains(container, needle)
end

local compareOperations = {
	[0] = function(a: any, b: any): boolean
		return a < b
	end,
	[1] = function(a: any, b: any): boolean
		return a <= b
	end,
	[2] = function(a: any, b: any): boolean
		return a == b
	end,
	[3] = function(a: any, b: any): boolean
		return a ~= b
	end,
	[4] = function(a: any, b: any): boolean
		return a > b
	end,
	[5] = function(a: any, b: any): boolean
		return a >= b
	end,
	[6] = function(a: any, b: any): boolean
		return identityCompare(a, b)
	end,
	[7] = function(a: any, b: any): boolean
		return not identityCompare(a, b)
	end,
	[8] = function(a: any, b: any): boolean
		local needle = coercePyObject(a)
		local container = coercePyObject(b)
		return membershipContains(needle, container)
	end,
	[9] = function(a: any, b: any): boolean
		local needle = coercePyObject(a)
		local container = coercePyObject(b)
		return not membershipContains(needle, container)
	end,
}

local function unwrapPrimitive(value: any): any
	if type(value) == "table" and value.__type ~= nil then
		local t = value.__type
		if t == "NoneType" then
			return nil
		end
		local inner = value.__value
		if inner ~= nil then
			if t == "int" or t == "float" or t == "bool" or t == "str" then
				return inner
			end
		end
	end
	return value
end

Helpers.isPyObject = isPyObject
Helpers.coercePyObject = coercePyObject
Helpers.coerceSingletonCandidate = coerceSingletonCandidate
Helpers.pyEquals = pyEquals
Helpers.identityCompare = identityCompare
Helpers.iterMembershipContains = iterMembershipContains
Helpers.membershipContains = membershipContains
Helpers.compareOperations = compareOperations
Helpers.unwrapPrimitive = unwrapPrimitive

return Helpers


local Base = require("../../objects/base")

local Iteration = {}

local ITERATOR_TYPES = {
	list_iterator = true,
	tuple_iterator = true,
	set_iterator = true,
	dict_iterator = true,
	str_iterator = true,
}

export type PyObject = Base.PyObject

function Iteration.createIterator(iterable: any): any
	local obj = iterable
	if type(obj) ~= "table" or obj.__type == nil then
		obj = Base.ensurePyObject(iterable)
	end

	local objType = obj.__type
	if objType == "list" then
		local listValues = obj.__value :: { PyObject }
		return {
			__type = "list_iterator",
			__value = listValues,
			__index = 1,
			__length = #listValues,
		}
	elseif objType == "tuple" then
		local tupleValues = obj.__value :: { PyObject }
		return {
			__type = "tuple_iterator",
			__value = tupleValues,
			__index = 1,
			__length = #tupleValues,
		}
	elseif objType == "set" then
		local arr = {}
		for _, entry in pairs(obj.__value) do
			arr[#arr + 1] = entry.value
		end
		return {
			__type = "set_iterator",
			__value = arr,
			__index = 1,
			__length = #arr,
		}
	elseif objType == "dict" then
		local keys = {}
		for _, entry in pairs(obj.__value) do
			keys[#keys + 1] = entry.key
		end
		return {
			__type = "dict_iterator",
			__value = keys,
			__index = 1,
			__length = #keys,
		}
	elseif objType == "str" then
		local chars = {}
		local str = obj.__value :: string
		for i = 1, #str do
			chars[i] = Base.newPyObject("str", str:sub(i, i))
		end
		return {
			__type = "str_iterator",
			__value = chars,
			__index = 1,
			__length = #chars,
		}
	elseif objType == "range" then
		local data = obj.__value
		return {
			__type = "range_iterator",
			__value = {
				current = data.start,
				stop = data.stop,
				step = data.step,
			},
		}
	elseif objType == "generator" then
		return obj
	elseif objType and ITERATOR_TYPES[objType] then
		return obj
	end

	local okIter, iterMethod = pcall(function()
		return Base.getattr(obj, "__iter__")
	end)
	if okIter and iterMethod then
		return Base.call(iterMethod, {})
	end

	error("TypeError: '" .. tostring(objType or type(iterable)) .. "' object is not iterable", 2)
end

function Iteration.getNextValue(iterator: any): any?
	if type(iterator) == "table" then
		local iter = iterator :: any
		local iterType = iter.__type
		if iterType == "range_iterator" then
			local state = iter.__value
			local current = state.current
			local step = state.step
			local stop = state.stop
			if step > 0 then
				if current >= stop then
					return nil
				end
			else
				if current <= stop then
					return nil
				end
			end
			state.current = current + step
			return Base.newInt(current)
		elseif iter.__index and iter.__length and iter.__index <= iter.__length then
			local value = iter.__value[iter.__index]
			iter.__index = iter.__index + 1
			return value
		elseif iter.__typeobj ~= nil then
			local okNext, nextMethod = pcall(function()
				return Base.getattr(iter, "__next__")
			end)
			if okNext and nextMethod then
				local okCall, resultOrErr = pcall(function()
					return Base.call(nextMethod, {})
				end)
				if okCall then
					return resultOrErr
				else
					local err = resultOrErr
					if type(err) == "table" and err.__type == "StopIteration" then
						return nil
					end
					local errMessage = tostring(err)
					if string.find(errMessage, "StopIteration", 1, true) then
						return nil
					end
					error(err, 0)
				end
			end
		end
	end

	return nil
end

return Iteration


local FrameModule = require("../frame")

local State = {}

export type Frame = FrameModule.Frame

export type VMState = {
	current_frame: Frame?,
	call_stack: { Frame },
	globals: { [string]: any },
	builtins: { [string]: any },
	return_value: any,
}

function State.newState(globals: { [string]: any }?, builtins: { [string]: any }?): VMState
	return {
		current_frame = nil,
		call_stack = {},
		globals = globals or {},
		builtins = builtins or {},
		return_value = nil,
	}
end

return State


local Stack = {}

-- Value stack for VM operations
export type Stack = {
	values: { any },
	top: number,
}

-- Create a new stack
function Stack.new(): Stack
	return {
		values = {},
		top = 0,
	}
end

-- Push value onto stack
function Stack.push(stack: Stack, value: any)
	stack.top = stack.top + 1
	stack.values[stack.top] = value
end

-- Pop value from stack
function Stack.pop(stack: Stack): any
	if stack.top <= 0 then
		error("Stack underflow", 2)
	end

	local value = stack.values[stack.top]
	stack.values[stack.top] = nil
	stack.top = stack.top - 1
	return value
end

-- Truncate the stack to a specific size
function Stack.truncate(stack: Stack, targetTop: number)
	if targetTop < 0 then
		targetTop = 0
	end
	if targetTop < stack.top then
		for i = stack.top, math.max(targetTop + 1, 1), -1 do
			stack.values[i] = nil
		end
		stack.top = targetTop
	elseif targetTop > stack.top then
		error("Cannot increase stack size via truncate", 2)
	end
end

-- Peek at top value without popping
function Stack.peek(stack: Stack): any
	if stack.top <= 0 then
		error("Stack is empty", 2)
	end

	return stack.values[stack.top]
end

-- Get stack size
function Stack.size(stack: Stack): number
	return stack.top
end

-- Check if stack is empty
function Stack.isEmpty(stack: Stack): boolean
	return stack.top <= 0
end

-- Rotate top 2 stack items (equivalent to ROT_TWO)
function Stack.rotTwo(stack: Stack)
	if stack.top < 2 then
		error("Not enough items on stack for ROT_TWO", 2)
	end

	local a = stack.values[stack.top]
	local b = stack.values[stack.top - 1]

	stack.values[stack.top] = b
	stack.values[stack.top - 1] = a
end

-- Rotate top 3 stack items (equivalent to ROT_THREE)
function Stack.rotThree(stack: Stack)
	if stack.top < 3 then
		error("Not enough items on stack for ROT_THREE", 2)
	end

	local top = stack.values[stack.top]
	local second = stack.values[stack.top - 1]
	local third = stack.values[stack.top - 2]

	stack.values[stack.top] = second
	stack.values[stack.top - 1] = third
	stack.values[stack.top - 2] = top
end

-- Duplicate top stack item (equivalent to DUP_TOP)
function Stack.duplicate(stack: Stack)
	local value = Stack.peek(stack)
	Stack.push(stack, value)
end

-- Pop multiple values and return as array
function Stack.popN(stack: Stack, n: number): { any }
	if n <= 0 then
		return {}
	end

	if stack.top < n then
		error("Not enough items on stack to pop " .. n, 2)
	end

	local values: { any } = {}
	for i = 1, n do
		values[i] = Stack.pop(stack) -- Keep popped order (top first)
	end

	return values
end

-- Get item at relative position from top (0 = top, 1 = second from top, etc.)
function Stack.getRelative(stack: Stack, offset: number): any
	local index = stack.top - offset
	if index <= 0 or index > stack.top then
		error("Stack index out of bounds", 2)
	end
	return stack.values[index]
end

return Stack


--!strict
-- PyLua Test Framework
-- Simple testing utilities for PyLua development

local TestFramework = {}

-- Configuration (can be overridden via configure())
export type FrameworkConfig = {
	showPassed: boolean?, -- Print each passed assertion
	color: boolean?, -- Use ANSI colors
	showFailuresInline: boolean?, -- Show failure details immediately
	autoSummaryPerFile: boolean?, -- Automatically print summary after each suite (for single-file runs)
}

local config: FrameworkConfig = {
	showPassed = false,
	color = true,
	showFailuresInline = true,
	autoSummaryPerFile = true,
}

-- ANSI color helpers
local ANSI = {
	reset = "\27[0m",
	bold = "\27[1m",
	dim = "\27[2m",
	red = "\27[31m",
	green = "\27[32m",
	yellow = "\27[33m",
	cyan = "\27[36m",
}

local function colorize(code: string, text: string): string
	if not config.color then
		return text
	end
	return code .. text .. ANSI.reset
end

-- Test result tracking (assertion-level)
local totalAssertions = 0
local passedAssertions = 0
local failedAssertions = 0

-- Test case tracking
local totalTestCases = 0
local failedTestCases = 0

-- Failure records
type FailureRecord = { testName: string, message: string, trace: string }
local failures: { FailureRecord } = {}

-- Test suite definition
export type TestSuite = { name: string, tests: { TestCase } }

export type TestCase = {
	name: string,
	test: () -> (),
}

-- Assert functions
function TestFramework.assert(condition: boolean, message: string?)
	totalAssertions += 1
	if condition then
		passedAssertions += 1
		if config.showPassed then
			print(colorize(ANSI.green, "✓ " .. (message or "Assertion passed")))
		end
	else
		failedAssertions += 1
		local msg = message or "Assertion failed"
		if config.showFailuresInline then
			print(colorize(ANSI.red, "✗ " .. msg))
		end
		error(msg, 3) -- Raise error at the caller level
	end
end

function TestFramework.assertEqual(actual: any, expected: any, message: string?)
	local msg = message or ("Expected " .. tostring(expected) .. " but got " .. tostring(actual))
	TestFramework.assert(actual == expected, msg)
end

function TestFramework.assertNotEqual(actual: any, expected: any, message: string?)
	local msg = message or ("Expected not " .. tostring(expected) .. " but got " .. tostring(actual))
	TestFramework.assert(actual ~= expected, msg)
end

function TestFramework.assertType(value: any, expectedType: string, message: string?)
	local actualType = type(value)
	local msg = message or ("Expected type " .. expectedType .. " but got " .. actualType)
	TestFramework.assert(actualType == expectedType, msg)
end

function TestFramework.assertTrue(condition: boolean, message: string?)
	local msg = message or "Expected true but got false"
	TestFramework.assert(condition == true, msg)
end

function TestFramework.assertFalse(condition: boolean, message: string?)
	local msg = message or "Expected false but got true"
	TestFramework.assert(condition == false, msg)
end

function TestFramework.assertError(func: () -> any, message: string?)
	totalAssertions += 1
	local success, err = pcall(func)
	if success then
		failedAssertions += 1
		local msg = message or "Expected error but none occurred"
		if config.showFailuresInline then
			print(colorize(ANSI.red, "✗ " .. msg))
		end
		error(msg, 2)
	else
		passedAssertions += 1
		if config.showPassed then
			print(colorize(ANSI.green, "✓ " .. (message or ("Expected error occurred: " .. tostring(err)))))
		end
	end
end

-- Allow external configuration
function TestFramework.configure(userConfig: FrameworkConfig)
	for k, v in pairs(userConfig) do
		(config :: any)[k] = v
	end
end

-- Run a test suite
function TestFramework.runSuite(suite: TestSuite)
	print("\n" .. colorize(ANSI.cyan, "Suite: " .. suite.name))
	local line = {}
	for _, testCase in ipairs(suite.tests) do
		totalTestCases += 1
		local testName = testCase.name
		local success, err = pcall(function()
			testCase.test()
		end)
		if success then
			table.insert(line, config.showPassed and colorize(ANSI.green, "·") or ".")
		else
			failedTestCases += 1
			table.insert(line, colorize(ANSI.red, "F"))
			table.insert(failures, { testName = testName, message = tostring(err), trace = debug.traceback() })
			if config.showFailuresInline then
				print(colorize(ANSI.red, "\nFailure: " .. testName .. " -> " .. tostring(err)))
			end
		end
	end
	print(table.concat(line))
	if config.autoSummaryPerFile then
		-- Print incremental summary without resetting counts so a single file run is informative
		TestFramework.printSummary()
	end
end

-- Print test results summary
function TestFramework.printSummary()
	if #failures > 0 then
		print("\n" .. colorize(ANSI.red, "Failures (" .. tostring(#failures) .. "):"))
		for i, f in ipairs(failures) do
			print(colorize(ANSI.red, tostring(i) .. ") " .. f.testName .. ": " .. f.message))
		end
	end
	print("\n" .. colorize(ANSI.bold, "=== Test Summary ==="))
	print(string.format("Test cases: %d total, %d failed", totalTestCases, failedTestCases))
	print(
		string.format(
			"Assertions : %d total, %d passed, %d failed",
			totalAssertions,
			passedAssertions,
			failedAssertions
		)
	)
	if failedTestCases == 0 and failedAssertions == 0 then
		print(colorize(ANSI.green, "✓ All tests passed"))
	else
		print(colorize(ANSI.red, "✗ Some tests failed"))
	end
	return failedTestCases == 0 and failedAssertions == 0
end

-- Reset test counters
function TestFramework.reset()
	totalAssertions = 0
	passedAssertions = 0
	failedAssertions = 0
	totalTestCases = 0
	failedTestCases = 0
	failures = {}
end

return TestFramework


--!strict
-- PyLua Test Runner

local TestFramework = require("./framework")
-- Aggregate run: disable per-suite summary; final summary printed manually
TestFramework.configure({ showPassed = false, color = true, showFailuresInline = false, autoSummaryPerFile = false })

require("./suites/test_api")
require("./suites/test_ast")
require("./suites/test_lexer")
require("./suites/test_parser")
require("./suites/test_objects_base")
require("./suites/test_objects_builtins")
require("./suites/test_objects_collections")
require("./suites/test_builtins")
require("./suites/test_classes")
require("./suites/test_bytecode")
require("./suites/test_vm")
require("./suites/test_functions")
require("./suites/test_generators")
require("./suites/test_control_flow")
require("./suites/test_exceptions")
require("./suites/test_interop")
require("./suites/test_modules")
require("./suites/test_modules_advanced")
require("./suites/test_pep451")
require("./suites/test_error_reporting")

TestFramework.printSummary()


--!strict
-- PyLua API Tests
-- Test the main PyLua API interface

local TestFramework = require("../framework")
local PyLua = require("../../src/PyLua")

-- Test suite for PyLua API
local apiTests: TestFramework.TestSuite = {
	name = "PyLua API Tests",
	tests = {
		{
			name = "Create Runtime Instance",
			test = function()
				local python = PyLua.new()
				TestFramework.assertType(python, "table", "Runtime should be a table")

				-- Test with options
				local pythonWithOpts = PyLua.new({ debug = true, timeout = 10 })
				TestFramework.assertType(pythonWithOpts, "table", "Runtime with options should be a table")
			end,
		},

		{
			name = "Runtime Methods Exist",
			test = function()
				local python = PyLua.new()

				TestFramework.assertType(python.execute, "function", "execute method should exist")
				TestFramework.assertType(python.eval, "function", "eval method should exist")
				TestFramework.assertType(python.compile, "function", "compile method should exist")
				TestFramework.assertType(python.runBytecode, "function", "runBytecode method should exist")
				TestFramework.assertType(python.globals, "function", "globals method should exist")
				TestFramework.assertType(python.setGlobal, "function", "setGlobal method should exist")
				TestFramework.assertType(python.getGlobal, "function", "getGlobal method should exist")
			end,
		},

		{
			name = "Global Variable Access",
			test = function()
				local python = PyLua.new()

				-- Set and get global
				python:setGlobal("test_var", 42)
				local value = python:getGlobal("test_var")
				TestFramework.assertEqual(value, 42, "Global variable should be retrievable")

				-- Access globals table
				local globals = python:globals()
				TestFramework.assertEqual(globals.test_var, 42, "Global should be in globals table")
			end,
		},

		{
			name = "Eval Handles Function Calls",
			test = function()
				local python = PyLua.new()
				python:execute([[def add(a, b):
	return a + b
]])
				local result = python:eval("add(10, 32)")
				TestFramework.assertEqual(result, 42, "eval should compile and execute arbitrary expressions")
			end,
		},

		{
			name = "Version Information",
			test = function()
				TestFramework.assertType(PyLua.version, "string", "Version should be a string")
				TestFramework.assertType(PyLua.description, "string", "Description should be a string")

				-- Version should contain "0.3"
				TestFramework.assert(string.find(PyLua.version, "0.3") ~= nil, "Version should contain '0.3'")
			end,
		},
	},
}

TestFramework.runSuite(apiTests)

return {}


--!strict
-- PyLua AST System Tests
-- Phase 1.3 - Test AST node creation and visitor pattern

local TestFramework = require("../framework")
local nodes = require("../../src/PyLua/ast/nodes")
local visitor = require("../../src/PyLua/ast/visitor")

local astTests = {
	name = "PyLua AST Tests",
	tests = {
		{
			name = "Create base AST node with location info",
			test = function()
				local node = nodes.new_node("Test", 1, 0)
				TestFramework.assertEqual(node.type, "Test", "Node type set")
				TestFramework.assertEqual(node.lineno, 1, "Line number set")
				TestFramework.assertEqual(node.col_offset, 0, "Col offset set")
				TestFramework.assertTrue(node.end_lineno == nil, "End line nil")
				TestFramework.assertTrue(node.end_col_offset == nil, "End col nil")
			end,
		},
		{
			name = "Set end location information",
			test = function()
				local node = nodes.new_node("Test", 1, 0)
				nodes.set_end_location(node, 2, 10)
				TestFramework.assertEqual(node.end_lineno, 2, "End line set")
				TestFramework.assertEqual(node.end_col_offset, 10, "End col set")
			end,
		},
		{
			name = "Copy location information",
			test = function()
				local from_node = nodes.new_node("From", 5, 10)
				nodes.set_end_location(from_node, 6, 20)
				local to_node = nodes.new_node("To", 1, 0)
				nodes.copy_location(from_node, to_node)
				TestFramework.assertEqual(to_node.lineno, 5, "Copied line")
				TestFramework.assertEqual(to_node.col_offset, 10, "Copied col")
				TestFramework.assertEqual(to_node.end_lineno, 6, "Copied end line")
				TestFramework.assertEqual(to_node.end_col_offset, 20, "Copied end col")
			end,
		},
		{
			name = "Create visitor instance",
			test = function()
				local v = visitor.new()
				TestFramework.assertTrue(v ~= nil, "Visitor created")
				TestFramework.assertType(v.visit, "function", "visit exists")
				TestFramework.assertType(v.generic_visit, "function", "generic_visit exists")
			end,
		},
		{
			name = "Visit simple node via generic override",
			test = function()
				local v = visitor.new()
				local visited = {}
				function v:generic_visit(node)
					table.insert(visited, node.type)
					visitor.generic_visit(self, node)
					return nil
				end
				v:visit(nodes.new_node("TestNode", 1, 0))
				TestFramework.assertEqual(#visited, 1, "One visited")
				TestFramework.assertEqual(visited[1], "TestNode", "Visited TestNode")
			end,
		},
		{
			name = "Dispatch specific visit method",
			test = function()
				local v = visitor.new()
				local called = false
				function v:visit_Constant(node)
					called = true
					TestFramework.assertEqual(node.type, "Constant", "Constant node")
					return nil
				end
				local cn = nodes.new_node("Constant", 1, 0)
				local anyCn = cn :: any
				anyCn.value = 42
				v:visit(cn)
				TestFramework.assertTrue(called, "Specific visit called")
			end,
		},
		{
			name = "Create node collector",
			test = function()
				local collector = visitor.new_collector()
				TestFramework.assertTrue(collector ~= nil, "Collector created")
				TestFramework.assertType(collector.collect, "function", "collect exists")
				TestFramework.assertEqual(type(collector.collected), "table", "collected table exists")
			end,
		},
		{
			name = "Collect specific node types",
			test = function()
				local collector = visitor.new_collector()
				local testNode = nodes.new_node("TestType", 1, 0)
				local collected = collector:collect(testNode, { "TestType" })
				TestFramework.assertEqual(#collected, 1, "One collected")
				TestFramework.assertEqual(collected[1].type, "TestType", "Correct type collected")
			end,
		},
	},
}

TestFramework.runSuite(astTests)

return {}


--!strict
-- PyLua Bytecode / Compiler Tests (Phase 4.1 minimal subset)

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local instr = require("../../src/PyLua/bytecode/instructions")
local Collections = require("../../src/PyLua/objects/collections")
local Base = require("../../src/PyLua/objects/base")
local Interpreter = require("../../src/PyLua/vm/interpreter")

local function compileSource(src: string)
	local tokens = Lexer.tokenize(src)
	local ast = Parser.parse(tokens)
	return Compiler.compile(ast)
end

local bytecodeTests: TestFramework.TestSuite = {
	name = "Bytecode Compiler Tests",
	tests = {
		{
			name = "Compile constant expression at module level",
			test = function()
				local code = compileSource("42")
				-- Expect: LOAD_CONST 0; POP_TOP; LOAD_CONST <None>; RETURN_VALUE
				TestFramework.assertTrue(#code.bytecode >= 4, "At least 4 instructions")
				TestFramework.assertEqual(code.bytecode[1].opcode, "LOAD_CONST", "First LOAD_CONST")
				TestFramework.assertEqual(code.constants[1], 42, "Const 42 present (index 0)")
			end,
		},
		{
			name = "Compile simple assignment",
			test = function()
				local code = compileSource("x = 1")
				-- LOAD_CONST 0; STORE_NAME 0; LOAD_CONST None; RETURN_VALUE
				TestFramework.assertEqual(code.bytecode[1].opcode, "LOAD_CONST", "Load const")
				TestFramework.assertEqual(code.bytecode[2].opcode, "STORE_NAME", "Store name")
				TestFramework.assertEqual(code.names[1], "x", "Name x recorded")
			end,
		},
		{
			name = "Compile chained comparison emits short-circuit sequence",
			test = function()
				local code = compileSource("a < b < c")
				local opcodesSeen: { string } = {}
				for _, instruction in ipairs(code.bytecode) do
					opcodesSeen[#opcodesSeen + 1] = instruction.opcode
				end
				local joined = table.concat(opcodesSeen, ",")
				TestFramework.assertTrue(
					string.find(joined, "DUP_TOP", 1, true) ~= nil,
					"Should duplicate comparator for chaining"
				)
				TestFramework.assertTrue(
					string.find(joined, "ROT_THREE", 1, true) ~= nil,
					"Should rotate stack to preserve operands"
				)
				TestFramework.assertTrue(
					string.find(joined, "JUMP_IF_FALSE_OR_POP", 1, true) ~= nil,
					"Should short-circuit on failed comparison"
				)
				local compareCount = 0
				for _, opcode in ipairs(opcodesSeen) do
					if opcode == "COMPARE_OP" then
						compareCount += 1
					end
				end
				TestFramework.assertEqual(compareCount, 2, "Should emit COMPARE_OP for each comparator")
				TestFramework.assertTrue(
					string.find(joined, "ROT_TWO", 1, true) ~= nil,
					"Should rotate stack during cleanup"
				)
				TestFramework.assertTrue(
					string.find(joined, "POP_TOP", 1, true) ~= nil,
					"Should remove duplicated operand on early exit"
				)
			end,
		},
		{
			name = "Compile binary addition expression statement",
			test = function()
				local code = compileSource("1 + 2")
				-- LOAD_CONST a; LOAD_CONST b; BINARY_ADD; POP_TOP; LOAD_CONST None; RETURN_VALUE
				local ops = {}
				for _, ins in ipairs(code.bytecode) do
					table.insert(ops, ins.opcode)
				end
				local joined = table.concat(ops, ",")
				TestFramework.assertTrue(string.find(joined, "BINARY_ADD") ~= nil, "Contains BINARY_ADD")
			end,
		},
		{
			name = "Compile return inside function-like synthetic module wrapper",
			test = function()
				-- Simulate by directly compiling a Return node (not via parser for now)
				local returnAst = {
					type = "Module",
					body = {
						{
							type = "Return",
							value = { type = "Constant", value = 5, lineno = 1, col_offset = 0 },
							lineno = 1,
							col_offset = 0,
						},
					},
					lineno = 1,
					col_offset = 0,
				}
				local code = Compiler.compile(returnAst)
				local last = code.bytecode[#code.bytecode]
				TestFramework.assertEqual(last.opcode, "RETURN_VALUE", "Ends with RETURN_VALUE")
			end,
		},
		{
			name = "Instruction rendering utility",
			test = function()
				local code = compileSource("1 + 2")
				local rendered = instr.renderAll(code.bytecode)
				TestFramework.assertTrue(#rendered > 0, "Rendered lines produced")
			end,
		},
		{
			name = "Compile and execute bytes literal",
			test = function()
				-- Compile expression so result is returned
				local tokens = Lexer.tokenize("b'AB'")
				local exprAst = Parser.parseExpression(tokens)
				local code = Compiler.compile(exprAst)
				-- Execute via interpreter to realize bytes object
				local result = Interpreter.execute(code.bytecode, {}, {}, code.constants, code.names)
				TestFramework.assertType(result, "table", "Result should be PyObject")
				TestFramework.assertEqual(result.__type, "bytes", "Type should be bytes")
				TestFramework.assertEqual(result.__value, "AB", "Bytes content should match")
			end,
		},
		{
			name = "Compile list comprehension and execute",
			test = function()
				local tokens = Lexer.tokenize("[x for x in data]")
				local exprAst = Parser.parseExpression(tokens)
				local code = Compiler.compile(exprAst)
				local dataList = Collections.newList({ Base.newInt(1), Base.newInt(2), Base.newInt(3) })
				local globals = { data = dataList }
				local result = Interpreter.execute(code.bytecode, globals, {}, code.constants, code.names)
				TestFramework.assertType(result, "table", "Result should be PyObject list")
				TestFramework.assertEqual(result.__type, "list", "Result should be list")
				TestFramework.assertEqual(#result.__value, 3, "List should have three elements")
				TestFramework.assertEqual(result.__value[1].__value, 1, "First element should be 1")
				TestFramework.assertEqual(result.__value[3].__value, 3, "Last element should be 3")
				TestFramework.assertEqual(globals.x, nil, "Comprehension target should not leak to globals")
			end,
		},
		{
			name = "Compile floor division expression",
			test = function()
				local tokens = Lexer.tokenize("7 // 3")
				local exprAst = Parser.parseExpression(tokens)
				local code = Compiler.compile(exprAst)
				local seenFloorDiv = false
				for _, ins in ipairs(code.bytecode) do
					if ins.opcode == "BINARY_FLOOR_DIVIDE" then
						seenFloorDiv = true
						break
					end
				end
				TestFramework.assertTrue(seenFloorDiv, "Bytecode should include BINARY_FLOOR_DIVIDE")
				local result = Interpreter.execute(code.bytecode, {}, {}, code.constants, code.names)
				TestFramework.assertEqual(result, 2, "7 // 3 should evaluate to 2")
			end,
		},
		{
			name = "Compile matrix multiplication expression",
			test = function()
				local tokens = Lexer.tokenize("left @ right")
				local exprAst = Parser.parseExpression(tokens)
				local code = Compiler.compile(exprAst)
				local sawMatmul = false
				for _, ins in ipairs(code.bytecode) do
					if ins.opcode == "BINARY_MATRIX_MULTIPLY" then
						sawMatmul = true
						break
					end
				end
				TestFramework.assertTrue(sawMatmul, "Bytecode should include BINARY_MATRIX_MULTIPLY")
				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 result = Interpreter.execute(code.bytecode, globals, {}, code.constants, code.names)
				TestFramework.assertType(result, "table", "Result should be PyObject list")
				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[1].__value[2].__value, 22, "[0][1] should equal 22")
				TestFramework.assertEqual(result.__value[2].__value[1].__value, 43, "[1][0] should equal 43")
				TestFramework.assertEqual(result.__value[2].__value[2].__value, 50, "[1][1] should equal 50")
			end,
		},
	},
}

TestFramework.runSuite(bytecodeTests)

return {}


--!strict
-- Tests for Python class definitions and inheritance

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local Interpreter = require("../../src/PyLua/vm/interpreter")
local Builtins = require("../../src/PyLua/builtins/functions")

local function executeCode(code: string): { [string]: any }
	local tokens = Lexer.tokenize(code)
	local ast = Parser.parse(tokens)
	local bytecode = Compiler.compile(ast)
	local globals = {}
	local builtins = Builtins.createBuiltins()
	Interpreter.execute(bytecode.bytecode, globals, builtins, bytecode.constants, bytecode.names)
	return globals
end

local classTests: TestFramework.TestSuite = {
	name = "Class Tests",
	tests = {
		{
			name = "Class attributes accessible via instances",
			test = function()
				local globals = executeCode([[class Box:
    value = 5

box = Box()
inst_value = box.value
class_value = Box.value
]])
				TestFramework.assertEqual(globals.inst_value, 5, "Instance attribute should match class value")
				TestFramework.assertEqual(globals.class_value, 5, "Class attribute read should succeed")
			end,
		},
		{
			name = "__init__ and method binding",
			test = function()
				local globals = executeCode([[class Counter:
    def __init__(self, start):
        self.count = start
    def inc(self):
        self.count = self.count + 1

counter = Counter(3)
counter.inc()
after = counter.count
]])
				TestFramework.assert(globals.after ~= nil, "Counter should expose updated field")
				TestFramework.assertEqual(globals.after, 4, "Method should have incremented state")
			end,
		},
		{
			name = "Inheritance shares base methods",
			test = function()
				local globals = executeCode([[class Animal:
    def value(self):
        return 41

class Dog(Animal):
    pass

dog = Dog()
answer = dog.value()
]])
				TestFramework.assert(globals.answer ~= nil, "Method call should return value")
				TestFramework.assertEqual(globals.answer, 41, "Subclass should inherit base methods")
			end,
		},
		{
			name = "isinstance recognizes inheritance",
			test = function()
				local globals = executeCode([[class Base:
    pass

class Child(Base):
    pass

item = Child()
is_base = isinstance(item, Base)
is_child = isinstance(item, Child)
is_tuple = isinstance(item, (int, Child))
]])
				TestFramework.assertEqual(globals.is_base, true, "Child should be instance of Base")
				TestFramework.assertEqual(globals.is_child, true, "Child should be instance of Child")
				TestFramework.assertEqual(globals.is_tuple, true, "Tuple target should short-circuit on matching type")
			end,
		},
		{
			name = "Explicit object base is accepted",
			test = function()
				local globals = executeCode([[class Ident(object):
    pass

inst = Ident()
]])
				TestFramework.assert(globals.inst ~= nil, "Instance should be created when using explicit object base")
			end,
		},
		{
			name = "super() calls base __init__",
			test = function()
				local globals = executeCode([[class Base:
    def __init__(self, value):
        self.tag = "Base:" + value

class Child(Base):
    def __init__(self, value):
        super().__init__(value)
        self.tag = self.tag + "->Child"

child = Child("X")
result = child.tag
]])
				TestFramework.assertEqual(globals.result, "Base:X->Child", "super() should invoke parent __init__")
			end,
		},
		{
			name = "super explicit arguments",
			test = function()
				local globals = executeCode([[class Parent:
    def greet(self):
        return "parent"

class Kid(Parent):
    def greet(self):
        sup = super(Kid, self)
        return sup.greet() + " and child"

k = Kid()
greeting = k.greet()
]])
				TestFramework.assertEqual(globals.greeting, "parent and child", "super with explicit arguments should access base methods")
			end,
		},
	},
}

TestFramework.runSuite(classTests)

return {}


--!strict
-- Tests for control flow statements (if, while, for, break, continue)

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local Interpreter = require("../../src/PyLua/vm/interpreter")

-- Helper function to compile and execute Python code
local function executeCode(code: string): { [string]: any }
	local tokens = Lexer.tokenize(code)
	local ast = Parser.parse(tokens)
	local bytecode = Compiler.compile(ast)
	local globals = {}
	Interpreter.execute(bytecode.bytecode, globals, {}, bytecode.constants, bytecode.names)
	return globals
end

-- Test suite for control flow
local controlFlowTests: TestFramework.TestSuite = {
	name = "Control Flow Tests",
	tests = {
		{
			name = "If Statement - True Condition",
			test = function()
				local globals = executeCode([[
x = 5
if x > 3:
	result = "greater"
else:
	result = "not greater"
]])
				TestFramework.assertEqual(globals.result, "greater", "If condition true should execute if body")
			end,
		},

		{
			name = "If Statement - False Condition",
			test = function()
				local globals = executeCode([[
x = 2
if x > 3:
	result = "greater"
else:
	result = "not greater"
]])
				TestFramework.assertEqual(globals.result, "not greater", "If condition false should execute else body")
			end,
		},

		{
			name = "If Statement - Without Else",
			test = function()
				local globals = executeCode([[
x = 5
result = "initial"
if x > 3:
	result = "changed"
]])
				TestFramework.assertEqual(globals.result, "changed", "If without else should work")
			end,
		},

		{
			name = "If Statement - Elif Chain",
			test = function()
				local globals = executeCode([[
x = 5
if x < 0:
	result = "negative"
elif x < 5:
	result = "small"
elif x == 5:
	result = "equal"
else:
	result = "large"
]])
				TestFramework.assertEqual(globals.result, "equal", "Elif chain should select matching branch")
			end,
		},

		{
			name = "If Statement - Elif Else Fallback",
			test = function()
				local globals = executeCode([[
x = -1
if x > 0:
	result = "positive"
elif x == 0:
	result = "zero"
else:
	result = "negative"
]])
				TestFramework.assertEqual(globals.result, "negative", "Elif chain should fall back to else clause")
			end,
		},

		{
			name = "While Loop - Basic Execution",
			test = function()
				local globals = executeCode([[
x = 0
while x < 3:
	x = x + 1
]])
				TestFramework.assertEqual(globals.x, 3, "While loop should execute repeatedly")
			end,
		},

		{
			name = "While Loop - False Condition",
			test = function()
				local globals = executeCode([[
x = 5
while x < 3:
	x = x + 1
]])
				TestFramework.assertEqual(globals.x, 5, "While loop with false condition should not execute")
			end,
		},

		{
			name = "Pass Statement",
			test = function()
				local globals = executeCode([[
x = 5
if x > 3:
	pass
else:
	x = 0
]])
				TestFramework.assertEqual(globals.x, 5, "Pass statement should do nothing")
			end,
		},

		{
			name = "Compiler - If Statement Bytecode",
			test = function()
				local tokens = Lexer.tokenize("if x > 3:\n\ty = 1\nelse:\n\ty = 2")
				local ast = Parser.parse(tokens)
				local bytecode = Compiler.compile(ast)

				-- Check that bytecode contains control flow instructions
				local hasJump = false
				for _, instruction in ipairs(bytecode.bytecode) do
					if instruction.opcode == "POP_JUMP_IF_FALSE" or instruction.opcode == "JUMP_FORWARD" then
						hasJump = true
						break
					end
				end
				TestFramework.assertTrue(hasJump, "If statement should generate jump instructions")
			end,
		},

		{
			name = "Compiler - While Loop Bytecode",
			test = function()
				local tokens = Lexer.tokenize("while x < 5:\n\tx = x + 1")
				local ast = Parser.parse(tokens)
				local bytecode = Compiler.compile(ast)

				-- Check that bytecode contains loop control instructions
				local hasLoop = false
				for _, instruction in ipairs(bytecode.bytecode) do
					if instruction.opcode == "POP_JUMP_IF_FALSE" or instruction.opcode == "JUMP_FORWARD" then
						hasLoop = true
						break
					end
				end
				TestFramework.assertTrue(hasLoop, "While loop should generate loop instructions")
			end,
		},

		{
			name = "Compiler - For Loop Bytecode",
			test = function()
				local tokens = Lexer.tokenize("for i in [1, 2, 3]:\n    x = i")
				local ast = Parser.parse(tokens)
				local bytecode = Compiler.compile(ast)

				-- Check that bytecode contains iteration instructions
				local hasGetIter = false
				local hasForIter = false
				for _, instruction in ipairs(bytecode.bytecode) do
					if instruction.opcode == "GET_ITER" then
						hasGetIter = true
					elseif instruction.opcode == "FOR_ITER" then
						hasForIter = true
					end
				end
				TestFramework.assertTrue(hasGetIter, "For loop should generate GET_ITER instruction")
				TestFramework.assertTrue(hasForIter, "For loop should generate FOR_ITER instruction")
			end,
		},

		{
			name = "For Loop - Basic Execution",
			test = function()
				local globals = executeCode([[
total = 0
for i in [1, 2, 3]:
    total = total + i
]])
				TestFramework.assertEqual(globals.total, 6, "For loop should iterate over list elements")
			end,
		},

		{
			name = "For Loop - Empty List",
			test = function()
				local globals = executeCode([[
count = 0
for i in []:
    count = count + 1
]])
				TestFramework.assertEqual(globals.count, 0, "For loop over empty list should not execute body")
			end,
		},

		{
			name = "For Loop - Single Element",
			test = function()
				local globals = executeCode([[
result = 0
for i in [42]:
    result = i
]])
				TestFramework.assertEqual(globals.result, 42, "For loop over single element should work")
			end,
		},

		{
			name = "For Loop - Variable Scope",
			test = function()
				local globals = executeCode([[
for i in [1, 2, 3]:
    pass
result = i
]])
				TestFramework.assertEqual(globals.result, 3, "Loop variable should be accessible after loop ends")
			end,
		},

		{
			name = "For Loop - Nested Loops",
			test = function()
				local globals = executeCode([[
total = 0
for i in [1, 2]:
    for j in [10, 20]:
        total = total + i + j
]])
				-- Expected: (1+10) + (1+20) + (2+10) + (2+20) = 11 + 21 + 12 + 22 = 66
				TestFramework.assertEqual(globals.total, 66, "Nested for loops should work correctly")
			end,
		},

		{
			name = "For Loop - Different Data Types",
			test = function()
				local globals = executeCode([[
result = 0
for i in [1, 2.5, 3]:
    result = result + i
]])
				TestFramework.assertEqual(globals.result, 6.5, "For loop should work with mixed number types")
			end,
		},

		{
			name = "Break Statement - For Loop",
			test = function()
				local globals = executeCode([[
result = 0
for i in [1, 2, 3, 4, 5]:
    if i == 3:
        break
    result = result + i
]])
				TestFramework.assertEqual(globals.result, 3, "Break should exit loop early") -- 1 + 2 = 3
			end,
		},

		{
			name = "Continue Statement - For Loop",
			test = function()
				local globals = executeCode([[
result = 0
for i in [1, 2, 3, 4, 5]:
    if i == 3:
        continue
    result = result + i
]])
				TestFramework.assertEqual(globals.result, 12, "Continue should skip iteration") -- 1 + 2 + 4 + 5 = 12
			end,
		},

		{
			name = "Break Statement - While Loop",
			test = function()
				local globals = executeCode([[
x = 0
while True:
    x = x + 1
    if x >= 3:
        break
]])
				TestFramework.assertEqual(globals.x, 3, "Break should exit while loop")
			end,
		},

		{
			name = "Continue Statement - While Loop",
			test = function()
				local globals = executeCode([[
x = 0
result = 0
while x < 5:
    x = x + 1
    if x == 3:
        continue
    result = result + x
]])
				TestFramework.assertEqual(globals.result, 12, "Continue should skip while iteration") -- 1 + 2 + 4 + 5 = 12
			end,
		},

		{
			name = "Nested Loops with Break",
			test = function()
				local globals = executeCode([[
result = 0
for i in [1, 2, 3]:
    for j in [10, 20, 30]:
        if j == 20:
            break
        result = result + i + j
]])
				-- Expected: (1+10) + (2+10) + (3+10) = 11 + 12 + 13 = 36
				TestFramework.assertEqual(globals.result, 36, "Break should only exit inner loop")
			end,
		},

		{
			name = "Nested Loops with Continue",
			test = function()
				local globals = executeCode([[
result = 0
for i in [1, 2]:
    for j in [10, 20, 30]:
        if j == 20:
            continue
        result = result + i + j
]])
				-- Expected: (1+10) + (1+30) + (2+10) + (2+30) = 11 + 31 + 12 + 32 = 86
				TestFramework.assertEqual(globals.result, 86, "Continue should only skip inner iteration")
			end,
		},

		{
			name = "For Loop - Tuple Destructuring (Parenthesized)",
			test = function()
				local globals = executeCode([[
result = []
for (x, y) in [(1, 2), (3, 4), (5, 6)]:
    result.append(x + y)
]])
				TestFramework.assertEqual(globals.result.__value[1].__value, 3, "First tuple should unpack to 1+2=3")
				TestFramework.assertEqual(globals.result.__value[2].__value, 7, "Second tuple should unpack to 3+4=7")
				TestFramework.assertEqual(globals.result.__value[3].__value, 11, "Third tuple should unpack to 5+6=11")
			end,
		},

		{
			name = "For Loop - Tuple Destructuring (Implicit)",
			test = function()
				local globals = executeCode([[
result = []
for x, y in [(1, 2), (3, 4)]:
    result.append(x * y)
]])
				TestFramework.assertEqual(globals.result.__value[1].__value, 2, "First tuple should unpack to 1*2=2")
				TestFramework.assertEqual(globals.result.__value[2].__value, 12, "Second tuple should unpack to 3*4=12")
			end,
		},

		{
			name = "For Loop - Triple Tuple Destructuring",
			test = function()
				local globals = executeCode([[
result = 0
for x, y, z in [(1, 2, 3), (4, 5, 6)]:
    result = result + x + y + z
]])
				-- Expected: (1+2+3) + (4+5+6) = 6 + 15 = 21
				TestFramework.assertEqual(globals.result, 21, "Triple tuple destructuring should work")
			end,
		},

		{
			name = "For Loop - Nested Tuple Destructuring",
			test = function()
				local globals = executeCode([[
result = []
for (a, b), c in [((1, 2), 3), ((4, 5), 6)]:
    result.append(a + b + c)
]])
				-- Expected: (1+2+3) = 6, (4+5+6) = 15
				TestFramework.assertEqual(globals.result.__value[1].__value, 6, "First nested tuple should unpack correctly")
				TestFramework.assertEqual(globals.result.__value[2].__value, 15, "Second nested tuple should unpack correctly")
			end,
		},
	},
}

TestFramework.runSuite(controlFlowTests)

return {}


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

local errorTests = {
	name = "Error Reporting Tests",
	tests = {
		{
			name = "Parse Error with Line Number (Indentation)",
			test = function()
				local code = "x = 1\nif x:\n    y = 2\n  z = 3" -- Indentation error at line 4
				local tokens = Lexer.tokenize(code)

                local success, err = pcall(function()
                    Parser.parse(tokens)
                end)

                TestFramework.assertFalse(success, "Should have raised an error")
                local errStr = tostring(err)
                -- print("Error message: " .. errStr)

                local hasLine = string.find(errStr, "line 4")
                TestFramework.assert(hasLine ~= nil, "Error message should contain 'line 4'. Got: " .. errStr)
			end,
		},
        {
            name = "Unexpected Token Error with Line Number",
            test = function()
                local code = "x = (\n    1 +\n    * 2\n)"
                -- Line 1: x = (
                -- Line 2:     1 +
                -- Line 3:     * 2
                -- Error should be at line 3 (STAR unexpected)

                local tokens = Lexer.tokenize(code)
                local success, err = pcall(function()
                    Parser.parse(tokens)
                end)

                TestFramework.assertFalse(success, "Should have raised an error")
                local errStr = tostring(err)
                -- print("Error message: " .. errStr)

                local hasLine = string.find(errStr, "line 3")
                TestFramework.assert(hasLine ~= nil, "Error message should contain 'line 3'. Got: " .. errStr)
            end
        }
	},
}

TestFramework.runSuite(errorTests)

return errorTests


--!strict
-- Tests for exception handling (try/except)

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local Interpreter = require("../../src/PyLua/vm/interpreter")
local BuiltinFunctions = require("../../src/PyLua/builtins/functions")

local function executeCode(code: string): { [string]: any }
	local tokens = Lexer.tokenize(code)
	local ast = Parser.parse(tokens)
	local bytecode = Compiler.compile(ast)
	local globals = {}
	local builtins = BuiltinFunctions.createBuiltins()
	Interpreter.execute(bytecode.bytecode, globals, builtins, bytecode.constants, bytecode.names)
	return globals
end

local exceptionTests: TestFramework.TestSuite = {
	name = "Exception Handling",
	tests = {
		{
			name = "ValueError caught with alias",
			test = function()
				local globals = executeCode([[
result = "unset"
err_type = None
try:
	int("abc")
except ValueError as err:
	result = str(err)
	err_type = type(err)
]])
				local expectedMessage = "ValueError: invalid literal for int() with base 10"
				local actualMessage = globals.result
				local debugMessage = "ValueError message should propagate into handler (got '" .. tostring(actualMessage) .. "')"
				TestFramework.assertEqual(actualMessage, expectedMessage, debugMessage)
				local typeObj = globals.err_type
				TestFramework.assertType(typeObj, "table", "type() should return a PyObject table")
				TestFramework.assertEqual(typeObj.__value.name, "ValueError", "type(err) should report ValueError")
			end,
		},
		{
			name = "Else block executes when no exception",
			test = function()
				local globals = executeCode([[
result = "unset"
try:
	x = 5
except ValueError:
	result = "caught"
else:
	result = "no error"
]])
				TestFramework.assertEqual(globals.result, "no error", "Else clause should run when try succeeds")
			end,
		},
		{
			name = "Ordered handlers evaluate sequentially",
			test = function()
				local globals = executeCode([[
result = "unset"
try:
	int("abc")
except TypeError:
	result = "type"
except ValueError:
	result = "value"
]])
				TestFramework.assertEqual(globals.result, "value", "Matching handler should execute in order")
			end,
		},
		{
			name = "Bare except captures NameError",
			test = function()
				local globals = executeCode([[
result = "unset"
try:
	missing_variable
except:
	result = "caught"
]])
				TestFramework.assertEqual(globals.result, "caught", "Bare except should catch NameError")
			end,
		},
		{
			name = "Unhandled exception propagates",
			test = function()
				TestFramework.assertError(function()
					return executeCode([[
try:
	int("abc")
except TypeError:
	result = "type"
]])
				end, "Unhandled exceptions should propagate out of try")
			end,
		},
		{
			name = "Tuple handler captures ValueError",
			test = function()
				local globals = executeCode([[
result = "unset"
try:
	int("abc")
except (TypeError, ValueError) as err:
	result = str(err)
]])
				local expected = "ValueError: invalid literal for int() with base 10"
				TestFramework.assertEqual(globals.result, expected, "Tuple except should match any provided type")
			end,
		},
		{
			name = "Finally executes on success",
			test = function()
				local globals = executeCode([[
log = ""
try:
	log += "body"
finally:
	log += "finally"
]])
				TestFramework.assertEqual(globals.log, "bodyfinally", "Finally block should run after successful try")
			end,
		},
		{
			name = "Finally executes before exception handler",
			test = function()
				local globals = executeCode([[
log = ""
try:
	try:
		int("abc")
	finally:
		log += "F"
except ValueError:
	log += "E"
]])
				TestFramework.assertEqual(globals.log, "FE", "Finally should run before matching except handler")
			end,
		},
		{
			name = "Finally executes before return",
			test = function()
				local globals = executeCode([[
log = []
def run():
	try:
		log.append("body")
		return "done"
	finally:
		log.append("finally")
result = run()
]])
				TestFramework.assertEqual(globals.result, "done", "Function should return original value")
				local logObj = globals.log
				TestFramework.assertType(logObj, "table", "log should be a PyObject list")
				local entries = logObj.__value
				TestFramework.assertEqual(#entries, 2, "Finally should record entry even on return")
				TestFramework.assertEqual(entries[2].__value, "finally", "Finally entry should be appended after body entry")
			end,
		},
		{
			name = "Finally executes on break",
			test = function()
				local globals = executeCode([[
log = ""
for item in [1, 2, 3]:
	try:
		log += "L"
		break
	finally:
		log += "F"
]])
				TestFramework.assertEqual(globals.log, "LF", "Finally should execute even when breaking out of loop")
			end,
		},
	},
}

TestFramework.runSuite(exceptionTests)

return {}


--!strict
-- Tests for Python function support

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local Interpreter = require("../../src/PyLua/vm/interpreter")
local Builtins = require("../../src/PyLua/builtins/functions")

-- Helper function to compile and execute Python code
local function executeCode(code: string): { [string]: any }
	local tokens = Lexer.tokenize(code)
	local ast = Parser.parse(tokens)
	local bytecode = Compiler.compile(ast)
	local globals = {}
	local builtins = Builtins.createBuiltins()
	Interpreter.execute(bytecode.bytecode, globals, builtins, bytecode.constants, bytecode.names)
	return globals
end

-- Test suite for functions
local functionTests: TestFramework.TestSuite = {
	name = "Function Tests",
	tests = {
		{
			name = "Simple Function Definition",
			test = function()
				local globals = executeCode([[
def test_func():
	pass

result = test_func
]])
				-- Function should be defined in globals
				TestFramework.assert(globals.test_func ~= nil, "Function should be defined")
				TestFramework.assert(globals.result ~= nil, "Function assignment should work")
			end,
		},

		{
			name = "Function with Return Value",
			test = function()
				local globals = executeCode([[
def get_value():
	return 42

result = get_value()
]])
				TestFramework.assertEqual(globals.result, 42, "Function should return correct value")
			end,
		},

		{
			name = "Function with Arguments",
			test = function()
				local globals = executeCode([[
def add_numbers(a, b):
	return a + b

result = add_numbers(3, 4)
]])
				TestFramework.assertEqual(globals.result, 7, "Function should handle arguments correctly")
			end,
		},

		{
			name = "Function with Local Variables",
			test = function()
				local globals = executeCode([[
def calculate():
	x = 10
	y = 20
	return x + y

result = calculate()
]])
				TestFramework.assertEqual(globals.result, 30, "Function should handle local variables")
				-- Local variables should not leak to global scope
				TestFramework.assert(globals.x == nil, "Local variable x should not be in global scope")
				TestFramework.assert(globals.y == nil, "Local variable y should not be in global scope")
			end,
		},
		{
			name = "Multiple Function Calls",
			test = function()
				local globals = executeCode([[
def double(x):
	return x * 2

result1 = double(5)
result2 = double(10)
]])
				TestFramework.assertEqual(globals.result1, 10, "First function call should work")
				TestFramework.assertEqual(globals.result2, 20, "Second function call should work")
			end,
		},

		{
			name = "Lambda Basic Invocation",
			test = function()
				local globals = executeCode([[result = (lambda x: x + 1)(5)]])
				TestFramework.assertEqual(globals.result, 6, "Lambda should compute expression body")
			end,
		},

		{
			name = "Lambda With Default Argument",
			test = function()
				local globals = executeCode([[f = lambda x, y=2: x * y
result1 = f(3)
result2 = f(3, 4)]])
				TestFramework.assertEqual(globals.result1, 6, "Lambda should use default when omitted")
				TestFramework.assertEqual(globals.result2, 12, "Lambda should use explicit argument")
			end,
		},

		{
			name = "Function Calling Function",
			test = function()
				local globals = executeCode([[
def helper(x):
	return x + 1

def main(x):
	return helper(x) * 2

result = main(5)
]])
				TestFramework.assertEqual(globals.result, 12, "Function should be able to call other functions")
			end,
		},

		{
			name = "Function with Single Default Argument",
			test = function()
				local globals = executeCode([[
def greet(name, greeting="Hello"):
	return greeting

result1 = greet("Alice", "Hi")
result2 = greet("Bob")
]])
				TestFramework.assertEqual(globals.result1, "Hi", "Explicit argument should override default")
				TestFramework.assertEqual(globals.result2, "Hello", "Should use default argument value")
			end,
		},

		{
			name = "Function with Multiple Default Arguments",
			test = function()
				local globals = executeCode([[
def add(a, b=5, c=10):
	return a + b + c

result1 = add(1, 2, 3)
result2 = add(1, 2)
result3 = add(1)
]])
				TestFramework.assertEqual(globals.result1, 6, "All arguments explicitly provided")
				TestFramework.assertEqual(globals.result2, 13, "Should use default for c")
				TestFramework.assertEqual(globals.result3, 16, "Should use defaults for b and c")
			end,
		},

		{
			name = "Default Arguments with Expressions",
			test = function()
				local globals = executeCode([[
def compute(x, y=2*3):
	return x * y

result1 = compute(5)
result2 = compute(5, 10)
]])
				TestFramework.assertEqual(globals.result1, 30, "Should evaluate default expression (2*3=6, 5*6=30)")
				TestFramework.assertEqual(globals.result2, 50, "Explicit argument should override default expression")
			end,
		},

		{
			name = "Function with All Defaults",
			test = function()
				local globals = executeCode([[
def get_value(x=42):
	return x

result1 = get_value()
result2 = get_value(100)
]])
				TestFramework.assertEqual(globals.result1, 42, "Should use default when no argument provided")
				TestFramework.assertEqual(globals.result2, 100, "Should use provided argument")
			end,
		},

		{
			name = "F-string basic interpolation",
			test = function()
				local globals = executeCode([[
name = "World"
message = f"Hello, {name}!"
]])
				TestFramework.assertEqual(globals.message, "Hello, World!", "F-string should interpolate expressions")
			end,
		},

		{
			name = "F-string conversions and format spec",
			test = function()
				local globals = executeCode([[
value = 3.14159
converted = f"{value!r} -> {value:.2f}"
]])
				TestFramework.assertEqual(globals.converted, "3.14159 -> 3.14", "F-string should honor conversions and format specs")
			end,
		},

		{
			name = "F-string escaped braces",
			test = function()
				local globals = executeCode([[
count = 5
text = f"{{count}} = {count}"
]])
				TestFramework.assertEqual(globals.text, "{count} = 5", "Double braces should escape braces in f-strings")
			end,
		},
	},
}

TestFramework.runSuite(functionTests)

return {}


--!strict
-- Tests for generator functions and yield semantics

local TestFramework = require("../framework")
local Lexer = require("../../src/PyLua/lexer")
local Parser = require("../../src/PyLua/parser")
local Compiler = require("../../src/PyLua/compiler")
local Interpreter = require("../../src/PyLua/vm/interpreter")
local Builtins = require("../../src/PyLua/builtins/functions")

local function executeCode(code: string): { [string]: any }
	local tokens = Lexer.tokenize(code)
	local ast = Parser.parse(tokens)
	local bytecode = Compiler.compile(ast)
	local globals = {}
	local builtins = Builtins.createBuiltins()
	Interpreter.execute(bytecode.bytecode, globals, builtins, bytecode.constants, bytecode.names)
	return globals
end

local generatorTests: TestFramework.TestSuite = {
	name = "Generator Tests",
	tests = {
		{
			name = "Basic generator yields",
			test = function()
				local globals = executeCode([[
def counter():
    yield 1
    yield 2
    yield 3

values = []
gen = counter()
values.append(next(gen))
values.append(next(gen))
values.append(next(gen))
try:
    next(gen)
    exhausted = False
except StopIteration:
    exhausted = True
]])
				local values = globals.values
				TestFramework.assert(values ~= nil, "Expected results list to exist")
				local listObj = values :: any
				local inner = listObj.__value :: { any }
				TestFramework.assertEqual(#inner, 3, "Generator should yield exactly three values")
				TestFramework.assertEqual(inner[1].__value, 1, "First yield should be 1")
				TestFramework.assertEqual(inner[2].__value, 2, "Second yield should be 2")
				TestFramework.assertEqual(inner[3].__value, 3, "Third yield should be 3")
				TestFramework.assert(globals.exhausted == true, "Generator should raise StopIteration when exhausted")
			end,
		},
		{
			name = "next builtin uses default",
			test = function()
				local globals = executeCode([[
def empty():
    if False:
        yield None

g = empty()
value = next(g, 123)
]])
				TestFramework.assertEqual(globals.value, 123, "next() should return provided default when iterator is exhausted")
			end,
		},
		{
			name = "Generator iterates in for loop",
			test = function()
				local globals = executeCode([[
def letters():
    yield "a"
    yield "b"
    yield "c"

seen = []
for value in letters():
    seen.append(value)
]])
				local seen = globals.seen
				TestFramework.assert(seen ~= nil, "Expected collected values list")
				local data = seen.__value :: { any }
				TestFramework.assertEqual(#data, 3, "For loop should exhaust generator")
				TestFramework.assertEqual(data[1].__value, "a", "First entry should match first yield")
				TestFramework.assertEqual(data[2].__value, "b", "Second entry should match second yield")
				TestFramework.assertEqual(data[3].__value, "c", "Third entry should match third yield")
			end,
		},
		{
			name = "Generator return value surfaces on StopIteration",
			test = function()
				local globals = executeCode([[
def gen():
    yield "step"
    return 99

g = gen()
first = next(g)
status = "not raised"
final = None
try:
    next(g)
except StopIteration as exc:
    status = "raised"
    final = exc.value
]])
				TestFramework.assertEqual(globals.first, "step", "First yielded value should be captured")
				TestFramework.assertEqual(globals.status, "raised", "Generator should raise StopIteration when exhausted")
				TestFramework.assertEqual(globals.final, 99, "StopIteration.value should expose generator return value")
			end,
		},
	},
}

TestFramework.runSuite(generatorTests)

return {}


--!strict
-- Interop tests ensure Luau ↔ Python conversions behave as expected

local TestFramework = require("../framework")
local PyLua = require("../../src/PyLua")
local Bridge = require("../../src/PyLua/interop/bridge")

local interopSuite: TestFramework.TestSuite = {
	name = "Interop Tests",
	tests = {
		{
			name = "Calls Luau Function From Python",
			test = function()
				local python = PyLua.new()
				local globals = python:globals()
				globals.multiply = function(a: number, b: number)
					return a * b
				end
				python:execute("result = multiply(6, 7)")
				local result = python:getGlobal("result")
				TestFramework.assertEqual(result, 42, "Python should call Luau function and receive Luau number")
			end,
		},
		{
			name = "Converts Luau Tables To Python Lists",
			test = function()
				local python = PyLua.new()
				local globals = python:globals()
				globals.numbers = { 1, 2, 3, 4 }
				python:execute("total = sum(numbers)")
				local total = python:getGlobal("total")
				TestFramework.assertEqual(total, 10, "Luau table should behave like Python list")
				local roundTrip = globals.numbers
				TestFramework.assertType(roundTrip, "table", "Proxy should expose Luau table")
				TestFramework.assertEqual(roundTrip[1], 1, "Proxy should preserve values")
			end,
		},
		{
			name = "Returns Python Collections As Luau Tables",
			test = function()
				local python = PyLua.new()
				python:execute([[py_list = [1, 2, 3]
py_dict = {'a': 1, 'b': 2}
]])
				local pyList = python:getGlobal("py_list")
				TestFramework.assertType(pyList, "table", "Python list should convert to Luau table")
				TestFramework.assertEqual(#pyList, 3, "Converted list keeps length")
				TestFramework.assertEqual(pyList[2], 2, "Converted list keeps ordering")
				local pyDict = python:getGlobal("py_dict")
				TestFramework.assertType(pyDict, "table", "Python dict should convert to Luau table")
				TestFramework.assertEqual(pyDict.a, 1, "Dict keys convert to Luau keys")
			end,
		},
		{
			name = "Propagates Luau Errors As Python Exceptions",
			test = function()
				local python = PyLua.new()
				local globals = python:globals()
				globals.fail = function()
					error("ValueError: failing", 0)
				end
				local ok, err = pcall(function()
					python:execute("fail()")
				end)
				TestFramework.assertFalse(ok, "Luau error should surface as Python exception")
				TestFramework.assertTrue(string.find(tostring(err), "ValueError") ~= nil, "Error message should include ValueError")
			end,
		},
		{
			name = "Binds Luau Tables As Python Objects",
			test = function()
				Bridge.clearClassBindings()
				Bridge.registerClassBinding({
					pyTypeName = "Widget",
					predicate = function(value: any): boolean
						return type(value) == "table" and value.__kind == "Widget"
					end,
				})
				local python = PyLua.new()
				local widget = {
					__kind = "Widget",
					value = 5,
					increment = function(self: any, amount: number)
						self.value += amount
						return self.value
					end,
				}
				python:setGlobal("widget", widget)
				python:execute([[before = widget.value
after = widget.increment(7)
widget.value = after + 1]])
				local before = python:getGlobal("before")
				local after = python:getGlobal("after")
				TestFramework.assertEqual(before, 5, "Python should see initial attribute value")
				TestFramework.assertEqual(after, 12, "Method binding should return updated value")
				TestFramework.assertEqual(widget.value, 13, "Python assignment should update Luau table")
				local roundTrip = python:getGlobal("widget")
				TestFramework.assertEqual(roundTrip, widget, "Proxy should roundtrip original table")
			end,
		},
		{
			name = "Supports Non Self-Bound Methods",
			test = function()
				Bridge.clearClassBindings()
				Bridge.registerClassBinding({
					pyTypeName = "Utility",
					predicate = function(value: any): boolean
						return type(value) == "table" and value.__kind == "Utility"
					end,
					bindSelf = false,
				})
				local python = PyLua.new()
				local utility = {
					__kind = "Utility",
					double = function(amount: number)
						return amount * 2
					end,
				}
				python:setGlobal("utility", utility)
				python:execute("result = utility.double(6)")
				local result = python:getGlobal("result")
				TestFramework.assertEqual(result, 12, "bindSelf=false should avoid injecting table")
				local roundTrip = python:getGlobal("utility")
				TestFramework.assertEqual(roundTrip, utility, "Utility should roundtrip to original table")
			end,
		},
		{
			name = "Supports Custom Attribute Setters",
			test = function()
				Bridge.clearClassBindings()
				Bridge.registerClassBinding({
					pyTypeName = "Counter",
					predicate = function(value: any): boolean
						return type(value) == "table" and value.__kind == "Counter"
					end,
					setAttr = function(target, attr, newValue)
						if attr == "value" then
							target.value = math.floor(newValue)
						else
							target[attr] = newValue
						end
					end,
				})
				local python = PyLua.new()
				local counter = {
					__kind = "Counter",
					value = 0,
				}
				python:setGlobal("counter", counter)
				python:execute("counter.value = 7.9")
				TestFramework.assertEqual(counter.value, 7, "Custom setter should coerce values")
			end,
		},
	},
}

TestFramework.runSuite(interopSuite)

return {}


--!strict
-- PyLua Lexer Tests
-- Test the Python tokenization functionality

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

local function dedentsBeforeDef(tokens: { any }, targetDefIndex: number): number
	local defsSeen = 0
	local dedents = 0
	for _, token in ipairs(tokens) do
		if token.type == "DEF" then
			defsSeen += 1
			if defsSeen == targetDefIndex then
				break
			end
		elseif token.type == "DEDENT" then
			dedents += 1
		end
	end

	return dedents
end

-- Test suite for PyLua Lexer
local lexerTests: TestFramework.TestSuite = {
	name = "PyLua Lexer Tests",
	tests = {
		{
			name = "Simple Number Tokenization",
			test = function()
				local tokens = Lexer.tokenize("42")
				TestFramework.assertEqual(#tokens, 2, "Should have 2 tokens (number + endmarker)")
				TestFramework.assertEqual(tokens[1].type, "NUMBER", "First token should be NUMBER")
				TestFramework.assertEqual(tokens[1].value, "42", "Number value should be '42'")
				TestFramework.assertEqual(tokens[2].type, "ENDMARKER", "Second token should be ENDMARKER")
			end,
		},

		{
			name = "Float Number Tokenization",
			test = function()
				local tokens = Lexer.tokenize("3.14")
				TestFramework.assertEqual(tokens[1].type, "NUMBER", "Should tokenize float")
				TestFramework.assertEqual(tokens[1].value, "3.14", "Float value should be correct")
			end,
		},

		{
			name = "Scientific Notation",
			test = function()
				local tokens = Lexer.tokenize("1e5")
				TestFramework.assertEqual(tokens[1].type, "NUMBER", "Should tokenize scientific notation")
				TestFramework.assertEqual(tokens[1].value, "1e5", "Scientific notation value should be correct")
			end,
		},

		{
			name = "Binary Number",
			test = function()
				local tokens = Lexer.tokenize("0b1010")
				TestFramework.assertEqual(tokens[1].type, "NUMBER", "Should tokenize binary number")
				TestFramework.assertEqual(tokens[1].value, "0b1010", "Binary value should be correct")
			end,
		},

		{
			name = "Hex Number",
			test = function()
				local tokens = Lexer.tokenize("0xFF")
				TestFramework.assertEqual(tokens[1].type, "NUMBER", "Should tokenize hex number")
				TestFramework.assertEqual(tokens[1].value, "0xFF", "Hex value should be correct")
			end,
		},

		{
			name = "String Tokenization",
			test = function()
				local tokens = Lexer.tokenize('"hello"')
				TestFramework.assertEqual(tokens[1].type, "STRING", "Should tokenize string")
				TestFramework.assertEqual(tokens[1].value, '"hello"', "String value should include quotes")
			end,
		},

		{
			name = "Prefixed Strings (u/b)",
			test = function()
				local t1 = Lexer.tokenize("u'hello'")
				TestFramework.assertEqual(t1[1].type, "STRING", "u'' is STRING")
				TestFramework.assertEqual((t1[1] :: any).stringPrefix, "u", "prefix is u")
				local t2 = Lexer.tokenize("b'hi'")
				TestFramework.assertEqual(t2[1].type, "STRING", "b'' is STRING")
				TestFramework.assertEqual((t2[1] :: any).stringPrefix, "b", "prefix is b")
				local t3 = Lexer.tokenize("br'raw'")
				TestFramework.assertEqual((t3[1] :: any).stringPrefix, "br", "prefix is br")
			end,
		},

		{
			name = "F-string Tokenization",
			test = function()
				local tokens = Lexer.tokenize('f"hello {name}"')
				TestFramework.assertEqual(tokens[1].type, "STRING", "Should tokenize f-string as STRING")
				TestFramework.assertEqual((tokens[1] :: any).stringPrefix, "f", "Prefix should include f")
				TestFramework.assertEqual(
					(tokens[1] :: any).stringContent,
					"hello {name}",
					"Content should keep braces"
				)
			end,
		},

		{
			name = "Single Quote String",
			test = function()
				local tokens = Lexer.tokenize("'world'")
				TestFramework.assertEqual(tokens[1].type, "STRING", "Should tokenize single quote string")
				TestFramework.assertEqual(tokens[1].value, "'world'", "String value should include quotes")
			end,
		},

		{
			name = "Triple Quote String",
			test = function()
				local tokens = Lexer.tokenize('"""multi\nline"""')
				TestFramework.assertEqual(tokens[1].type, "STRING", "Should tokenize triple quote string")
				TestFramework.assertTrue(tokens[1].value:find('"""') ~= nil, "Should contain triple quotes")
			end,
		},

		{
			name = "Identifier Tokenization",
			test = function()
				local tokens = Lexer.tokenize("variable_name")
				TestFramework.assertEqual(tokens[1].type, "NAME", "Should tokenize identifier")
				TestFramework.assertEqual(tokens[1].value, "variable_name", "Identifier value should be correct")
			end,
		},

		{
			name = "Keyword Tokenization",
			test = function()
				local tokens = Lexer.tokenize("if elif else")
				TestFramework.assertEqual(tokens[1].type, "IF", "Should tokenize 'if' keyword")
				TestFramework.assertEqual(tokens[2].type, "ELIF", "Should tokenize 'elif' keyword")
				TestFramework.assertEqual(tokens[3].type, "ELSE", "Should tokenize 'else' keyword")
			end,
		},

		{
			name = "Boolean Keywords",
			test = function()
				local tokens = Lexer.tokenize("True False None")
				TestFramework.assertEqual(tokens[1].type, "TRUE", "Should tokenize True")
				TestFramework.assertEqual(tokens[2].type, "FALSE", "Should tokenize False")
				TestFramework.assertEqual(tokens[3].type, "NONE", "Should tokenize None")
			end,
		},

		{
			name = "Operators",
			test = function()
				local tokens = Lexer.tokenize("+ - * / % ** //")
				TestFramework.assertEqual(tokens[1].type, "PLUS", "Should tokenize +")
				TestFramework.assertEqual(tokens[2].type, "MINUS", "Should tokenize -")
				TestFramework.assertEqual(tokens[3].type, "STAR", "Should tokenize *")
				TestFramework.assertEqual(tokens[4].type, "SLASH", "Should tokenize /")
				TestFramework.assertEqual(tokens[5].type, "PERCENT", "Should tokenize %")
				TestFramework.assertEqual(tokens[6].type, "DOUBLESTAR", "Should tokenize **")
				TestFramework.assertEqual(tokens[7].type, "DOUBLESLASH", "Should tokenize //")
			end,
		},

		{
			name = "Comparison Operators",
			test = function()
				local tokens = Lexer.tokenize("== != < > <= >=")
				TestFramework.assertEqual(tokens[1].type, "EQEQUAL", "Should tokenize ==")
				TestFramework.assertEqual(tokens[2].type, "NOTEQUAL", "Should tokenize !=")
				TestFramework.assertEqual(tokens[3].type, "LESS", "Should tokenize <")
				TestFramework.assertEqual(tokens[4].type, "GREATER", "Should tokenize >")
				TestFramework.assertEqual(tokens[5].type, "LESSEQUAL", "Should tokenize <=")
				TestFramework.assertEqual(tokens[6].type, "GREATEREQUAL", "Should tokenize >=")
			end,
		},

		{
			name = "Delimiters",
			test = function()
				local tokens = Lexer.tokenize("()[]{},:;.")
				TestFramework.assertEqual(tokens[1].type, "LPAR", "Should tokenize (")
				TestFramework.assertEqual(tokens[2].type, "RPAR", "Should tokenize )")
				TestFramework.assertEqual(tokens[3].type, "LSQB", "Should tokenize [")
				TestFramework.assertEqual(tokens[4].type, "RSQB", "Should tokenize ]")
				TestFramework.assertEqual(tokens[5].type, "LBRACE", "Should tokenize {")
				TestFramework.assertEqual(tokens[6].type, "RBRACE", "Should tokenize }")
				TestFramework.assertEqual(tokens[7].type, "COMMA", "Should tokenize ,")
				TestFramework.assertEqual(tokens[8].type, "COLON", "Should tokenize :")
				TestFramework.assertEqual(tokens[9].type, "SEMICOLON", "Should tokenize ;")
				TestFramework.assertEqual(tokens[10].type, "DOT", "Should tokenize .")
			end,
		},

		{
			name = "Assignment",
			test = function()
				local tokens = Lexer.tokenize("x = 42")
				TestFramework.assertEqual(tokens[1].type, "NAME", "Should tokenize variable name")
				TestFramework.assertEqual(tokens[2].type, "EQUAL", "Should tokenize =")
				TestFramework.assertEqual(tokens[3].type, "NUMBER", "Should tokenize number")
			end,
		},

		{
			name = "Newlines",
			test = function()
				local tokens = Lexer.tokenize("a\nb")
				TestFramework.assertEqual(tokens[1].type, "NAME", "First token should be NAME")
				TestFramework.assertEqual(tokens[2].type, "NEWLINE", "Should tokenize newline")
				TestFramework.assertEqual(tokens[3].type, "NAME", "Third token should be NAME")
			end,
		},

		{
			name = "Complex Expression",
			test = function()
				local tokens = Lexer.tokenize("result = (x + y) * 2")
				TestFramework.assertEqual(tokens[1].type, "NAME", "Should start with identifier")
				TestFramework.assertEqual(tokens[2].type, "EQUAL", "Should have assignment")
				TestFramework.assertEqual(tokens[3].type, "LPAR", "Should have opening paren")
				-- Test basic structure without checking every token
				TestFramework.assertTrue(#tokens > 8, "Should have multiple tokens for complex expression")
			end,
		},

		{
			name = "Token Positions",
			test = function()
				local tokens = Lexer.tokenize("abc")
				TestFramework.assertEqual(tokens[1].line, 1, "Should be on line 1")
				TestFramework.assertEqual(tokens[1].column, 1, "Should start at column 1")
				TestFramework.assertEqual(tokens[1].endColumn, 3, "Should end at column 3")
			end,
		},

		{
			name = "Blank indented lines do not dedent",
			test = function()
				local source = table.concat({
					"class Person:",
					"    def __init__(self):",
					"        pass",
					"    ",
					"    def greet(self):",
					"        pass",
					"",
				}, "\n")

				local tokens = Lexer.tokenize(source)
				local dedents = dedentsBeforeDef(tokens, 2)
				TestFramework.assertEqual(dedents, 1, "Only the method body should DEDENT before the second method definition")
			end,
		},

		{
			name = "Blank lines without spaces do not dedent",
			test = function()
				local source = table.concat({
					"class Foo:",
					"    def bar(self):",
					"        pass",
					"",
					"    def baz(self):",
					"        pass",
				}, "\n")

				local tokens = Lexer.tokenize(source)
				local dedents = dedentsBeforeDef(tokens, 2)
				TestFramework.assertEqual(dedents, 1, "Only the inner block should DEDENT before the next method")
			end,
		},

		{
			name = "Comment-only lines do not dedent",
			test = function()
				local source = table.concat({
					"class Foo:",
					"    # Comment",
					"    def bar(self):",
					"        pass",
				}, "\n")

				local tokens = Lexer.tokenize(source)
				local dedents = dedentsBeforeDef(tokens, 1)
				TestFramework.assertEqual(dedents, 0, "Comment line should not collapse class indentation")
			end,
		},
	},
}

TestFramework.runSuite(lexerTests)

return {}


local TestFramework = require("../framework")
local PyLua = require("../../src/PyLua")

local modulesTests: TestFramework.TestSuite = {
	name = "Modules",
	tests = {
		{
			name = "import sys",
			test = function()
				local py = PyLua.new()
				py:execute("import sys")
				local sysPy = py:getGlobalPy("sys")
				TestFramework.assertEqual(sysPy.__type, "module", "sys should be a module")

				-- Check sys.modules
				py:execute("modules = sys.modules")
				local modules = py:getGlobalPy("modules")
				TestFramework.assertEqual(modules.__type, "dict", "sys.modules should be a dict")
			end,
		},
		{
			name = "import sys as s",
			test = function()
				local py = PyLua.new()
				py:execute("import sys as s")
				local s = py:getGlobalPy("s")
				TestFramework.assertEqual(s.__type, "module", "s should be a module")

				local sys = py:getGlobalPy("sys")
				TestFramework.assertEqual(sys, nil, "sys should not be in globals")
			end,
		},
		{
			name = "from sys import modules",
			test = function()
				local py = PyLua.new()
				py:execute("from sys import modules")
				local modules = py:getGlobalPy("modules")
				TestFramework.assertEqual(modules.__type, "dict", "modules should be a dict")
			end,
		},
		{
			name = "from sys import modules as m",
			test = function()
				local py = PyLua.new()
				py:execute("from sys import modules as m")
				local m = py:getGlobalPy("m")
				TestFramework.assertEqual(m.__type, "dict", "m should be a dict")
			end,
		},
		{
			name = "sys.path exists",
			test = function()
				local py = PyLua.new()
				py:execute("import sys")
				local path = py:eval("sys.path")
				TestFramework.assertEqual(path.__type, "list", "sys.path should be a list")
			end,
		},
		{
			name = "sys.path configuration",
			test = function()
				local py = PyLua.new({ searchPath = { "/foo", "/bar" } })
				py:execute("import sys")
				local path = py:eval("sys.path")
				TestFramework.assertEqual(path.__type, "list", "sys.path should be a list")
				local values = path.__value
				TestFramework.assertEqual(#values, 2, "sys.path should have 2 elements")
				TestFramework.assertEqual(values[1].__value, "/foo", "first element should be /foo")
				TestFramework.assertEqual(values[2].__value, "/bar", "second element should be /bar")
			end,
		},
		{
			name = "import non_existent",
			test = function()
				local py = PyLua.new()
				TestFramework.assertError(function()
					py:execute("import non_existent")
					return nil
				end, "ModuleNotFoundError")
			end,
		},
	},
}

TestFramework.runSuite(modulesTests)

return {}


local TestFramework = require("../framework")
local PyLua = require("../../src/PyLua")

local advancedModulesTests = {
	name = "Advanced Modules",
	tests = {
		{
			name = "Native module import",
			test = function()
				local nativeMod = {
					foo = 42,
					bar = function(x: number)
						return x * 2
					end,
				}

				local py = PyLua.new({
					nativeModules = {
						["my_native"] = nativeMod,
					},
				})

				py:execute([[
import my_native
x = my_native.foo
y = my_native.bar(10)
]])
				TestFramework.assertEqual(py:getGlobal("x"), 42)
				TestFramework.assertEqual(py:getGlobal("y"), 20)
			end,
		},
		{
			name = "Virtual filesystem import (file)",
			test = function()
				local files = {
					["/lib/mymod.py"] = "x = 100\ndef get_x(): return x",
				}

				local mockFs = {
					readFile = function(path)
						return files[path]
					end,
					exists = function(path)
						return files[path] ~= nil
					end,
					isDir = function(path)
						return false
					end,
				}

				local py = PyLua.new({
					searchPath = { "/lib" },
					fileSystem = mockFs,
					enableFilesystem = true,
				})

				py:execute([[
import mymod
val_x = mymod.x
]])
				TestFramework.assertEqual(py:getGlobal("val_x"), 100)
			end,
		},
		{
			name = "Virtual filesystem import (package)",
			test = function()
				local files = {
					["/lib/pkg/__init__.py"] = "y = 200",
				}

				local mockFs = {
					readFile = function(path)
						return files[path]
					end,
					exists = function(path)
						if path == "/lib/pkg" then
							return true
						end
						return files[path] ~= nil
					end,
					isDir = function(path)
						return path == "/lib/pkg"
					end,
				}

				local py = PyLua.new({
					searchPath = { "/lib" },
					fileSystem = mockFs,
					enableFilesystem = true,
				})

				py:execute([[
import pkg
val_y = pkg.y
]])
				TestFramework.assertEqual(py:getGlobal("val_y"), 200)
			end,
		},
	},
}

TestFramework.runSuite(advancedModulesTests)

return {}


--!strict
-- PyLua Object System Tests - Phase 3.1 Base Object System

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

local tests: TestFramework.TestSuite = {
	name = "PyLua Base Object System",
	tests = {
		{
			name = "Create primitive objects via ensurePyObject",
			test = function()
				local i = base.ensurePyObject(5)
				TestFramework.assertEqual(i.__type, "int", "Integers infer int type")
				local f = base.ensurePyObject(3.14)
				TestFramework.assertEqual(f.__type, "float", "Floats infer float type")
				local s = base.ensurePyObject("hi")
				TestFramework.assertEqual(s.__type, "str", "Strings infer str type")
				local bTrue = base.ensurePyObject(true)
				TestFramework.assertEqual(bTrue.__type, "bool", "Boolean true type")
				local none = base.ensurePyObject(nil)
				TestFramework.assertEqual(none.__type, "NoneType", "nil maps to NoneType")
			end,
		},
		{
			name = "Register custom type and create object",
			test = function()
				local t = base.registerType("Custom", {})
				local obj = base.newPyObject("Custom", {})
				TestFramework.assertEqual(obj.__type, "Custom", "Custom object has type")
				local snap = base.typeRegistrySnapshot()
				TestFramework.assertTrue(snap.Custom == true, "Type present in registry")
			end,
		},
		{
			name = "Set and get simple attribute",
			test = function()
				local obj = base.newPyObject("object", {})
				local val = base.ensurePyObject(10)
				base.setattr(obj, "x", val)
				local attr = base.getattr(obj, "x")
				TestFramework.assertEqual(attr.__type, "int", "Attribute retrieved")
				TestFramework.assertTrue(base.hasattr(obj, "x"), "hasattr true")
				TestFramework.assertFalse(base.hasattr(obj, "missing"), "hasattr false")
			end,
		},
		{
			name = "Method resolution and bound method call",
			test = function()
				-- Register a type with a method
				local t = base.registerType("Point", {
					methods = {
						magnitude = function(selfObj)
							-- selfObj.__value assumed table with x,y
							local data: any = selfObj.__value
							local mag = math.sqrt(
								(data.x :: number) * (data.x :: number) + (data.y :: number) * (data.y :: number)
							)
							-- Force float representation even if integer
							return base.newFloat(mag)
						end,
					},
				})
				local p = base.newPyObject("Point", { x = 3, y = 4 })
				local magMethod = base.getattr(p, "magnitude")
				TestFramework.assertEqual(magMethod.__type, "builtin_function_or_method", "Bound method type")
				local result = base.call(magMethod, {})
				TestFramework.assertTrue(result.__type == "float" or result.__type == "int", "Numeric result type")
				TestFramework.assertEqual(math.floor(result.__value + 0.5), 5, "Magnitude 5")
			end,
		},
		{
			name = "__getattr fallback mechanism",
			test = function()
				local dynType = base.registerType("Dynamic", {
					__getattr = function(selfObj, name: string)
						if name == "forty_two" then
							return base.ensurePyObject(42)
						end
						return nil :: any
					end,
				})
				local d = base.newPyObject("Dynamic", {})
				local v = base.getattr(d, "forty_two")
				TestFramework.assertEqual(v.__type, "int", "Dynamic attribute produced")
				TestFramework.assertError(function(): any
					return base.getattr(d, "other")
				end, "Missing dynamic attr errors")
			end,
		},
		{
			name = "__setattr hook",
			test = function()
				local storage: { [string]: any } = {}
				local hookType = base.registerType("Hooked", {
					__setattr = function(selfObj, name: string, value)
						storage[name] = value.__value
					end,
				})
				local h = base.newPyObject("Hooked", {})
				base.setattr(h, "a", base.ensurePyObject(7))
				TestFramework.assertEqual(storage.a, 7, "Custom setattr executed")
				-- getattr should still error (not stored in dict)
				TestFramework.assertError(function(): any
					return base.getattr(h, "a")
				end, "Attribute not actually in dict")
			end,
		},
		{
			name = "Calling non-callable raises error",
			test = function()
				local obj = base.newPyObject("object", {})
				TestFramework.assertError(function(): any
					return base.call(obj, {})
				end, "Non callable should error")
			end,
		},
	},
}

TestFramework.runSuite(tests)

return {}


--!strict
-- Tests for Phase 3.2 Built-in Types (numbers, strings, booleans, None, type objects placeholder)

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

local suite: TestFramework.TestSuite = {
	name = "PyLua Built-in Types",
	tests = {
		{
			name = "None singleton",
			test = function()
				local n1 = builtins.None()
				local n2 = builtins.None()
				TestFramework.assertTrue(n1 == n2, "None should be singleton reference")
				TestFramework.assertEqual(n1.__type, "NoneType", "Type is NoneType")
				TestFramework.assertFalse(base.truthy(n1), "None is falsy")
			end,
		},
		{
			name = "Boolean values",
			test = function()
				local t = builtins.True()
				local f = builtins.False()
				TestFramework.assertEqual(t.__type, "bool", "True type bool")
				TestFramework.assertTrue(base.truthy(t), "True truthy")
				TestFramework.assertFalse(base.truthy(f), "False falsy")
			end,
		},
		{
			name = "Int arithmetic",
			test = function()
				local a = builtins.Int(2)
				local b = builtins.Int(3)
				local sum = base.operate("add", a, b)
				TestFramework.assertEqual(sum.__value, 5, "2 + 3 = 5")
				local prod = base.operate("mul", a, b)
				TestFramework.assertEqual(prod.__value, 6, "2 * 3 = 6")
				local sub = base.operate("sub", b, a)
				TestFramework.assertEqual(sub.__value, 1, "3 - 2 = 1")
			end,
		},
		{
			name = "Float promotion",
			test = function()
				local a = builtins.Int(2)
				local b = builtins.Float(0.5)
				local result = base.operate("add", a, b)
				TestFramework.assertEqual(result.__type, "float", "int + float -> float")
				TestFramework.assertEqual(result.__value, 2.5, "2 + 0.5 = 2.5")
			end,
		},
		{
			name = "Division yields float",
			test = function()
				local a = builtins.Int(5)
				local b = builtins.Int(2)
				local div = base.operate("truediv", a, b)
				TestFramework.assertEqual(div.__type, "float", "Division returns float")
				TestFramework.assertEqual(div.__value, 2.5, "5 / 2 = 2.5")
			end,
		},
		{
			name = "String concatenation and repetition",
			test = function()
				local s1 = builtins.Str("ab")
				local s2 = builtins.Str("cd")
				local cat = base.operate("add", s1, s2)
				TestFramework.assertEqual(cat.__value, "abcd", "Concatenate")
				local times = base.operate("mul", s1, builtins.Int(3))
				TestFramework.assertEqual(times.__value, "ababab", "Repeat 3x")
			end,
		},
		{
			name = "String indexing",
			test = function()
				local s = builtins.Str("hi")
				local getter = base.getattr(s, "__getitem__")
				local first = base.call(getter, { builtins.Int(0) })
				TestFramework.assertEqual(first.__type, "str", "Indexing should return str")
				TestFramework.assertEqual(first.__value, "h", "First character should be 'h'")
				local last = base.call(getter, { builtins.Int(-1) })
				TestFramework.assertEqual(last.__value, "i", "Negative index should wrap from end")
				TestFramework.assertError(function(): any
					base.call(getter, { builtins.Int(2) })
					return nil
				end, "Out of range index should error")
			end,
		},

		{
			name = "builtins.bytes basic construction",
			test = function()
				local b1 = builtins.Bytes("")
				TestFramework.assertEqual(b1.__type, "bytes", "empty bytes failed")
				TestFramework.assertEqual(b1.__value, "", "empty bytes value failed")
				local b2 = builtins.Bytes({ 65, 66, 67 })
				TestFramework.assertEqual(b2.__value, "ABC", "bytes from table failed")
				local b3 = builtins.Bytes("\255\001")
				TestFramework.assertEqual(#b3.__value, 2, "bytes from string failed")
			end,
		},

		{
			name = "builtins.bytes len and getitem",
			test = function()
				local b = builtins.Bytes({ 0, 1, 2, 255 })
				local len = base.call(base.getattr(b, "__len__"), {})
				TestFramework.assertEqual(len.__type, "int", "bytes __len__ type failed")
				TestFramework.assertEqual(len.__value, 4, "bytes __len__ failed")
				local get0 = base.call(base.getattr(b, "__getitem__"), { builtins.Int(0) })
				TestFramework.assertEqual(get0.__type, "int", "getitem 0 type failed")
				TestFramework.assertEqual(get0.__value, 0, "getitem 0 failed")
				local getLast = base.call(base.getattr(b, "__getitem__"), { builtins.Int(-1) })
				TestFramework.assertEqual(getLast.__value, 255, "getitem -1 failed")
			end,
		},

		{
			name = "builtins.bytes eq and repr",
			test = function()
				local b1 = builtins.Bytes({ 65, 66 })
				local b2 = builtins.Bytes("AB")
				local eq = base.call(base.getattr(b1, "__eq__"), { b2 })
				TestFramework.assertEqual(eq.__type, "bool", "bytes __eq__ type failed")
				TestFramework.assertEqual(eq.__value, true, "bytes __eq__ failed")
				local r = base.call(base.getattr(b1, "__repr__"), {})
				TestFramework.assertEqual(r.__type, "str", "bytes __repr__ type failed")
				TestFramework.assertEqual(r.__value, "b'AB'", "bytes __repr__ basic failed")
			end,
		},

		{
			name = "builtins.bytes add and mul",
			test = function()
				local a = builtins.Bytes("A")
				local b = builtins.Bytes("B")
				local cat = base.operate("add", a, b)
				TestFramework.assertEqual(cat.__type, "bytes", "bytes add type")
				TestFramework.assertEqual(cat.__value, "AB", "bytes add value")
				local rep = base.operate("mul", a, builtins.Int(3))
				TestFramework.assertEqual(rep.__value, "AAA", "bytes mul value")
			end,
		},
		{
			name = "Truthiness basics",
			test = function()
				TestFramework.assertFalse(base.truthy(builtins.Str("")), "Empty string falsy")
				TestFramework.assertTrue(base.truthy(builtins.Str("x")), "Non-empty string truthy")
				TestFramework.assertFalse(base.truthy(builtins.Int(0)), "Zero int falsy")
				TestFramework.assertTrue(base.truthy(builtins.Int(7)), "Non-zero int truthy")
			end,
		},
		{
			name = "Error on unsupported operand",
			test = function()
				local a = builtins.Int(1)
				TestFramework.assertError(function(): any
					return base.operate("add", a, builtins.Str("x"))
				end, "int + str should error")
			end,
		},
	},
}

TestFramework.runSuite(suite)

return {}