// @vitest-environment node

import { createHash } from "node:crypto";
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { writeEngineChecksums } from "./checksummed-engine-pack.mjs";
import { DOTNET_LOCK, verifyDotNetPack } from "./dotnet-engine-pack.mjs";

const temporaryDirectories: string[] = [];

afterEach(async () => {
  await Promise.all(
    temporaryDirectories
      .splice(0)
      .map((directory) => rm(directory, { recursive: true, force: true })),
  );
});

describe(".NET engine-pack gate", () => {
  it("verifies the installed exact runtime, bridge sources and closed checksums", async () => {
    await expect(
      verifyDotNetPack(path.resolve("public", "engines", "dotnet")),
    ).resolves.toEqual(
      expect.objectContaining({
        engineVersion: ".NET 10.0.10 System.Text.RegularExpressions",
        source: expect.objectContaining({
          runtimeVersion: "10.0.10",
          runtimeCommit: "f7d90799ce",
        }),
        toolchain: expect.objectContaining({
          sdkVersion: "10.0.302",
          sdkCommit: "35b593bebf",
          workload: "wasm-tools",
        }),
      }),
    );
  });

  it("pins the compiler/runtime identity used by the checked-in pack", () => {
    expect(DOTNET_LOCK).toEqual(
      expect.objectContaining({
        sdkVersion: "10.0.302",
        runtimeVersion: "10.0.10",
        bridgeAbi: 1,
      }),
    );
  });

  it("rejects modified framework assets", async () => {
    const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
    temporaryDirectories.push(directory);
    const pack = path.join(directory, "dotnet");
    await cp(path.resolve("public", "engines", "dotnet"), pack, {
      recursive: true,
    });
    const metadata = JSON.parse(
      await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
    ) as {
      files: readonly { path: string }[];
    };
    const runtime = metadata.files.find(({ path: name }) =>
      /^_framework\/System\.Text\.RegularExpressions\..+\.wasm$/u.test(name),
    );
    expect(runtime).toBeDefined();
    await writeFile(path.join(pack, runtime?.path ?? ""), "modified");

    await expect(verifyDotNetPack(pack)).rejects.toThrow(/SHA-256 mismatch/u);
  });

  it("rejects substituted notices even when self-reported checksums are regenerated", async () => {
    const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
    temporaryDirectories.push(directory);
    const pack = path.join(directory, "dotnet");
    await cp(path.resolve("public", "engines", "dotnet"), pack, {
      recursive: true,
    });
    const replacement = Buffer.from("substituted notices\n");
    await writeFile(path.join(pack, "THIRD-PARTY-NOTICES.txt"), replacement);
    const metadataFile = path.join(pack, "engine-metadata.json");
    const metadata = JSON.parse(await readFile(metadataFile, "utf8")) as {
      files: { path: string; sha256: string; bytes: number }[];
    };
    const record = metadata.files.find(
      ({ path: name }) => name === "THIRD-PARTY-NOTICES.txt",
    );
    expect(record).toBeDefined();
    if (!record) return;
    record.sha256 = createHash("sha256").update(replacement).digest("hex");
    record.bytes = replacement.byteLength;
    await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
    await writeEngineChecksums(pack);

    await expect(verifyDotNetPack(pack)).rejects.toThrow(
      /pinned runtime contract/u,
    );
  });

  it("rejects a build-host path hidden in WebAssembly after checksums are regenerated", async () => {
    const directory = await mkdtemp(path.join(tmpdir(), "regex-dotnet-pack-"));
    temporaryDirectories.push(directory);
    const pack = path.join(directory, "dotnet");
    await cp(path.resolve("public", "engines", "dotnet"), pack, {
      recursive: true,
    });
    const metadataFile = path.join(pack, "engine-metadata.json");
    const metadata = JSON.parse(await readFile(metadataFile, "utf8")) as {
      files: { path: string; sha256: string; bytes: number }[];
    };
    const record = metadata.files.find(({ path: name }) =>
      /^_framework\/RegexTools\.DotNet\..+\.wasm$/u.test(name),
    );
    expect(record).toBeDefined();
    if (!record) return;
    const assetFile = path.join(pack, record.path);
    const replacement = Buffer.concat([
      await readFile(assetFile),
      Buffer.from("/mnt/private/regex-tools/obj/linked/RegexTools.DotNet.pdb"),
    ]);
    await writeFile(assetFile, replacement);
    record.sha256 = createHash("sha256").update(replacement).digest("hex");
    record.bytes = replacement.byteLength;
    await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
    await writeEngineChecksums(pack);

    await expect(verifyDotNetPack(pack)).rejects.toThrow(
      /Local absolute path/u,
    );
  });
});
