import { describe, expect, it } from "vitest";
import { EcmaScriptSyntaxProvider } from "../syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import type { NormalizedRegexNode } from "../model/syntax";
import {
  analyseStaticRisk,
  MAXIMUM_STATIC_ANALYSIS_NODES,
} from "./static-risk";

const provider = new EcmaScriptSyntaxProvider();

async function analyse(
  pattern: string,
  overrides: {
    readonly flags?: readonly string[];
    readonly scanAll?: boolean;
    readonly replacement?: string;
  } = {},
) {
  const flags = overrides.flags ?? [];
  const syntax = await provider.parsePattern({
    flavour: "ecmascript",
    flavourVersion: "2025",
    pattern,
    flags,
    options: {},
  });
  expect(syntax.accepted).toBe(true);
  return analyseStaticRisk({
    flavour: "ecmascript",
    root: syntax.root,
    flags,
    scanAll: overrides.scanAll ?? false,
    replacement: overrides.replacement,
  });
}

describe("ECMAScript static risk analysis", () => {
  it("identifies nested, nullable and unanchored repetition without claiming vulnerability", async () => {
    const report = await analyse("(a*)+$");
    const rules = report.findings.map((finding) => finding.rule);

    expect(rules).toEqual(
      expect.arrayContaining([
        "nested-quantifier",
        "nullable-repeated-expression",
        "unanchored-expensive-prefix",
      ]),
    );
    expect(report.findings[0]).toEqual(
      expect.objectContaining({
        flavour: "ecmascript",
        evidence: "static",
        confidence: expect.stringMatching(/low|medium|high/u),
        limitations: expect.any(String),
        suggestedInvestigation: expect.any(String),
      }),
    );
    expect(report.summary).toMatch(/potential/u);
    expect(JSON.stringify(report)).not.toMatch(
      /\bdefinitely vulnerable\b|\bguaranteed linear\b/iu,
    );
  });

  it("detects ambiguous alternatives, wildcards and repeated backreferences", async () => {
    const alternatives = await analyse("(?:a|aa)+$");
    expect(alternatives.findings.map((finding) => finding.rule)).toEqual(
      expect.arrayContaining([
        "ambiguous-repeated-alternatives",
        "overlapping-alternatives",
      ]),
    );

    const wildcard = await analyse("(?:.*)+x");
    expect(wildcard.findings.map((finding) => finding.rule)).toContain(
      "repeated-wildcard",
    );

    const backreference = await analyse("(a)(?:\\1+)+$");
    expect(backreference.findings.map((finding) => finding.rule)).toContain(
      "backreference-in-repetition",
    );
  });

  it("reports repeated lookaround bodies and replacement expansion", async () => {
    const report = await analyse("(?=(a+)+)a", {
      flags: ["g"],
      replacement: "$`$'",
    });

    expect(report.findings.map((finding) => finding.rule)).toEqual(
      expect.arrayContaining([
        "repeated-lookaround-body",
        "potential-replacement-expansion",
      ]),
    );
  });

  it("uses the required cautious empty result wording", async () => {
    const report = await analyse("^\\d{4}-\\d{2}-\\d{2}$");

    expect(report.findings).toEqual([]);
    expect(report.summary).toBe(
      "No issue found by this analyser. This is not a safety guarantee.",
    );
  });

  it("does not apply ECMAScript heuristics to another flavour", async () => {
    const syntax = await provider.parsePattern({
      flavour: "ecmascript",
      pattern: "(a+)+",
      flags: [],
      options: {},
    });
    const report = analyseStaticRisk({
      flavour: "pcre2",
      root: syntax.root,
      flags: [],
      scanAll: false,
    });

    expect(report.analyser.support).toBe("unsupported-flavour");
    expect(report.findings).toEqual([]);
    expect(report.summary).toContain("unavailable for pcre2");
  });

  it("bounds normalized-tree traversal independently of render limits", () => {
    const literal = (index: number): NormalizedRegexNode => ({
      id: `literal-${index}`,
      kind: "literal",
      range: { startUtf16: index, endUtf16: index + 1 },
      raw: "a",
      explanation: "literal",
      children: [],
      properties: {
        zeroWidth: false,
        nullable: false,
        minimumLength: 1,
        maximumLength: 1,
        consumesInput: true,
      },
      support: {
        flavour: "ecmascript",
        status: "supported",
        notes: [],
      },
      provenance: {
        provider: "fixture",
        providerVersion: "1",
        source: "parsed",
      },
    });
    const root: NormalizedRegexNode = {
      ...literal(0),
      id: "root",
      kind: "pattern",
      range: {
        startUtf16: 0,
        endUtf16: MAXIMUM_STATIC_ANALYSIS_NODES + 10,
      },
      raw: "a".repeat(MAXIMUM_STATIC_ANALYSIS_NODES + 10),
      children: Array.from(
        { length: MAXIMUM_STATIC_ANALYSIS_NODES + 10 },
        (_, index) => literal(index),
      ),
    };

    const report = analyseStaticRisk({
      flavour: "ecmascript",
      root,
      flags: [],
      scanAll: false,
    });

    expect(report.analysedNodes).toBe(MAXIMUM_STATIC_ANALYSIS_NODES);
    expect(report.traversalTruncated).toBe(true);
    expect(report.limitations.at(-1)).toContain("Traversal stopped");
  });
});
