import { describe, expect, it } from "vitest";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../../model/match";
import {
  Pcre2EngineAdapter,
  PCRE2_BRIDGE_ABI_VERSION,
  PCRE2_ENGINE_VERSION,
  PCRE2_FLAVOUR_VERSION,
  pcre2Limits,
} from "./Pcre2EngineAdapter";
import type { Pcre2EmscriptenModule } from "./pcre2-module";

const encoder = new TextEncoder();
const decoder = new TextDecoder();

class FakePcre2Module implements Pcre2EmscriptenModule {
  readonly HEAPU8 = new Uint8Array(4 * 1024 * 1024);
  readonly UTF8ToString = (pointer: number) => {
    let end = pointer;
    while (this.HEAPU8[end] !== 0) end += 1;
    return decoder.decode(this.HEAPU8.slice(pointer, end));
  };
  readonly allocated: number[] = [];
  readonly freed: number[] = [];
  executeCalls = 0;
  substituteCalls = 0;
  #nextPointer = 1_024;
  readonly #versionPointer: number;

  constructor() {
    const version = encoder.encode(`${PCRE2_ENGINE_VERSION}\0`);
    this.#versionPointer = 64;
    this.HEAPU8.set(version, this.#versionPointer);
  }

  readonly _malloc = (size: number) => {
    const pointer = this.#nextPointer;
    this.#nextPointer += Math.max(1, size) + 16;
    this.allocated.push(pointer);
    return pointer;
  };

  readonly _free = (pointer: number) => {
    this.freed.push(pointer);
  };

  readonly _regex_pcre2_bridge_abi_version = () => PCRE2_BRIDGE_ABI_VERSION;
  readonly _regex_pcre2_config_flags = () => 1;
  readonly _regex_pcre2_version = () => this.#versionPointer;
  readonly _regex_pcre2_self_test = () => 0;

  readonly _regex_pcre2_error_message = (
    errorCode: number,
    outputPointer: number,
    outputCapacity: number,
  ) => {
    const message = encoder.encode(
      errorCode === 101 ? "missing closing parenthesis" : "fixture error",
    );
    const length = Math.min(message.length, outputCapacity - 1);
    this.HEAPU8.set(message.slice(0, length), outputPointer);
    this.HEAPU8[outputPointer + length] = 0;
    return length;
  };

  #writeSuccessfulResult(
    recordsPointer: number,
    namesPointer: number,
    nameBytesPointer: number,
    resultPointer: number,
  ): void {
    const view = new DataView(this.HEAPU8.buffer);
    for (const [index, group] of [0, 1].entries()) {
      const pointer = recordsPointer + index * 16;
      view.setUint32(pointer, 1, true);
      view.setUint32(pointer + 4, group, true);
      view.setUint32(pointer + 8, 1, true);
      view.setUint32(pointer + 12, 5, true);
    }
    const name = encoder.encode("emoji");
    view.setUint32(namesPointer, 1, true);
    view.setUint32(namesPointer + 4, 0, true);
    view.setUint32(namesPointer + 8, name.length, true);
    this.HEAPU8.set(name, nameBytesPointer);
    view.setInt32(resultPointer, 0, true);
    view.setUint32(resultPointer + 12, 1, true);
    view.setUint32(resultPointer + 16, 1, true);
    view.setUint32(resultPointer + 20, 1, true);
    view.setUint32(resultPointer + 24, 2, true);
    view.setUint32(resultPointer + 28, 1, true);
    view.setUint32(resultPointer + 32, name.length, true);
  }

  readonly _regex_pcre2_execute = (
    patternPointer: number,
    patternLength: number,
    _subjectPointer: number,
    _subjectLength: number,
    _applicationFlags: number,
    _limitsPointer: number,
    recordsPointer: number,
    _recordCapacity: number,
    namesPointer: number,
    _nameCapacity: number,
    nameBytesPointer: number,
    _nameBytesCapacity: number,
    resultPointer: number,
  ) => {
    this.executeCalls += 1;
    const pattern = decoder.decode(
      this.HEAPU8.slice(patternPointer, patternPointer + patternLength),
    );
    const view = new DataView(this.HEAPU8.buffer);
    if (pattern.endsWith("(")) {
      view.setInt32(resultPointer, 101, true);
      view.setUint32(resultPointer + 4, 2, true);
      view.setUint32(resultPointer + 8, encoder.encode(pattern).length, true);
      return 101;
    }
    this.#writeSuccessfulResult(
      recordsPointer,
      namesPointer,
      nameBytesPointer,
      resultPointer,
    );
    return 0;
  };

  readonly _regex_pcre2_substitute = (
    _patternPointer: number,
    _patternLength: number,
    _subjectPointer: number,
    _subjectLength: number,
    _replacementPointer: number,
    _replacementLength: number,
    _applicationFlags: number,
    _limitsPointer: number,
    recordsPointer: number,
    _recordCapacity: number,
    namesPointer: number,
    _nameCapacity: number,
    nameBytesPointer: number,
    _nameBytesCapacity: number,
    outputPointer: number,
    _outputCapacity: number,
    resultPointer: number,
  ) => {
    this.substituteCalls += 1;
    this.#writeSuccessfulResult(
      recordsPointer,
      namesPointer,
      nameBytesPointer,
      resultPointer,
    );
    const output = encoder.encode("x[😀]é");
    this.HEAPU8.set(output, outputPointer);
    const view = new DataView(this.HEAPU8.buffer);
    view.setUint32(resultPointer + 48, output.length, true);
    view.setUint32(resultPointer + 52, 1, true);
    return 0;
  };

  readonly _regex_pcre2_trace = () => 0;
}

function request(
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "pcre2",
    flavourVersion: PCRE2_FLAVOUR_VERSION,
    pattern: "(?<emoji>😀)",
    flags: ["g"],
    options: {
      matchLimit: 1_000_000,
      depthLimit: 1_000,
      heapLimitKib: 32_768,
    },
    subject: "x😀é",
    captureMetadata: [],
    scanAll: false,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

describe("PCRE2 WebAssembly adapter", () => {
  it("normalizes native UTF-8 byte ranges to editor UTF-16 ranges", async () => {
    const module = new FakePcre2Module();
    const adapter = new Pcre2EngineAdapter(module, "fixture WebAssembly");
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "pcre2",
        engineVersion: PCRE2_ENGINE_VERSION,
        offsetUnit: "utf8-byte",
      }),
    );

    const result = await adapter.execute(request());

    expect(result.accepted).toBe(true);
    expect(result.matches[0]).toEqual(
      expect.objectContaining({
        value: "😀",
        range: { startUtf16: 1, endUtf16: 3 },
        nativeRange: { start: 1, end: 5, unit: "utf8-byte" },
      }),
    );
    expect(result.matches[0]?.captures[0]).toEqual(
      expect.objectContaining({
        groupNumber: 1,
        groupName: "emoji",
        value: "😀",
        range: { startUtf16: 1, endUtf16: 3 },
      }),
    );
    expect(module.executeCalls).toBe(1);
    expect(new Set(module.freed)).toEqual(new Set(module.allocated));
  });

  it("returns bounded native PCRE2 substitution output", async () => {
    const module = new FakePcre2Module();
    const adapter = new Pcre2EngineAdapter(module);
    const replacementRequest: RegexReplacementRequest = {
      ...request(),
      replacement: "[$<emoji>]",
      maximumOutputBytes: 1_024,
    };

    const result = await adapter.replace(replacementRequest);

    expect(result.output).toBe("x[😀]é");
    expect(result.outputBytes).toBe(9);
    expect(result.execution.matches[0]?.value).toBe("😀");
    expect(module.substituteCalls).toBe(1);
    expect(new Set(module.freed)).toEqual(new Set(module.allocated));
  });

  it("normalizes compile-error byte offsets and rejects lone surrogates", async () => {
    const module = new FakePcre2Module();
    const adapter = new Pcre2EngineAdapter(module);
    const compileError = await adapter.execute(request({ pattern: "é(" }));
    expect(compileError.accepted).toBe(false);
    expect(compileError.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        range: { startUtf16: 2, endUtf16: 2 },
      }),
    );

    const invalidUnicode = await adapter.execute(
      request({ subject: `x${String.fromCharCode(0xd800)}y` }),
    );
    expect(invalidUnicode.accepted).toBe(false);
    expect(invalidUnicode.diagnostics[0]?.code).toBe("invalid-unicode-input");
    expect(module.executeCalls).toBe(1);
  });

  it("validates typed native resource limits before entering WebAssembly", () => {
    expect(
      pcre2Limits({
        matchLimit: 2_000,
        depthLimit: 20,
        heapLimitKib: 4_096,
      }),
    ).toEqual({
      matchLimit: 2_000,
      depthLimit: 20,
      heapLimitKib: 4_096,
    });
    expect(() => pcre2Limits({ matchLimit: 100_000_001 })).toThrow(
      /matchLimit/u,
    );
  });

  it("rejects mismatched profiles and unsupported request fields", async () => {
    const module = new FakePcre2Module();
    const adapter = new Pcre2EngineAdapter(module);

    await expect(
      adapter.execute(request({ flavourVersion: "PCRE2 10.46" })),
    ).rejects.toThrow(/version .* unsupported/u);
    await expect(adapter.execute(request({ flags: [""] }))).rejects.toThrow(
      "unsupported PCRE2 flags",
    );
    await expect(
      adapter.execute(request({ options: { jit: true } })),
    ).rejects.toThrow("unsupported PCRE2 engine options");
    await expect(
      adapter.execute(request({ maximumCaptureRows: 0 })),
    ).rejects.toThrow("outside the bounded contract");
    expect(module.executeCalls).toBe(0);
  });
});
