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

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

// Mock the logger before other imports to spy on its methods
vi.mock('../services/logger.client', () => ({
  logger: {
    warn: vi.fn(),
  },
}));

// 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,
  // Explicitly type the mock function to accept a number and return the correct promise type.
  // This resolves the TypeScript error when using mockImplementation with arguments later.
  getPage: vi.fn<(pageNumber: number) => Promise<typeof mockPdfPage>>(() => 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 return partial results if one page fails to get a canvas context', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    // Mock getContext to fail for the first page
    mockGetContext.mockReturnValueOnce(null);
    const mockedLogger = logger as Mocked<typeof logger>;

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

    // Should still report 3 total pages
    expect(pageCount).toBe(3);
    // But only 2 images should be successfully created
    expect(imageFiles).toHaveLength(2);
    // And a warning should be logged for the failed page
    expect(mockedLogger.warn).toHaveBeenCalledWith(
      { error: new Error('Could not get canvas context') },
      'A page failed to convert during PDF processing.',
    );
  });

  it('should throw an error if canvas.toBlob fails for the only page', 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);
    });

    // The function should throw the generic "zero images" error because the only page failed.
    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow(
      'PDF conversion resulted in zero images, though the PDF has pages. It might be corrupted or contain non-standard content.',
    );
  });

  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 a specific error if all pages of a non-empty PDF fail to convert', 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 only page. This simulates a scenario where
    // the PDF has pages, but none can be rendered, causing the `imageFiles` array
    // to be empty.
    vi.mocked(mockPdfDocument.getPage).mockRejectedValueOnce(new Error('Corrupted page'));

    // Act & Assert: The function should now catch the settled promise, find that no
    // images were generated, and throw the specific "zero images" error, covering line 133.
    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow(
      'PDF conversion resulted in zero images, though the PDF has pages. It might be corrupted or contain non-standard content.',
    );
  });

  it('should successfully process a PDF even if some pages fail to convert', async () => {
    // Arrange: 3-page PDF where the 2nd page will fail
    mockPdfDocument.numPages = 3;
    const pdfFile = new File(['pdf-content'], 'partial-success.pdf', { type: 'application/pdf' });
    const onProgress = vi.fn();
    const mockedLogger = logger as Mocked<typeof logger>;

    // Mock getPage to fail only for the second page
    vi.mocked(mockPdfDocument.getPage).mockImplementation(async (pageNumber: number) => {
      if (pageNumber === 2) {
        throw new Error('Simulated page 2 corruption');
      }
      // Return the standard mock page for other pages
      return mockPdfPage;
    });

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

    // Assert
    // Total page count should still be 3
    expect(pageCount).toBe(3);
    // Only 2 pages should have converted successfully
    expect(imageFiles).toHaveLength(2);
    // The progress callback should have been called for the 2 successful pages
    expect(onProgress).toHaveBeenCalledTimes(2);
    expect(onProgress).toHaveBeenCalledWith(1, 3);
    expect(onProgress).toHaveBeenCalledWith(3, 3);
    // The failure of page 2 should be logged as a warning
    expect(mockedLogger.warn).toHaveBeenCalledWith(
      { error: new Error('Simulated page 2 corruption') },
      'A page failed to convert during PDF processing.',
    );
  });

  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',
    );
  });

  it('should throw an error if FileReader result is not an ArrayBuffer', async () => {
    const pdfFile = new File(['pdf-content'], 'flyer.pdf', { type: 'application/pdf' });
    // Force the use of FileReader
    Object.defineProperty(pdfFile, 'arrayBuffer', { value: undefined });

    // Mock FileReader to return a string instead of an ArrayBuffer
    interface MockFileReader {
      readAsArrayBuffer: () => void;
      onload: (() => void) | null;
      result: string | ArrayBuffer;
    }
    const MockBadResultReader = vi.fn(function (this: MockFileReader) {
      this.result = 'this is a string, not an array buffer';
      this.readAsArrayBuffer = () => {
        if (this.onload) this.onload();
      };
    });
    vi.stubGlobal('FileReader', MockBadResultReader);

    await expect(convertPdfToImageFiles(pdfFile)).rejects.toThrow(
      'FileReader result was not an ArrayBuffer',
    );
  });
});
