import { describe, expect, it } from "vitest";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../../../model/match";
import {
  PythonEngineAdapter,
  PYODIDE_ENGINE_VERSION,
  PYTHON_BRIDGE_ABI_VERSION,
  PYTHON_ENGINE_VERSION,
  PYTHON_FLAVOUR_VERSION,
} from "./PythonEngineAdapter";
import type { PyodideRuntime, PythonCallableProxy } from "./python-module";

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

function callable(
  responder: (
    operation: "identity" | "execute" | "replace",
  ) => Record<string, unknown>,
): PythonCallableProxy & { destroyed: boolean } {
  const bridge = ((operation: "identity" | "execute" | "replace") =>
    JSON.stringify(responder(operation))) as unknown as PythonCallableProxy & {
    destroyed: boolean;
  };
  bridge.destroyed = false;
  bridge.destroy = () => {
    bridge.destroyed = true;
  };
  return bridge;
}

class FakePyodide implements PyodideRuntime {
  readonly version = PYODIDE_ENGINE_VERSION;
  readonly bridge: PythonCallableProxy & { destroyed: boolean };
  runPythonCalls = 0;

  constructor(
    responder: (
      operation: "identity" | "execute" | "replace",
    ) => Record<string, unknown>,
  ) {
    this.bridge = callable(responder);
  }

  runPython(source: string): unknown {
    void source;
    this.runPythonCalls += 1;
    return this.bridge;
  }
}

function identity(): Record<string, unknown> {
  return {
    abi: PYTHON_BRIDGE_ABI_VERSION,
    ok: true,
    identity: {
      implementation: "cpython",
      pythonVersion: "3.14.2",
      reModule: "re",
    },
  };
}

describe("Python Pyodide adapter", () => {
  it("normalizes authoritative code-point ranges and capture names", async () => {
    const runtime = new FakePyodide((operation) =>
      operation === "identity"
        ? identity()
        : {
            abi: PYTHON_BRIDGE_ABI_VERSION,
            ok: true,
            groupCount: 2,
            groupNames: [[1, "emoji"]],
            matches: [
              {
                span: [1, 2],
                captures: [[1, 2], null],
              },
            ],
            resultsTruncated: false,
          },
    );
    const adapter = new PythonEngineAdapter(runtime, "unit-test runtime");

    await expect(adapter.load()).resolves.toEqual(
      expect.objectContaining({
        flavour: "python",
        engineVersion: PYTHON_ENGINE_VERSION,
        offsetUnit: "code-point",
      }),
    );
    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: 2, unit: "code-point" },
      }),
    );
    expect(result.matches[0]?.captures).toEqual([
      expect.objectContaining({
        groupNumber: 1,
        groupName: "emoji",
        value: "😀",
        range: { startUtf16: 1, endUtf16: 3 },
      }),
      {
        groupNumber: 2,
        status: "did-not-participate",
      },
    ]);
    expect(runtime.runPythonCalls).toBe(1);
  });

  it("normalizes compile positions and scan-all flags", async () => {
    const runtime = new FakePyodide((operation) =>
      operation === "identity"
        ? identity()
        : {
            abi: PYTHON_BRIDGE_ABI_VERSION,
            ok: false,
            phase: "compile",
            code: "compile-error",
            message: "missing )",
            position: 2,
          },
    );
    const adapter = new PythonEngineAdapter(runtime);
    const result = await adapter.execute(
      request({ pattern: "😀(", flags: ["i"], scanAll: true }),
    );

    expect(result.accepted).toBe(false);
    expect(result.flags).toEqual({
      userFlags: "i",
      effectiveFlags: "gi",
      internallyAddedIndicesFlag: false,
      internallyAddedGlobalFlag: true,
    });
    expect(result.diagnostics[0]).toEqual(
      expect.objectContaining({
        code: "compile-error",
        range: { startUtf16: 3, endUtf16: 3 },
      }),
    );
  });

  it("returns bounded native replacement output and disposes its proxy", async () => {
    const runtime = new FakePyodide((operation) => {
      if (operation === "identity") return identity();
      return {
        abi: PYTHON_BRIDGE_ABI_VERSION,
        ok: true,
        groupCount: 1,
        groupNames: [[1, "emoji"]],
        matches: [{ span: [1, 2], captures: [[1, 2]] }],
        resultsTruncated: false,
        outputBase64: "YVvwn5iAXQ==",
        outputBytes: 7,
        outputTruncated: true,
      };
    });
    const adapter = new PythonEngineAdapter(runtime);
    const replacementRequest: RegexReplacementRequest = {
      ...request(),
      replacement: "[\\g<emoji>]",
      maximumOutputBytes: 7,
    };

    const result = await adapter.replace(replacementRequest);

    expect(result.output).toBe("a[😀]");
    expect(result.outputBytes).toBe(7);
    expect(result.outputTruncated).toBe(true);
    expect(result.execution.diagnostics).toEqual([
      expect.objectContaining({ code: "replacement-output-limit" }),
    ]);
    adapter.terminate();
    expect(runtime.bridge.destroyed).toBe(true);
    await expect(adapter.execute(request())).rejects.toThrow("terminated");
  });

  it("rejects raw or inconsistently declared replacement output", async () => {
    const response: Record<string, unknown> = {
      abi: PYTHON_BRIDGE_ABI_VERSION,
      ok: true,
      groupCount: 0,
      groupNames: [],
      matches: [],
      resultsTruncated: false,
      output: "raw",
      outputBase64: "cmF3",
      outputBytes: 3,
      outputTruncated: false,
    };
    const adapter = new PythonEngineAdapter(
      new FakePyodide((operation) =>
        operation === "identity" ? identity() : response,
      ),
    );
    await expect(
      adapter.replace({
        ...request({ pattern: "x", subject: "x" }),
        replacement: "raw",
        maximumOutputBytes: 64,
      }),
    ).rejects.toThrow(/invalid encoded output/u);

    delete response.output;
    response.outputBytes = 2;
    await expect(
      adapter.replace({
        ...request({ pattern: "x", subject: "x" }),
        replacement: "raw",
        maximumOutputBytes: 64,
      }),
    ).rejects.toThrow(/output limit/u);
  });

  it("rejects mismatched profiles and malformed bounded requests", async () => {
    const runtime = new FakePyodide(identity);
    const adapter = new PythonEngineAdapter(runtime);

    await expect(
      adapter.execute(request({ flavourVersion: "Python 3.13" })),
    ).rejects.toThrow(/version .* unsupported/u);
    await expect(adapter.execute(request({ flavour: "ruby" }))).rejects.toThrow(
      "cannot execute ruby",
    );
    await expect(adapter.execute(request({ flags: ["u"] }))).rejects.toThrow(
      "unsupported Python re flags",
    );
    await expect(
      adapter.execute(request({ options: { locale: true } })),
    ).rejects.toThrow("does not accept engine options");
    await expect(
      adapter.execute(request({ maximumMatches: 0 })),
    ).rejects.toThrow("outside the bounded contract");
    await expect(
      adapter.replace({
        ...request(),
        replacement: "x",
        maximumOutputBytes: 0,
      }),
    ).rejects.toThrow("outside the bounded contract");
  });
});
