import { describe, it, expect, vi, afterEach } from 'vitest';
import { createTranscriptionBlocks } from './useTranscriptionBlocks.svelte';
import type { TranscriptionBlockData } from '$lib/shared/types';

afterEach(() => {
	vi.restoreAllMocks();
});

const baseBlock = (overrides: Partial<TranscriptionBlockData> = {}): TranscriptionBlockData =>
	({
		id: 'b-1',
		annotationId: 'ann-1',
		text: 'Hello',
		sortOrder: 1,
		reviewed: false,
		mentionedPersons: [],
		updatedAt: '2026-01-01T00:00:00Z',
		...overrides
	}) as TranscriptionBlockData;

function makeFetch(handlers: Record<string, () => Response | Promise<Response>>) {
	return vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
		const u = url.toString();
		const method = init?.method ?? 'GET';
		for (const [match, fn] of Object.entries(handlers)) {
			if (u.includes(match) && (match.includes(':') || true)) {
				return fn();
			}
		}
		const key = `${method} ${u}`;
		for (const [match, fn] of Object.entries(handlers)) {
			if (key.includes(match)) return fn();
		}
		return new Response('not found', { status: 404 });
	});
}

describe('createTranscriptionBlocks — initial state', () => {
	it('starts with no blocks, no derived metadata', () => {
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1' });
		expect(ctrl.blocks).toEqual([]);
		expect(ctrl.hasBlocks).toBe(false);
		expect(ctrl.blockNumbers).toEqual({});
		expect(ctrl.lastEditedAt).toBeNull();
		expect(ctrl.annotationReloadKey).toBe(0);
	});
});

describe('createTranscriptionBlocks.load', () => {
	it('fetches and stores blocks on success', async () => {
		const fetchImpl = makeFetch({
			'/api/documents/doc-1/transcription-blocks': () =>
				new Response(
					JSON.stringify([baseBlock({ id: 'b1' }), baseBlock({ id: 'b2', sortOrder: 2 })]),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				)
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();

		expect(ctrl.blocks).toHaveLength(2);
		expect(ctrl.hasBlocks).toBe(true);
	});

	it('is a no-op when documentId is empty', async () => {
		const fetchImpl = vi.fn();
		const ctrl = createTranscriptionBlocks({ documentId: () => '', fetchImpl });
		await ctrl.load();
		expect(fetchImpl).not.toHaveBeenCalled();
	});

	it('keeps blocks empty on non-OK response', async () => {
		const fetchImpl = makeFetch({
			'transcription-blocks': () => new Response('boom', { status: 500 })
		});
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		expect(ctrl.blocks).toEqual([]);
	});

	it('swallows network errors during load', async () => {
		const fetchImpl = vi.fn(async () => {
			throw new Error('network');
		});
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await expect(ctrl.load()).resolves.toBeUndefined();
		expect(ctrl.blocks).toEqual([]);
	});
});

describe('createTranscriptionBlocks — derived state', () => {
	it('computes blockNumbers in sortOrder', async () => {
		const fetchImpl = makeFetch({
			'transcription-blocks': () =>
				new Response(
					JSON.stringify([
						baseBlock({ id: 'b3', annotationId: 'a3', sortOrder: 3 }),
						baseBlock({ id: 'b1', annotationId: 'a1', sortOrder: 1 }),
						baseBlock({ id: 'b2', annotationId: 'a2', sortOrder: 2 })
					]),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				)
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();

		expect(ctrl.blockNumbers).toEqual({ a1: 1, a2: 2, a3: 3 });
	});

	it('lastEditedAt picks the most recent updatedAt', async () => {
		const fetchImpl = makeFetch({
			'transcription-blocks': () =>
				new Response(
					JSON.stringify([
						baseBlock({ id: 'b1', updatedAt: '2026-04-15T10:00:00Z' }),
						baseBlock({ id: 'b2', updatedAt: '2026-04-20T10:00:00Z' }),
						baseBlock({ id: 'b3', updatedAt: '2026-04-10T10:00:00Z' })
					]),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				)
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();

		expect(ctrl.lastEditedAt).toBe(new Date('2026-04-20T10:00:00Z').toISOString());
	});

	it('lastEditedAt is null when no block has updatedAt', async () => {
		const fetchImpl = makeFetch({
			'transcription-blocks': () =>
				new Response(JSON.stringify([baseBlock({ id: 'b1', updatedAt: undefined })]), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				})
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();

		expect(ctrl.lastEditedAt).toBeNull();
	});
});

describe('createTranscriptionBlocks.delete', () => {
	it('removes the block locally and bumps annotationReloadKey on success', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/transcription-blocks/b-1') && method === 'DELETE') {
				return new Response(null, { status: 204 });
			}
			if (u.endsWith('/transcription-blocks')) {
				return new Response(JSON.stringify([baseBlock({ id: 'b-1' }), baseBlock({ id: 'b-2' })]), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('', { status: 404 });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		expect(ctrl.blocks).toHaveLength(2);
		const keyBefore = ctrl.annotationReloadKey;

		await ctrl.delete('b-1');

		expect(ctrl.blocks).toHaveLength(1);
		expect(ctrl.blocks[0].id).toBe('b-2');
		expect(ctrl.annotationReloadKey).toBe(keyBefore + 1);
	});

	it('throws on non-OK delete response', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const method = init?.method ?? 'GET';
			if (method === 'DELETE') return new Response('boom', { status: 500 });
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await expect(ctrl.delete('b-1')).rejects.toThrow();
	});
});

describe('createTranscriptionBlocks.reviewToggle', () => {
	it('updates the block after a successful PUT', async () => {
		let updated = false;
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/review') && method === 'PUT') {
				updated = true;
				return new Response(JSON.stringify(baseBlock({ id: 'b-1', reviewed: true })), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response(JSON.stringify([baseBlock({ id: 'b-1', reviewed: false })]), {
				status: 200,
				headers: { 'Content-Type': 'application/json' }
			});
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await ctrl.reviewToggle('b-1');

		expect(updated).toBe(true);
		expect(ctrl.blocks[0].reviewed).toBe(true);
	});

	it('is a no-op when PUT returns non-OK', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const method = init?.method ?? 'GET';
			if (method === 'PUT') return new Response('', { status: 500 });
			return new Response(JSON.stringify([baseBlock({ reviewed: false })]), {
				status: 200,
				headers: { 'Content-Type': 'application/json' }
			});
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await ctrl.reviewToggle('b-1');
		expect(ctrl.blocks[0].reviewed).toBe(false);
	});
});

describe('createTranscriptionBlocks.markAllReviewed', () => {
	it('updates each matching block', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/review-all') && method === 'PUT') {
				return new Response(
					JSON.stringify([
						{ id: 'b-1', reviewed: true },
						{ id: 'b-2', reviewed: true }
					]),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				);
			}
			return new Response(
				JSON.stringify([
					baseBlock({ id: 'b-1', reviewed: false }),
					baseBlock({ id: 'b-2', reviewed: false })
				]),
				{ status: 200, headers: { 'Content-Type': 'application/json' } }
			);
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await ctrl.markAllReviewed();

		expect(ctrl.blocks.every((b) => b.reviewed)).toBe(true);
	});

	it('throws and leaves blocks unchanged when PUT returns non-OK', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/review-all') && method === 'PUT') {
				return new Response(JSON.stringify({ code: 'INTERNAL_ERROR' }), {
					status: 500,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response(JSON.stringify([baseBlock({ id: 'b-1', reviewed: false })]), {
				status: 200,
				headers: { 'Content-Type': 'application/json' }
			});
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await expect(ctrl.markAllReviewed()).rejects.toThrow('INTERNAL_ERROR');
		expect(ctrl.blocks[0].reviewed).toBe(false);
	});

	it('throws INTERNAL_ERROR when PUT returns non-JSON body (e.g. nginx 502)', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/review-all') && method === 'PUT') {
				return new Response('Bad Gateway', { status: 502 });
			}
			return new Response(JSON.stringify([baseBlock({ id: 'b-1', reviewed: false })]), {
				status: 200,
				headers: { 'Content-Type': 'application/json' }
			});
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await expect(ctrl.markAllReviewed()).rejects.toThrow('INTERNAL_ERROR');
		expect(ctrl.blocks[0].reviewed).toBe(false);
	});
});

describe('createTranscriptionBlocks.createFromDraw', () => {
	it('appends a created block on 200', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.endsWith('/transcription-blocks') && method === 'POST') {
				return new Response(JSON.stringify(baseBlock({ id: 'b-new', annotationId: 'ann-new' })), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		const created = await ctrl.createFromDraw({
			x: 0.1,
			y: 0.1,
			width: 0.1,
			height: 0.1,
			pageNumber: 1
		});

		expect(created?.id).toBe('b-new');
		expect(ctrl.blocks.find((b) => b.id === 'b-new')).toBeDefined();
	});

	it('returns null and does not append on non-OK response', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const method = init?.method ?? 'GET';
			if (method === 'POST') return new Response('boom', { status: 500 });
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		const created = await ctrl.createFromDraw({
			x: 0,
			y: 0,
			width: 0.1,
			height: 0.1,
			pageNumber: 1
		});

		expect(created).toBeNull();
		expect(ctrl.blocks).toHaveLength(0);
	});

	it('returns null on network error', async () => {
		const fetchImpl = vi.fn(async () => {
			throw new Error('network');
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		const created = await ctrl.createFromDraw({
			x: 0,
			y: 0,
			width: 0.1,
			height: 0.1,
			pageNumber: 1
		});

		expect(created).toBeNull();
	});
});

describe('createTranscriptionBlocks.toggleTrainingLabel', () => {
	it('PATCHes the training-labels endpoint', async () => {
		const fetchImpl = vi.fn(async () => new Response('', { status: 200 }));
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.toggleTrainingLabel('KURRENT_RECOGNITION', true);
		expect(fetchImpl).toHaveBeenCalledWith(
			'/api/documents/doc-1/training-labels',
			expect.objectContaining({ method: 'PATCH' })
		);
	});

	it('throws on non-OK response', async () => {
		const fetchImpl = vi.fn(async () => new Response('boom', { status: 500 }));
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await expect(ctrl.toggleTrainingLabel('X', true)).rejects.toThrow();
	});
});

describe('createTranscriptionBlocks.deleteAnnotation', () => {
	it('deletes the linked block when one exists', async () => {
		let blockDeleted = false;
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/transcription-blocks/b-1') && method === 'DELETE') {
				blockDeleted = true;
				return new Response(null, { status: 204 });
			}
			if (u.endsWith('/transcription-blocks')) {
				return new Response(JSON.stringify([baseBlock({ id: 'b-1', annotationId: 'ann-1' })]), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('', { status: 200 });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await ctrl.deleteAnnotation('ann-1');

		expect(blockDeleted).toBe(true);
		expect(ctrl.blocks).toHaveLength(0);
	});

	it('deletes the bare annotation when no block is linked', async () => {
		let annotationDeleted = false;
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/annotations/ann-orphan') && method === 'DELETE') {
				annotationDeleted = true;
				return new Response(null, { status: 204 });
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		const keyBefore = ctrl.annotationReloadKey;
		await ctrl.deleteAnnotation('ann-orphan');

		expect(annotationDeleted).toBe(true);
		expect(ctrl.annotationReloadKey).toBe(keyBefore + 1);
	});

	it('throws when the bare-annotation DELETE fails', async () => {
		const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
			const u = url.toString();
			const method = init?.method ?? 'GET';
			if (u.includes('/annotations/') && method === 'DELETE') {
				return new Response('boom', { status: 500 });
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();
		await expect(ctrl.deleteAnnotation('ann-orphan')).rejects.toThrow();
	});
});

describe('createTranscriptionBlocks.findByAnnotationId', () => {
	it('returns the block whose annotationId matches', async () => {
		const fetchImpl = makeFetch({
			'transcription-blocks': () =>
				new Response(
					JSON.stringify([
						baseBlock({ id: 'b1', annotationId: 'ann-a' }),
						baseBlock({ id: 'b2', annotationId: 'ann-b' })
					]),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				)
		});

		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1', fetchImpl });
		await ctrl.load();

		expect(ctrl.findByAnnotationId('ann-b')?.id).toBe('b2');
		expect(ctrl.findByAnnotationId('ann-missing')).toBeUndefined();
	});
});

describe('createTranscriptionBlocks.bumpAnnotationReloadKey', () => {
	it('increments annotationReloadKey by 1', () => {
		const ctrl = createTranscriptionBlocks({ documentId: () => 'doc-1' });
		expect(ctrl.annotationReloadKey).toBe(0);
		ctrl.bumpAnnotationReloadKey();
		expect(ctrl.annotationReloadKey).toBe(1);
		ctrl.bumpAnnotationReloadKey();
		expect(ctrl.annotationReloadKey).toBe(2);
	});
});
