import { describe, expect, it } from "vitest";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../../model/match";
import type {
  PortableBridgeRequest,
  PortableRegexBridge,
} from "../portable/PortableEngineAdapter";
import {
  PHP_ENGINE_VERSION,
  PHP_FLAVOUR_VERSION,
  PhpEngineAdapter,
} from "./PhpEngineAdapter";

class FakePhpBridge implements PortableRegexBridge {
  request: PortableBridgeRequest | undefined;
  terminated = false;
  result: unknown = {
    abi: 1,
    ok: true,
    groupCount: 1,
    groupNames: [[1, "letter"]],
    matches: [{ span: [4, 6], captures: [[4, 6]] }],
    resultsTruncated: false,
  };

  load(): Promise<Readonly<Record<string, unknown>>> {
    return Promise.resolve({
      abi: 1,
      ok: true,
      identity: {
        bridge: "regex-tools-php-json",
        bridgeVersion: 1,
        phpVersion: "8.5.8",
        phpSapi: "wasm",
        pcreVersion: "10.44",
        pcreVersionFull: "10.44 2024-06-07",
        integerBytes: 8,
        offsetUnit: "utf8-byte",
        selfTest: true,
      },
    });
  }

  run(request: PortableBridgeRequest): Promise<unknown> {
    this.request = request;
    return Promise.resolve(this.result);
  }

  terminate(): void {
    this.terminated = true;
  }
}

function request(
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "php",
    flavourVersion: PHP_FLAVOUR_VERSION,
    pattern: "(?<letter>é)",
    flags: ["u"],
    subject: "😀é",
    captureMetadata: [],
    scanAll: true,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

describe("PHP preg portable adapter", () => {
  it("validates exact runtime identity and exposes PHP capabilities", async () => {
    const adapter = new PhpEngineAdapter(new FakePhpBridge(), "unit runtime");
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "php",
        engineVersion: PHP_ENGINE_VERSION,
        offsetUnit: "utf8-byte",
        capabilities: expect.objectContaining({
          matching: true,
          replacement: true,
          namedCaptures: true,
        }),
      }),
    );
  });

  it("normalizes native byte offsets and adds global for scan-all", async () => {
    const bridge = new FakePhpBridge();
    const result = await new PhpEngineAdapter(bridge).execute(request());

    expect(bridge.request?.flags).toBe("gu");
    expect(result.matches[0]).toEqual(
      expect.objectContaining({
        value: "é",
        range: { startUtf16: 2, endUtf16: 3 },
        nativeRange: { start: 4, end: 6, unit: "utf8-byte" },
        captures: [
          expect.objectContaining({
            groupName: "letter",
            value: "é",
            range: { startUtf16: 2, endUtf16: 3 },
          }),
        ],
      }),
    );
  });

  it("maps bridge failures to reported diagnostics", async () => {
    const bridge = new FakePhpBridge();
    bridge.result = {
      abi: 1,
      ok: false,
      phase: "compile",
      code: "compile-error",
      message: "missing closing parenthesis",
      position: 1,
    };
    const result = await new PhpEngineAdapter(bridge).execute(
      request({ pattern: "(" }),
    );

    expect(result.accepted).toBe(false);
    expect(result.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        range: { startUtf16: 1, endUtf16: 1 },
      }),
    );
  });

  it("validates replacement output and termination", async () => {
    const bridge = new FakePhpBridge();
    bridge.result = {
      abi: 1,
      ok: true,
      groupCount: 1,
      groupNames: [[1, "letter"]],
      matches: [{ span: [4, 6], captures: [[4, 6]] }],
      resultsTruncated: false,
      output: "é",
      outputBytes: 2,
      outputTruncated: false,
    };
    const adapter = new PhpEngineAdapter(bridge);
    const replacement: RegexReplacementRequest = {
      ...request({ scanAll: false }),
      replacement: "$1",
      maximumOutputBytes: 10,
    };
    await expect(adapter.replace(replacement)).resolves.toEqual(
      expect.objectContaining({
        output: "é",
        outputBytes: 2,
        outputTruncated: false,
      }),
    );
    adapter.terminate();
    expect(bridge.terminated).toBe(true);
  });

  it("rejects unsupported versions, flags, and lone surrogates", async () => {
    const adapter = new PhpEngineAdapter(new FakePhpBridge());
    await expect(
      adapter.execute(request({ flavourVersion: "PHP 7" })),
    ).rejects.toThrow(/version/u);
    await expect(adapter.execute(request({ flags: ["q"] }))).rejects.toThrow(
      /unsupported PHP preg flags/u,
    );
    await expect(
      adapter.execute(request({ subject: "\ud800" })),
    ).rejects.toThrow(/unpaired UTF-16 surrogate/u);
  });
});
