import { unzipSync } from "fflate";
import { describe, expect, it } from "vitest";
import type { CorpusBatchResult } from "./corpus.types";
import {
  assertCorpusZipByteLength,
  canExportAppliedCorpus,
  corpusAppliedDownloadName,
  createAppliedCorpusZip,
  MAXIMUM_CORPUS_REPORT_BYTES,
  MAXIMUM_CORPUS_ZIP_BYTES,
  serializeCorpusReport,
} from "./corpus-export";

const batch: CorpusBatchResult = {
  operation: "replace",
  subjectMode: "document",
  status: "complete",
  documents: [
    {
      documentId: "one",
      name: "../same.txt",
      inputBytes: 3,
      lineCount: 1,
      status: "complete",
      matchCount: 1,
      elapsedMs: 2,
      output: "ONE",
      outputBytes: 3,
      changed: true,
      captureSummaries: [],
      captureSummariesTruncated: false,
      diagnostics: [],
    },
    {
      documentId: "two",
      name: "..\\same.txt",
      inputBytes: 3,
      lineCount: 1,
      status: "complete",
      matchCount: 1,
      elapsedMs: 2,
      output: "TWO",
      outputBytes: 3,
      changed: true,
      captureSummaries: [],
      captureSummariesTruncated: false,
      diagnostics: [],
    },
  ],
  processedDocuments: 2,
  totalDocuments: 2,
  totalMatches: 2,
  totalElapsedMs: 4,
  retainedOutputBytes: 6,
  message: "Complete.",
};

describe("corpus exports", () => {
  it("exports a content-free, versioned JSON report", () => {
    const serialized = serializeCorpusReport(batch, {
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern: "secret-pattern",
      flags: ["g"],
    });
    const parsed = JSON.parse(serialized) as {
      schemaVersion: number;
      documents: readonly Record<string, unknown>[];
    };

    expect(parsed.schemaVersion).toBe(1);
    expect(serialized).not.toContain("ONE");
    expect(serialized).toContain('"subjectMode": "document"');
    expect(parsed.documents[0]).not.toHaveProperty("output");
  });

  it("exports content-free NDJSON and spreadsheet-safe CSV summaries", () => {
    const formulaBatch: CorpusBatchResult = {
      ...batch,
      documents: [
        {
          ...batch.documents[0]!,
          name: '=HYPERLINK("https://invalid.test")',
        },
      ],
      totalDocuments: 1,
      processedDocuments: 1,
      totalMatches: 1,
    };
    const configuration = {
      flavour: "ecmascript",
      flavourVersion: "2025",
      pattern: "+formula",
      flags: ["g"],
    };
    const ndjson = serializeCorpusReport(formulaBatch, configuration, "ndjson");
    const lines = ndjson
      .trimEnd()
      .split("\n")
      .map((line) => JSON.parse(line) as unknown);
    expect(lines).toHaveLength(2);
    expect(lines[0]).toMatchObject({
      type: "metadata",
      subjectMode: "document",
    });
    expect(lines[1]).toMatchObject({
      type: "document",
      name: '=HYPERLINK("https://invalid.test")',
    });
    expect(ndjson).not.toContain("ONE");

    const csv = serializeCorpusReport(formulaBatch, configuration, "csv");
    expect(csv).toContain(`"'+formula"`);
    expect(csv).toContain(`"'=HYPERLINK(""https://invalid.test"")"`);
    expect(csv).not.toContain("ONE");
  });

  it("sanitizes archive paths and makes collisions unique", async () => {
    expect(corpusAppliedDownloadName(batch.documents[0]!)).toBe(
      "same.replaced.txt",
    );
    const archive = await createAppliedCorpusZip(batch).promise;
    const files = unzipSync(archive);

    expect(Object.keys(files).sort()).toEqual([
      "same.replaced-2.txt",
      "same.replaced.txt",
    ]);
    expect(new TextDecoder().decode(files["same.replaced.txt"])).toBe("ONE");
    expect(new TextDecoder().decode(files["same.replaced-2.txt"])).toBe("TWO");
  });

  it("blocks applied exports when any result is incomplete", () => {
    const partial: CorpusBatchResult = {
      ...batch,
      status: "partial",
      documents: [
        { ...batch.documents[0]!, status: "partial" },
        batch.documents[1]!,
      ],
    };
    expect(canExportAppliedCorpus(partial)).toBe(false);
    expect(() => createAppliedCorpusZip(partial)).toThrow(
      /completed exactly/iu,
    );
  });

  it("enforces an explicit archive byte limit", () => {
    expect(() =>
      assertCorpusZipByteLength(MAXIMUM_CORPUS_ZIP_BYTES),
    ).not.toThrow();
    expect(() =>
      assertCorpusZipByteLength(MAXIMUM_CORPUS_ZIP_BYTES + 1),
    ).toThrow(/68 MiB/iu);
    expect(() => assertCorpusZipByteLength(Number.NaN)).toThrow(/68 MiB/iu);
  });

  it("bounds every report format before download", () => {
    const oversized: CorpusBatchResult = {
      ...batch,
      documents: [
        {
          ...batch.documents[0]!,
          message: "x".repeat(MAXIMUM_CORPUS_REPORT_BYTES),
        },
      ],
      totalDocuments: 1,
      processedDocuments: 1,
      totalMatches: 1,
    };
    for (const format of ["json", "csv", "ndjson"] as const) {
      expect(() =>
        serializeCorpusReport(
          oversized,
          {
            flavour: "ecmascript",
            pattern: "x",
            flags: [],
          },
          format,
        ),
      ).toThrow(/4 MiB/iu);
    }
  });
});
