package tests.wurstscript.tests;

import com.google.common.base.Charsets;
import com.google.common.io.Files;
import org.wurstscript.projectconfig.WurstProjectConfigData;
import de.peeeq.wurstio.WurstCompilerJassImpl;
import de.peeeq.wurstscript.RunArgs;
import de.peeeq.wurstscript.ast.WurstModel;
import de.peeeq.wurstscript.gui.WurstGui;
import de.peeeq.wurstscript.gui.WurstGuiCliImpl;
import de.peeeq.wurstscript.luaAst.LuaCompilationUnit;
import de.peeeq.wurstscript.validation.GlobalCaches;
import org.testng.annotations.Ignore;
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.testng.AssertJUnit.*;


public class LuaTranslationTests extends WurstScriptTest {

    private void assertFunctionReturns(String output, String functionName, String returnValue) {
        /*
        The function functionName must only return returnValue and do nothing else.
         */
        Pattern pattern = Pattern.compile("function\\s+" + functionName + "\\(.*?\\)\\s+return\\s+" + returnValue + "\\s+end");
        Matcher matcher = pattern.matcher(output);
        assertTrue("Function " + functionName + " with return value " + returnValue + " was not found.", matcher.find());
    }

    private void assertFunctionCall(String output, String functionName, String arguments) {
        /*
        The function declaration is ignored by using negative lookbehind.
        All function calls must use the specified arguments.
         */
        Pattern pattern = Pattern.compile("(?<!\\sfunction\\s)" + functionName + "\\((.*)\\)");
        Matcher matcher = pattern.matcher(output);
        boolean findAtLeastOne = false;
        while (matcher.find()) {
            assertEquals(arguments, matcher.group(1));
            findAtLeastOne = true;
        }
        assertTrue("Function call to function " + functionName + " with arguments (" + arguments + ") was not found.", findAtLeastOne);
    }

    private void assertFunctionBodyContains(String output, String functionName, String search, boolean mustContain) {
        Pattern pattern = Pattern.compile("function\\s*" + functionName + "\\s*\\(.*\\).*\\n" + "((?:\\n|.)*?)end");
        Matcher matcher = pattern.matcher(output);
        boolean found = false;
        while (matcher.find()) {
            found = true;
            String body = matcher.group(1);
            if(!body.contains(search) && mustContain) {
                fail("Function " + functionName + " must contain " + search + ".");
            }
            if(body.contains(search) && !mustContain) {
                fail("Function " + functionName + " must not contain " + search + ".");
            }
        }
        assertTrue("Function " + functionName + " was not found.", found);
    }

    /** Skip the calling test when selective GetHandleId shimming is disabled. */
    private static void requireSelectiveGetHandleIdShimming() {
        if (!de.peeeq.wurstscript.translation.imtranslation.LuaNativeLowering.ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING) {
            throw new org.testng.SkipException("Skipped: selective GetHandleId shimming is disabled (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING=false)");
        }
    }

    private String getFunctionBody(String output, String functionName) {
        Pattern pattern = Pattern.compile("function\\s*" + functionName + "\\s*\\(.*\\).*\\n" + "((?:\\n|.)*?)end");
        Matcher matcher = pattern.matcher(output);
        if (!matcher.find()) {
            fail("Function " + functionName + " was not found.");
        }
        return matcher.group(1);
    }

    private void assertDoesNotContainRegex(String output, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(output);
        assertFalse("Pattern must not occur: " + regex, matcher.find());
    }

    private void assertContainsRegex(String output, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(output);
        assertTrue("Pattern must occur: " + regex, matcher.find());
    }

    private void assertOccursBefore(String output, String first, String second) {
        int firstPos = output.indexOf(first);
        int secondPos = output.indexOf(second);
        assertTrue("Expected to find: " + first, firstPos >= 0);
        assertTrue("Expected to find: " + second, secondPos >= 0);
        assertTrue("Expected '" + first + "' before '" + second + "'.", firstPos < secondPos);
    }

    private List<String> uniqueMatches(String output, String regex, int group) {
        Matcher matcher = Pattern.compile(regex).matcher(output);
        List<String> result = new ArrayList<>();
        while (matcher.find()) {
            String value = matcher.group(group);
            if (!result.contains(value)) {
                result.add(value);
            }
        }
        return result;
    }

    private String singleMatch(String output, String regex, int group) {
        Matcher matcher = Pattern.compile(regex).matcher(output);
        assertTrue("Expected pattern to occur: " + regex, matcher.find());
        String result = matcher.group(group);
        assertFalse("Expected exactly one match for pattern: " + regex, matcher.find());
        return result;
    }

    private List<String> nonBaseSubclassBindings(String output, String baseName, String slotName) {
        Matcher matcher = Pattern.compile("([A-Za-z0-9_]+)\\." + Pattern.quote(slotName) + "\\s*=\\s*[A-Za-z0-9_]+").matcher(output);
        List<String> result = new ArrayList<>();
        while (matcher.find()) {
            String owner = matcher.group(1);
            if (!owner.equals(baseName) && owner.startsWith(baseName + "_") && !result.contains(owner)) {
                result.add(owner);
            }
        }
        return result;
    }

    private List<String> subclassCreateClasses(String output, String baseName) {
        Matcher matcher = Pattern.compile("function\\s+(" + Pattern.quote(baseName) + "_[A-Za-z0-9_]+):create\\d*\\s*\\(").matcher(output);
        List<String> result = new ArrayList<>();
        while (matcher.find()) {
            String owner = matcher.group(1);
            if (!result.contains(owner)) {
                result.add(owner);
            }
        }
        return result;
    }

    private String compileLuaWithRunArgs(String testName, boolean withStdLib, String... lines) {
        return compileLuaWithCUs(testName, withStdLib, Collections.emptyList(), lines);
    }

    private String compileLuaWithCUs(String testName, boolean withStdLib, List<CU> extraCUs, String... lines) {
        RunArgs runArgs = new RunArgs().with("-lua", "-inline", "-localOptimizations", "-stacktraces");
        WurstGui gui = new WurstGuiCliImpl();
        WurstCompilerJassImpl compiler = new WurstCompilerJassImpl(null, gui, null, runArgs);
        List<CU> inputs = new ArrayList<>();
        inputs.addAll(extraCUs);
        inputs.add(new CU(testName + ".wurst", String.join("\n", lines)));

        WurstModel model = parseFiles(Collections.emptyList(), inputs, withStdLib, compiler);
        assertNotNull("parse returned null model, errors = " + gui.getErrorList(), model);
        assertTrue("unexpected parse/type errors: " + gui.getErrorList(), gui.getErrorList().isEmpty());
        compiler.checkProg(model);
        assertTrue("unexpected compile errors: " + gui.getErrorList(), gui.getErrorList().isEmpty());

        compiler.translateProgToIm(model);
        compiler.runCompiletime(WurstProjectConfigData.empty(), false, false);
        LuaCompilationUnit luaCode = compiler.transformProgToLua();

        StringBuilder sb = new StringBuilder();
        luaCode.print(sb, 0);
        return sb.toString();
    }

    @Test
    public void testStdLib() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "   print(compiletime)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_testStdLib.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("MagicFunctions_compiletime"));
        assertTrue(compiled.contains("function __wurst_InitHashtable("));
        assertTrue(compiled.contains("function __wurst_SaveInteger("));
    }

    @Test
    public void stacktraceStringsNotInjectedIntoNumericComparisons() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_stacktraceStringsNotInjectedIntoNumericComparisons",
            false,
            "package Test",
            "public tuple Vec2(real x, real y)",
            "public tuple scanResult(boolean found, Vec2 pos)",
            "class MyRect",
            "    real minX",
            "    real minY",
            "    real maxX",
            "    real maxY",
            "    construct(real minX, real minY, real maxX, real maxY)",
            "        this.minX = minX",
            "        this.minY = minY",
            "        this.maxX = maxX",
            "        this.maxY = maxY",
            "    function contains(Vec2 p) returns boolean",
            "        return p.x > minX and p.x < maxX and p.y > minY and p.y < maxY",
            "function gridAnchor(MyRect r, int dirX, int dirY, boolean isBehind) returns Vec2",
            "    let ax = dirX >= 0 ? r.minX + (isBehind ? 0 : 64) : r.maxX + (isBehind ? 0 : 64)",
            "    let ay = dirY >= 0 ? r.minY : r.maxY",
            "    return Vec2(ax, ay)",
            "function stepsAlongY(MyRect r, real startY, int dirY, real stepSize) returns int",
            "    if dirY >= 0 and stepSize > 0 and r.maxY > startY",
            "        return 1",
            "    return 0",
            "class BuildScanIterator",
            "    private var scanWidth = 128.",
            "    function tryRect(MyRect r, int dirX, int dirY) returns scanResult",
            "        if r == null",
            "            return scanResult(false, Vec2(0., 0.))",
            "        let start = gridAnchor(r, dirX, dirY, true)",
            "        if not r.contains(start)",
            "            return scanResult(false, Vec2(0., 0.))",
            "        let maxY = stepsAlongY(r, start.y, dirY, scanWidth)",
            "        if maxY >= 0",
            "            return scanResult(true, start)",
            "        return scanResult(false, Vec2(0., 0.))",
            "init",
            "    let b = new BuildScanIterator",
            "    b.tryRect(new MyRect(0., 0., 256., 256.), 1, 1)"
        );
        assertContainsRegex(compiled, "\"when calling contains");
        assertDoesNotContainRegex(compiled, "\"when calling contains[^\"]*\"\\s*[<>]=?");
        assertDoesNotContainRegex(compiled, "(?s)if not\\((\\w+)\\) then.*?stepsAlongY\\([^\\n]*,\\s*\\1\\s*,");
    }

    @Test
    public void continueLoweringInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native testSuccess()",
            "init",
            "    int i = 0",
            "    int sum = 0",
            "    while i < 5",
            "        i++",
            "        if i mod 2 == 0",
            "            continue",
            "        sum += i",
            "    if sum == 9",
            "        testSuccess()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_continueLoweringInLua.lua"), Charsets.UTF_8);
        assertTrue("expected continue lowering helper flag in lua output", compiled.contains("continueFlag_"));
    }

    @Test
    public void noContinueDoesNotEmitContinueFlagInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native testSuccess()",
            "init",
            "    int i = 0",
            "    while i < 3",
            "        i++",
            "        if i > 10",
            "            skip",
            "    testSuccess()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_noContinueDoesNotEmitContinueFlagInLua.lua"), Charsets.UTF_8);
        assertFalse("continue flag helper should not be emitted when no continue is present", compiled.contains("continueFlag_"));
    }

    @Ignore
    @Test
    public void testExecution() {
        test().testLua(true).executeProg().lines(
            "package Test",
            "native testSuccess()",
            "init",
            "   testSuccess()"
        );
    }

    @Test
    public void nullString1() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "function nullString() returns string",
            "	return null",
            "init",
            "   nullString()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullString1.lua"), Charsets.UTF_8);
        assertFunctionReturns(compiled, "nullString", "\"\"");
    }

    @Test
    public void nullString2() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "function takesString(string s)",
            "init",
            "   takesString(null)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullString2.lua"), Charsets.UTF_8);
        assertFunctionCall(compiled, "takesString", "\"\"");
    }

    @Ignore
    @Test
    public void nullString3() {
        test().testLua(true).executeProg().lines(
            "package Test",
            "native testSuccess()",
            "function nullString() returns string",
            "   return null",
            "function returnsString(string s) returns string",
            "   return s + nullString() + s",
            "init",
            "   let s = \".\"",
            "   let r = returnsString(s)",
            "   if r == s + s and r == \"..\"",
            "       testSuccess()"
        );
    }

    @Test
    public void nullObject1() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class A",
            "function nullObject() returns A",
            "	return null",
            "init",
            "   nullObject()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullObject1.lua"), Charsets.UTF_8);
        assertFunctionReturns(compiled, "nullObject", "nil");
    }

    @Test
    public void nullObject2() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class A",
            "function takesObject(A a)",
            "init",
            "   takesObject(null)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullObject2.lua"), Charsets.UTF_8);
        assertFunctionCall(compiled, "takesObject", "nil");
    }

    @Test
    public void nullUnit1() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "function nullUnit() returns unit",
            "	return null",
            "init",
            "   nullUnit()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullUnit1.lua"), Charsets.UTF_8);
        assertFunctionReturns(compiled, "nullUnit", "nil");
    }

    @Test
    public void nullUnit2() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "function takesUnit(unit u)",
            "init",
            "   takesUnit(null)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nullUnit2.lua"), Charsets.UTF_8);
        assertFunctionCall(compiled, "takesUnit", "nil");
    }

    @Test
    public void stringConcatenation() throws IOException {
        // Use local variables to test if it works even when local types are eliminated.
        test().testLua(true).lines(
            "package Test",
            "native takesString(string s)",
            "function test()",
            "    let s1 = \"1\"",
            "    let s2 = \"2\"",
            "    takesString(s1 + s2)",
            "init",
            "    test()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringConcatenation.lua"), Charsets.UTF_8);
        // strings use the stringConcat function in lua instead of + operator
        assertFunctionBodyContains(compiled, "test", "+", false);
        assertFunctionBodyContains(compiled, "test", "stringConcat", true);
    }

    @Test
    public void numericOpsDoNotUseGlobalEnsureWrapping() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "function cmp(int a, int b) returns boolean",
            "    return a < b",
            "function divi(int a, int b) returns int",
            "    return a div b",
            "function addi(int a, int b) returns int",
            "    return a + b",
            "init",
            "    cmp(1, 2)",
            "    divi(2, 1)",
            "    addi(1, 2)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_numericOpsDoNotUseGlobalEnsureWrapping.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "cmp", "intEnsure", false);
        assertFunctionBodyContains(compiled, "cmp", "realEnsure", false);
        assertFunctionBodyContains(compiled, "divi", "intEnsure", false);
        assertFunctionBodyContains(compiled, "divi", "realEnsure", false);
        assertFunctionBodyContains(compiled, "addi", "intEnsure", false);
        assertFunctionBodyContains(compiled, "addi", "realEnsure", false);
    }

    @Test
    public void lazyGenericClosureDispatchWorksInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native testSuccess()",
            "public function lazy<T:>(Lazy<T> l) returns Lazy<T>",
            "    return l",
            "public abstract class Lazy<T:>",
            "    T val = null",
            "    var wasRetrieved = false",
            "    abstract function retrieve() returns T",
            "    function get() returns T",
            "        if not wasRetrieved",
            "            val = retrieve()",
            "            wasRetrieved = true",
            "        return val",
            "init",
            "    let l = lazy<int>(() -> 5)",
            "    if l.get() == 5",
            "        testSuccess()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_lazyGenericClosureDispatchWorksInLua.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("Lazy_lazy_Test.Lazy_retrieve ="));
        assertTrue(compiled.contains("Lazy_Lazy_get(l)") || compiled.contains("l:Lazy_get()"));
    }

    @Test
    public void stringArrayReadIsEnsured() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "string array playerName",
            "init",
            "    let i = 0",
            "    SetPlayerName(Player(i), playerName[i])"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsured.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "SetPlayerName\\(Player\\([^\\)]*\\),\\s*stringEnsure\\(");
    }

    @Test
    public void methodFieldNameCollision() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class Foo",
            "    int size = 3",
            "    function size() returns int",
            "        return size",
            "init",
            "    let f = new Foo()",
            "    f.size()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_methodFieldNameCollision.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "Foo_Foo_size", "Foo_size_field", true);
        assertFunctionBodyContains(compiled, "Foo_Foo_size", "return this.Foo_size\n", false);
    }

    @Test
    public void overloadedMethodsDoNotAliasInLuaDispatchTables() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class Writer",
            "    function write(int i)",
            "        skip",
            "    function write(string s)",
            "        skip",
            "init",
            "    let w = new Writer()",
            "    w.write(1)",
            "    w.write(\"x\")"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_overloadedMethodsDoNotAliasInLuaDispatchTables.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("Writer.Writer_write = Writer_Writer_write"));
        assertTrue(compiled.contains("Writer.Writer_write1 = Writer_Writer_write1"));
        assertFalse(compiled.contains("Writer.Writer_write = Writer_Writer_write1"));
    }

    @Test
    public void overloadedOverrideDispatchDoesNotCollapseLuaSlots() throws IOException {
        test().testLua(true).lines(
            "package DispatchOverloadBug",
            "class Base",
            "    function doThing(int a)",
            "        this.doThing(a, 0)",
            "    function doThing(int a, int b)",
            "        this.doThing(a, b, false)",
            "    function doThing(int a, int b, boolean flag)",
            "        skip",
            "class Child extends Base",
            "    override function doThing(int a, int b)",
            "        super.doThing(a, b)",
            "init",
            "    let c = new Child()",
            "    c.doThing(1)"
        );

        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_overloadedOverrideDispatchDoesNotCollapseLuaSlots.lua"), Charsets.UTF_8);
        Matcher slotMatcher = Pattern.compile("Child\\.(Base_doThing\\d*)\\s*=\\s*Child_Child_doThing").matcher(compiled);
        List<String> overriddenSlots = new ArrayList<>();
        while (slotMatcher.find()) {
            overriddenSlots.add(slotMatcher.group(1));
        }
        assertEquals("Expected Child override to bind exactly one dispatch slot.", 1, overriddenSlots.size());

        Matcher baseSlotsMatcher = Pattern.compile("Base\\.(Base_doThing\\d*)\\s*=").matcher(compiled);
        List<String> baseSlots = new ArrayList<>();
        while (baseSlotsMatcher.find()) {
            String slot = baseSlotsMatcher.group(1);
            if (!baseSlots.contains(slot)) {
                baseSlots.add(slot);
            }
        }
        assertEquals("Expected three distinct Base overload dispatch slots.", 3, baseSlots.size());
        assertTrue(compiled.contains("this:Base_doThing1(a, 0)"));
        assertTrue(compiled.contains("Base_Base_doThing2(this1, a1, b, false)"));
        assertTrue(compiled.contains("Child.Base_doThing1 = Child_Child_doThing"));
        assertTrue(compiled.contains("Child.Base_doThing2 = Base_Base_doThing2"));
    }

    @Test
    public void moduleProvidedOverloadedOverrideDoesNotCollapseLuaSlots() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_moduleProvidedOverloadedOverrideDoesNotCollapseLuaSlots",
            false,
            "package Test",
            "module M",
            "    function setup(int a)",
            "        this.setup(a, 0)",
            "    function setup(int a, int b)",
            "        this.setup(a, b, false)",
            "    function setup(int a, int b, boolean c)",
            "        skip",
            "class Base",
            "    use M",
            "class Child extends Base",
            "    override function setup(int a, int b)",
            "        super.setup(a, b)",
            "init",
            "    let c = new Child()",
            "    c.setup(1)"
        );

        List<String> overriddenSlots = uniqueMatches(compiled, "Child\\.Base(?:_M)?_setup(\\d*)\\s*=\\s*Child_Child_setup", 1);
        List<String> baseSlots = uniqueMatches(compiled, "Base\\.Base(?:_M)?_setup(\\d*)\\s*=", 1);

        assertEquals("Expected exactly one overridden setup overload family from module-provided methods.", 1, overriddenSlots.size());
        assertEquals("Expected three distinct setup slots on Base.", 3, baseSlots.size());
        assertTrue(compiled.contains(":Base_M_setup1(") || compiled.contains(":Base_setup1("));
        assertContainsRegex(compiled, "Child\\.Base(?:_M)?_setup" + Pattern.quote(overriddenSlots.get(0)) + "\\s*=\\s*Child_Child_setup");
    }

    @Test
    public void multiLevelOverloadedOverridesKeepDistinctLuaSlots() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_multiLevelOverloadedOverridesKeepDistinctLuaSlots",
            false,
            "package Test",
            "class Base",
            "    function setup(int a)",
            "        this.setup(a, 0)",
            "    function setup(int a, int b)",
            "        this.setup(a, b, false)",
            "    function setup(int a, int b, boolean c)",
            "        skip",
            "class Mid extends Base",
            "    override function setup(int a, int b)",
            "        super.setup(a, b)",
            "class Child extends Mid",
            "    override function setup(int a, int b, boolean c)",
            "        super.setup(a, b, c)",
            "init",
            "    let c = new Child()",
            "    c.setup(1)"
        );

        List<String> midSlots = uniqueMatches(compiled, "Mid\\.(Base_setup\\d*)\\s*=\\s*Mid_Mid_setup", 1);
        List<String> childSlots = uniqueMatches(compiled, "Child\\.(Base_setup\\d*)\\s*=\\s*Child_Child_setup", 1);
        List<String> baseSlots = uniqueMatches(compiled, "Base\\.(Base_setup\\d*)\\s*=", 1);

        assertEquals("Expected Mid to override exactly one base setup slot.", 1, midSlots.size());
        assertEquals("Expected Child to override exactly one base setup slot.", 1, childSlots.size());
        assertEquals("Expected three distinct setup slots across hierarchy.", 3, baseSlots.size());
        assertFalse("Expected different overload slots for Mid and Child overrides.", midSlots.get(0).equals(childSlots.get(0)));
        assertTrue(compiled.contains("Child." + midSlots.get(0) + " = Mid_Mid_setup"));
        assertTrue(compiled.contains("Child." + childSlots.get(0) + " = Child_Child_setup"));
    }

    @Test
    public void generatedOverloadDispatchMatrixKeepsDistinctLuaSlots() {
        for (int overloadCount = 3; overloadCount <= 5; overloadCount++) {
            for (int overrideIndex = 2; overrideIndex < overloadCount; overrideIndex++) {
                String testName = "LuaTranslationTests_generatedOverloadDispatchMatrixKeepsDistinctLuaSlots_"
                    + overloadCount + "_" + overrideIndex;
                String compiled = compileLuaWithRunArgs(
                    testName,
                    false,
                    generatedOverloadDispatchMatrixLines(overloadCount, overrideIndex)
                );

                List<String> baseSlots = uniqueMatches(compiled, "Base\\.(Base_route\\d*)\\s*=", 1);
                List<String> overriddenSlots = uniqueMatches(compiled, "Child\\.(Base_route\\d*)\\s*=\\s*Child_Child_route", 1);

                assertEquals("Expected one overridden route slot for overloadCount=" + overloadCount + ", overrideIndex=" + overrideIndex,
                    1, overriddenSlots.size());
                assertEquals("Expected distinct route slots to match overload count for overloadCount=" + overloadCount,
                    overloadCount, baseSlots.size());
                assertTrue("Missing child binding for overridden slot " + overriddenSlots.get(0),
                    compiled.contains("Child." + overriddenSlots.get(0) + " = Child_Child_route"));

                for (int stubIndex = Math.max(overrideIndex - 1, 1); stubIndex < overloadCount - 1; stubIndex++) {
                    String functionName = stubIndex == 0 ? "Base_Base_route" : "Base_Base_route" + stubIndex;
                    String nextFunctionName = "Base_Base_route" + (stubIndex + 1);
                    String body = getFunctionBody(compiled, functionName);
                    assertTrue("Expected " + functionName + " to directly call " + nextFunctionName,
                        body.contains(nextFunctionName + "("));
                    assertFalse("Expected " + functionName + " to avoid virtual route dispatch in stub body.",
                        body.contains(":Base_route("));
                    assertFalse("Expected " + functionName + " to avoid virtual route dispatch to any numbered route slot.",
                        body.contains(":Base_route1(")
                            || body.contains(":Base_route2(")
                            || body.contains(":Base_route3(")
                            || body.contains(":Base_route4("));
                }
            }
        }
    }

    @Test
    public void middleOverloadStubDirectCallsFinalImplInLua() {
        test().testLua(true).lines(
            "package Test",
            "class Base",
            "    function setup(int a, int b, int c, int d, int e, int f)",
            "        skip",
            "    function setup(int a, int b, int c, int d, int e, int f, int g)",
            "        this.setup(a, b, c, d, e, f, g, 0)",
            "    function setup(int a, int b, int c, int d, int e, int f, int g, int h)",
            "        this.setup(a, b, c, d, e, f, g, h, 0)",
            "    function setup(int a, int b, int c, int d, int e, int f, int g, int h, int i)",
            "        skip",
            "class Child extends Base",
            "    override function setup(int a, int b, int c, int d, int e, int f, int g, int h)",
            "        super.setup(a, b, c, d, e, f, g, h)",
            "init",
            "    let c = new Child()",
            "    c.setup(1, 2, 3, 4, 5, 6, 7)"
        );

        try {
            String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_middleOverloadStubDirectCallsFinalImplInLua.lua"), Charsets.UTF_8);
            String middleBody = getFunctionBody(compiled, "Base_Base_setup1");
            assertTrue(middleBody.contains("Base_Base_setup2("));
            assertFalse(middleBody.contains(":Base_setup("));
            assertFalse(middleBody.contains(":Base_setup1("));
            assertFalse(middleBody.contains(":Base_setup2("));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void kuiStyleMiddleOverrideChainUsesDirectFinalImplCallInLua() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_kuiStyleMiddleOverrideChainUsesDirectFinalImplCallInLua",
            false,
            "package Test",
            "class KUIFrame",
            "    function setup(int a, int b, int c, int d, int e, int f, int g)",
            "        skip",
            "class KUIWindow extends KUIFrame",
            "    function setup(int a, int b, int c, int d, int e, int f, int g, int h)",
            "        this.setup(a, b, c, d, e, f, g, h, 0)",
            "    function setup(int a, int b, int c, int d, int e, int f, int g, int h, int i)",
            "        this.setup(a, b, c, d, e, f, g, h, i, 0)",
            "    function setup(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j)",
            "        skip",
            "class KUITargetUnit extends KUIWindow",
            "    override function setup(int a, int b, int c, int d, int e, int f, int g, int h, int i)",
            "        super.setup(a, b, c, d, e, f, g, h, i)",
            "init",
            "    let x = new KUITargetUnit()",
            "    x.setup(1, 2, 3, 4, 5, 6, 7, 8)"
        );

        String setupBody = getFunctionBody(compiled, "KUIWindow_KUIWindow_setup1");
        assertTrue(setupBody.contains("KUIWindow_KUIWindow_setup2("));
        assertFalse(setupBody.contains(":KUIWindow_setup("));
        assertFalse(setupBody.contains(":KUITargetUnit_setup("));
    }

    @Test
    public void closureInterfaceCallsitesUseSubclassBindingsForSameLuaSlot() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_closureInterfaceCallsitesUseSubclassBindingsForSameLuaSlot",
            false,
            "package Test",
            "interface LLItrClosure",
            "    function run(int t)",
            "class LinkedList",
            "    function forEach(LLItrClosure f)",
            "        f.run(1)",
            "function feed(LinkedList xs)",
            "    xs.forEach((int t) -> skip)",
            "    xs.forEach((int t) -> skip)",
            "init",
            "    let xs = new LinkedList()",
            "    feed(xs)"
        );

        String forEachBody = getFunctionBody(compiled, "LinkedList_LinkedList_forEach");
        String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1);
        assertEquals("run", slotName);
        List<String> subclassBindings = nonBaseSubclassBindings(compiled, "LLItrClosure", slotName);

        assertTrue("Expected generated closure subclasses for LLItrClosure to bind slot " + slotName,
            subclassBindings.size() >= 2);
        assertContainsRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\." + Pattern.quote(slotName) + "\\s*=");
    }

    @Test
    public void abstractCallbackFamiliesKeepCallsiteAndSubclassSlotNamesAlignedInLua() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_abstractCallbackFamiliesKeepCallsiteAndSubclassSlotNamesAlignedInLua",
            false,
            "package Test",
            "public abstract class OtherCallback",
            "    abstract function callback(int elem)",
            "class OtherRegistry",
            "    function applyTo(OtherCallback cb)",
            "        cb.callback(0)",
            "public abstract class ForElementCallback",
            "    abstract function callback(int elem)",
            "class Registry",
            "    function forEachIn(ForElementCallback cb)",
            "        cb.callback(1)",
            "function runAll(Registry r, OtherRegistry o)",
            "    r.forEachIn() elem ->",
            "        skip",
            "    r.forEachIn() elem ->",
            "        skip",
            "    o.applyTo() elem ->",
            "        skip",
            "init",
            "    runAll(new Registry(), new OtherRegistry())"
        );

        String forEachBody = getFunctionBody(compiled, "Registry_Registry_forEachIn");
        String callbackSlot = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1);
        assertEquals("callback", callbackSlot);
        List<String> subclassBindings = nonBaseSubclassBindings(compiled, "ForElementCallback", callbackSlot);

        assertTrue("Expected generated ForElementCallback closure subclasses to bind slot " + callbackSlot,
            subclassBindings.size() >= 2);
        assertContainsRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\." + Pattern.quote(callbackSlot) + "\\s*=");

        String otherBody = getFunctionBody(compiled, "OtherRegistry_OtherRegistry_applyTo");
        String otherSlot = singleMatch(otherBody, ":([A-Za-z0-9_]+)\\(", 1);
        assertTrue(otherSlot.startsWith("callback"));
        List<String> otherSubclassBindings = nonBaseSubclassBindings(compiled, "OtherCallback", otherSlot);

        assertTrue("Expected generated OtherCallback closure subclass to bind slot " + otherSlot,
            otherSubclassBindings.size() >= 1);
        assertContainsRegex(compiled, "OtherCallback_[A-Za-z0-9_]+\\." + Pattern.quote(otherSlot) + "\\s*=");
    }

    @Test
    public void genericClosureInterfacesKeepPrefixedBaseSlotNamesInLua() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_genericClosureInterfacesKeepPrefixedBaseSlotNamesInLua",
            false,
            "package Test",
            "class Box<T>",
            "    T elem",
            "    construct(T elem)",
            "        this.elem = elem",
            "class LinkedList<X>",
            "    Box<X> stored = null",
            "    function add(X x)",
            "        stored = new Box<X>(x)",
            "    function forEach(LLItrClosure<X> f)",
            "        if stored != null",
            "            f.run(stored.elem)",
            "public interface LLItrClosure<T>",
            "    function run(T t)",
            "function test()",
            "    let xs = new LinkedList<int>()",
            "    xs.add(1)",
            "    xs.forEach() itr ->",
            "        skip",
            "init",
            "    test()"
        );

        String forEachBody = getFunctionBody(compiled, "LinkedList_LinkedList_forEach");
        String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1);
        assertEquals("LLItrClosure_run", slotName);
        assertContainsRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\." + Pattern.quote(slotName) + "\\s*=");
        assertDoesNotContainRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\.LLItrClosure_run\\d+\\s*=");
    }

    @Test
    public void genericAbstractCallbacksKeepPrefixedBaseSlotNamesInLua() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_genericAbstractCallbacksKeepPrefixedBaseSlotNamesInLua",
            false,
            "package Test",
            "class Registry<T>",
            "    T stored = null",
            "    construct(T stored)",
            "        this.stored = stored",
            "    function forEachIn(ForElementCallback<T> cb)",
            "        if stored != null",
            "            cb.callback(stored)",
            "public abstract class ForElementCallback<T>",
            "    abstract function callback(T elem)",
            "function test()",
            "    let r = new Registry<int>(1)",
            "    r.forEachIn() elem ->",
            "        skip",
            "init",
            "    test()"
        );

        String forEachBody = getFunctionBody(compiled, "Registry_Registry_forEachIn");
        String slotName = singleMatch(forEachBody, ":([A-Za-z0-9_]+)\\(", 1);
        assertEquals("ForElementCallback_callback", slotName);
        assertContainsRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\." + Pattern.quote(slotName) + "\\s*=");
        assertDoesNotContainRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\.ForElementCallback_callback\\d+\\s*=");
    }

    @Test
    public void genericRegistryModuleCallbacksKeepModuleQualifiedLuaSlotNames() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "public interface ForElementCallback<T>",
            "    function callback(T elem)",
            "public module RegistryModule<T>",
            "    static T stored = null",
            "    protected static function addToRegistry(T obj) returns int",
            "        stored = obj",
            "        return 0",
            "    static function forEachIn(ForElementCallback<T> cb) returns ForElementCallback<T>",
            "        if stored != null",
            "            cb.callback(stored)",
            "        return cb",
            "class Dungeon",
            "    use RegistryModule<Dungeon>",
            "    static function makeOne()",
            "        addToRegistry(new Dungeon())",
            "function setup()",
            "    Dungeon.makeOne()",
            "    Dungeon.forEachIn((dungeon) -> begin",
            "        skip",
            "    end)",
            "init",
            "    setup()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_genericRegistryModuleCallbacksKeepModuleQualifiedLuaSlotNames.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "function\\s+[A-Za-z0-9_]*forEachIn\\(");
        assertContainsRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\.(RegistryModule_)?ForElementCallback_callback\\s*=");
        assertDoesNotContainRegex(compiled, "ForElementCallback_[A-Za-z0-9_]+\\.ForElementCallback_callback\\d+\\s*=");
    }

    @Test
    public void nestedGenericLinkedListClosuresKeepPrefixedLuaSlotNames() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "public interface CallbackSingle",
            "    function run()",
            "public function _Logging_executeCustom(string msg, CallbackSingle cb)",
            "    cb.run()",
            "public interface LLItrClosure<T>",
            "    function run(T t)",
            "class Node<T>",
            "    T elem",
            "    Node<T> next = null",
            "class LinkedList<T>",
            "    Node<T> first = null",
            "    function add(T elem)",
            "        let n = new Node<T>()",
            "        n.elem = elem",
            "        n.next = first",
            "        first = n",
            "    function forEach(LLItrClosure<T> itr)",
            "        var cur = first",
            "        while cur != null",
            "            itr.run(cur.elem)",
            "            cur = cur.next",
            "function setup()",
            "    let ids = new LinkedList<int>()",
            "    ids.add(1)",
            "    ids.forEach() id ->",
            "        _Logging_executeCustom(\"x\") ->",
            "            skip",
            "init",
            "    setup()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nestedGenericLinkedListClosuresKeepPrefixedLuaSlotNames.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "function\\s+LinkedList_[A-Za-z0-9_]*forEach\\(");
        assertContainsRegex(compiled, "LLItrClosure_[A-Za-z0-9_]+\\.LLItrClosure_run\\s*=");
    }

    @Test
    public void erasedLinkedListClosureCallsitesKeepSuffixedRunSlotsAlignedAcrossAllSubclasses() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_erasedLinkedListClosureCallsitesKeepSuffixedRunSlotsAlignedAcrossAllSubclasses",
            false,
            "package Test",
            "interface Alpha0",
            "    function run()",
            "interface Alpha1",
            "    function run()",
            "interface Alpha2",
            "    function run()",
            "interface Alpha3",
            "    function run()",
            "interface Alpha4",
            "    function run()",
            "interface Alpha5",
            "    function run()",
            "public interface MapClosure<T,S>",
            "    function run(T t) returns S",
            "public interface LLItrClosure<T>",
            "    function run(T t)",
            "class Node<T>",
            "    T elem",
            "    Node<T> next = null",
            "class LinkedList<T>",
            "    Node<T> first = null",
            "    function add(T elem)",
            "        let n = new Node<T>()",
            "        n.elem = elem",
            "        n.next = first",
            "        first = n",
            "    function forEach(LLItrClosure<T> itr)",
            "        var cur = first",
            "        while cur != null",
            "            itr.run(cur.elem)",
            "            cur = cur.next",
            "    function map<S>(MapClosure<T,S> itr) returns LinkedList<S>",
            "        let out = new LinkedList<S>()",
            "        forEach() t ->",
            "            out.add(itr.run(t))",
            "        return out",
            "function usePlain(LinkedList<int> xs)",
            "    xs.forEach() x ->",
            "        skip",
            "function useMapped(LinkedList<int> xs)",
            "    xs.map((int x) -> x + 1)",
            "function useNested(LinkedList<int> xs)",
            "    xs.forEach() x ->",
            "        xs.map((int y) -> y + x)",
            "init",
            "    let xs = new LinkedList<int>()",
            "    xs.add(1)",
            "    usePlain(xs)",
            "    useMapped(xs)",
            "    useNested(xs)"
        );

        List<String> subclasses = subclassCreateClasses(compiled, "LLItrClosure");
        assertTrue("Expected multiple generated LLItrClosure subclasses", subclasses.size() >= 3);
        List<String> suffixedSlots = uniqueMatches(compiled, "LLItrClosure_[A-Za-z0-9_]+\\.(run\\d+)\\s*=", 1);
        assertTrue("Expected at least one suffixed LLItrClosure run slot", !suffixedSlots.isEmpty());
        String slotName = suffixedSlots.get(0);
        for (String subclass : subclasses) {
            assertContainsRegex(compiled, Pattern.quote(subclass) + "\\." + Pattern.quote(slotName) + "\\s*=");
        }
    }

    @Test
    public void mainAndConfigNamesFixed() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native takesInt(int i)",
            "function helper()",
            "    let main = 1",
            "    let config = 2",
            "    takesInt(main)",
            "    takesInt(config)",
            "init",
            "    helper()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_mainAndConfigNamesFixed.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "helper", "local main1", true);
        assertFunctionBodyContains(compiled, "helper", "local config1", true);
        assertTrue(compiled.contains("function main("));
        assertTrue(compiled.contains("function config("));
        assertFalse(compiled.contains("function main2("));
        assertFalse(compiled.contains("function config2("));
    }

    @Test
    public void luaHeavyBootstrapStateIsSeededFromMain() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class C",
            "function id(int x) returns int",
            "    return x",
            "init",
            "    let c = new C()",
            "    if c == null",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaHeavyBootstrapStateIsSeededFromMain.lua"), Charsets.UTF_8);
        int mainPos = compiled.indexOf("function main(");
        assertTrue(mainPos >= 0);
        int configPos = compiled.indexOf("function config(", mainPos);
        String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length());
        String beforeMain = compiled.substring(0, mainPos);
        assertFalse(beforeMain.contains("__wurst_objectIndexMap = ({"));
        assertFalse(beforeMain.contains("__wurst_string_index_map = ({"));
        assertTrue(mainSection.contains("__wurst_objectIndexMap = ({"));
        assertTrue(mainSection.contains("__wurst_string_index_map = ({"));
    }

    @Test
    public void luaClassDispatchTablesAreInitializedInsideMain() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class A",
            "    function f() returns int",
            "        return 1",
            "init",
            "    let a = new A()",
            "    if a.f() > 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaClassDispatchTablesAreInitializedInsideMain.lua"), Charsets.UTF_8);
        int mainPos = compiled.indexOf("function main(");
        assertTrue(mainPos >= 0);
        int configPos = compiled.indexOf("function config(", mainPos);
        String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length());
        String beforeMain = compiled.substring(0, mainPos);
        assertFalse(beforeMain.contains("A.__wurst_supertypes ="));
        assertFalse(beforeMain.contains("A.__typeId__ ="));
        assertTrue(mainSection.contains("A.__wurst_supertypes ="));
        assertTrue(mainSection.contains("A.__typeId__ ="));
        assertTrue(mainSection.contains("A.A_f ="));
    }

    @Test
    public void luaDeferredBootstrapRunsBeforeInitGlobalsInMain() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class C",
            "    function f() returns int",
            "        return 1",
            "init",
            "    let c = new C()",
            "    if c.f() > 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaDeferredBootstrapRunsBeforeInitGlobalsInMain.lua"), Charsets.UTF_8);
        int mainPos = compiled.indexOf("function main(");
        assertTrue(mainPos >= 0);
        int configPos = compiled.indexOf("function config(", mainPos);
        String mainSection = compiled.substring(mainPos, configPos > mainPos ? configPos : compiled.length());

        assertOccursBefore(mainSection, "__wurst_objectIndexMap = ({", "initGlobals()");
        assertOccursBefore(mainSection, "__wurst_string_index_map = ({", "initGlobals()");
        assertOccursBefore(mainSection, "C.__wurst_supertypes =", "initGlobals()");
        assertOccursBefore(mainSection, "C.__typeId__ =", "initGlobals()");
        assertOccursBefore(mainSection, "C.C_f =", "initGlobals()");
    }

    @Test
    public void luaClassTablesExistBeforeCreateMethodDefinitions() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class A",
            "init",
            "    let a = new A()",
            "    if a == null",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaClassTablesExistBeforeCreateMethodDefinitions.lua"), Charsets.UTF_8);
        assertOccursBefore(compiled, "A = ({})", "function A:create(");
    }


    @Test
    public void intCasting() throws IOException {
        // Use local variables to test if it works even when local types are eliminated.
        test().testLua(true).lines(
            "package Test",
            "native takesInt(int i)",
            "enum MyEnum",
            "    ZERO",
            "    ONE",
            "    TWO",
            "function testEnum()",
            "    let zeroEnum = MyEnum.ZERO",
            "    let zeroInt = zeroEnum castTo int",
            "    let zeroEnum2 = zeroInt castTo MyEnum",
            "    takesInt(zeroEnum castTo int)",
            "    takesInt(zeroInt)",
            "    takesInt(zeroEnum2 castTo int)",
            "class C",
            "native takesC(C c)",
            "function testClass()",
            "    let cObj = new C()",
            "    let cInt = cObj castTo int",
            "    let cObj2 = cInt castTo C",
            "    takesC(cObj)",
            "    takesInt(cInt)",
            "    takesC(cObj2)",
            "init",
            "    testEnum()",
            "    testClass()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_intCasting.lua"), Charsets.UTF_8);
        // enums are cast implicitly, because they are ints
        assertFunctionBodyContains(compiled, "testEnum", "objectToIndex", false);
        assertFunctionBodyContains(compiled, "testEnum", "zeroEnum = 0", true);
        assertFunctionBodyContains(compiled, "testEnum", "zeroInt = zeroEnum", true);
        assertFunctionBodyContains(compiled, "testEnum", "zeroEnum2 = zeroInt", true);
        // classes are cast with objectToIndex and objectFromIndex in lua
        assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true);
        assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true);
        assertFunctionBodyContains(compiled, "testClass", "cInt = cObj", false);
        assertFunctionBodyContains(compiled, "testClass", "cObj2 = cInt", false);
    }

    @Test
    public void configEntrypointNotRenamedWhenUserHasConfigFunction() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "function config()",
            "    skip",
            "init",
            "    config()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_configEntrypointNotRenamedWhenUserHasConfigFunction.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("function config("));
        assertFalse(compiled.contains("function config2("));
        assertTrue(compiled.contains("function config1("));
        assertTrue(compiled.contains("config1()"));
    }

    @Test
    public void stacktracesAreNotInjectedIntoConfigSeededLuaFunctions() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_stacktracesAreNotInjectedIntoConfigSeededLuaFunctions",
            false,
            "package Test",
            "@noinline function configHelper()",
            "    skip",
            "@noinline function runtimeHelper()",
            "    skip",
            "function config()",
            "    configHelper()",
            "init",
            "    runtimeHelper()"
        );

        assertDoesNotContainRegex(compiled, "function\\s+config\\([^\\)]*__wurst_stackPos");
        assertDoesNotContainRegex(compiled, "function\\s+config1\\([^\\)]*__wurst_stackPos");
        assertDoesNotContainRegex(compiled, "function\\s+configHelper\\([^\\)]*__wurst_stackPos");
        assertDoesNotContainRegex(compiled, "configHelper\\(\"when calling configHelper");

        assertContainsRegex(compiled, "function\\s+runtimeHelper\\([^\\)]*__wurst_stackPos");
        assertContainsRegex(compiled, "runtimeHelper\\(\"when calling runtimeHelper");
    }

    @Test
    public void stacktracesStayInjectedForLuaHelpersSharedByConfigAndRuntime() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_stacktracesStayInjectedForLuaHelpersSharedByConfigAndRuntime",
            false,
            "package Test",
            "@noinline function sharedLeaf()",
            "    skip",
            "@noinline function sharedHelper()",
            "    sharedLeaf()",
            "@noinline function configOnlyHelper()",
            "    skip",
            "function config()",
            "    configOnlyHelper()",
            "    sharedHelper()",
            "init",
            "    sharedHelper()"
        );

        assertDoesNotContainRegex(compiled, "function\\s+configOnlyHelper\\([^\\)]*__wurst_stackPos");
        assertDoesNotContainRegex(compiled, "configOnlyHelper\\(\"when calling configOnlyHelper");

        assertContainsRegex(compiled, "function\\s+sharedHelper\\([^\\)]*__wurst_stackPos");
        assertContainsRegex(compiled, "function\\s+sharedLeaf\\([^\\)]*__wurst_stackPos");
        assertContainsRegex(compiled, "sharedHelper\\(\"when calling sharedHelper");
        assertContainsRegex(compiled, "sharedLeaf\\(\"when calling sharedLeaf");
    }

    @Test
    public void objectIndexFunctionsDoNotCollideWithUserFunctions() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "function objectToIndex(int i) returns int",
            "    return i + 1",
            "function objectFromIndex(int i) returns int",
            "    return i - 1",
            "native takesInt(int i)",
            "class C",
            "function testClass()",
            "    let cObj = new C()",
            "    let cInt = cObj castTo int",
            "    let cObj2 = cInt castTo C",
            "    takesInt(objectToIndex(3))",
            "    takesInt(objectFromIndex(5))",
            "    if cObj2 == null",
            "        skip",
            "init",
            "    testClass()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_objectIndexFunctionsDoNotCollideWithUserFunctions.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("function objectToIndex("));
        assertTrue(compiled.contains("function objectFromIndex("));
        assertFunctionBodyContains(compiled, "testClass", "__wurst_objectToIndex", true);
        assertFunctionBodyContains(compiled, "testClass", "__wurst_objectFromIndex", true);
    }

    @Test
    public void tupleReturningMethodNamedFromIndexIsNotTypecastWrapper() throws IOException {
        test().testLua(true).lines(
            "package TupleFromIndexRepro",
            "public tuple vec2(real x, real y)",
            "public tuple searchResult(boolean found, vec2 pos)",
            "public constant ZERO2 = vec2(0., 0.)",
            "public class A",
            "    function next() returns searchResult",
            "        return nextFromIndex(1)",
            "    function nextFromIndex(int startIndex) returns searchResult",
            "        return searchResult(false, ZERO2)",
            "public class B extends A",
            "init",
            "    let b = new B()",
            "    let r = b.next()",
            "    if r.found",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_tupleReturningMethodNamedFromIndexIsNotTypecastWrapper.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "function\\s+[^\\n]*nextFromIndex[^\\n]*\\(");
        assertDoesNotContainRegex(compiled, "return\\s+__wurst_objectFromIndex\\s*\\(\\s*this\\s*\\)");
    }

    @Test
    public void oldGenericsCastingDoesNotUseGetHandleId() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "class C",
            "native takesInt(int i)",
            "native takesC(C c)",
            "function testCast()",
            "    let cObj = new C()",
            "    let cInt = cObj castTo int",
            "    let cObj2 = cInt castTo C",
            "    takesInt(cInt)",
            "    takesC(cObj2)",
            "init",
            "    testCast()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_oldGenericsCastingDoesNotUseGetHandleId.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "\\bGetHandleId\\(");
        assertFunctionBodyContains(compiled, "testCast", "__wurst_objectToIndex", true);
        assertFunctionBodyContains(compiled, "testCast", "__wurst_objectFromIndex", true);
    }

    @Test
    public void newGenericsDoNotUseOldTypecastHelpersInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class C",
            "function id<T>(T x) returns T",
            "    return x",
            "function testGeneric()",
            "    let c = new C()",
            "    let c2 = id<C>(c)",
            "    if c2 == null",
            "        skip",
            "init",
            "    testGeneric()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsDoNotUseOldTypecastHelpersInLua.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "testGeneric", "__wurst_objectToIndex", false);
        assertFunctionBodyContains(compiled, "testGeneric", "__wurst_objectFromIndex", false);
    }

    @Test
    public void newGenericsStringFieldAssignmentRoundTripsInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class C<T:>",
            "    T x",
            "function stringToIndex(string s) returns int",
            "    return 42",
            "function stringFromIndex(int i) returns string",
            "    return \"42\"",
            "function testGenericStringField() returns boolean",
            "    C<string> c = new C<string>",
            "    c.x = \"42\"",
            "    return c.x == \"42\"",
            "init",
            "    if testGenericStringField()",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsStringFieldAssignmentRoundTripsInLua.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "testGenericStringField", "c.C_x = \"42\"", true);
        assertFunctionBodyContains(compiled, "testGenericStringField", "stringEnsure(c.C_x)", true);
        assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringToIndex", false);
        assertFunctionBodyContains(compiled, "testGenericStringField", "__wurst_stringFromIndex", false);
    }

    @Test
    public void genericOverrideChainBindsRootSlotToMostSpecificImplInLua() throws IOException {
        test().testLua(true).compilationUnits(genericOverrideReproUnits());
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_genericOverrideChainBindsRootSlotToMostSpecificImplInLua.lua"), Charsets.UTF_8);

        Matcher slotMatcher = Pattern.compile("FSM_currentState:([A-Za-z0-9_]*_update)\\(").matcher(compiled);
        assertTrue("Expected FSM to dispatch through a virtual *_update slot.", slotMatcher.find());
        String dispatchedSlot = slotMatcher.group(1);

        String[] states = {"FindBuilder", "PlanNextAction", "FindSpot", "BuildAtTarget", "QuickBuild", "RescueStrikeTarget"};
        for (String state : states) {
            assertContainsRegex(compiled, state + "\\." + dispatchedSlot + "\\s*=\\s*" + state + "_" + state + "_update");
            assertDoesNotContainRegex(compiled, state + "\\." + dispatchedSlot + "\\s*=\\s*NoOpState_NoOpState_update");
        }
    }

    @Test
    public void luaOutputIsDeterministicForGenericOverrideSlots() throws IOException {
        test().testLua(true).compilationUnits(genericOverrideReproUnits());
        String first = Files.toString(new File("test-output/lua/LuaTranslationTests_luaOutputIsDeterministicForGenericOverrideSlots.lua"), Charsets.UTF_8);

        GlobalCaches.clearAll();

        test().testLua(true).compilationUnits(genericOverrideReproUnits());
        String second = Files.toString(new File("test-output/lua/LuaTranslationTests_luaOutputIsDeterministicForGenericOverrideSlots.lua"), Charsets.UTF_8);

        assertEquals(first, second);
    }

    @Test
    public void genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua() throws IOException {
        test().testLua(true).compilationUnits(genericOverrideGlobalStateReproUnits());
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_genericOverrideChainBindsGlobalStateSlotToMostSpecificImplInLua.lua"), Charsets.UTF_8);

        Matcher slotMatcher = Pattern.compile("FSM_globalState:([A-Za-z0-9_]*_update)\\(").matcher(compiled);
        assertTrue("Expected FSM global state to dispatch through a virtual *_update slot.", slotMatcher.find());
        String dispatchedSlot = slotMatcher.group(1);

        assertContainsRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*GlobalCheckState_GlobalCheckState_update");
        assertDoesNotContainRegex(compiled, "GlobalCheckState\\." + dispatchedSlot + "\\s*=\\s*NoOpState_NoOpState_update");
    }

    private CU[] genericOverrideReproUnits() {
        return new CU[]{
            compilationUnit("fsmLib.wurst",
                "package FSMReproLib",
                "",
                "public abstract class State<T:>",
                "    function enter(T owner)",
                "    function update(T owner, real dt)",
                "    function exit(T owner)",
                "",
                "public class NoOpState<T:> extends State<T>",
                "    override function enter(T owner)",
                "    override function update(T owner, real dt)",
                "    override function exit(T owner)",
                "",
                "public class FSM<T:>",
                "    T owner",
                "    State<T> currentState = null",
                "",
                "    construct(T owner)",
                "        this.owner = owner",
                "",
                "    function setInitialState(State<T> st)",
                "        currentState = st",
                "        if currentState != null",
                "            currentState.enter(owner)",
                "",
                "    function update(real dt)",
                "        if currentState != null",
                "            currentState.update(owner, dt)"
            ),
            compilationUnit("repro.wurst",
                "package GenericOverrideSlotRepro",
                "import FSMReproLib",
                "",
                "public class Owner",
                "    FSM<Owner> fsm = new FSM<Owner>(this)",
                "    int findBuilderTicks = 0",
                "    int planTicks = 0",
                "    int findSpotTicks = 0",
                "    int buildTicks = 0",
                "",
                "public constant globalCheckState = new GlobalCheckState()",
                "public constant findBuilderState = new FindBuilder()",
                "public constant planNextActionState = new PlanNextAction()",
                "public constant findSpotState = new FindSpot()",
                "public constant buildAtTargetState = new BuildAtTarget()",
                "public constant quickBuildState = new QuickBuild()",
                "public constant rescueStrikeTargetState = new RescueStrikeTarget()",
                "",
                "class GlobalCheckState extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "",
                "class FindBuilder extends NoOpState<Owner>",
                "    override function enter(Owner o)",
                "        o.findBuilderTicks = 0",
                "    override function update(Owner o, real dt)",
                "        o.findBuilderTicks++",
                "",
                "class PlanNextAction extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.planTicks++",
                "",
                "class FindSpot extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.findSpotTicks++",
                "",
                "class BuildAtTarget extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.buildTicks++",

                "class QuickBuild extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.buildTicks++",
                "",
                "class RescueStrikeTarget extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.buildTicks++",
                "",
                "function runOne(State<Owner> st) returns int",
                "    let o = new Owner()",
                "    o.fsm.setInitialState(st)",
                "    for i = 0 to 4",
                "        o.fsm.update(0.1)",
                "    return o.findBuilderTicks",
                "",
                "init",
                "    runOne(findBuilderState)",
                "    runOne(planNextActionState)",
                "    runOne(findSpotState)",
                "    runOne(buildAtTargetState)",
                "    runOne(quickBuildState)",
                "    runOne(rescueStrikeTargetState)"
            )
        };
    }

    private CU[] genericOverrideGlobalStateReproUnits() {
        return new CU[]{
            compilationUnit("fsmLib.wurst",
                "package FSMReproLibGlobal",
                "",
                "public abstract class State<T:>",
                "    function enter(T owner)",
                "    function update(T owner, real dt)",
                "    function exit(T owner)",
                "",
                "public class NoOpState<T:> extends State<T>",
                "    override function enter(T owner)",
                "    override function update(T owner, real dt)",
                "    override function exit(T owner)",
                "",
                "public class FSM<T:>",
                "    T owner",
                "    State<T> globalState = null",
                "    State<T> currentState = null",
                "",
                "    construct(T owner)",
                "        this.owner = owner",
                "",
                "    function setInitialState(State<T> st)",
                "        currentState = st",
                "        if currentState != null",
                "            currentState.enter(owner)",
                "",
                "    function setGlobalState(State<T> st)",
                "        globalState = st",
                "",
                "    function update(real dt)",
                "        if globalState != null",
                "            globalState.update(owner, dt)",
                "        if currentState != null",
                "            currentState.update(owner, dt)"
            ),
            compilationUnit("repro.wurst",
                "package GenericOverrideGlobalStateSlotRepro",
                "import FSMReproLibGlobal",
                "",
                "public class Owner",
                "    FSM<Owner> fsm = new FSM<Owner>(this)",
                "    int globalTicks = 0",
                "    int findBuilderTicks = 0",
                "    int planTicks = 0",
                "    int findSpotTicks = 0",
                "    int buildTicks = 0",
                "",
                "public constant globalCheckState = new GlobalCheckState()",
                "public constant findBuilderState = new FindBuilder()",
                "public constant planNextActionState = new PlanNextAction()",
                "public constant findSpotState = new FindSpot()",
                "public constant buildAtTargetState = new BuildAtTarget()",
                "public constant quickBuildState = new QuickBuild()",
                "",
                "class GlobalCheckState extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.globalTicks++",
                "",
                "class FindBuilder extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.findBuilderTicks++",
                "",
                "class PlanNextAction extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.planTicks++",
                "",
                "class FindSpot extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.findSpotTicks++",
                "",
                "class BuildAtTarget extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.buildTicks++",
                "",
                "class QuickBuild extends NoOpState<Owner>",
                "    override function update(Owner o, real dt)",
                "        o.buildTicks++",
                "",
                "init",
                "    let o = new Owner()",
                "    o.fsm.setGlobalState(globalCheckState)",
                "    o.fsm.setInitialState(buildAtTargetState)",
                "    o.fsm.update(0.1)"
            )
        };
    }

    private String[] generatedOverloadDispatchMatrixLines(int overloadCount, int overrideIndex) {
        List<String> lines = new ArrayList<>();
        lines.add("package Test");
        lines.add("class Base");
        for (int i = 1; i <= overloadCount; i++) {
            lines.add("    function route(" + routeParams(i) + ")");
            if (i < overloadCount) {
                lines.add("        this.route(" + routeArgsToNext(i) + ")");
            } else {
                lines.add("        skip");
            }
        }
        lines.add("class Child extends Base");
        lines.add("    override function route(" + routeParams(overrideIndex) + ")");
        lines.add("        super.route(" + routeArgsCurrent(overrideIndex) + ")");
        lines.add("init");
        lines.add("    let c = new Child()");
        lines.add("    c.route(" + routeStartArgs() + ")");
        return lines.toArray(new String[0]);
    }

    private String routeParams(int arity) {
        List<String> params = new ArrayList<>();
        for (int i = 1; i <= arity; i++) {
            params.add("int p" + i);
        }
        return String.join(", ", params);
    }

    private String routeArgsToNext(int arity) {
        List<String> args = new ArrayList<>();
        for (int i = 1; i <= arity; i++) {
            args.add("p" + i);
        }
        args.add("0");
        return String.join(", ", args);
    }

    private String routeArgsCurrent(int arity) {
        List<String> args = new ArrayList<>();
        for (int i = 1; i <= arity; i++) {
            args.add("p" + i);
        }
        return String.join(", ", args);
    }

    private String routeStartArgs() {
        return "1";
    }

    @Test
    public void largeFunctionSpillsLocalsIntoTableInLua() throws IOException {
        List<String> lines = new ArrayList<>();
        lines.add("package Test");
        lines.add("native takesInt(int i)");
        lines.add("function huge()");
        lines.add("    var sum = 0");
        for (int i = 0; i < 210; i++) {
            lines.add("    let v" + i + " = " + i);
            lines.add("    sum += v" + i);
        }
        lines.add("    takesInt(sum)");
        lines.add("init");
        lines.add("    huge()");

        test().testLua(true).lines(lines.toArray(new String[0]));
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_largeFunctionSpillsLocalsIntoTableInLua.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("function huge("));
        assertTrue(compiled.contains("__wurst_locals"));
        assertContainsRegex(compiled, "__wurst_locals\\[[0-9]+\\]");
        assertFalse(compiled.contains("local v0"));
        assertFalse(compiled.contains("local v209"));
        assertFalse(compiled.contains("\nsum = "));
        assertFalse(compiled.contains("\nv0 = "));
        assertFalse(compiled.contains("takesInt(sum)"));
    }

    @Test
    public void inlinerDoesNotForceSpillWhenCallerStaysBelowLimit() throws IOException {
        List<String> lines = new ArrayList<>();
        lines.add("package Test");
        lines.add("native takesInt(int i)");
        lines.add("function small(int x) returns int");
        lines.add("    let a = x");
        lines.add("    let b = a + 1");
        lines.add("    let c = b + 1");
        lines.add("    let d = c + 1");
        lines.add("    return d");
        lines.add("function caller()");
        lines.add("    var sum = 0");
        for (int i = 0; i < 195; i++) {
            lines.add("    let v" + i + " = " + i);
            lines.add("    sum += v" + i);
        }
        lines.add("    sum += small(1)");
        lines.add("    takesInt(sum)");
        lines.add("init");
        lines.add("    caller()");

        test().testLua(true).lines(lines.toArray(new String[0]));
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_inlinerDoesNotForceSpillWhenCallerStaysBelowLimit.lua"), Charsets.UTF_8);
        int start = compiled.indexOf("function caller(");
        assertTrue("caller function not found in generated lua output", start >= 0);
        int end = compiled.indexOf("\nend", start);
        assertTrue("caller function end not found in generated lua output", end > start);
        String callerBody = compiled.substring(start, end);

        assertFalse("caller should not spill locals into table in this shape", callerBody.contains("__wurst_locals"));
        assertTrue("caller should keep direct call in this shape", callerBody.contains("small(1)"));
    }

    @Test
    public void spilledLocalsKeepNestedBlockInitializationsInLua() throws IOException {
        List<String> lines = new ArrayList<>();
        lines.add("package Test");
        lines.add("native takesInt(int i)");
        lines.add("function hugeNested(boolean b)");
        lines.add("    var sum = 0");
        lines.add("    if b");
        lines.add("        let inside = 7");
        lines.add("        sum += inside");
        for (int i = 0; i < 210; i++) {
            lines.add("    let v" + i + " = " + i);
            lines.add("    sum += v" + i);
        }
        lines.add("    takesInt(sum)");
        lines.add("init");
        lines.add("    hugeNested(true)");

        test().testLua(true).lines(lines.toArray(new String[0]));
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_spilledLocalsKeepNestedBlockInitializationsInLua.lua"), Charsets.UTF_8);
        int start = compiled.indexOf("function hugeNested(");
        assertTrue("hugeNested function not found in generated lua output", start >= 0);
        int end = compiled.indexOf("\nend", start);
        assertTrue("hugeNested function end not found in generated lua output", end > start);
        String body = compiled.substring(start, end);

        assertTrue(body.contains("__wurst_locals"));
        assertContainsRegex(body, "__wurst_locals\\[[0-9]+\\]\\s*=\\s*7");
        assertFalse("nested block local declaration should be rewritten", body.contains("local inside"));
    }

    @Test
    public void spilledLocalsDeclareTableBeforeFirstUseInLua() throws IOException {
        List<String> lines = new ArrayList<>();
        lines.add("package Test");
        lines.add("native takesInt(int i)");
        lines.add("function huge()");
        lines.add("    var sum = 0");
        for (int i = 0; i < 210; i++) {
            lines.add("    let v" + i + " = " + i);
            lines.add("    sum += v" + i);
        }
        lines.add("    takesInt(sum)");
        lines.add("init");
        lines.add("    huge()");

        test().testLua(true).lines(lines.toArray(new String[0]));
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_spilledLocalsDeclareTableBeforeFirstUseInLua.lua"), Charsets.UTF_8);
        int start = compiled.indexOf("function huge(");
        assertTrue("huge function not found in generated lua output", start >= 0);
        int end = compiled.indexOf("\nend", start);
        assertTrue("huge function end not found in generated lua output", end > start);
        String body = compiled.substring(start, end);

        int declarationPos = body.indexOf("local __wurst_locals");
        int firstUsePos = body.indexOf("__wurst_locals[");
        assertTrue("expected __wurst_locals declaration in spilled function body", declarationPos >= 0);
        assertTrue("expected __wurst_locals use in spilled function body", firstUsePos >= 0);
        assertTrue("__wurst_locals must be declared before first table access", declarationPos < firstUsePos);
    }

    @Test
    public void luaInlinerInlinesFunctionsWithMultipleReturns() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_luaInlinerInlinesFunctionsWithMultipleReturns",
            false,
            "package Test",
            "native takesInt(int i)",
            "native randomBool() returns boolean",
            "function choose(boolean b, int x) returns int",
            "    if b",
                "        return x + 1",
            "    return x + 2",
            "function caller()",
            "    let v = choose(randomBool(), 40)",
            "    takesInt(v)",
            "init",
            "    caller()"
        );

        assertDoesNotContainRegex(compiled, "\\bchoose\\s*\\(\\s*randomBool\\s*\\(\\s*\\)\\s*,\\s*40\\s*\\)");
        assertContainsRegex(compiled, "inlineDone");
        assertContainsRegex(compiled, "inlineRet");
    }

    @Test
    public void luaInlinerMultiReturnRewriteIsExplicitWithInlAndLocalOpts() {
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_luaInlinerMultiReturnRewriteIsExplicitWithInlAndLocalOpts",
            false,
            "package Test",
            "native takesInt(int i)",
            "native randomBool() returns boolean",
            "function choose(boolean b, int x) returns int",
            "    if b",
            "        return x + 1",
            "    return x + 2",
            "function caller()",
            "    let v = choose(randomBool(), 40)",
            "    takesInt(v)",
            "init",
            "    caller()"
        );

        assertDoesNotContainRegex(compiled, "\\bchoose\\s*\\(\\s*randomBool\\s*\\(\\s*\\)\\s*,\\s*40\\s*\\)");
        assertContainsRegex(compiled, "inlineDone");
        assertContainsRegex(compiled, "inlineRet");
    }

    @Test
    public void reflectionNativesStubbedForLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native typeIdToTypeName(int typeId) returns string",
            "native maxTypeId() returns int",
            "native instanceCount(int typeId) returns int",
            "native maxInstanceCount(int typeId) returns int",
            "class A",
            "init",
            "    let name = typeIdToTypeName(A.typeId)",
            "    let m = maxTypeId()",
            "    let c = instanceCount(A.typeId)",
            "    let mc = maxInstanceCount(A.typeId)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativesStubbedForLua.lua"), Charsets.UTF_8);
        assertFalse(compiled.contains("The native 'typeIdToTypeName' is not implemented."));
        assertFalse(compiled.contains("The native 'maxTypeId' is not implemented."));
        assertFalse(compiled.contains("The native 'instanceCount' is not implemented."));
        assertFalse(compiled.contains("The native 'maxInstanceCount' is not implemented."));
        assertTrue(compiled.contains("typeIdToTypeName = function"));
        assertTrue(compiled.contains("maxTypeId = function"));
        assertTrue(compiled.contains("instanceCount = function"));
        assertTrue(compiled.contains("maxInstanceCount = function"));
    }

    @Test
    public void reflectionNativeStubBodiesReturnDefaults() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native typeIdToTypeName(int typeId) returns string",
            "native maxTypeId() returns int",
            "native instanceCount(int typeId) returns int",
            "native maxInstanceCount(int typeId) returns int",
            "class A",
            "init",
            "    let _n = typeIdToTypeName(A.typeId)",
            "    let _m = maxTypeId()",
            "    let _c = instanceCount(A.typeId)",
            "    let _mc = maxInstanceCount(A.typeId)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativeStubBodiesReturnDefaults.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("typeIdToTypeName = function"));
        assertTrue(compiled.contains("return \"\""));
        assertTrue(compiled.contains("maxTypeId = function"));
        assertTrue(compiled.contains("instanceCount = function"));
        assertTrue(compiled.contains("maxInstanceCount = function"));
        assertTrue(compiled.contains("return 0"));
    }

    @Test
    public void reflectionNativeStubsAreGuardedByExistingDefinitions() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "native typeIdToTypeName(int typeId) returns string",
            "native maxTypeId() returns int",
            "native instanceCount(int typeId) returns int",
            "native maxInstanceCount(int typeId) returns int",
            "class A",
            "init",
            "    let _n = typeIdToTypeName(A.typeId)",
            "    let _m = maxTypeId()",
            "    let _c = instanceCount(A.typeId)",
            "    let _mc = maxInstanceCount(A.typeId)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_reflectionNativeStubsAreGuardedByExistingDefinitions.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("if typeIdToTypeName then"));
        assertTrue(compiled.contains("if maxTypeId then"));
        assertTrue(compiled.contains("if instanceCount then"));
        assertTrue(compiled.contains("if maxInstanceCount then"));
    }

    @Test
    public void stdLibInitUsesXpcallInsteadOfTriggerEvaluateInMain() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibInitUsesXpcallInsteadOfTriggerEvaluateInMain.lua"), Charsets.UTF_8);
        // Package inits use direct xpcall — no WC3 trigger handle overhead
        assertTrue(compiled.contains("xpcall(init_"));
        assertTrue(compiled.contains("__wurst_init_ok"));
        // TriggerEvaluate pattern must NOT appear for init functions
        assertFalse(compiled.contains("if not(TriggerEvaluate("));
        assertFalse(compiled.contains("TriggerClearConditions"));
    }

    @Test
    public void stdLibDoesNotEmitTimerBjNatives() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibDoesNotEmitTimerBjNatives.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "\\bCreateTimerBJ\\(");
        assertDoesNotContainRegex(compiled, "\\bStartTimerBJ\\(");
        assertDoesNotContainRegex(compiled, "\\bGetLastCreatedTimerBJ\\(");
        assertFalse(compiled.contains("bj_lastStartedTimer"));
    }

    @Test
    public void stdLibDoesNotEmitWar3HashtableNatives() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stdLibDoesNotEmitWar3HashtableNatives.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "\\bInitHashtable\\(");
        assertDoesNotContainRegex(compiled, "\\bSaveInteger\\(");
        assertDoesNotContainRegex(compiled, "\\bSaveBoolean\\(");
        assertDoesNotContainRegex(compiled, "\\bSaveReal\\(");
        assertDoesNotContainRegex(compiled, "\\bSaveStr\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadInteger\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadBoolean\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadReal\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadStr\\(");
        assertDoesNotContainRegex(compiled, "\\bFlushChildHashtable\\(");
        assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\(");
        assertDoesNotContainRegex(compiled, "\\bRemoveSavedHandle\\(");
        assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\(");
        // With IM-level remapping, helpers are only emitted when called.
        // SaveInteger is used by stdlib init code, so its helper must be present.
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_SaveInteger\\s*\\(");
    }

    @Test
    public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import Hashtable",
            "init",
            "    let h = InitHashtable()",
            "    if h.hasHandle(1, 2)",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleExtensionsUseWurstLuaHelpers.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\(");
        assertContainsRegex(compiled, "\\b__wurst_HaveSavedHandle\\(");
    }

    @Test
    public void hashtableNativesAreReplacedByWurstHelpers() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let h = InitHashtable()",
            "    h.saveInt(1, 2, 7)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableNativesAreReplacedByWurstHelpers.lua"), Charsets.UTF_8);
        // Runtime guard overrides are removed; compile-time checks are authoritative.
        assertFalse(compiled.contains("Wurst experimental Lua assertion guards"));
        assertFalse(compiled.contains("__wurst_guard_ok"));
        // __wurst_ helper definitions must still be present.
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_InitHashtable\\s*\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_SaveInteger\\s*\\(");
    }

    @Test
    public void hashtableHandleLoadSaveUseWurstLuaHelpers() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import Hashtable",
            "init",
            "    let h = InitHashtable()",
            "    h.saveAbilityHandle(1, 2, null)",
            "    let a = h.loadAbilityHandle(1, 2)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleLoadSaveUseWurstLuaHelpers.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\(");
        assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\(");
        assertContainsRegex(compiled, "\\b__wurst_SaveAbilityHandle\\(");
        assertContainsRegex(compiled, "\\b__wurst_LoadAbilityHandle\\(");
    }

    @Test
    public void hashtableHelpersEmitPerTypeBucketsInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import Hashtable",
            "init",
            "    let h = InitHashtable()",
            "    h.saveInt(1, 2, 7)",
            "    h.saveReal(1, 2, 3.5)",
            "    h.saveString(1, 2, \"x\")",
            "    h.saveAbilityHandle(1, 2, null)",
            "    let i = h.loadInt(1, 2)",
            "    let r = h.loadReal(1, 2)",
            "    let s = h.loadString(1, 2)",
            "    let a = h.loadAbilityHandle(1, 2)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHelpersEmitPerTypeBucketsInLua.lua"), Charsets.UTF_8);
        assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_int", true);
        assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_real", true);
        assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_str", true);
        assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_handle", true);
        assertFunctionBodyContains(compiled, "__wurst_SaveInteger", "h.__wurst_ht_int", true);
        assertFunctionBodyContains(compiled, "__wurst_SaveReal", "h.__wurst_ht_real", true);
        assertFunctionBodyContains(compiled, "__wurst_SaveStr", "h.__wurst_ht_str", true);
        assertFunctionBodyContains(compiled, "__wurst_SaveAbilityHandle", "h.__wurst_ht_handle", true);
        assertFunctionBodyContains(compiled, "__wurst_LoadInteger", "h.__wurst_ht_int", true);
        assertFunctionBodyContains(compiled, "__wurst_LoadReal", "h.__wurst_ht_real", true);
        assertFunctionBodyContains(compiled, "__wurst_LoadStr", "h.__wurst_ht_str", true);
        assertFunctionBodyContains(compiled, "__wurst_LoadAbilityHandle", "h.__wurst_ht_handle", true);
    }

    @Test
    public void luaFunctionRefWrapperForwardsVarargs() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let f = CreateForce()",
            "    ForForce(f, () -> skip)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaFunctionRefWrapperForwardsVarargs.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("xpcall(function (...)"));
        assertTrue(compiled.contains(", ...)"));
        assertFalse(compiled.contains("local temp = ..."));
        assertFalse(compiled.contains("ForForce(f, function (...) \n\t\t\tlocal tempRes"));
    }

    @Test
    public void forForceIsRemappedToWurstHelperInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let f = CreateForce()",
            "    ForForce(f, () -> begin",
            "        if GetEnumPlayer() != null",
            "            skip",
            "    end)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_forForceIsRemappedToWurstHelperInLua.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "\\b__wurst_ForForce\\(");
        assertContainsRegex(compiled, "\\b__wurst_GetEnumPlayer\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForForce\\s*\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_GetEnumPlayer\\s*\\(");
    }

    @Test
    public void nestedForForceUsesRemappedHelpersInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "force g",
            "function inner()",
            "    ForForce(g, () -> begin",
            "        if GetEnumPlayer() != null",
            "            skip",
            "    end)",
            "init",
            "    g = CreateForce()",
            "    ForForce(g, () -> inner())"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nestedForForceUsesRemappedHelpersInLua.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForForce\\s*\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_GetEnumPlayer\\s*\\(");
        Matcher forForceCalls = Pattern.compile("\\b__wurst_ForForce\\s*\\(").matcher(compiled);
        int count = 0;
        while (forForceCalls.find()) {
            count++;
        }
        assertTrue("expected at least two remapped __wurst_ForForce call sites for nested loops", count >= 2);
    }

    @Test
    public void luaInlinerKeepsCallbackFuncRefFunctionsAsCallBoundary() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "player picked",
            "function pick(force f) returns player",
            "    picked = null",
            "    ForForce(f, () -> begin",
            "        picked = GetEnumPlayer()",
            "    end)",
            "    return picked",
            "function caller(force f) returns player",
            "    return pick(f)",
            "init",
            "    let f = CreateForce()",
            "    let p = caller(f)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaInlinerKeepsCallbackFuncRefFunctionsAsCallBoundary.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "function\\s+caller\\s*\\([^\\)]*\\)\\s+return\\s+pick\\([^\\)]*\\)\\s+end");
    }

    @Test
    public void wurstGetEnumPlayerPrefersNativeBeforeOverride() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let f = CreateForce()",
            "    ForForce(f, () -> begin",
            "        if GetEnumPlayer() != null",
            "            skip",
            "    end)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_wurstGetEnumPlayerPrefersNativeBeforeOverride.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "function\\s+__wurst_GetEnumPlayer\\s*\\(\\)");
        assertContainsRegex(compiled, "if GetEnumPlayer ~= nil then\\s+local p = GetEnumPlayer\\(\\)\\s+if p ~= nil then return p end\\s+end\\s+if __wurst_enumPlayer_override ~= nil then return __wurst_enumPlayer_override end");
    }

    @Test
    public void groupItemDestructableCallbacksUseWurstContextHelpersInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let g = CreateGroup()",
            "    ForGroup(g, () -> begin",
            "        if GetEnumUnit() != null",
            "            skip",
            "    end)",
            "    EnumItemsInRect(null, null, () -> begin",
            "        if GetEnumItem() != null",
            "            skip",
            "    end)",
            "    EnumDestructablesInRect(null, null, () -> begin",
            "        if GetEnumDestructable() != null",
            "            skip",
            "    end)"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_groupItemDestructableCallbacksUseWurstContextHelpersInLua.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "\\b__wurst_ForGroup\\(");
        assertContainsRegex(compiled, "\\b__wurst_GetEnumUnit\\(");
        assertContainsRegex(compiled, "\\b__wurst_EnumItemsInRect\\(");
        assertContainsRegex(compiled, "\\b__wurst_GetEnumItem\\(");
        assertContainsRegex(compiled, "\\b__wurst_EnumDestructablesInRect\\(");
        assertContainsRegex(compiled, "\\b__wurst_GetEnumDestructable\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForGroup\\s*\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_EnumItemsInRect\\s*\\(");
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_EnumDestructablesInRect\\s*\\(");
    }

    @Test
    public void i2sDivisionByZeroCrashTrapUsesAbortSentinelInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_i2sDivisionByZeroCrashTrapUsesAbortSentinelInLua.lua"), Charsets.UTF_8);
        assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*//\\s*0\\s*\\)");
        assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*/\\s*0\\s*\\)");
        assertDoesNotContainRegex(compiled, "I2S\\s*\\(\\s*1\\s*/\\s*0\\s*\\)");
        assertDoesNotContainRegex(compiled, "I2S\\s*\\(\\s*1\\s*//\\s*0\\s*\\)");
        assertTrue(compiled.contains("__wurst_abort_thread"));
        assertDoesNotContainRegex(compiled, "error\\s*\\(\\s*\"[^\"]*divide by zero[^\"]*\"");
    }

    @Test
    public void i2sAbortSentinelNotBrokenByEarlierI2SCallInLua() throws IOException {
        // Regression: ExprTranslation used f.setName("tostring") to rename the shared
        // LuaFunction for I2S. After the first ordinary I2S(x) call was translated, f.getName()
        // returned "tostring", so the sentinel check "I2S".equals(f.getName()) silently failed
        // for any later I2S(1/0) abort call — leaving actual //0 division in the Lua output.
        // This test forces a normal I2S call before the abort to expose the mutation bug.
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "import ErrorHandling",
            "init",
            "    let x = 42",
            "    print(I2S(x))",   // normal I2S call first — used to corrupt the cached LuaFunction
            "    error(\"test\")"  // triggers I2S(1 div 0) inside ErrorHandling.error()
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_i2sAbortSentinelNotBrokenByEarlierI2SCallInLua.lua"), Charsets.UTF_8);
        // The abort must be the sentinel, not a raw //0 that Lua would crash on at runtime
        assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*//\\s*0\\s*\\)");
        assertDoesNotContainRegex(compiled, "tostring\\s*\\(\\s*1\\s*/\\s*0\\s*\\)");
        assertTrue(compiled.contains("__wurst_abort_thread"));
        // tostring() must still be used for the normal I2S call
        assertContainsRegex(compiled, "tostring\\s*\\(");
    }

    @Test
    public void luaErrorWrapperIgnoresAbortSentinel() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "function f()",
            "    skip",
            "init",
            "    f()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaErrorWrapperIgnoresAbortSentinel.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("if err == \"__wurst_abort_thread\" then return end"));
        assertTrue(compiled.contains("if err2 == \"__wurst_abort_thread\" then return end"));
    }

    @Test
    public void removesUnusedClassesFromLuaOutput() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class Keep",
            "    function ping()",
            "        skip",
            "class Drop",
            "    function pong()",
            "        skip",
            "init",
            "    let k = new Keep()",
            "    k.ping()"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_removesUnusedClassesFromLuaOutput.lua"), Charsets.UTF_8);
        assertTrue(compiled.contains("Keep"));
        assertFalse(compiled.contains("Drop"));
    }

    @Test
    public void subclassAllocationIncludesInheritedFieldsInLua() throws IOException {
        test().testLua(true).lines(
            "package Test",
            "class Window",
            "    int anchorTop",
            "    int anchorBottom",
            "    function anchored() returns boolean",
            "        return anchorTop == 0 and anchorBottom == 0",
            "class Child extends Window",
            "    int ownField",
            "init",
            "    let c = new Child()",
            "    c.ownField = 7",
            "    if c.anchored()",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_subclassAllocationIncludesInheritedFieldsInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled,
            "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)\\s*\\n\\s*local new_inst = \\(\\{[^\\n]*Window_anchorTop=");
        assertContainsRegex(compiled,
            "function\\s+[A-Za-z0-9_]+:create\\d+\\s*\\(\\)\\s*\\n\\s*local new_inst = \\(\\{[^\\n]*Window_anchorBottom=");
    }

    // ----- GetHandleId remapping -----

    @Test
    public void getHandleIdIsRemappedToWurstPolyfillInLua() throws IOException {
        requireSelectiveGetHandleIdShimming();
        // Direct GetHandleId calls on opaque runtime handles should still use the Lua-side
        // identity shim.
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let u = GetTriggerUnit()",
            "    let id = GetHandleId(u)",
            "    print(I2S(id))"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_getHandleIdIsRemappedToWurstPolyfillInLua.lua"), Charsets.UTF_8);
        assertContainsRegex(compiled, "function\\s+__wurst_GetHandleId\\s*\\(");
        assertContainsRegex(compiled, "id\\s*=\\s*__wurst_GetHandleId\\(u\\)");
        String helperBody = getFunctionBody(compiled, "__wurst_GetHandleId");
        assertTrue(helperBody.contains("return __wurst_objectToIndex(h)"));
        assertFalse(helperBody.contains("__wurst_tryRegisterKnownEventHandle"));
        assertFalse(helperBody.contains("__wurst_canonicalHandleIdByHandle"));
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator.assertNoLeakedGetHandleIdCalls(compiled);
    }

    @Test
    public void closureEventsHandleGetHandleIdUsesNativeGetHandleIdInLua() throws IOException {
        test().testLua(true).withStdLib().executeProg(false)
            .file(new File("testscripts/realbugs/nullclosurebug.wurst"));

        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_closureEventsHandleGetHandleIdUsesNativeGetHandleIdInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "function\\s+handle_getHandleId\\s*\\(");
        String helperBody = getFunctionBody(compiled, "handle_getHandleId");
        assertTrue(helperBody.contains("return GetHandleId("));
        assertFalse(helperBody.contains("__wurst_GetHandleId("));
        assertContainsRegex(compiled, "eventid_toIntId\\(GetTriggerEventId\\(\\)\\)");
        assertContainsRegex(compiled, "function\\s+eventid_isPlayerunitEvent\\s*\\(");
        assertContainsRegex(compiled, "registerPlayerUnitEvent1\\(ConvertPlayerUnitEvent\\(eventId\\)");
        assertContainsRegex(compiled, "eventId\\s*=\\s*handle_getHandleId\\(evnt\\)");
        assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(evnt\\)");
        assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(GetTriggerEventId\\(\\)\\)");
    }

    @Test
    public void closureEventsEventIdCompatDoesNotUseOpaqueHandleShimInLua() throws IOException {
        requireSelectiveGetHandleIdShimming();
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import ClosureEvents",
            "init",
            "    let u = GetTriggerUnit()",
            "    let unitId = GetHandleId(u)",
            "    let ev = EVENT_PLAYER_UNIT_SPELL_EFFECT",
            "    let id = ev.getHandleId()",
            "    if unitId + id >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_closureEventsEventIdCompatDoesNotUseOpaqueHandleShimInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "function\\s+handle_getHandleId\\s*\\(");
        String helperBody = getFunctionBody(compiled, "handle_getHandleId");
        assertTrue(helperBody.contains("return GetHandleId("));
        assertFalse(helperBody.contains("__wurst_GetHandleId("));
        assertDoesNotContainRegex(compiled, "function\\s+handle_getHandleId\\s*\\([^\\)]*\\)\\s*\\n\\s*return\\s+__wurst_GetHandleId\\(");
        assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\(");
    }

    @Test
    public void registerEventsSpellCastKeepsNativeEventHandleIdsWhileUnitHandlesStayShimmedInLua() throws IOException {
        requireSelectiveGetHandleIdShimming();
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import RegisterEvents",
            "init",
            "    let u = GetTriggerUnit()",
            "    let unitId = GetHandleId(u)",
            "    registerPlayerUnitEvent(EVENT_PLAYER_UNIT_SPELL_CAST) ->",
            "        skip",
            "    if unitId >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_registerEventsSpellCastKeepsNativeEventHandleIdsWhileUnitHandlesStayShimmedInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "registerPlayerUnitEvent\\(EVENT_PLAYER_UNIT_SPELL_CAST");
        assertContainsRegex(compiled, "function\\s+registerPlayerUnitEvent\\s*\\(");
        assertContainsRegex(compiled, "hid\\s*=\\s*handle_getHandleId\\(");
        String helperBody = getFunctionBody(compiled, "handle_getHandleId");
        assertTrue(helperBody.contains("return GetHandleId("));
        assertFalse(helperBody.contains("__wurst_GetHandleId("));
        assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(p1\\)");
        assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\(");
    }

    @Test
    public void eventGetHandleIdMethodKeepsNativeSemanticsWhileOpaqueHandleMethodUsesLuaShim() throws IOException {
        requireSelectiveGetHandleIdShimming();
        test().testLua(true).withStdLib().lines(
            "package Test",
            "init",
            "    let u = GetTriggerUnit()",
            "    let uid = u.getHandleId()",
            "    let pe = EVENT_PLAYER_LEAVE",
            "    let pid = pe.getHandleId()",
            "    let pue = EVENT_PLAYER_UNIT_SPELL_CAST",
            "    let pueid = pue.getHandleId()",
            "    if uid + pid + pueid >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventGetHandleIdMethodKeepsNativeSemanticsWhileOpaqueHandleMethodUsesLuaShim.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "=\\s*__wurst_GetHandleId\\(");
        assertContainsRegex(compiled, "handle_getHandleId\\(");
        String helperBody = getFunctionBody(compiled, "handle_getHandleId");
        assertTrue(helperBody.contains("return GetHandleId("));
        assertFalse(helperBody.contains("__wurst_GetHandleId("));
    }

    @Test
    public void eventHandleIdCallKeepsNativeGetHandleIdInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let eventId = GetHandleId(EVENT_PLAYER_LEAVE)",
            "    if eventId >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventHandleIdCallKeepsNativeGetHandleIdInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "eventId\\s*=\\s*GetHandleId\\(EVENT_PLAYER_LEAVE\\)");
        assertDoesNotContainRegex(compiled, "__wurst_GetHandleId\\(EVENT_PLAYER_LEAVE\\)");
    }

    @Test
    public void eventTempHandleIdCallKeepsNativeGetHandleIdInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let ev = EVENT_PLAYER_UNIT_SPELL_EFFECT",
            "    let id = GetHandleId(ev)",
            "    if id >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_eventTempHandleIdCallKeepsNativeGetHandleIdInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "ev\\s*=\\s*EVENT_PLAYER_UNIT_SPELL_EFFECT");
        assertContainsRegex(compiled, "id\\s*=\\s*GetHandleId\\(ev\\)");
        assertDoesNotContainRegex(compiled, "id\\s*=\\s*__wurst_GetHandleId\\(ev\\)");
    }

    @Test
    public void enumConvertCallsStayNativeInLua() throws IOException {
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let pe = ConvertPlayerEvent(311)",
            "    let pc = ConvertPlayerColor(0)",
            "    let et = ConvertEffectType(0)",
            "    if GetHandleId(pe) >= 0 and GetHandleId(pc) >= 0 and GetHandleId(et) >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_enumConvertCallsStayNativeInLua.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "pe\\s*=\\s*ConvertPlayerEvent\\(311\\)");
        assertContainsRegex(compiled, "pc\\s*=\\s*ConvertPlayerColor\\(0\\)");
        assertContainsRegex(compiled, "et\\s*=\\s*ConvertEffectType\\(0\\)");
        assertContainsRegex(compiled, "GetHandleId\\(pe\\)");
        assertContainsRegex(compiled, "GetHandleId\\(pc\\)");
        assertContainsRegex(compiled, "GetHandleId\\(et\\)");
        assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertPlayerEvent\\s*\\(");
        assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertPlayerColor\\s*\\(");
        assertDoesNotContainRegex(compiled, "function\\s+__wurst_ConvertEffectType\\s*\\(");
    }

    @Test
    public void conservativeLuaHandleIdLoweringDoesNotEmitCanonicalEnumHelpers() throws IOException {
        requireSelectiveGetHandleIdShimming();
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let u = GetTriggerUnit()",
            "    let uid = GetHandleId(u)",
            "    let eid = GetHandleId(EVENT_PLAYER_LEAVE)",
            "    if uid + eid >= 0",
            "        skip"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_conservativeLuaHandleIdLoweringDoesNotEmitCanonicalEnumHelpers.lua"), Charsets.UTF_8);

        assertContainsRegex(compiled, "uid\\s*=\\s*__wurst_GetHandleId\\(u\\)");
        assertContainsRegex(compiled, "eid\\s*=\\s*GetHandleId\\(EVENT_PLAYER_LEAVE\\)");
        assertDoesNotContainRegex(compiled, "__wurst_registerCanonicalHandleId");
        assertDoesNotContainRegex(compiled, "__wurst_tryRegisterKnownEventHandle");
        assertDoesNotContainRegex(compiled, "__wurst_canonicalHandleIdByHandle");
    }

    @Test
    public void getHandleIdAssertionOnlyRequiresPolyfillDefinitionWhenUsed() {
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("GetHandleId(x)");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function Foo:GetHandleId(x) return 0 end");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function Foo.GetHandleId(x) return 0 end");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("-- GetHandleId(x) is allowed for enum-like handles");
        try {
            de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
                .assertNoLeakedGetHandleIdCalls("__wurst_GetHandleId(x)");
            fail("Expected RuntimeException for missing __wurst_GetHandleId helper definition");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().contains("__wurst_GetHandleId"));
        }
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function __wurst_GetHandleId(h) return 0 end\n__wurst_GetHandleId(x)");
    }

    @Test
    public void getHandleIdAssertionDetectsLeak() {
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("GetHandleId(x)");
        try {
            de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
                .assertNoLeakedGetHandleIdCalls("__wurst_GetHandleId(x)");
            fail("Expected RuntimeException for missing __wurst_GetHandleId helper definition");
        } catch (RuntimeException e) {
            assertTrue(e.getMessage().contains("__wurst_GetHandleId"));
        }
    }

    @Test
    public void getHandleIdAssertionDoesNotFalsePositiveOnDeclarations() {
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("GetHandleId(x)");
        // A function declaration named GetHandleId (e.g. from a class method) must not
        // trigger the assertion — only actual calls should.
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function Foo:GetHandleId(x) return 0 end");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function Foo.GetHandleId(x) return 0 end");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("-- GetHandleId(x) is allowed for enum-like handles");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("local s = \"GetHandleId(x)\"");
        de.peeeq.wurstscript.translation.lua.translation.LuaTranslator
            .assertNoLeakedGetHandleIdCalls("function __wurst_GetHandleId(h) return 0 end\n__wurst_GetHandleId(x)");
    }

    // ----- Null-safe extern native wrappers -----

    @Test
    public void externNativesWithHandleParamsGetNilSafetyWrapper() throws IOException {
        // IS_BJ functions with handle-type parameters must be wrapped at the IM level
        // so that passing nil returns a safe default instead of crashing the Lua runtime.
        // GetUnitTypeId is a common.j native (IS_BJ) with a handle param, returns integer.
        // Result is used in print() so the optimizer does not dead-code-eliminate the call.
        test().testLua(true).withStdLib().lines(
            "package Test",
            "import MagicFunctions",
            "init",
            "    let u = GetTriggerUnit()",
            "    print(I2S(GetUnitTypeId(u)))"
        );
        String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_externNativesWithHandleParamsGetNilSafetyWrapper.lua"), Charsets.UTF_8);
        // A nil-safety wrapper must be emitted for GetUnitTypeId (IS_BJ, handle → integer)
        assertContainsRegex(compiled, "\\bfunction\\s+__wurst_safe_GetUnitTypeId\\s*\\(");
        // Call site must be remapped to the wrapper
        assertContainsRegex(compiled, "\\b__wurst_safe_GetUnitTypeId\\(");
        // Return default for integer type must be 0
        assertContainsRegex(compiled, "return 0");
    }

    @Test
    public void externNativesWithOnlyPrimitiveParamsDoNotGetWrapper() throws IOException {
        // Natives that only take integer/real/boolean params cannot crash with nil,
        // so no wrapper should be emitted for them.
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_externNativesWithOnlyPrimitiveParamsDoNotGetWrapper",
            false,
            "package Test",
            "native Sin(real x) returns real",
            "native I2R(int i) returns real",
            "init",
            "    let s = Sin(1.0)",
            "    let r = I2R(1)"
        );
        // No nil-safety wrapper should be emitted for purely-primitive natives
        assertDoesNotContainRegex(compiled, "__wurst_safe_Sin");
        assertDoesNotContainRegex(compiled, "__wurst_safe_I2R");
    }

    @Test
    public void wurstInternalNativesDoNotGetNilSafetyWrapper() throws IOException {
        // Wurst-internal natives (__wurst_* names) must not be double-wrapped.
        String compiled = compileLuaWithRunArgs(
            "LuaTranslationTests_wurstInternalNativesDoNotGetNilSafetyWrapper",
            false,
            "package Test",
            "native testSuccess()"
        );
        // __wurst_* helpers must not have the nil-safety wrapper pattern applied
        assertDoesNotContainRegex(compiled, "__wurst_safe___wurst_");
    }

    @Test
    public void isLuaMagicConstantIsTrueInLuaMode() {
        // isLua must be replaced with true by LuaNativeLowering so the optimizer can
        // prune Jass-only branches at compile time. This test does NOT use withStdLib()
        // to avoid dependence on the stdlib version pin in StdLib.java.
        CU magicFunctions = new CU("MagicFunctions.wurst",
            "package MagicFunctions\npublic constant isLua = false\n");
        String compiled = compileLuaWithCUs(
            "LuaTranslationTests_isLuaMagicConstantIsTrueInLuaMode",
            false,
            Collections.singletonList(magicFunctions),
            "package Test",
            "import MagicFunctions",
            "native print(string s)",
            "init",
            "    if isLua",
            "        print(\"lua-path\")",
            "    else",
            "        print(\"jass-path\")"
        );
        // After LuaNativeLowering, all reads of MagicFunctions_isLua must be inlined to true
        assertFalse("MagicFunctions_isLua must be inlined away in Lua mode", compiled.contains("MagicFunctions_isLua"));
        // The lua-path branch must be preserved
        assertTrue("lua-path branch must be present", compiled.contains("lua-path"));
    }

}
