import { describe, expect, it } from "vitest";
import { EcmaScriptSyntaxProvider } from "./EcmaScriptSyntaxProvider";
import {
  DEFAULT_REGEX_LIMITS,
  TEMPLATE_PRESENTATION_LIMITS,
} from "../../../execution/request-limits";

const provider = new EcmaScriptSyntaxProvider();

describe("EcmaScriptSyntaxProvider", () => {
  it("normalizes named captures, ranges, quantifiers and assertions", async () => {
    const pattern = "^(?<date>\\d{4})$";
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern,
      flags: ["u"],
      options: {},
    });

    expect(result.accepted).toBe(true);
    expect(result.provider).toEqual({
      id: "regexpp-community",
      version: "4.12.2",
    });
    expect(result.coverage.status).toBe("partial");
    expect(result.root.range).toEqual({
      startUtf16: 0,
      endUtf16: pattern.length,
    });
    expect(result.captures).toEqual([
      expect.objectContaining({
        number: 1,
        name: "date",
        repeated: false,
      }),
    ]);
    expect(result.tokens.map((token) => token.kind)).toEqual(
      expect.arrayContaining([
        "anchor",
        "named-capture-group",
        "quantifier",
        "character-class",
      ]),
    );
    const quantifier = result.tokens.find(
      (token) => token.kind === "quantifier",
    );
    expect(quantifier?.raw).toBe("\\d{4}");
    expect(result.root.properties.nullable).toBe(false);
    expect(result.root.properties.minimumLength).toBe(4);
    expect(result.root.properties.maximumLength).toBe(4);
  });

  it("derives variable lengths for Unicode string-set alternatives", async () => {
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "[\\q{ab|c}]",
      flags: ["v"],
      options: {},
    });

    expect(result.accepted).toBe(true);
    expect(result.root.properties).toEqual(
      expect.objectContaining({
        nullable: false,
        minimumLength: 1,
        maximumLength: 2,
      }),
    );
  });

  it("does not invent exact lengths for Unicode properties of strings", async () => {
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "\\p{RGI_Emoji}",
      flags: ["v"],
      options: {},
    });

    expect(result.accepted).toBe(true);
    expect(result.root.properties.minimumLength).toBeUndefined();
    expect(result.root.properties.maximumLength).toBeUndefined();
    expect(result.root.properties.nullable).toBe(false);
  });

  it("preserves the provider's exact error position without inventing a span", async () => {
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "(?<name>",
      flags: [],
      options: {},
    });

    expect(result.accepted).toBe(false);
    expect(result.diagnostics).toHaveLength(1);
    expect(result.diagnostics[0]?.range).toEqual({
      startUtf16: 8,
      endUtf16: 8,
    });
    expect(result.root.provenance.source).toBe("derived");
    expect(result.root.support.notes).toContain(
      "No partial provider AST is claimed.",
    );
    expect(result.root.children.at(-1)?.kind).toBe("error-recovery");
  });

  it("parses ECMAScript replacement tokens using native token semantics", async () => {
    const result = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement: "Date: $<date>; $01; $$; $&; $9",
      captureMetadata: [
        {
          number: 1,
          name: "date",
          range: { startUtf16: 0, endUtf16: 4 },
          repeated: false,
        },
      ],
    });

    expect(result.tokens.map((token) => token.kind)).toEqual(
      expect.arrayContaining([
        "named-capture",
        "numbered-capture",
        "escaped-dollar",
        "full-match",
        "literal",
      ]),
    );
    expect(result.tokens).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          kind: "numbered-capture",
          raw: "$01",
          captureNumber: 1,
        }),
        expect.objectContaining({ kind: "literal", raw: "; $9" }),
      ]),
    );
    expect(result.diagnostics).toEqual([]);
  });

  it("distinguishes literal and empty unknown named references", async () => {
    const withoutNamedCaptures = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement: "$<missing>",
      captureMetadata: [
        {
          number: 1,
          range: { startUtf16: 0, endUtf16: 3 },
          repeated: false,
        },
      ],
    });
    expect(withoutNamedCaptures.tokens).toEqual([
      expect.objectContaining({ kind: "literal", raw: "$<missing>" }),
    ]);
    expect(withoutNamedCaptures.diagnostics).toEqual([]);

    const withNamedCaptures = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement: "$<missing>",
      captureMetadata: [
        {
          number: 1,
          name: "known",
          range: { startUtf16: 0, endUtf16: 10 },
          repeated: false,
        },
      ],
    });
    expect(withNamedCaptures.tokens).toEqual([
      expect.objectContaining({
        kind: "named-capture",
        raw: "$<missing>",
        captureName: "missing",
      }),
    ]);
    expect(withNamedCaptures.diagnostics[0]?.code).toBe(
      "replacement-unknown-capture",
    );
  });

  it("completely parses a maximum-sized $& template while bounding its UI diagnostics", async () => {
    const replacement = "$&".repeat(
      DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16 / 2,
    );
    const result = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement,
      captureMetadata: [],
    });

    expect(result.accepted).toBe(true);
    expect(result.tokens).toHaveLength(replacement.length / 2);
    expect(new Set(result.tokens.map((token) => token.id)).size).toBe(
      result.tokens.length,
    );

    const diagnostics = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement: "$<missing>".repeat(1_000),
      captureMetadata: [
        {
          number: 1,
          name: "known",
          range: { startUtf16: 0, endUtf16: 1 },
          repeated: false,
        },
      ],
    });
    expect(diagnostics.totalDiagnostics).toBe(1_000);
    expect(diagnostics.diagnostics).toHaveLength(
      TEMPLATE_PRESENTATION_LIMITS.replacementDiagnostics,
    );
    expect(diagnostics.diagnosticsTruncated).toBe(true);
    expect(diagnostics.diagnostics.at(-1)?.code).toBe(
      "replacement-diagnostic-limit",
    );
  });

  it("rejects an oversized replacement before producing a partial token model", async () => {
    const result = await provider.parseReplacement({
      flavour: "ecmascript",
      replacement: "$&".repeat(
        DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16 / 2 + 1,
      ),
      captureMetadata: [],
    });

    expect(result.accepted).toBe(false);
    expect(result.tokens).toEqual([]);
    expect(result.diagnostics[0]?.code).toBe("replacement-template-limit");
  });

  it("distinguishes nested and repeated capture definitions", async () => {
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "(?<outer>(?<inner>a)+)",
      flags: [],
      options: {},
    });

    expect(result.captures).toEqual([
      expect.objectContaining({ number: 1, name: "outer", repeated: false }),
      expect.objectContaining({
        number: 2,
        name: "inner",
        repeated: true,
        parentCaptureNumber: 1,
      }),
    ]);
  });

  it("records effective flags below inline modifier groups", async () => {
    const result = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "(?i:a)(?-i:b)",
      flags: ["m"],
      options: {},
    });
    const nodes = [result.root];
    const literals = [];
    while (nodes.length > 0) {
      const node = nodes.pop();
      if (!node) continue;
      if (node.kind === "literal") literals.push(node);
      nodes.push(...node.children);
    }

    expect(literals.find((node) => node.raw === "a")?.activeFlags).toEqual([
      "i",
      "m",
    ]);
    expect(literals.find((node) => node.raw === "b")?.activeFlags).toEqual([
      "m",
    ]);
  });
});
