import { describe, expect, it } from "vitest";
import {
  importProjectDocument,
  serializeProject,
} from "./project.serialization";
import type { RegexProjectV1 } from "./project.types";
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
import { parseRegexProject, parseRegexTests } from "./project.validation";
import {
  RegexFlavourRegistry,
  type RegexFlavourDefinition,
} from "../regex/flavours/flavour-registry";

const project: RegexProjectV1 = {
  schemaVersion: 1,
  id: "project-1",
  name: "Dates",
  createdAt: "2026-07-24T00:00:00.000Z",
  updatedAt: "2026-07-24T00:00:00.000Z",
  flavour: "ecmascript",
  pattern: "\\d+",
  flags: ["g"],
  options: {},
  replacement: "$&",
  mode: "match",
  testText: { included: true, value: "secret 42" },
  listTemplate: { source: "$0", tokens: [{ kind: "full-match" }] },
  tests: [
    {
      id: "test-1",
      name: "matches",
      enabled: true,
      flavour: "ecmascript",
      pattern: "\\d+",
      flags: [],
      options: {},
      subject: "42",
      expectation: { kind: "should-match" },
    },
  ],
  ui: { live: true, scanAll: false },
};

describe("project import and export", () => {
  it("excludes sensitive text by default and validates round trips", () => {
    const serialized = serializeProject(project, {
      includeTestText: false,
      includeUnitTestSubjects: false,
    });
    expect(serialized).not.toContain("secret 42");
    const imported = importProjectDocument(serialized);
    expect(imported.testText).toEqual({ included: false });
    expect(imported.tests[0]?.subject).toBe("");
    expect(imported.pattern).toBe("\\d+");
  });

  it("includes requested text explicitly", () => {
    const serialized = serializeProject(project, {
      includeTestText: true,
      includeUnitTestSubjects: true,
    });
    expect(serialized).toContain("secret 42");
    expect(serialized).toContain('"subject": "42"');
  });

  it("retains validated generation provenance when private subjects are omitted", () => {
    const generatedTest = {
      ...project.tests[0]!,
      generation: {
        kind: "generated" as const,
        generatorId: "regex-tools-ast-cases" as const,
        generatorVersion: "1" as const,
        seed: "project-seed-17",
        candidateId: "alternative:root:1",
        intendedOutcome: "match" as const,
      },
    };
    const imported = importProjectDocument(
      serializeProject(
        { ...project, tests: [generatedTest] },
        {
          includeTestText: false,
          includeUnitTestSubjects: false,
        },
      ),
    );

    expect(imported.tests[0]?.subject).toBe("");
    expect(imported.tests[0]?.generation).toEqual(generatedTest.generation);
  });

  it("persists corpus mode without accepting ephemeral corpus content", () => {
    const imported = importProjectDocument(
      serializeProject(
        { ...project, mode: "corpus" },
        {
          includeTestText: false,
          includeUnitTestSubjects: false,
        },
      ),
    );
    expect(imported.mode).toBe("corpus");

    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          mode: "corpus",
          corpus: [{ name: "secret.txt", text: "secret" }],
        }),
      ),
    ).toThrow(/ephemeral/iu);
  });

  it("preflights aggregate export size after applying privacy options", () => {
    const escapedSubject = "\0".repeat(2 * 1024 * 1024);
    const aggregateProject: RegexProjectV1 = {
      ...project,
      tests: Array.from({ length: 3 }, (_, index) => ({
        ...project.tests[0]!,
        id: `aggregate-${index}`,
        subject: escapedSubject,
      })),
    };

    expect(() =>
      serializeProject(aggregateProject, {
        includeTestText: false,
        includeUnitTestSubjects: true,
      }),
    ).toThrow(/32 MiB/u);
    expect(() =>
      serializeProject(aggregateProject, {
        includeTestText: false,
        includeUnitTestSubjects: false,
      }),
    ).not.toThrow();
  });

  it("validates richer versioned test expectations", () => {
    const serialized = JSON.stringify({
      ...project,
      tests: [
        {
          ...project.tests[0],
          expectation: {
            kind: "capture",
            matchIndex: 0,
            groupName: "number",
            status: "participated",
            value: "42",
          },
        },
        {
          ...project.tests[0],
          id: "replacement-test",
          replacement: "[$&]",
          expectation: { kind: "replacement", expected: "[42]" },
        },
      ],
    });
    const imported = importProjectDocument(serialized);
    expect(imported.tests[0]?.expectation).toEqual(
      expect.objectContaining({ kind: "capture", groupName: "number" }),
    );
    expect(imported.tests[1]?.expectation).toEqual({
      kind: "replacement",
      expected: "[42]",
    });
  });

  it("rejects future schemas, invalid flags and oversized imports", () => {
    expect(() => importProjectDocument('{"schemaVersion":2}')).toThrow(
      /schema version 1/u,
    );
    expect(() =>
      importProjectDocument(JSON.stringify({ ...project, flags: ["u", "v"] })),
    ).toThrow(/cannot be combined/u);
    expect(() =>
      importProjectDocument(" ".repeat(32 * 1024 * 1024 + 1)),
    ).toThrow(/32 MiB/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          replacement: "$&".repeat(
            DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16 / 2 + 1,
          ),
        }),
      ),
    ).toThrow(/Replacement exceeds/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          listTemplate: {
            source: "$0".repeat(
              DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16 / 2 + 1,
            ),
          },
        }),
      ),
    ).toThrow(/List template exceeds/u);
    const astralSubject = "😀".repeat(
      DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes / 4 + 1,
    );
    expect(astralSubject.length).toBeLessThan(
      DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes,
    );
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          testText: { included: true, value: astralSubject },
        }),
      ),
    ).toThrow(/Test text exceeds.*UTF-8 byte limit/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [{ ...project.tests[0], subject: astralSubject }],
        }),
      ),
    ).toThrow(/Test 1 subject exceeds.*UTF-8 byte limit/u);
  });

  it("accepts implemented flavours and rejects unsupported flavour or option semantics without rewriting", () => {
    expect(
      importProjectDocument(
        JSON.stringify({
          ...project,
          flavour: "pcre2",
          flavourVersion: "PCRE2 10.47",
          flags: ["g"],
          options: {
            matchLimit: 50_000,
            depthLimit: 500,
            heapLimitKib: 4_096,
          },
          tests: [],
        }),
      ),
    ).toEqual(
      expect.objectContaining({
        flavour: "pcre2",
        flavourVersion: "PCRE2 10.47",
        flags: ["g"],
        options: {
          matchLimit: 50_000,
          depthLimit: 500,
          heapLimitKib: 4_096,
        },
      }),
    );
    expect(() =>
      importProjectDocument(
        JSON.stringify({ ...project, flavourVersion: "ECMAScript 2026" }),
      ),
    ).toThrow(/flavour version.*unsupported/iu);
    expect(
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [{ ...project.tests[0], flavour: "python" }],
        }),
      ),
    ).toEqual(
      expect.objectContaining({
        tests: [
          expect.objectContaining({
            flavour: "python",
          }),
        ],
      }),
    );
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [{ ...project.tests[0], flavour: "unknown" }],
        }),
      ),
    ).toThrow(/flavour "unknown" is unavailable/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [
            {
              ...project.tests[0],
              flavourVersion: "ECMAScript 2026",
            },
          ],
        }),
      ),
    ).toThrow(/flavour version.*unsupported/iu);
    expect(() =>
      importProjectDocument(
        JSON.stringify({ ...project, options: { ignoreCase: true } }),
      ),
    ).toThrow(/does not implement JavaScript \(ECMAScript\) engine options/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [{ ...project.tests[0], options: { ascii: true } }],
        }),
      ),
    ).toThrow(/does not implement JavaScript \(ECMAScript\) engine options/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          resourceOverrides: { maximumMatches: 12 },
        }),
      ),
    ).toThrow(/resource overrides are not implemented/u);
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [project.tests[0], { ...project.tests[0] }],
        }),
      ),
    ).toThrow(/test id.*duplicated/iu);
  });

  it("keeps schema v1 while delegating future flavour fields to a contract", () => {
    const fixtureFlavour: RegexFlavourDefinition = {
      id: "pcre2",
      label: "Fixture flavour",
      versions: [
        {
          value: "fixture-version",
          label: "Fixture version",
          syntaxVersion: "fixture-version",
        },
      ],
      defaultVersion: "fixture-version",
      flags: [{ value: "i", label: "Caseless" }],
      defaultFlags: [],
      options: [
        {
          name: "ungreedy",
          label: "Ungreedy",
          kind: "boolean",
          defaultValue: false,
        },
      ],
    };
    const registry = new RegexFlavourRegistry([fixtureFlavour]);
    const imported = parseRegexProject(
      {
        ...project,
        flavour: "pcre2",
        flavourVersion: "fixture-version",
        flags: ["i"],
        options: { ungreedy: true },
        tests: [],
      },
      registry,
    );
    const importedTests = parseRegexTests(
      [
        {
          ...project.tests[0],
          flavour: "pcre2",
          flavourVersion: "fixture-version",
          flags: ["i"],
          options: { ungreedy: true },
        },
      ],
      registry,
    );

    expect(imported).toEqual(
      expect.objectContaining({
        schemaVersion: 1,
        flavour: "pcre2",
        flavourVersion: "fixture-version",
        flags: ["i"],
        options: { ungreedy: true },
      }),
    );
    expect(importedTests[0]).toEqual(
      expect.objectContaining({
        flavour: "pcre2",
        flavourVersion: "fixture-version",
        flags: ["i"],
        options: { ungreedy: true },
      }),
    );
  });

  it("imports legacy schema-v1 projects without a flavour-version field", () => {
    const imported = parseRegexProject(project);

    expect(imported.schemaVersion).toBe(1);
    expect(imported.flavour).toBe("ecmascript");
    expect(imported.flavourVersion).toBe(
      "ECMAScript 2025 syntax / current browser runtime",
    );
    expect(imported.tests[0]?.flavourVersion).toBeUndefined();
  });

  it("validates and preserves the supported per-test timeout limit", () => {
    const imported = importProjectDocument(
      JSON.stringify({
        ...project,
        tests: [
          {
            ...project.tests[0],
            resourceLimits: { manualExecutionTimeoutMs: 750 },
          },
        ],
      }),
    );
    expect(imported.tests[0]?.resourceLimits).toEqual({
      manualExecutionTimeoutMs: 750,
    });
    expect(() =>
      importProjectDocument(
        JSON.stringify({
          ...project,
          tests: [
            {
              ...project.tests[0],
              resourceLimits: { maximumMatches: 1 },
            },
          ],
        }),
      ),
    ).toThrow(/unsupported entries/u);
  });
});
