import { describe, expect, it } from "vitest";
import { FieldType, type BlueprintFieldDefinition } from "@capakraken/shared";
import {
  isSuspectRegexPattern,
  validateCustomFields,
  MAX_PATTERN_LENGTH,
  MAX_REGEX_INPUT_LENGTH,
} from "../blueprint/validator.js";

describe("blueprint validator — ReDoS hardening (#52)", () => {
  describe("isSuspectRegexPattern", () => {
    it("flags classic nested-quantifier shapes", () => {
      expect(isSuspectRegexPattern("(a+)+")).toBe(true);
      expect(isSuspectRegexPattern("(a*)*")).toBe(true);
      expect(isSuspectRegexPattern("(a+)*")).toBe(true);
      expect(isSuspectRegexPattern("(a*)+")).toBe(true);
      expect(isSuspectRegexPattern("(.+)*")).toBe(true);
      expect(isSuspectRegexPattern("(.*)+")).toBe(true);
    });

    it("flags grouped bounded-quantifier shapes", () => {
      expect(isSuspectRegexPattern("(a{2,})+")).toBe(true);
      expect(isSuspectRegexPattern("(a{2,5})*")).toBe(true);
    });

    it("flags the canonical ReDoS sample ^(a+)+$", () => {
      expect(isSuspectRegexPattern("^(a+)+$")).toBe(true);
    });

    it("flags non-capturing groups too", () => {
      expect(isSuspectRegexPattern("(?:a+)+")).toBe(true);
    });

    it("flags over-long patterns (DoS via compile cost)", () => {
      const long = "a".repeat(MAX_PATTERN_LENGTH + 1);
      expect(isSuspectRegexPattern(long)).toBe(true);
    });

    it("allows common safe patterns", () => {
      expect(isSuspectRegexPattern("^[a-z]+$")).toBe(false);
      expect(isSuspectRegexPattern("^\\d{3}-\\d{4}$")).toBe(false);
      expect(isSuspectRegexPattern("[A-Z0-9_]+")).toBe(false);
      expect(isSuspectRegexPattern("^https?://")).toBe(false);
      expect(isSuspectRegexPattern("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")).toBe(false);
    });
  });

  describe("validateCustomFields with ReDoS pattern", () => {
    const fieldDefs: BlueprintFieldDefinition[] = [
      {
        id: "f1",
        label: "Test Field",
        key: "test",
        type: FieldType.TEXT,
        required: false,
        order: 0,
        validation: { pattern: "^(a+)+$" },
      } as BlueprintFieldDefinition,
    ];

    it("rejects a suspect pattern immediately without running RegExp", () => {
      // Craft the classic ReDoS input: many 'a's followed by a non-matching
      // char. If the code ran RegExp.test unguarded, this would hang for
      // seconds. Because the pattern is rejected at validation time, we
      // get a fast failure.
      const attackInput = "a".repeat(30) + "!";
      const t0 = Date.now();
      const errors = validateCustomFields(fieldDefs, { test: attackInput });
      const elapsed = Date.now() - t0;
      expect(errors).toHaveLength(1);
      expect(errors[0]?.key).toBe("test");
      // Must complete in < 50 ms — well below the budget set by the
      // ticket's acceptance criteria.
      expect(elapsed).toBeLessThan(50);
    });

    it("still validates benign patterns correctly", () => {
      const safeFieldDefs: BlueprintFieldDefinition[] = [
        {
          ...fieldDefs[0]!,
          validation: { pattern: "^[a-z]+$" },
        } as BlueprintFieldDefinition,
      ];
      expect(validateCustomFields(safeFieldDefs, { test: "hello" })).toEqual([]);
      const errors = validateCustomFields(safeFieldDefs, { test: "HELLO" });
      expect(errors).toHaveLength(1);
    });

    it("caps input length before regex.test() (belt-and-suspenders)", () => {
      // Even with a benign pattern, a 10 MB input would be slow to match.
      // The validator slices to MAX_REGEX_INPUT_LENGTH first.
      const safeFieldDefs: BlueprintFieldDefinition[] = [
        {
          ...fieldDefs[0]!,
          validation: { pattern: "^[a-z]+$" },
        } as BlueprintFieldDefinition,
      ];
      const huge = "a".repeat(MAX_REGEX_INPUT_LENGTH * 3);
      const t0 = Date.now();
      const errors = validateCustomFields(safeFieldDefs, { test: huge });
      const elapsed = Date.now() - t0;
      expect(errors).toEqual([]);
      expect(elapsed).toBeLessThan(50);
    });

    it("handles syntactically-invalid patterns without throwing", () => {
      const badFieldDefs: BlueprintFieldDefinition[] = [
        {
          ...fieldDefs[0]!,
          validation: { pattern: "[unclosed" },
        } as BlueprintFieldDefinition,
      ];
      const errors = validateCustomFields(badFieldDefs, { test: "any" });
      expect(errors).toHaveLength(1);
    });
  });
});
