import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page as browserPage } from 'vitest/browser';

const mockPage = {
	url: new URL('http://localhost/documents/d1'),
	state: {}
};

vi.mock('$app/state', () => ({
	get page() {
		return mockPage;
	}
}));

vi.mock('$app/navigation', () => ({
	beforeNavigate: () => {},
	afterNavigate: () => {},
	goto: vi.fn(),
	invalidate: vi.fn(),
	invalidateAll: vi.fn(),
	preloadCode: vi.fn(),
	preloadData: vi.fn(),
	pushState: vi.fn(),
	replaceState: vi.fn(),
	disableScrollHandling: vi.fn(),
	onNavigate: () => () => {}
}));

import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';

const { default: DocumentDetailPage } = await import('./+page.svelte');

afterEach(cleanup);

const baseDoc = {
	id: 'd1',
	title: 'Brief an Helene',
	originalFilename: 'brief.pdf',
	documentDate: '1923-04-15',
	sender: null,
	receivers: [],
	tags: [],
	filePath: null,
	contentType: null,
	location: null,
	status: 'UPLOADED',
	fileHash: null
};

const baseData = (overrides: Record<string, unknown> = {}) => ({
	document: baseDoc,
	canWrite: false,
	canBlogWrite: false,
	user: null,
	geschichten: [],
	inferredRelationship: null,
	...overrides
});

describe('documents/[id] page', () => {
	it('renders the DocumentTopBar and resolves the document title in svelte:head', async () => {
		mockPage.url = new URL('http://localhost/documents/d1');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData() }
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
		await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
	});

	it('mounts the page region with the [data-hydrated] container', async () => {
		mockPage.url = new URL('http://localhost/documents/d1');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData() }
		});

		expect(document.querySelector('[data-hydrated]')).not.toBeNull();
	});

	it('persists last-visited document ID to localStorage on mount', async () => {
		localStorage.removeItem('familienarchiv.lastVisited');
		mockPage.url = new URL('http://localhost/documents/d1');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData() }
		});

		await vi.waitFor(() => {
			const stored = localStorage.getItem('familienarchiv.lastVisited');
			expect(stored).toContain('d1');
		});
	});

	it('uses doc.title as the document title when set', async () => {
		mockPage.url = new URL('http://localhost/documents/d1');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData() }
		});

		await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
	});

	it('falls back to originalFilename when title is empty', async () => {
		mockPage.url = new URL('http://localhost/documents/d2');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: { ...baseDoc, id: 'd2', title: '', originalFilename: 'fallback.pdf' }
				})
			}
		});

		await vi.waitFor(() => expect(document.title).toContain('fallback.pdf'));
	});

	it('falls back to "Dokument" when title and originalFilename are empty', async () => {
		mockPage.url = new URL('http://localhost/documents/d3');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: { ...baseDoc, id: 'd3', title: '', originalFilename: '' }
				})
			}
		});

		await vi.waitFor(() => expect(document.title).toContain('Dokument'));
	});

	it('renders the topbar Edit-link affordance when canWrite is true', async () => {
		mockPage.url = new URL('http://localhost/documents/d4');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData({ canWrite: true }) }
		});

		await expect.element(browserPage.getByRole('link', { name: 'Bearbeiten' })).toBeVisible();
	});

	it('renders the topbar when geschichten and inferredRelationship are passed through', async () => {
		mockPage.url = new URL('http://localhost/documents/d5');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }],
					inferredRelationship: { label: 'PARENT_OF', from: 'p1', to: 'p2' },
					canBlogWrite: true
				})
			}
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
		await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
	});

	it('renders the topbar even when doc.id is empty (defensive)', async () => {
		mockPage.url = new URL('http://localhost/documents/d-empty');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData({ document: { ...baseDoc, id: '', title: 'No ID' } }) }
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
		await vi.waitFor(() => expect(document.title).toContain('No ID'));
	});

	it('renders sender data in the metadata drawer when sender is populated', async () => {
		mockPage.url = new URL('http://localhost/documents/d7');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: {
						...baseDoc,
						id: 'd7',
						sender: { id: 's1', displayName: 'Anna Schmidt' },
						receivers: [{ id: 'r1', displayName: 'Bert Meier' }]
					}
				})
			}
		});

		// The topbar chip row is hidden below md; the drawer renders the full displayName at any viewport.
		await browserPage.getByRole('button', { name: 'Details' }).click();
		await expect.element(browserPage.getByText('Anna Schmidt')).toBeVisible();
	});

	it('renders the topbar when filePath is set on the document', async () => {
		mockPage.url = new URL('http://localhost/documents/d8');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: {
						...baseDoc,
						id: 'd8',
						filePath: 's3://bucket/file.pdf',
						contentType: 'application/pdf'
					}
				})
			}
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
		await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
	});

	it('renders the topbar with a complete user object passed through', async () => {
		mockPage.url = new URL('http://localhost/documents/d9');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: { ...baseDoc, id: 'd9' },
					user: { id: 'u1', firstName: 'Anna', lastName: 'S', email: 'a@x' }
				})
			}
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
	});

	it('Escape keydown leaves the transcribe panel hidden when not already in transcribe mode', async () => {
		mockPage.url = new URL('http://localhost/documents/d10');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData({ document: { ...baseDoc, id: 'd10' } }) }
		});

		document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));

		expect(document.querySelector('[data-testid="panel-close"]')).toBeNull();
	});

	it('non-Escape keydown does not affect the transcribe panel state', async () => {
		mockPage.url = new URL('http://localhost/documents/d11');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData({ document: { ...baseDoc, id: 'd11' } }) }
		});

		document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
		expect(document.querySelector('[data-testid="panel-close"]')).toBeNull();
	});

	it('renders the topbar with a deep-link comment query param', async () => {
		mockPage.url = new URL('http://localhost/documents/d12?comment=c-abc');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: { data: baseData({ document: { ...baseDoc, id: 'd12' } }) }
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
	});

	it('renders sender name and Edit affordance with all metadata populated', async () => {
		mockPage.url = new URL('http://localhost/documents/d-meta');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: {
						...baseDoc,
						id: 'd-meta',
						sender: { id: 's1', displayName: 'Anna' },
						receivers: [
							{ id: 'r1', displayName: 'Bert' },
							{ id: 'r2', displayName: 'Carl' }
						],
						tags: [
							{ id: 't1', name: 'Familie' },
							{ id: 't2', name: 'Reise' }
						],
						location: 'Berlin',
						scriptType: 'KURRENT',
						trainingLabels: ['KURRENT_RECOGNITION']
					},
					user: { id: 'u1', firstName: 'Anna' },
					geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }],
					inferredRelationship: { labelFromA: 'Vater', labelFromB: 'Tochter' },
					canBlogWrite: true,
					canWrite: true
				})
			}
		});

		// Edit affordance is always rendered when canWrite=true; sender chip is below md, so
		// open the drawer and assert sender displayName surfaces there.
		await expect.element(browserPage.getByRole('link', { name: 'Bearbeiten' })).toBeVisible();
		await browserPage.getByRole('button', { name: 'Details' }).click();
		await expect.element(browserPage.getByText('Anna')).toBeVisible();
	});

	it('enters transcribe mode and shows the panel close button when ?task=transcribe is set', async () => {
		mockPage.url = new URL('http://localhost/documents/d-task?task=transcribe');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: { ...baseDoc, id: 'd-task' },
					canWrite: true
				})
			}
		});

		await vi.waitFor(() => {
			expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
		});
	});

	it('keeps the transcribe panel mounted when the transcription-block fetch rejects', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network'));
		try {
			mockPage.url = new URL('http://localhost/documents/d-fail?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: {
					data: baseData({ document: { ...baseDoc, id: 'd-fail' } })
				}
			});
			await vi.waitFor(() => {
				expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
			});
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('renders fetched block text in read mode after blocks resolve', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
			new Response(
				JSON.stringify([
					{
						id: 'b1',
						annotationId: 'ann-1',
						text: 'Erster',
						sortOrder: 1,
						reviewed: false,
						mentionedPersons: [],
						label: null
					}
				]),
				{ status: 200, headers: { 'Content-Type': 'application/json' } }
			)
		);
		try {
			mockPage.url = new URL('http://localhost/documents/d-blocks?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ document: { ...baseDoc, id: 'd-blocks' } }) }
			});
			await expect.element(browserPage.getByText('Erster')).toBeVisible();
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('overwrites a previously stored last-visited document with the current one', async () => {
		localStorage.setItem(
			'familienarchiv.lastVisited',
			JSON.stringify({ id: 'old-doc', title: 'Old' })
		);
		mockPage.url = new URL('http://localhost/documents/d-new');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({ document: { ...baseDoc, id: 'd-new', title: 'New Doc' } })
			}
		});
		await vi.waitFor(() => {
			const stored = JSON.parse(localStorage.getItem('familienarchiv.lastVisited') ?? '{}');
			expect(stored.id).toBe('d-new');
		});
	});

	it('does not show the OCR-running spinner when ocr-status fetch returns 500', async () => {
		const fetchSpy = vi
			.spyOn(globalThis, 'fetch')
			.mockResolvedValue(new Response('error', { status: 500 }));
		try {
			mockPage.url = new URL('http://localhost/documents/d-ocr-fail?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ canWrite: true, document: { ...baseDoc, id: 'd-ocr-fail' } }) }
			});
			await vi.waitFor(() => {
				expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
			});
			expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('shows the OCR-running spinner heading when ocr-status returns RUNNING with a jobId', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
			const u = url.toString();
			if (u.includes('ocr-status')) {
				return new Response(JSON.stringify({ status: 'RUNNING', jobId: 'job-1' }), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			if (u.includes('/ocr/jobs/')) {
				return new Response(JSON.stringify({ status: 'RUNNING', progressMessage: 'WORKING' }), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});
		try {
			mockPage.url = new URL('http://localhost/documents/d-ocr-run?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ canWrite: true, document: { ...baseDoc, id: 'd-ocr-run' } }) }
			});
			await expect.element(browserPage.getByText('OCR läuft')).toBeVisible();
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('renders the topbar when the document has all OCR-relevant fields populated', async () => {
		mockPage.url = new URL('http://localhost/documents/d-ocr-meta?task=transcribe');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: {
						...baseDoc,
						id: 'd-ocr-meta',
						scriptType: 'KURRENT',
						trainingLabels: ['KURRENT_RECOGNITION'],
						filePath: 's3://bucket/file.pdf',
						fileHash: 'hash-abc'
					},
					canWrite: true,
					user: { id: 'u1', firstName: 'Anna' }
				})
			}
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
	});

	it('treats undefined geschichten as the empty array (geschichten ?? [] branch)', async () => {
		mockPage.url = new URL('http://localhost/documents/d-no-stories');
		render(DocumentDetailPage, {
			context: new Map([[CONFIRM_KEY, createConfirmService()]]),
			props: {
				data: baseData({
					document: { ...baseDoc, id: 'd-no-stories' },
					geschichten: undefined
				})
			}
		});

		expect(document.querySelector('[data-topbar]')).not.toBeNull();
	});

	it('does not show the OCR-running spinner when ocr-status returns DONE', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
			const u = url.toString();
			if (u.includes('ocr-status')) {
				return new Response(JSON.stringify({ status: 'DONE', jobId: null }), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});
		try {
			mockPage.url = new URL('http://localhost/documents/d-ocr-done?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-done' } }) }
			});
			await vi.waitFor(() => {
				expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
			});
			expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('does not show the OCR-running spinner when ocr-status returns PENDING without jobId', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
			const u = url.toString();
			if (u.includes('ocr-status')) {
				return new Response(JSON.stringify({ status: 'PENDING', jobId: null }), {
					status: 200,
					headers: { 'Content-Type': 'application/json' }
				});
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});
		try {
			mockPage.url = new URL('http://localhost/documents/d-ocr-no-job?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-no-job' } }) }
			});
			await vi.waitFor(() => {
				expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
			});
			expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
		} finally {
			fetchSpy.mockRestore();
		}
	});

	it('does not show the OCR-running spinner when the ocr-status fetch throws', async () => {
		const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
			const u = url.toString();
			if (u.includes('ocr-status')) {
				throw new Error('network down');
			}
			return new Response('[]', { status: 200, headers: { 'Content-Type': 'application/json' } });
		});
		try {
			mockPage.url = new URL('http://localhost/documents/d-ocr-throw?task=transcribe');
			render(DocumentDetailPage, {
				context: new Map([[CONFIRM_KEY, createConfirmService()]]),
				props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-throw' } }) }
			});
			await vi.waitFor(() => {
				expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
			});
			expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
		} finally {
			fetchSpy.mockRestore();
		}
	});
});
