import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import { join } from "node:path";
import { isDeepStrictEqual } from "node:util";
import { hasOwnErrorCode } from "../error-codes.js";
import { runHarnessPair } from "../loader/run.js";
let nextReplayId = 0;
export async function assertReplayEquivalent(path, modulesFor) {
    const snapshotPath = await createSnapshotPath();
    const hostCallStorePath = `${snapshotPath}.host-calls.json`;
    const originalBackend = new MemorySnapshotBackend();
    try {
        const original = await runHarnessPair(path, {
            clock: createDeterministicClock(),
            modulesFor,
            preserveSnapshotOnSuccess: true,
            resume: false,
            snapshotBackend: originalBackend,
            // Force the scheduler threshold into the past so every yielded snapshot is captured.
            snapshotIntervalMs: -1,
            snapshotPath
        });
        const originalReturnValue = readReturnValue(original, "original");
        const originalHostCalls = await readOptionalTextFile(hostCallStorePath);
        const snapshots = [...originalBackend.writes, original.snapshot];
        for (let index = 0; index < snapshots.length; index += 1) {
            const replaySnapshot = snapshots[index];
            await restoreOptionalTextFile(hostCallStorePath, originalHostCalls);
            const replayReturnValue = readReturnValue(await runHarnessPair(path, {
                clock: createDeterministicClock(),
                modulesFor,
                preserveSnapshotOnSuccess: true,
                snapshotBackend: new MemorySnapshotBackend(replaySnapshot),
                snapshotIntervalMs: 0,
                snapshotPath
            }), `replay ${index + 1}`);
            if (!isDeepStrictEqual(replayReturnValue, originalReturnValue)) {
                throw new Error([
                    `Replay equivalence failed: non-deterministic return value from snapshot ${index + 1}/${snapshots.length}.`,
                    `Original: ${formatValue(originalReturnValue)}`,
                    `Replay: ${formatValue(replayReturnValue)}`
                ].join("\n"));
            }
        }
    }
    finally {
        await Promise.all([
            rm(snapshotPath, { force: true }),
            rm(hostCallStorePath, { force: true }),
            rm(`${snapshotPath}.tmp`, { force: true })
        ]);
    }
}
class MemorySnapshotBackend {
    snapshot;
    writes = [];
    constructor(snapshot) {
        this.snapshot = snapshot;
    }
    async read() {
        return this.snapshot;
    }
    async write(snapshot) {
        const copy = copySnapshot(snapshot);
        this.writes.push(copy);
        this.snapshot = copy;
    }
    async remove() {
        this.snapshot = undefined;
    }
}
function readReturnValue(result, label) {
    if (result.ok) {
        return result.returnValue;
    }
    throw new Error(`Cannot assert replay equivalence because the ${label} run failed.`);
}
function createDeterministicClock() {
    let next = 1_700_000_000_000;
    return {
        now() {
            const value = next;
            next += 1;
            return value;
        }
    };
}
async function createSnapshotPath() {
    const tempRoot = os.tmpdir();
    await mkdir(tempRoot, { recursive: true });
    nextReplayId += 1;
    return join(tempRoot, `poe-harness-replay-${process.pid}-${nextReplayId}.json`);
}
async function readOptionalTextFile(path) {
    try {
        return await readFile(path, "utf8");
    }
    catch (error) {
        if (hasErrorCode(error, "ENOENT")) {
            return undefined;
        }
        throw error;
    }
}
async function restoreOptionalTextFile(path, content) {
    if (content === undefined) {
        await rm(path, { force: true });
        return;
    }
    await writeFile(path, content);
}
function copySnapshot(snapshot) {
    return JSON.parse(JSON.stringify(snapshot));
}
function formatValue(value) {
    try {
        return JSON.stringify(value);
    }
    catch {
        return String(value);
    }
}
function hasErrorCode(error, code) {
    return hasOwnErrorCode(error, code);
}
