import { describe, expect, it, vi } from 'vitest';
import {
  collectCommandOutputs,
  DEFAULT_RESULT_COLLECTION_LIMITS,
  DEFAULT_RESULT_ZIP_LIMITS,
  ExportResultCollection,
  ExportResultLimitError,
  MissingCommandOutputError,
  saveCollectionResult,
  type BlobSaver,
  type ExportResultInput,
} from '../../src/export';
import type { FFmpegCommandPlan } from '../../src/commands';

function input(
  id: string,
  fileName = 'result.mp4',
  contents = 'result'
): ExportResultInput {
  return {
    id,
    planId: 'plan',
    outputId: id,
    fileName,
    blob: new Blob([contents]),
    mimeType: 'video/mp4',
    role: 'media',
  };
}

describe('export result collection', () => {
  it('reserves one ZIP entry for the optional report', () => {
    expect(DEFAULT_RESULT_COLLECTION_LIMITS.maxResults).toBe(255);
    expect(DEFAULT_RESULT_ZIP_LIMITS.maxEntries).toBe(
      DEFAULT_RESULT_COLLECTION_LIMITS.maxResults + 1
    );
  });

  it('owns result Blobs and revokes every replaced or released object URL', () => {
    let urlIndex = 0;
    const revokeObjectURL = vi.fn();
    const collection = new ExportResultCollection({
      objectUrlApi: {
        createObjectURL: () => `blob:result-${urlIndex++}`,
        revokeObjectURL,
      },
      now: () => new Date('2026-07-24T10:00:00.000Z'),
    });
    const first = collection.add(input('one'));
    expect(first).toMatchObject({
      fileName: 'result.mp4',
      mimeType: 'video/mp4',
      size: 6,
      createdAt: '2026-07-24T10:00:00.000Z',
    });
    expect(collection.objectUrl('one')).toBe('blob:result-0');
    expect(collection.objectUrl('one')).toBe('blob:result-0');

    collection.add(input('two', 'RESULT.mp4'));
    expect(collection.get('two')?.fileName).toBe('RESULT-2.mp4');
    collection.replace(input('one', 'replacement.mp4', 'new'));
    expect(revokeObjectURL).toHaveBeenCalledWith('blob:result-0');
    expect(collection.totalBytes).toBe(9);

    collection.objectUrl('two');
    expect(collection.remove('two')).toBe(true);
    expect(revokeObjectURL).toHaveBeenCalledWith('blob:result-1');
    collection.dispose();
    expect(collection.totalBytes).toBe(0);
    expect(() => collection.list()).toThrow(/disposed/iu);
    expect(() => collection.dispose()).not.toThrow();
  });

  it('adds a batch atomically and enforces count and byte limits', () => {
    const collection = new ExportResultCollection({
      limits: {
        maxResults: 2,
        maxSingleResultBytes: 5,
        maxTotalBytes: 8,
      },
    });
    expect(() =>
      collection.addMany([
        input('one', 'one.bin', '1234'),
        input('bad', 'bad.bin', '123456'),
      ])
    ).toThrowError(ExportResultLimitError);
    expect(collection.size).toBe(0);

    collection.add(input('one', 'one.bin', '1234'));
    expect(() => collection.add(input('two', 'two.bin', '12345'))).toThrowError(
      expect.objectContaining({ limit: 'total-result-bytes' })
    );
    expect(collection.size).toBe(1);
  });

  it('collects all planned outputs atomically from IDs or virtual paths', () => {
    const collection = new ExportResultCollection();
    const plan: Pick<FFmpegCommandPlan, 'id' | 'outputs'> = {
      id: 'split-plan',
      outputs: [
        {
          id: 'one',
          path: '/work/job/one.mp4',
          fileName: 'clip-001.mp4',
          mediaType: 'video/mp4',
          role: 'media',
          timeRange: { startSeconds: 0, endSeconds: 5 },
        },
        {
          id: 'two',
          path: '/work/job/two.mp4',
          fileName: 'clip-002.mp4',
          mediaType: 'video/mp4',
          role: 'media',
        },
      ],
    };
    const results = collectCommandOutputs(
      plan,
      new Map<string, Blob | Uint8Array>([
        ['one', Uint8Array.of(1, 2)],
        ['/work/job/two.mp4', new Blob(['two'])],
      ]),
      collection
    );
    expect(results.map((result) => result.outputId)).toEqual(['one', 'two']);
    expect(results[0]?.blob.type).toBe('video/mp4');
    expect(results[0]?.timeRange).toEqual({
      startSeconds: 0,
      endSeconds: 5,
    });

    const empty = new ExportResultCollection();
    expect(() =>
      collectCommandOutputs(plan, new Map([['one', new Blob(['one'])]]), empty)
    ).toThrowError(MissingCommandOutputError);
    expect(empty.size).toBe(0);
  });
});

describe('browser result saving', () => {
  it('sanitizes the download name and releases a successfully saved result', async () => {
    const collection = new ExportResultCollection();
    collection.add(input('one', 'result.mp4'));
    const saver = vi.fn<BlobSaver>(async () => 'downloaded');

    await expect(
      saveCollectionResult(collection, 'one', {
        fileName: '../../My result.mp4',
        saver,
      })
    ).resolves.toBe('downloaded');
    expect(saver).toHaveBeenCalledWith(
      expect.any(Blob),
      'My-result.mp4',
      'video/mp4'
    );
    expect(collection.size).toBe(0);
  });

  it('retains the result when the save picker is cancelled', async () => {
    const collection = new ExportResultCollection();
    collection.add(input('one'));
    const saver: BlobSaver = async () => 'cancelled';
    await saveCollectionResult(collection, 'one', { saver });
    expect(collection.size).toBe(1);
  });
});
