// Proves the fork's GENERATED @data class (from --rpcModule) matches the AS @data
// wire byte-for-byte. Compiles spec.ts (which has `@data class Foo`) with the
// ToilScript fork, emitting both the wasm and the generated server.ts, then checks
// the generated Foo class against the AS Foo.encode() bytes, both directions.
// Run with: node tests/data-parity/generated-parity.ts
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawnSync } from "node:child_process";
const here = dirname(fileURLToPath(import.meta.url));
// The generated module imports DataWriter/DataReader from this specifier.
const codec = join(here, "..", "..", "src", "io", "codec.ts");
const fork = "/root/toil-stuff/toilscript";
const spec = join(here, "spec.ts");
const tmp = mkdtempSync(join(tmpdir(), "gen-parity-"));
const wasmPath = join(tmp, "spec.wasm");
const modPath = join(tmp, "server.ts");
writeFileSync(join(tmp, "package.json"), '{ "type": "module" }\n');

const compile = spawnSync(
    "node",
    [join(fork, "bin", "toilscript.js"), spec, "-o", wasmPath, "--runtime", "stub", "--initialMemory", "32", "--rpcModule", modPath, "--rpcRuntime", codec],
    { stdio: "inherit" },
);
if (compile.status !== 0) {
    console.error("generated parity: COMPILE FAILED");
    rmSync(tmp, { recursive: true, force: true });
    process.exit(1);
}

function fail(msg: string): never {
    console.error("generated parity: FAIL,", msg);
    rmSync(tmp, { recursive: true, force: true });
    process.exit(1);
}
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
    return true;
}
const hex = (b: Uint8Array): string => Buffer.from(b).toString("hex");

// the known sample (must match spec.ts `sample()`)
const ID = 0xcafebabedeadbeefn;
const COUNT = -42;
const FLAG = true;
const BIG = 123456789n;
const NAME = "cross-lang";

// AS side: instantiate and read the sample bytes out of linear memory.
const { instance } = await WebAssembly.instantiate(readFileSync(wasmPath), {
    env: { abort: (_m: number, _f: number, line: number) => { throw new Error("wasm abort @ line " + line); } },
});
const x = instance.exports as Record<string, CallableFunction> & { memory: WebAssembly.Memory };
const SCRATCH = 0x100000;
const len = x.encodeSampleTo(SCRATCH) as number;
const wasmBytes = new Uint8Array(x.memory.buffer, SCRATCH, len).slice();

// Generated side: build the same value with the generated Foo class and encode.
const gen = (await import(pathToFileURL(modPath).href)) as {
    Foo: new () => {
        id: bigint; count: number; flag: boolean; big: bigint; name: string;
        encode(): Uint8Array;
    } & Record<string, unknown>;
};
const FooClass = gen.Foo as unknown as {
    new (): { id: bigint; count: number; flag: boolean; big: bigint; name: string; encode(): Uint8Array };
    decode(buf: Uint8Array): { id: bigint; count: number; flag: boolean; big: bigint; name: string };
    dataId(): number;
};

const foo = new FooClass();
foo.id = ID;
foo.count = COUNT;
foo.flag = FLAG;
foo.big = BIG;
foo.name = NAME;
const genBytes = foo.encode();

// 1) byte-for-byte identical to the AS @data encoding.
if (!bytesEqual(genBytes, wasmBytes)) {
    fail(`byte mismatch\n  AS:        ${hex(wasmBytes)}\n  generated: ${hex(genBytes)}`);
}

// 2) the generated decode round-trips the AS bytes back to the sample.
const back = FooClass.decode(wasmBytes);
if (back.id !== ID) fail("id (decode)");
if (back.count !== COUNT) fail("count (decode)");
if (back.flag !== FLAG) fail("flag (decode)");
if (back.big !== BIG) fail("big (decode)");
if (back.name !== NAME) fail("name (decode)");

// 3) the AS side accepts the generated bytes.
new Uint8Array(x.memory.buffer, SCRATCH, genBytes.length).set(genBytes);
if ((x.checkBytes(SCRATCH, genBytes.length) as number) !== 1) fail("AS rejected generated bytes");

console.log(`@data generated-class parity: PASS (generated <-> AS both ways, byte-for-byte, ${len} bytes)`);
rmSync(tmp, { recursive: true, force: true });
