import { describe, expect, it } from "vitest";
import {
  AVAILABLE_REGEX_FLAVOURS,
  defaultRegexOptions,
} from "../flavours/flavour-registry";
import { requireSyntaxProvider, SYNTAX_PROVIDERS } from "./provider-registry";

describe("syntax provider registry", () => {
  it("registers a flavour-owned provider for every selectable profile", () => {
    expect(
      AVAILABLE_REGEX_FLAVOURS.definitions
        .map((definition) => definition.id)
        .sort(),
    ).toEqual(Object.keys(SYNTAX_PROVIDERS).sort());
    for (const definition of AVAILABLE_REGEX_FLAVOURS.definitions) {
      expect(requireSyntaxProvider(definition.id).supportedFlavours).toContain(
        definition.id,
      );
    }
    expect(() => requireSyntaxProvider("pcre")).toThrow(
      "No syntax provider is registered for pcre.",
    );
  });

  it.each([
    ["php", "(?P<word>[a-z]+)", "word"],
    ["perl", "(?'word'[a-z]+)", "word"],
    ["ruby", "(?<word>[a-z]+)", "word"],
    ["cpp", "([a-z]+)", undefined],
    ["go", "(?P<word>[a-z]+)", "word"],
    ["rust", "(?<word>[a-z]+)", "word"],
    ["dotnet", "(?'word'[a-z]+)", "word"],
    ["scala", "(?<word>[a-z]+)", "word"],
  ] as const)(
    "returns explicitly partial lexical %s capture metadata",
    async (flavour, pattern, expectedName) => {
      const definition = AVAILABLE_REGEX_FLAVOURS.require(flavour);
      const result = await requireSyntaxProvider(flavour).parsePattern({
        flavour,
        flavourVersion: definition.versions[0]?.syntaxVersion,
        pattern,
        flags: definition.defaultFlags,
        options: defaultRegexOptions(definition),
      });

      expect(result.accepted).toBe(true);
      expect(result.root.support).toEqual(
        expect.objectContaining({ flavour, status: "partial" }),
      );
      expect(result.coverage.status).toBe("partial");
      expect(result.coverage.unsupportedConstructs.length).toBeGreaterThan(0);
      expect(result.captures).toHaveLength(1);
      expect(result.captures[0]?.name).toBe(expectedName);
      expect(result.provider.id).toContain(flavour);
    },
  );

  it("does not invent C++ capture metadata for non-ECMAScript grammars or nosubs", async () => {
    const provider = requireSyntaxProvider("cpp");
    const base = {
      flavour: "cpp" as const,
      pattern: "\\(word\\)",
      flags: ["g"],
    };

    const basic = await provider.parsePattern({
      ...base,
      options: { grammar: "basic" },
    });
    const noSubs = await provider.parsePattern({
      ...base,
      pattern: "(word)",
      flags: ["g", "n"],
      options: { grammar: "ECMAScript" },
    });

    expect(basic.captures).toEqual([]);
    expect(noSubs.captures).toEqual([]);
    expect(basic.coverage.status).toBe("partial");
  });

  it("keeps replacement tokenization flavour-specific and surfaces the Scala compatibility limit", async () => {
    const go = await requireSyntaxProvider("go").parseReplacement({
      flavour: "go",
      replacement: "${word}-$1",
      captureMetadata: [
        {
          number: 1,
          name: "word",
          range: { startUtf16: 0, endUtf16: 9 },
          repeated: false,
        },
      ],
    });
    expect(go.accepted).toBe(true);
    expect(go.tokens.map((token) => token.kind)).toEqual([
      "named-capture",
      "literal",
      "numbered-capture",
    ]);
    expect(go.diagnostics).toEqual([
      expect.objectContaining({
        code: "replacement-preview-partial",
        flavour: "go",
      }),
    ]);

    const scala = await requireSyntaxProvider("scala").parseReplacement({
      flavour: "scala",
      replacement: "${word}",
      captureMetadata: [
        {
          number: 1,
          name: "word",
          range: { startUtf16: 0, endUtf16: 9 },
          repeated: false,
        },
      ],
    });
    expect(scala.accepted).toBe(false);
    expect(scala.diagnostics).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          code: "engine-compatibility",
          flavour: "scala",
        }),
      ]),
    );
  });

  it("recognizes the fixed Perl bridge replacement subset without evaluating interpolation", async () => {
    const result = await requireSyntaxProvider("perl").parseReplacement({
      flavour: "perl",
      replacement: "${word}-$1-$$-$+{word}",
      captureMetadata: [
        {
          number: 1,
          name: "word",
          range: { startUtf16: 0, endUtf16: 9 },
          repeated: false,
        },
      ],
    });

    expect(result.tokens).toEqual([
      expect.objectContaining({
        kind: "named-capture",
        raw: "${word}",
        captureName: "word",
      }),
      expect.objectContaining({ kind: "literal", raw: "-" }),
      expect.objectContaining({
        kind: "numbered-capture",
        raw: "$1",
        captureNumber: 1,
      }),
      expect.objectContaining({ kind: "literal", raw: "-" }),
      expect.objectContaining({ kind: "escaped-dollar", raw: "$$" }),
      expect.objectContaining({ kind: "literal", raw: "-$+{word}" }),
    ]);
  });
});
