import { unzipSync } from 'fflate';
import { describe, expect, it } from 'vitest';
import {
  createResultZip,
  saveResultsZip,
  type BlobSaver,
  type ExportResult,
} from '../../src/export';

function result(id: string, fileName: string, contents: string): ExportResult {
  const blob = new Blob([contents], { type: 'text/plain' });
  return {
    id,
    planId: 'plan',
    outputId: id,
    fileName,
    blob,
    mimeType: 'text/plain',
    role: 'analysis',
    size: blob.size,
    createdAt: '2026-07-24T10:00:00.000Z',
  };
}

describe('deterministic result ZIP creation', () => {
  it('sorts and deduplicates safe paths with fixed archive metadata', async () => {
    const entries = [
      { fileName: '../../Z.txt', blob: new Blob(['z']) },
      { fileName: 'a.txt', blob: new Blob(['first']) },
      { fileName: 'A.TXT', blob: new Blob(['second']) },
    ];
    const first = await createResultZip(entries, {
      fileName: '../My outputs.any',
    });
    const second = await createResultZip(entries, {
      fileName: '../My outputs.any',
    });

    expect(first.fileName).toBe('My-outputs.zip');
    expect(first.entryNames).toEqual(['A-2.txt', 'Z.txt', 'a.txt']);
    expect(first.totalInputBytes).toBe(12);
    expect(new Uint8Array(await first.blob.arrayBuffer())).toEqual(
      new Uint8Array(await second.blob.arrayBuffer())
    );

    const extracted = unzipSync(new Uint8Array(await first.blob.arrayBuffer()));
    expect(Object.keys(extracted)).toEqual(['A-2.txt', 'Z.txt', 'a.txt']);
    expect(new TextDecoder().decode(extracted['a.txt'])).toBe('first');
    expect(new TextDecoder().decode(extracted['A-2.txt'])).toBe('second');
  });

  it('rejects empty, oversized, and pre-cancelled batches', async () => {
    await expect(createResultZip([])).rejects.toThrow(/at least one/iu);
    await expect(
      createResultZip([{ fileName: 'large.bin', blob: new Blob(['12345']) }], {
        limits: {
          maxEntries: 2,
          maxEntryBytes: 4,
          maxTotalInputBytes: 4,
          maxArchiveBytes: 100,
        },
      })
    ).rejects.toThrowError(
      expect.objectContaining({
        limit: 'entry-bytes',
      })
    );

    const controller = new AbortController();
    controller.abort();
    await expect(
      createResultZip([{ fileName: 'one.txt', blob: new Blob(['one']) }], {
        signal: controller.signal,
      })
    ).rejects.toMatchObject({ name: 'AbortError' });
  });

  it('packages an optional report and passes the ZIP to the save adapter', async () => {
    const saverCalls: { fileName: string; type: string }[] = [];
    const saver: BlobSaver = async (blob, fileName) => {
      saverCalls.push({ fileName, type: blob.type });
      return 'saved';
    };
    const saved = await saveResultsZip([result('one', 'one.txt', 'contents')], {
      fileName: 'batch.zip',
      saver,
      report: {
        schemaVersion: 1,
        generatedAt: '2026-07-24T10:00:00.000Z',
        plan: { id: 'plan', operation: 'convert' },
        outputs: [],
        diagnostics: [],
        privacy: {
          includesSourceMedia: false,
          includesCommandArguments: false,
          privatePathsRedacted: true,
        },
      },
    });
    expect(saved.outcome).toBe('saved');
    expect(saved.archive.entryNames).toEqual([
      'convert-export-report.json',
      'one.txt',
    ]);
    expect(saverCalls).toEqual([
      { fileName: 'batch.zip', type: 'application/zip' },
    ]);
  });
});
