import { describe, expect, it } from "vitest";
import type { PhpRuntime } from "./php-runtime";
import { PhpWasmBridge } from "./PhpWasmBridge";

class FakePhp {
  readonly files = new Map<string, string>();
  readonly directories = new Set<string>();
  response = {
    exitCode: 0,
    errors: "",
    text: JSON.stringify({
      abi: 1,
      ok: true,
      identity: { bridge: "regex-tools-php-json" },
    }),
  };

  isDir(path: string): boolean {
    return this.directories.has(path);
  }

  mkdir(path: string): void {
    this.directories.add(path);
  }

  writeFile(path: string, value: string | Uint8Array): void {
    this.files.set(
      path,
      typeof value === "string" ? value : new TextDecoder().decode(value),
    );
  }

  run(): Promise<typeof this.response> {
    return Promise.resolve(this.response);
  }
}

describe("PHP fixed VFS/JSON bridge", () => {
  it("keeps user values in JSON and never interpolates them into PHP source", async () => {
    const php = new FakePhp();
    const bridge = new PhpWasmBridge(php as unknown as PhpRuntime);
    const hostile = `"'; throw new Exception('executed'); // <?php`;
    php.response.text = JSON.stringify({
      abi: 1,
      ok: true,
      groupCount: 0,
      groupNames: [],
      matches: [],
      resultsTruncated: false,
    });

    await bridge.run({
      abi: 1,
      operation: "execute",
      pattern: hostile,
      flags: "gu",
      options: {},
      subject: hostile,
      scanAll: true,
      maximumMatches: 10,
      maximumCaptureRows: 10,
    });

    const source = php.files.get("/regex-tools/bridge.php") ?? "";
    const encoded = php.files.get("/regex-tools/request.json") ?? "";
    expect(source).toContain("Fixed PHP bridge");
    expect(source).not.toContain(hostile);
    expect(JSON.parse(encoded)).toEqual(
      expect.objectContaining({ pattern: hostile, subject: hostile }),
    );
  });

  it("rejects malformed output, stderr, and use after termination", async () => {
    const php = new FakePhp();
    const bridge = new PhpWasmBridge(php as unknown as PhpRuntime);
    php.response.text = "not-json";
    await expect(bridge.load()).rejects.toThrow(/malformed JSON/u);

    const stderrPhp = new FakePhp();
    stderrPhp.response.errors = "unexpected warning";
    const stderrBridge = new PhpWasmBridge(stderrPhp as unknown as PhpRuntime);
    await expect(stderrBridge.load()).rejects.toThrow(/wrote to stderr/u);

    bridge.terminate();
    await expect(bridge.load()).rejects.toThrow(/terminated/u);
  });

  it("decodes bounded base64 replacement output and rejects invalid envelopes", async () => {
    const php = new FakePhp();
    const bridge = new PhpWasmBridge(php as unknown as PhpRuntime);
    const replacementRequest = {
      abi: 1 as const,
      operation: "replace" as const,
      pattern: ".",
      flags: "gu",
      options: {},
      subject: "a",
      scanAll: true,
      maximumMatches: 10,
      maximumCaptureRows: 10,
      replacement: "😀",
      maximumOutputBytes: 4,
    };
    php.response.text = JSON.stringify({
      abi: 1,
      ok: true,
      groupCount: 0,
      groupNames: [],
      matches: [],
      resultsTruncated: false,
      outputBase64: "8J+YgA==",
      outputBytes: 4,
      outputTruncated: false,
    });
    await expect(bridge.run(replacementRequest)).resolves.toEqual(
      expect.objectContaining({ output: "😀" }),
    );

    php.response.text = JSON.stringify({
      abi: 1,
      ok: true,
      outputBase64: "not base64",
      outputBytes: 4,
    });
    await expect(bridge.run(replacementRequest)).rejects.toThrow(
      /invalid base64/u,
    );

    php.response.text = JSON.stringify({
      abi: 1,
      ok: true,
      outputBase64: "YWE=",
      outputBytes: 1,
    });
    await expect(bridge.run(replacementRequest)).rejects.toThrow(
      /output limit/u,
    );

    php.response.text = JSON.stringify({
      abi: 1,
      ok: false,
      phase: "replacement",
      code: "failure",
      message: "failure",
      outputBase64: "",
      outputBytes: 0,
    });
    await expect(bridge.run(replacementRequest)).rejects.toThrow(
      /failure envelope/u,
    );
  });
});
