#!/usr/bin/env node
"use strict";

const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;

const assert = require("assert").strict;
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");

const libwabt = require("../../build/libwabt.js")();

try {
    var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86;
}
catch(e) {
    console.error(e);
    console.error("Failed to import build/libv86-debug.js. Run " +
                  "`make build/libv86-debug.js` first.");
    process.exit(1);
}

const TEST_NAME = process.env.TEST_NAME;

const LOG_LEVEL = 0;
const MIN_MEMORY_OFFSET = 4096;

const GIT_DIFF_FLAGS = ["--no-index", "--patience", "--color=always"];

const TEST_DIR = path.join(__dirname, "tests");
const BUILD_DIR = path.join(TEST_DIR, "build");

function run_all()
{
    const asm_files = fs.readdirSync(TEST_DIR).filter(filename => filename.endsWith(".asm"));

    const files = asm_files.map(asm_file => {
        const name = asm_file.slice(0, -4);
        return {
            name,
            expect_file: path.relative(".", path.join(TEST_DIR, name + ".wast")),
            actual_file: path.relative(".", path.join(BUILD_DIR, name + ".actual.wast")),
            actual_wasm: path.relative(".", path.join(BUILD_DIR, name + ".wasm")),
            asm_file: path.join(TEST_DIR, name + ".asm"),
            executable_file: path.join(BUILD_DIR, name + ".bin"),
        };
    }).filter(({ name }) => !TEST_NAME || name === TEST_NAME);

    next_test(0);

    function next_test(i)
    {
        if(files[i])
        {
            run_test(files[i], () => next_test(i + 1));
        }
    }
}

// Remove parts that may not be stable between multiple runs
function normalise_wast(wast)
{
    return wast.replace(/offset=(\d+)/g, function(match, offset)
        {
            offset = Number(offset);

            if(offset >= MIN_MEMORY_OFFSET)
            {
                return "offset={normalised output}";
            }
            else
            {
                return match;
            }
        }).replace(/memory \$[\w\.]+ \d+/g, "memory {normalised output}");
}

function run_test({ name, executable_file, expect_file, actual_file, actual_wasm, asm_file }, onfinished)
{
    const emulator = new V86({
        autostart: false,
        memory_size: 2 * 1024 * 1024,
        log_level: LOG_LEVEL,
    });

    const executable = fs.readFileSync(executable_file);
    const asm = fs.readFileSync(asm_file);

    const is_32 = asm.includes("BITS 32\n");

    emulator.add_listener("emulator-loaded", function()
        {
            const cpu = emulator.v86.cpu;

            const hook_not_called_timeout = setTimeout(() => {
                throw new Error("Hook for code generation not called");
            }, 1000);

            cpu.test_hook_did_generate_wasm = function(wasm)
            {
                const wast = normalise_wast(disassemble_wasm(wasm));

                clearTimeout(hook_not_called_timeout);
                fs.writeFileSync(actual_file, wast);
                fs.writeFileSync(actual_wasm, wasm);

                cpu.test_hook_did_generate_wasm = function()
                {
                    cpu.test_hook_did_generate_wasm = function() {};
                    throw new Error("Hook for wasm generation called multiple times");
                };

                if(!fs.existsSync(expect_file))
                {
                    // enhanced workflow: If file doesn't exist yet print full diff
                    var expect_file_for_diff = "/dev/null";
                }
                else
                {
                    expect_file_for_diff = expect_file;
                }

                const result = spawnSync("git",
                    [].concat(
                        "diff",
                        GIT_DIFF_FLAGS,
                        expect_file_for_diff,
                        actual_file
                    ),
                    { encoding: "utf8" });

                if(result.status)
                {
                    console.log(result.stdout);
                    console.log(result.stderr);

                    if(process.argv.includes("--accept-all"))
                    {
                        console.log(`Running: cp ${actual_file} ${expect_file}`);
                        fs.copyFileSync(actual_file, expect_file);
                    }
                    else
                    {
                        const failure_message = `${name}.asm failed:
The code generator produced different code. If you believe this change is intentional,
verify the diff above and run the following command to accept the change:

    cp ${actual_file} ${expect_file}

When done, re-run this test to confirm that all expect-tests pass.

Hint: Use tests/expect/run.js --accept-all to accept all changes (use git diff to verify).
`;

                        console.log(failure_message);

                        process.exit(1);
                    }
                }
                else
                {
                    console.log("%s ok", name);
                    assert(!result.stdout);
                    assert(!result.stderr);
                }

                onfinished();
            };

            if(is_32)
            {
                cpu.is_32[0] = true;
                cpu.stack_size_32[0] = true;
            }

            const START_ADDRESS = 0x1000;

            cpu.mem8.set(executable, START_ADDRESS);
            cpu.update_state_flags();
            cpu.jit_force_generate(START_ADDRESS);
        });
}

function disassemble_wasm(wasm)
{
    // Need to make a small copy otherwise libwabt goes nuts trying to copy
    // the whole underlying buffer
    wasm = wasm.slice();

    try
    {
        var module = libwabt.readWasm(wasm, { readDebugNames: false });
        module.generateNames();
        module.applyNames();
        return module.toText({ foldExprs: true, inlineExport: true });
    }
    catch(e)
    {
        console.error("Error while running libwabt: " + e.toString());
        console.error("Did you forget an ending hlt instruction?\n");
        throw e;
    }
    finally
    {
        module && module.destroy();
    }
}

run_all();
