import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

const imageFilesMock = vi.hoisted(() => ({
  get: vi.fn(),
  put: vi.fn(),
  delete: vi.fn(),
}));

vi.mock('@/lib/utils/database', () => ({
  db: {
    imageFiles: imageFilesMock,
  },
}));

import {
  extractOriginalImageId,
  loadImageMapping,
  loadPdfBlob,
  storeImages,
  storePdfBlob,
} from '@/lib/utils/image-storage';

class TestFileReader {
  result: string | ArrayBuffer | null = null;
  onloadend: FileReader['onloadend'] = null;
  onerror: FileReader['onerror'] = null;

  readAsDataURL(blob: Blob): void {
    void blob.arrayBuffer().then(
      (buffer) => {
        this.result = `data:${blob.type};base64,${Buffer.from(buffer).toString('base64')}`;
        this.onloadend?.call(this as unknown as FileReader, {} as ProgressEvent<FileReader>);
      },
      () => {
        this.onerror?.call(this as unknown as FileReader, {} as ProgressEvent<FileReader>);
      },
    );
  }
}

function bytesOf(buffer: ArrayBuffer): number[] {
  return Array.from(new Uint8Array(buffer));
}

beforeAll(() => {
  vi.stubGlobal('FileReader', TestFileReader);
});

afterAll(() => {
  vi.unstubAllGlobals();
});

beforeEach(() => {
  imageFilesMock.get.mockReset();
  imageFilesMock.put.mockReset();
  imageFilesMock.delete.mockReset();
});

describe('image file persistence', () => {
  it('stores a PDF as an ArrayBuffer with its exact metadata', async () => {
    const bytes = [0x25, 0x50, 0x44, 0x46, 0x01, 0xff];
    const file = new File([Uint8Array.from(bytes)], 'lesson.pdf', {
      type: 'application/pdf',
    });

    const storageKey = await storePdfBlob(file);

    expect(imageFilesMock.put).toHaveBeenCalledOnce();
    const record = imageFilesMock.put.mock.calls[0][0];
    expect(record.id).toBe(storageKey);
    expect(record.blob).toBeInstanceOf(ArrayBuffer);
    expect(bytesOf(record.blob)).toEqual(bytes);
    expect(record.mimeType).toBe('application/pdf');
    expect(record.filename).toBe('lesson.pdf');
    expect(record.size).toBe(record.blob.byteLength);
  });

  it('stores a page image as an ArrayBuffer with its decoded bytes and MIME type', async () => {
    const bytes = [0x00, 0x01, 0x02, 0xfd, 0xfe];
    const src = `data:image/jpeg;base64,${Buffer.from(bytes).toString('base64')}`;

    await storeImages([{ id: 'img_1', src, pageNumber: 1 }]);

    expect(imageFilesMock.put).toHaveBeenCalledOnce();
    const record = imageFilesMock.put.mock.calls[0][0];
    expect(record.blob).toBeInstanceOf(ArrayBuffer);
    expect(bytesOf(record.blob)).toEqual(bytes);
    expect(record.mimeType).toBe('image/jpeg');
    expect(record.size).toBe(record.blob.byteLength);
  });

  it('loads an ArrayBuffer PDF record as a Blob with exact bytes and MIME type', async () => {
    const bytes = [0x25, 0x50, 0x44, 0x46, 0x02];
    imageFilesMock.get.mockResolvedValue({
      blob: Uint8Array.from(bytes).buffer,
      mimeType: 'application/pdf',
    });

    const blob = await loadPdfBlob('pdf-key');

    expect(blob).toBeInstanceOf(Blob);
    expect(blob?.type).toBe('application/pdf');
    expect(bytesOf(await blob!.arrayBuffer())).toEqual(bytes);
  });

  it('returns a legacy typed Blob unchanged', async () => {
    const legacyBlob = new Blob([Uint8Array.from([1, 2, 3])], { type: 'application/pdf' });
    imageFilesMock.get.mockResolvedValue({
      blob: legacyBlob,
      mimeType: 'application/pdf',
    });

    await expect(loadPdfBlob('legacy-pdf')).resolves.toBe(legacyBlob);
  });

  it('reconstructs a legacy empty-type Blob with the record MIME type and exact bytes', async () => {
    const bytes = [3, 1, 4, 1, 5];
    const legacyBlob = new Blob([Uint8Array.from(bytes)]);
    imageFilesMock.get.mockResolvedValue({
      blob: legacyBlob,
      mimeType: 'application/pdf',
    });

    const blob = await loadPdfBlob('legacy-empty-type-pdf');

    expect(blob).toBeInstanceOf(Blob);
    expect(blob).not.toBe(legacyBlob);
    expect(blob?.type).toBe('application/pdf');
    expect(bytesOf(await blob!.arrayBuffer())).toEqual(bytes);
  });

  it('loads an ArrayBuffer page-image record as an exact data URL mapping', async () => {
    const bytes = [0xde, 0xad, 0xbe, 0xef];
    imageFilesMock.get.mockResolvedValue({
      blob: Uint8Array.from(bytes).buffer,
      mimeType: 'image/webp',
    });

    await expect(loadImageMapping(['session_abc123def4_img_1'])).resolves.toEqual({
      img_1: `data:image/webp;base64,${Buffer.from(bytes).toString('base64')}`,
    });
  });

  it('rolls back every earlier image when a later write fails and preserves the cause', async () => {
    const writeFailure = new Error('IndexedDB write failed');
    imageFilesMock.put
      .mockResolvedValueOnce(undefined)
      .mockResolvedValueOnce(undefined)
      .mockRejectedValueOnce(writeFailure);
    imageFilesMock.delete.mockResolvedValue(undefined);
    const images = [
      { id: 'img_1', src: 'data:image/png;base64,AQ==' },
      { id: 'img_2', src: 'data:image/png;base64,Ag==' },
      { id: 'img_3', src: 'data:image/png;base64,Aw==' },
    ];

    const result = storeImages(images);

    await expect(result).rejects.toMatchObject({
      message: 'Failed to store image bundle at image img_3',
      cause: writeFailure,
    });
    const successfulIds = imageFilesMock.put.mock.calls.slice(0, 2).map(([record]) => record.id);
    expect(imageFilesMock.delete.mock.calls.map(([id]) => id)).toEqual(successfulIds);
  });
});

describe('extractOriginalImageId', () => {
  it('extracts an image id from a normal session storage id', () => {
    expect(extractOriginalImageId('session_abc123def4_img_15')).toBe('img_15');
  });

  it('does not truncate a nanoid containing underscores', () => {
    expect(extractOriginalImageId('session_ab_cd_efgh_img_20')).toBe('img_20');
  });

  it('preserves nonnumeric image ids', () => {
    expect(extractOriginalImageId('session_abc123def4_img_data')).toBe('img_data');
  });

  it('preserves image ids containing an image-like delimiter', () => {
    expect(extractOriginalImageId('session_abc123def4_hero_img_1')).toBe('hero_img_1');
  });

  it('rejects malformed storage ids', () => {
    expect(extractOriginalImageId('session_abc123')).toBeUndefined();
    expect(extractOriginalImageId('session_abc123def4_')).toBeUndefined();
    expect(extractOriginalImageId('other_abc123def4_img_1')).toBeUndefined();
  });
});