import { describe, expect, it } from "vitest";
import { EcmaScriptSyntaxProvider } from "../syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { formatEcmaScriptPattern } from "./ecmascript-format";

const provider = new EcmaScriptSyntaxProvider();

async function format(pattern: string, flags: readonly string[] = []) {
  const syntax = await provider.parsePattern({
    flavour: "ecmascript",
    pattern,
    flags,
    options: {},
  });
  expect(syntax.accepted).toBe(true);
  return formatEcmaScriptPattern(pattern, syntax);
}

describe("formatEcmaScriptPattern", () => {
  it("canonicalizes literal slashes and raw controls without changing structure", async () => {
    const source =
      "^(a/b)\u0000\t\n\r\f\v\u0008\u001f\u007f\u2028\u2029(?<tail> z )$";
    const preview = await format(source, ["u"]);

    expect(preview.formattedPattern).toBe(
      "^(a\\/b)\\x00\\t\\n\\r\\f\\v\\x08\\x1f\\x7f\\u2028\\u2029(?<tail> z )$",
    );
    expect(preview.totalChanges).toBe(12);
    expect(preview.changes.map((change) => change.kind)).toEqual(
      expect.arrayContaining([
        "literal-slash",
        "raw-control",
        "raw-line-terminator",
      ]),
    );
    expect(preview.withinPatternLimit).toBe(true);
  });

  it("preserves existing escapes and is idempotent", async () => {
    const source = String.raw`a\/b\\\/c\t\n\x00\u2028[ / ]`;
    const first = await format(source, ["u"]);
    const secondSyntax = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: first.formattedPattern,
      flags: ["u"],
      options: {},
    });
    const second = formatEcmaScriptPattern(
      first.formattedPattern,
      secondSyntax,
    );

    expect(first.formattedPattern).toBe(
      String.raw`a\/b\\\/c\t\n\x00\u2028[ \/ ]`,
    );
    expect(second.formattedPattern).toBe(first.formattedPattern);
    expect(second.changed).toBe(false);
  });

  it("canonicalizes legacy escaped raw controls without adding a backslash", async () => {
    const source = "\\\t[\\\u0000]";
    const preview = await format(source);

    expect(preview.formattedPattern).toBe("\\t[\\x00]");
    expect(preview.changes).toEqual([
      expect.objectContaining({
        before: "\\\t",
        after: "\\t",
        startUtf16: 0,
        endUtf16: 2,
      }),
      expect.objectContaining({
        before: "\\\u0000",
        after: "\\x00",
        startUtf16: 3,
        endUtf16: 5,
      }),
    ]);
    for (const subject of ["\t\u0000", "\t", "\\t\u0000"]) {
      expect(new RegExp(preview.formattedPattern).exec(subject)).toEqual(
        new RegExp(source).exec(subject),
      );
    }
  });

  it.each(["", "u", "v"])(
    "retains native match behavior under %j flags",
    async (flagText) => {
      const flags = flagText ? [flagText] : [];
      const source = "^(?<path>a/b)\t[\u0000-\u001f]\u2028$";
      const preview = await format(source, flags);
      const before = new RegExp(source, flagText);
      const after = new RegExp(preview.formattedPattern, flagText);
      for (const subject of [
        "a/b\t\u0000\u2028",
        "a/b\t\u001f\u2028",
        "a/b x",
        "a\\/b\t\u0000\u2028",
      ]) {
        expect(after.exec(subject)).toEqual(before.exec(subject));
      }
    },
  );

  it("rejects recovered or stale syntax snapshots", async () => {
    const rejected = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "(",
      flags: [],
      options: {},
    });
    expect(() => formatEcmaScriptPattern("(", rejected)).toThrow(
      "accepted, non-recovered",
    );

    const accepted = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "a",
      flags: [],
      options: {},
    });
    expect(() => formatEcmaScriptPattern("b", accepted)).toThrow(
      "accepted, non-recovered",
    );
  });
});
