// @vitest-environment node
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
  PythonEngineAdapter,
  PYODIDE_ENGINE_VERSION,
  PYTHON_ENGINE_VERSION,
  PYTHON_FLAVOUR_VERSION,
} from "../../src/regex/execution/adapters/python/PythonEngineAdapter";
import type {
  PyodideModule,
  PyodideRuntime,
} from "../../src/regex/execution/adapters/python/python-module";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../src/regex/model/match";

const engineDirectory = path.resolve(
  process.cwd(),
  "public",
  "engines",
  "python",
);

function request(
  overrides: Partial<RegexExecutionRequest> = {},
): RegexExecutionRequest {
  return {
    flavour: "python",
    flavourVersion: PYTHON_FLAVOUR_VERSION,
    pattern: "a",
    flags: ["g"],
    subject: "a",
    captureMetadata: [],
    scanAll: false,
    maximumMatches: 100,
    maximumCaptureRows: 1_000,
    ...overrides,
  };
}

describe("CPython re 3.14.2 conformance through self-hosted Pyodide", () => {
  let runtime: PyodideRuntime;
  let adapter: PythonEngineAdapter;

  beforeAll(async () => {
    const moduleUrl = pathToFileURL(path.join(engineDirectory, "pyodide.mjs"));
    const imported = (await import(
      /* @vite-ignore */ moduleUrl.href
    )) as PyodideModule;
    expect(imported.version).toBe(PYODIDE_ENGINE_VERSION);
    runtime = await imported.loadPyodide({
      indexURL: `${engineDirectory}${path.sep}`,
      stdout: () => undefined,
      stderr: () => undefined,
    });
    adapter = new PythonEngineAdapter(runtime, "Node conformance runtime");
    await adapter.load();
  }, 30_000);

  afterAll(() => {
    adapter?.terminate();
  });

  it("reports the exact engine, runtime and native offset identity", async () => {
    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "python",
        engineName: "CPython re via Pyodide",
        engineVersion: PYTHON_ENGINE_VERSION,
        runtimeVersion: expect.stringContaining("Pyodide 314.0.3"),
        offsetUnit: "code-point",
      }),
    );
  });

  it("uses native Python named groups and preserves optional captures", async () => {
    const result = await adapter.execute(
      request({
        pattern: "(?P<word>[A-Za-z]+)(?:-(\\d+))?",
        subject: "abc-12 xyz",
      }),
    );

    expect(result.accepted).toBe(true);
    expect(result.matches).toHaveLength(2);
    expect(result.matches[0]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "word",
        value: "abc",
      }),
      expect.objectContaining({ groupNumber: 2, value: "12" }),
    ]);
    expect(result.matches[1]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "word",
        value: "xyz",
      }),
      { groupNumber: 2, status: "did-not-participate" },
    ]);
  });

  it("normalizes astral and zero-length code-point offsets to UTF-16", async () => {
    const result = await adapter.execute(
      request({
        pattern: "(?=.)",
        subject: "😀a",
      }),
    );

    expect(
      result.matches.map((match) => ({
        native: match.nativeRange,
        editor: match.range,
        status: match.valueStatus,
      })),
    ).toEqual([
      {
        native: { start: 0, end: 0, unit: "code-point" },
        editor: { startUtf16: 0, endUtf16: 0 },
        status: "complete",
      },
      {
        native: { start: 1, end: 1, unit: "code-point" },
        editor: { startUtf16: 2, endUtf16: 2 },
        status: "complete",
      },
    ]);
  });

  it("implements g, a, i, m, s and x with scan-all normalization", async () => {
    const combined = await adapter.execute(
      request({
        pattern: "^ a . b $",
        flags: ["i", "m", "s", "x"],
        subject: "z\nA\nB\nq",
        scanAll: true,
      }),
    );
    expect(combined.accepted).toBe(true);
    expect(combined.flags.effectiveFlags).toBe("gimsx");
    expect(combined.flags.internallyAddedGlobalFlag).toBe(true);
    expect(combined.matches.map((match) => match.value)).toEqual(["A\nB"]);

    const ascii = await adapter.execute(
      request({
        pattern: "\\w+",
        flags: ["g", "a"],
        subject: "é az",
      }),
    );
    expect(ascii.matches.map((match) => match.value)).toEqual(["az"]);
  });

  it("uses native match.expand syntax for named, numbered and zero-length replacement", async () => {
    const replacementRequest: RegexReplacementRequest = {
      ...request({
        pattern: "(?P<word>[a-z]+)-(\\d+)",
        subject: "a-1 b-22",
      }),
      replacement: "\\g<word>[\\2]",
      maximumOutputBytes: 1_024,
    };
    const replaced = await adapter.replace(replacementRequest);

    expect(replaced.execution.accepted).toBe(true);
    expect(replaced.output).toBe("a[1] b[22]");
    expect(replaced.outputTruncated).toBe(false);

    const zeroLength = await adapter.replace({
      ...request({
        pattern: "(?=.)",
        subject: "😀a",
      }),
      replacement: "|",
      maximumOutputBytes: 1_024,
    });
    expect(zeroLength.output).toBe("|😀|a");
  });

  it("bounds UTF-8 output without splitting an astral scalar", async () => {
    const result = await adapter.replace({
      ...request({ pattern: ".", subject: "abc" }),
      replacement: "😀",
      maximumOutputBytes: 5,
    });

    expect(result.execution.accepted).toBe(true);
    expect(result.output).toBe("😀");
    expect(result.outputBytes).toBe(4);
    expect(result.outputTruncated).toBe(true);
    expect(result.truncated).toBe(true);
  });

  it("reports native compile and replacement errors", async () => {
    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 replacementError = await adapter.replace({
      ...request({ pattern: "(?P<word>a)" }),
      replacement: "\\g<missing>",
      maximumOutputBytes: 1_024,
    });
    expect(replacementError.execution.accepted).toBe(false);
    expect(replacementError.execution.diagnostics[0]?.code).toBe(
      "replacement-error",
    );
  });

  it("enforces authoritative match and capture-row collection caps", async () => {
    const matchCap = await adapter.execute(
      request({
        pattern: "a",
        subject: "aaa",
        maximumMatches: 1,
      }),
    );
    expect(matchCap.matches).toHaveLength(1);
    expect(matchCap.truncated).toBe(true);

    const rowCap = await adapter.execute(
      request({
        pattern: "(a)(b)",
        subject: "ab",
        maximumCaptureRows: 1,
      }),
    );
    expect(rowCap.matches).toHaveLength(0);
    expect(rowCap.truncated).toBe(true);
  });
});
