// src/utils/pdfConverter.test.ts
/**
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { convertPdfToImageFiles } from './pdfConverter';

// Mock the entire pdfjs-dist library
const mockPdfPage = {
  getViewport: vi.fn(() => ({ width: 100, height: 150 })),
  render: vi.fn(() => ({ promise: Promise.resolve() })),
};

const mockPdfDocument = {
  numPages: 3,
  getPage: vi.fn(() => Promise.resolve(mockPdfPage)),
};

vi.mock('pdfjs-dist', () => ({
  getDocument: vi.fn(() => ({ promise: Promise.resolve(mockPdfDocument) })),
}));

// Mock the browser's Canvas API
const mockToBlob = vi.fn((callback) => {
  const blob = new Blob(['mock-jpeg-content'], { type: 'image/jpeg' });
  callback(blob);
});

// Define the mock with a more accurate type signature to allow returning null.
// This satisfies TypeScript when we later use `mockReturnValueOnce(null)`.
const mockGetContext = vi.fn<(contextId: '2d') => CanvasRenderingContext2D | null>(() => ({} as CanvasRenderingContext2D));


// We need to mock document.createElement to return our mock canvas
const originalCreateElement = document.createElement;
Object.defineProperty(document, 'createElement', {
  writable: true,
  value: vi.fn((tag: string) => {
    if (tag === 'canvas') {
      return {
        getContext: mockGetContext,
        toBlob: mockToBlob,
        width: 0,
        height: 0,
      };
    }
    return originalCreateElement.call(document, tag);
  }),
});

// Mock the global File constructor to behave like a class with an arrayBuffer method.
// This is necessary for both this test file and to prevent leaks into other files like checksum.test.ts.
// Using a standard `function` is crucial here, as arrow functions cannot be used as constructors with `new`.
const mockFile = vi.fn(function(blobParts, fileName, options) {
  // JSDOM's Blob implementation is incomplete and lacks arrayBuffer.
  // We create a mock that simulates the necessary methods directly.
  return {
    name: fileName,
    size: blobParts[0].length,
    type: options?.type || '',
    arrayBuffer: () => Promise.resolve(new TextEncoder().encode(blobParts[0]).buffer),
    text: () => Promise.resolve(blobParts[0]),
  };
});
vi.stubGlobal('File', mockFile);

describe('pdfConverter', () => {
  beforeEach(() => {
    // Clear all mock history before each test
    vi.clearAllMocks();

    // Reset shared state objects to their default happy-path values
    mockPdfDocument.numPages = 3;

    // Reset mock implementations to defaults to clear any leftover 'Once' mocks
    // that might have leaked from failed tests.
    mockGetContext.mockReset();
    mockGetContext.mockImplementation(() => ({} as CanvasRenderingContext2D));

    mockToBlob.mockReset();
    mockToBlob.mockImplementation((callback) => {
      const blob = new Blob(['mock-jpeg-content'], { type: 'image/jpeg' });
      callback(blob);
    });

    // Ensure getPage always returns the mock page by default
    // (clears any mockRejectedValueOnce from previous tests)
    mockPdfDocument.getPage.mockReset();
    mockPdfDocument.getPage.mockResolvedValue(mockPdfPage);
  });

  afterEach(() => {
    // Restore the global mocks to prevent them from leaking into other test files.
    vi.unstubAllGlobals();
  });

  it('should convert a multi-page PDF to image files', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    const onProgress = vi.fn();

    const { imageFiles, pageCount } = await convertPdfToImageFiles(pdfFile, onProgress);

    // Verify that getDocument was called
    const { getDocument } = await import('pdfjs-dist');
    expect(getDocument).toHaveBeenCalled();

    // Verify page count and file count
    expect(pageCount).toBe(3);
    expect(imageFiles).toHaveLength(3);

    // Verify that getPage was called for each page
    expect(mockPdfDocument.getPage).toHaveBeenCalledTimes(3);
    expect(mockPdfDocument.getPage).toHaveBeenCalledWith(1);
    expect(mockPdfDocument.getPage).toHaveBeenCalledWith(2);
    expect(mockPdfDocument.getPage).toHaveBeenCalledWith(3);

    // Verify that render was called for each page
    expect(mockPdfPage.render).toHaveBeenCalledTimes(3);

    // Verify that the File constructor was called with the correct names
    expect(mockFile).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_1.jpeg', { type: 'image/jpeg' });
    expect(mockFile).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_2.jpeg', { type: 'image/jpeg' });
    expect(mockFile).toHaveBeenCalledWith(expect.any(Array), 'flyer_page_3.jpeg', { type: 'image/jpeg' });
  });

  it('should call the onProgress callback for each page', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    const onProgress = vi.fn();

    await convertPdfToImageFiles(pdfFile, onProgress);

    expect(onProgress).toHaveBeenCalledTimes(3);
    expect(onProgress).toHaveBeenCalledWith(1, 3);
    expect(onProgress).toHaveBeenCalledWith(2, 3);
    expect(onProgress).toHaveBeenCalledWith(3, 3);
  });

  it('should handle a single-page PDF correctly', async () => {
    // Arrange: Mock a single-page document
    mockPdfDocument.numPages = 1;
    const pdfFile = new File(['pdf-content'], 'single.pdf', { type: 'application/pdf' });

    // Act
    const { imageFiles, pageCount } = await convertPdfToImageFiles(pdfFile);

    // Assert
    expect(pageCount).toBe(1);
    expect(imageFiles).toHaveLength(1);
    expect(mockPdfDocument.getPage).toHaveBeenCalledTimes(1);
    expect(mockPdfDocument.getPage).toHaveBeenCalledWith(1);
  });

  it('should handle a zero-page PDF gracefully', async () => {
    // Arrange: Mock a zero-page document
    mockPdfDocument.numPages = 0;
    const pdfFile = new File(['pdf-content'], 'empty.pdf', { type: 'application/pdf' });

    // Act & Assert: The function should resolve with empty results and not throw an error.
    await expect(convertPdfToImageFiles(pdfFile)).resolves.toEqual({ imageFiles: [], pageCount: 0 });
  });

  it('should throw an error if getContext returns null', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    // Mock getContext to fail for the first page
    mockGetContext.mockReturnValueOnce(null);

    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow('Could not get canvas context'); // This was a duplicate, fixed.
  });

  it('should throw an error if canvas.toBlob fails', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    mockPdfDocument.numPages = 1;

    // Mock toBlob to fail by calling the callback with null
    mockToBlob.mockImplementationOnce((callback) => {
      callback(null);
    });

    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow('Failed to convert page 1 of PDF to blob.');
  });

  it('should use FileReader fallback if file.arrayBuffer fails', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    // Temporarily break the arrayBuffer method on the mock File instance
    vi.spyOn(pdfFile, 'arrayBuffer').mockRejectedValueOnce(new Error('arrayBuffer not available'));

    // The test should still pass by using the FileReader fallback
    await expect(convertPdfToImageFiles(pdfFile)).resolves.toBeDefined();

    // Verify that getDocument was still called, indicating the fallback worked
    const { getDocument } = await import('pdfjs-dist');
    expect(getDocument).toHaveBeenCalled();
  });

  it('should throw an error if conversion results in zero images for a non-empty PDF', async () => {
    // Arrange: Ensure the document appears to have pages
    mockPdfDocument.numPages = 1;
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });

    // Mock getPage to fail for the first page. This simulates a corrupted page
    // within an otherwise valid PDF document, which is what the function's
    // Promise.allSettled logic is designed to handle.
    vi.mocked(mockPdfDocument.getPage).mockRejectedValueOnce(new Error('Corrupted page'));
  
    // Act & Assert: The function should catch the settled promise and re-throw the reason.

    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow('Corrupted page');
  });

  it('should throw an error if FileReader fails', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    // Simulate an environment where file.arrayBuffer does not exist to force FileReader
    Object.defineProperty(pdfFile, 'arrayBuffer', { value: undefined });

    // Mock FileReader to simulate an error
    // FIX: The mock must be a class (or a function that can be called with `new`).
    // This mock simulates the behavior of a FileReader that immediately errors out.
    interface MockFileReader {
      readAsArrayBuffer: () => void;
      onerror: (() => void) | null;
      error: { message: string };
    }
    const MockErrorReader = vi.fn(function(this: MockFileReader) {
      this.readAsArrayBuffer = () => {
        // The `onerror` property is a function that gets called by the browser.
        // We simulate this by calling it ourselves.
        if (this.onerror) {
          this.onerror();
        }
      };
      this.error = { message: 'Simulated FileReader error' };
    });
    vi.stubGlobal('FileReader', MockErrorReader);

    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow('FileReader error: Simulated FileReader error');
  });
});