import { describe, it, expect, vi } from 'vitest';
import { createPdfRenderer } from './usePdfRenderer.svelte';
import { makeFakeLibLoader } from './testHelpers';
import { m } from '$lib/paraglide/messages.js';

function makeRenderingLib(renderPromise: Promise<void>): typeof import('pdfjs-dist') {
	const page = {
		getViewport: vi.fn().mockReturnValue({ width: 100, height: 100 }),
		render: vi.fn().mockReturnValue({ promise: renderPromise, cancel: vi.fn() }),
		streamTextContent: vi.fn().mockReturnValue(new ReadableStream())
	};
	return {
		GlobalWorkerOptions: { workerSrc: '' },
		getDocument: vi.fn().mockReturnValue({
			promise: Promise.resolve({ numPages: 1, getPage: vi.fn().mockResolvedValue(page) })
		}),
		TextLayer: class {
			render() {
				return Promise.resolve();
			}
			cancel() {}
		}
	} as unknown as typeof import('pdfjs-dist');
}

// Note: init() and loadDocument() require pdfjsLib (browser module).
// These tests cover pure state logic only — bounds clamping and zoom limits.

describe('createPdfRenderer', () => {
	it('starts at page 1 with scale 1.5 and no error', () => {
		const r = createPdfRenderer();
		expect(r.currentPage).toBe(1);
		expect(r.scale).toBe(1.5);
		expect(r.totalPages).toBe(0);
		expect(r.loading).toBe(false);
		expect(r.error).toBeNull();
		expect(r.isLoaded).toBe(false);
		expect(r.pdfjsReady).toBe(false);
	});

	it('prevPage does not go below page 1', () => {
		const r = createPdfRenderer();
		r.prevPage();
		expect(r.currentPage).toBe(1);
	});

	it('nextPage does not exceed totalPages', () => {
		const r = createPdfRenderer();
		// totalPages = 0, so 1 < 0 is false → stays at 1
		r.nextPage();
		expect(r.currentPage).toBe(1);
	});

	it('goToPage does not navigate when n > totalPages', () => {
		const r = createPdfRenderer();
		r.goToPage(5);
		expect(r.currentPage).toBe(1);
	});

	it('goToPage does not navigate when n < 1', () => {
		const r = createPdfRenderer();
		r.goToPage(0);
		expect(r.currentPage).toBe(1);
	});

	it('zoomIn increases scale by 0.25', () => {
		const r = createPdfRenderer();
		r.zoomIn();
		expect(r.scale).toBeCloseTo(1.75);
	});

	it('zoomOut decreases scale by 0.25', () => {
		const r = createPdfRenderer();
		r.zoomOut();
		expect(r.scale).toBeCloseTo(1.25);
	});

	it('zoomOut does not go below 0.5', () => {
		const r = createPdfRenderer();
		for (let i = 0; i < 20; i++) r.zoomOut();
		expect(r.scale).toBeCloseTo(0.5);
	});

	it('loadDocument is a no-op when pdfjsLib not initialized', async () => {
		const r = createPdfRenderer();
		await r.loadDocument('/some/path');
		// No-op because pdfjsLib is null (init not called)
		expect(r.error).toBeNull();
		expect(r.loading).toBe(false);
	});

	it('renderCurrentPage is a no-op when pdfjsLib is not initialized', async () => {
		const r = createPdfRenderer();
		// Should not throw — early-return branch
		await expect(r.renderCurrentPage()).resolves.toBeUndefined();
	});

	it('prerender is a no-op when pdfDoc is null', async () => {
		const r = createPdfRenderer();
		await expect(r.prerender()).resolves.toBeUndefined();
	});

	it('destroy is safe to call when no document is loaded', () => {
		const r = createPdfRenderer();
		expect(() => r.destroy()).not.toThrow();
	});

	it('setElements stores canvas and text layer refs', () => {
		const r = createPdfRenderer();
		const canvas = document.createElement('canvas');
		const textLayer = document.createElement('div');
		expect(() => r.setElements(canvas, textLayer)).not.toThrow();
	});

	it('isLoaded reflects totalPages > 0', () => {
		const r = createPdfRenderer();
		// Initial state — totalPages=0 → not loaded
		expect(r.isLoaded).toBe(false);
	});

	it('multiple zoomIn calls accumulate', () => {
		const r = createPdfRenderer();
		r.zoomIn();
		r.zoomIn();
		r.zoomIn();
		expect(r.scale).toBeCloseTo(2.25);
	});

	it('mixed zoom in then zoom out lands back at start', () => {
		const r = createPdfRenderer();
		r.zoomIn();
		r.zoomIn();
		r.zoomOut();
		r.zoomOut();
		expect(r.scale).toBeCloseTo(1.5);
	});

	it('zoomOut at the floor does nothing', () => {
		const r = createPdfRenderer();
		// Force scale down to 0.5
		for (let i = 0; i < 20; i++) r.zoomOut();
		const before = r.scale;
		r.zoomOut();
		expect(r.scale).toBe(before);
	});

	it('init() sets pdfjsReady to true when loader resolves', async () => {
		const r = createPdfRenderer(makeFakeLibLoader());
		await expect(r.init()).resolves.toBeUndefined();
		expect(r.pdfjsReady).toBe(true);
	});

	it('after init, loadDocument completes and loading returns to false', async () => {
		const r = createPdfRenderer(makeFakeLibLoader());
		await r.init();

		await r.loadDocument('/some/path');
		expect(r.loading).toBe(false);
	});

	it('renderCurrentPage is a no-op when canvasEl is null but pdfjsLib is initialized', async () => {
		const r = createPdfRenderer(makeFakeLibLoader());
		await r.init();
		// Without setElements, canvasEl is null — early return
		await expect(r.renderCurrentPage()).resolves.toBeUndefined();
	});

	it('renderCurrentPage is a no-op when textLayerEl is null', async () => {
		const r = createPdfRenderer(makeFakeLibLoader());
		await r.init();
		// Without setElements, textLayerEl is null — early return
		await expect(r.renderCurrentPage()).resolves.toBeUndefined();
	});

	it('init() can be called multiple times safely', async () => {
		const r = createPdfRenderer(makeFakeLibLoader());
		await r.init();
		await r.init();
		expect(r.pdfjsReady).toBe(true);
	});

	it('zoomIn after multiple zoomOuts lands at predictable scale', () => {
		const r = createPdfRenderer();
		// 1.5 -> 0.5 (floor) -> 0.75
		for (let i = 0; i < 10; i++) r.zoomOut();
		r.zoomIn();
		expect(r.scale).toBeCloseTo(0.75);
	});

	it('goToPage(1) works when totalPages would be at least 1 (no-op currently)', () => {
		const r = createPdfRenderer();
		r.goToPage(1);
		expect(r.currentPage).toBe(1);
	});

	it('calls injected libLoader during init and sets pdfjsReady', async () => {
		const fakePdfjs = {
			GlobalWorkerOptions: { workerSrc: '' },
			getDocument: vi.fn(),
			TextLayer: class {}
		} as unknown as typeof import('pdfjs-dist');
		const fakeLoader = vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
		const r = createPdfRenderer(fakeLoader);
		await r.init();
		expect(fakeLoader).toHaveBeenCalledOnce();
		expect(r.pdfjsReady).toBe(true);
	});

	it('leaves pdfjsReady false when libLoader rejects', async () => {
		const failingLoader = vi.fn().mockRejectedValue(new Error('load failed'));
		const r = createPdfRenderer(failingLoader);
		await expect(r.init()).rejects.toThrow('load failed');
		expect(r.pdfjsReady).toBe(false);
	});

	it('init() is idempotent — libLoader called only once on repeated calls', async () => {
		const fakePdfjs = {
			GlobalWorkerOptions: { workerSrc: '' },
			getDocument: vi.fn(),
			TextLayer: class {}
		} as unknown as typeof import('pdfjs-dist');
		const fakeLoader = vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
		const r = createPdfRenderer(fakeLoader);
		await r.init();
		await r.init();
		expect(fakeLoader).toHaveBeenCalledOnce();
	});

	it('passes a non-null wasmUrl directory (ending in /) to getDocument, not a bare src string', async () => {
		const getDocument = vi.fn().mockReturnValue({
			promise: Promise.resolve({ numPages: 1, getPage: vi.fn() })
		});
		const lib = {
			GlobalWorkerOptions: { workerSrc: '' },
			getDocument,
			TextLayer: class {
				render() {
					return Promise.resolve();
				}
				cancel() {}
			}
		} as unknown as typeof import('pdfjs-dist');
		const r = createPdfRenderer(vi.fn().mockResolvedValue([lib, { default: '' }] as const));
		await r.init();
		await r.loadDocument('/api/documents/abc/file');

		expect(getDocument).toHaveBeenCalledTimes(1);
		const arg = getDocument.mock.calls[0][0] as { url?: string; wasmUrl?: string };
		expect(arg.url).toBe('/api/documents/abc/file');
		expect(typeof arg.wasmUrl).toBe('string');
		expect(arg.wasmUrl).not.toBe('');
		expect(arg.wasmUrl?.endsWith('/')).toBe(true);
	});

	it('renderCurrentPage sets a localized error when the render rejects (not silently blank)', async () => {
		const lib = makeRenderingLib(Promise.reject(new Error('JBig2 failed to initialize')));
		const r = createPdfRenderer(vi.fn().mockResolvedValue([lib, { default: '' }] as const));
		await r.init();
		r.setElements(document.createElement('canvas'), document.createElement('div'));
		await r.loadDocument('/x');
		await r.renderCurrentPage();

		expect(r.error).toBe(m.doc_render_failed());
	});

	it('renderCurrentPage does NOT set an error when the render is cancelled', async () => {
		const cancelled = Object.assign(new Error('cancelled'), {
			name: 'RenderingCancelledException'
		});
		const lib = makeRenderingLib(Promise.reject(cancelled));
		const r = createPdfRenderer(vi.fn().mockResolvedValue([lib, { default: '' }] as const));
		await r.init();
		r.setElements(document.createElement('canvas'), document.createElement('div'));
		await r.loadDocument('/x');
		await r.renderCurrentPage();

		expect(r.error).toBeNull();
	});

	it('loadDocument sets a localized error (not the raw pdf.js message) when getDocument rejects', async () => {
		const failingLib = {
			GlobalWorkerOptions: { workerSrc: '' },
			getDocument: vi.fn().mockReturnValue({
				promise: Promise.reject(new Error('PDF not found'))
			}),
			TextLayer: class {
				render() {
					return Promise.resolve();
				}
				cancel() {}
			}
		} as unknown as typeof import('pdfjs-dist');
		const r = createPdfRenderer(vi.fn().mockResolvedValue([failingLib, { default: '' }] as const));
		await r.init();
		await r.loadDocument('/bad/path');
		expect(r.loading).toBe(false);
		expect(r.error).toBe(m.doc_render_failed());
		expect(r.error).not.toContain('PDF not found');
	});
});
