import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
  generatePcre2C,
  type Pcre2CGenerationRequest,
} from "../src/regex/codegen/pcre2-c";

function request(operation: "match" | "replace"): Pcre2CGenerationRequest {
  return {
    flavour: "pcre2",
    flavourVersion: "PCRE2 10.47 8-bit WebAssembly",
    operation,
    pattern: String.raw`(?<word>\p{L}+)-(\d+)`,
    flags: ["g"],
    options: {
      matchLimit: 1_000_000,
      depthLimit: 1_000,
      heapLimitKib: 32_768,
    },
    subject: "x Grüße-42 y alpha-7",
    scanAll: true,
    ...(operation === "replace" ? { replacement: "$2:$<word>" } : {}),
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    maximumOutputBytes: 4_096,
  };
}

function sha256(value: string): string {
  return createHash("sha256").update(value).digest("hex");
}

const exactToolchainPrefix = process.env.PCRE2_CODEGEN_PREFIX;
const compileWithExactPcre2 = exactToolchainPrefix ? it : it.skip;

describe("PCRE2 C code-generation golden", () => {
  it("retains a reviewed deterministic source identity", () => {
    expect(sha256(generatePcre2C(request("match")).source)).toBe(
      "22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da",
    );
    expect(sha256(generatePcre2C(request("replace")).source)).toBe(
      "ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636",
    );
  });

  compileWithExactPcre2(
    "compiles and executes against the explicitly supplied PCRE2 10.47 C toolchain",
    async () => {
      if (!exactToolchainPrefix) return;
      const header = path.join(exactToolchainPrefix, "include", "pcre2.h");
      const library =
        process.env.PCRE2_CODEGEN_LIBRARY ??
        path.join(exactToolchainPrefix, "lib64", "libpcre2-8.a");
      await expect(readFile(header)).resolves.toBeInstanceOf(Buffer);
      await expect(readFile(library)).resolves.toBeInstanceOf(Buffer);
      const directory = await mkdtemp(
        path.join(tmpdir(), "regex-tools-codegen-"),
      );
      try {
        for (const operation of ["match", "replace"] as const) {
          const generated = generatePcre2C(request(operation));
          const source = path.join(directory, generated.fileName);
          const executable = path.join(directory, `pcre2-${operation}`);
          await writeFile(source, generated.source);
          const compiled = spawnSync(
            process.env.CC ?? "cc",
            [
              "-std=c17",
              "-Wall",
              "-Wextra",
              "-Werror",
              `-I${path.join(exactToolchainPrefix, "include")}`,
              source,
              library,
              "-o",
              executable,
            ],
            { encoding: "utf8" },
          );
          expect(
            `${compiled.stdout}${compiled.stderr}`,
            `${operation} fixture did not compile`,
          ).toBe("");
          expect(compiled.status).toBe(0);
          const executed = spawnSync(executable, [], { encoding: "utf8" });
          expect(
            executed.status,
            `${operation} fixture failed: ${executed.stdout}${executed.stderr}`,
          ).toBe(0);
          if (operation === "match") {
            expect(executed.stdout).toContain("match 1: UTF-8 bytes 2..12");
            expect(executed.stdout).toContain("group 1 / word");
            expect(executed.stdout).toContain(
              "completed: 2 match(es), 4 capture row(s)",
            );
          } else {
            expect(executed.stdout).toBe("x 42:Grüße y 7:alpha");
            expect(executed.stderr).toContain("completed: 2 substitution(s)");
          }
        }
      } finally {
        await rm(directory, { recursive: true, force: true });
      }
    },
  );
});
