import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import TranscriptionReadView from './TranscriptionReadView.svelte';
import type { TranscriptionBlockData } from '$lib/shared/types';

const blocks: TranscriptionBlockData[] = [
	{
		id: 'b1',
		annotationId: 'ann-1',
		documentId: 'doc-1',
		text: 'First paragraph text.',
		label: null,
		sortOrder: 1,
		version: 1,
		source: 'MANUAL',
		reviewed: false,
		mentionedPersons: []
	},
	{
		id: 'b2',
		annotationId: 'ann-2',
		documentId: 'doc-1',
		text: 'Second paragraph text.',
		label: null,
		sortOrder: 2,
		version: 1,
		source: 'MANUAL',
		reviewed: false,
		mentionedPersons: []
	}
];

describe('TranscriptionReadView', () => {
	it('should render one paragraph per block', async () => {
		render(TranscriptionReadView, {
			blocks,
			onParagraphClick: () => {}
		});

		await expect.element(page.getByText('First paragraph text.')).toBeInTheDocument();
		await expect.element(page.getByText('Second paragraph text.')).toBeInTheDocument();

		const paragraphs = document.querySelectorAll('[data-block-id]');
		expect(paragraphs.length).toBe(2);
	});

	it('should render [unleserlich] as italic muted text', async () => {
		render(TranscriptionReadView, {
			blocks: [
				{
					id: 'b1',
					annotationId: 'ann-1',
					documentId: 'doc-1',
					text: 'Text before [unleserlich] text after',
					label: null,
					sortOrder: 1,
					version: 1,
					source: 'MANUAL',
					reviewed: false,
					mentionedPersons: []
				}
			],
			onParagraphClick: () => {}
		});

		const marker = document.querySelector('[data-marker]');
		expect(marker).not.toBeNull();
		expect(marker!.textContent).toBe('[unleserlich]');
		expect(marker!.tagName.toLowerCase()).toBe('em');
	});

	it('should render [...] as italic muted text', async () => {
		render(TranscriptionReadView, {
			blocks: [
				{
					id: 'b1',
					annotationId: 'ann-1',
					documentId: 'doc-1',
					text: 'Some [...] text',
					label: null,
					sortOrder: 1,
					version: 1,
					source: 'MANUAL',
					reviewed: false,
					mentionedPersons: []
				}
			],
			onParagraphClick: () => {}
		});

		const marker = document.querySelector('[data-marker]');
		expect(marker).not.toBeNull();
		expect(marker!.textContent).toBe('[...]');
	});

	it('should call onParagraphClick with annotationId when paragraph is clicked', async () => {
		const onParagraphClick = vi.fn();
		render(TranscriptionReadView, {
			blocks,
			onParagraphClick
		});

		const paragraph = document.querySelector('[data-block-id="b1"]')!;
		paragraph.dispatchEvent(new MouseEvent('click', { bubbles: true }));
		expect(onParagraphClick).toHaveBeenCalledWith('ann-1');
	});

	it('should render blocks sorted by sortOrder', async () => {
		render(TranscriptionReadView, {
			blocks: [
				{ ...blocks[1], sortOrder: 1 },
				{ ...blocks[0], sortOrder: 2 }
			],
			onParagraphClick: () => {}
		});

		const paragraphs = document.querySelectorAll('[data-block-id]');
		expect(paragraphs[0].getAttribute('data-block-id')).toBe('b2');
		expect(paragraphs[1].getAttribute('data-block-id')).toBe('b1');
	});

	it('should apply flash-highlight class when highlightBlockId matches', async () => {
		render(TranscriptionReadView, {
			blocks: [blocks[0]],
			onParagraphClick: () => {},
			highlightBlockId: 'b1'
		});

		const el = document.querySelector('[data-block-id="b1"]')!;
		expect(el.classList.contains('flash-highlight')).toBe(true);
	});

	it('should not apply flash-highlight class when highlightBlockId does not match', async () => {
		render(TranscriptionReadView, {
			blocks: [blocks[0]],
			onParagraphClick: () => {},
			highlightBlockId: 'other-id'
		});

		const el = document.querySelector('[data-block-id="b1"]')!;
		expect(el.classList.contains('flash-highlight')).toBe(false);
	});

	it('should render empty state when no blocks', async () => {
		render(TranscriptionReadView, {
			blocks: [],
			onParagraphClick: () => {}
		});

		const paragraphs = document.querySelectorAll('[data-block-id]');
		expect(paragraphs.length).toBe(0);
	});
});

describe('TranscriptionReadView — person-mention rendering', () => {
	const PERSON_ID = '550e8400-e29b-41d4-a716-446655440000';

	const mentionBlock: TranscriptionBlockData = {
		id: 'b1',
		annotationId: 'ann-1',
		documentId: 'doc-1',
		text: 'Brief an @Auguste Raddatz vom Mai',
		label: null,
		sortOrder: 1,
		version: 1,
		source: 'MANUAL',
		reviewed: false,
		mentionedPersons: [{ personId: PERSON_ID, displayName: 'Auguste Raddatz' }]
	};

	beforeEach(() => {
		// Default: any /api/persons/{id} call returns 404 unless a test overrides it.
		// Tests that need loaded data stub fetch themselves.
		vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ status: 404, ok: false, json: vi.fn() }));
	});

	afterEach(() => {
		vi.unstubAllGlobals();
		cleanup();
	});

	it('renders a person mention as an anchor link with the person URL', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector(`a.person-mention[data-person-id="${PERSON_ID}"]`)!;
		expect(link).not.toBeNull();
		expect(link.getAttribute('href')).toBe(`/persons/${PERSON_ID}`);
		expect(link.textContent).toBe('Auguste Raddatz');
	});

	it('strips the @ trigger from the rendered link text (read mode)', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const block = document.querySelector('[data-block-id="b1"]')!;
		expect(block.textContent).not.toContain('@Auguste Raddatz');
		expect(block.textContent).toContain('Auguste Raddatz');
	});

	it('renders mention link AND [unleserlich] marker correctly when both occur in the same block (B19b)', async () => {
		const block: TranscriptionBlockData = {
			...mentionBlock,
			text: 'Hallo @Auguste Raddatz [unleserlich] Marie'
		};
		render(TranscriptionReadView, {
			blocks: [block],
			onParagraphClick: () => {}
		});

		// Mention rendered as an anchor
		const link = document.querySelector('a.person-mention')!;
		expect(link).not.toBeNull();
		expect(link.textContent).toBe('Auguste Raddatz');

		// Marker rendered as <em data-marker>
		const marker = document.querySelector('[data-marker]')!;
		expect(marker).not.toBeNull();
		expect(marker.textContent).toBe('[unleserlich]');

		// Marker text is NOT inside the anchor — they are siblings, not nested
		expect(link.contains(marker)).toBe(false);

		// No double-escape — text content reads cleanly
		const blockEl = document.querySelector('[data-block-id="b1"]')!;
		expect(blockEl.textContent).not.toContain('&amp;');
		expect(blockEl.textContent).not.toContain('&lt;');
	});

	it('does not render mention link for plain text without the @ trigger', async () => {
		const plain: TranscriptionBlockData = {
			...mentionBlock,
			text: 'Auguste Raddatz war hier',
			mentionedPersons: [{ personId: PERSON_ID, displayName: 'Auguste Raddatz' }]
		};
		render(TranscriptionReadView, {
			blocks: [plain],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention');
		expect(link).toBeNull();
	});

	it('escapes HTML in the block text — no stored XSS via raw text', async () => {
		const xss: TranscriptionBlockData = {
			...mentionBlock,
			text: '<img src=x onerror=alert(1)>',
			mentionedPersons: []
		};
		render(TranscriptionReadView, {
			blocks: [xss],
			onParagraphClick: () => {}
		});

		// No raw <img> tag in DOM
		expect(document.querySelector('[data-block-id="b1"] img')).toBeNull();
		// The escaped text is visible
		const block = document.querySelector('[data-block-id="b1"]')!;
		expect(block.textContent).toContain('<img src=x onerror=alert(1)>');
	});

	it('triggers fetch for the person on mention mouseenter (B15.5 cache, single call)', async () => {
		const fetchMock = vi.fn().mockResolvedValue({
			status: 404,
			ok: false,
			json: vi.fn()
		});
		vi.stubGlobal('fetch', fetchMock);

		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));

		await vi.waitFor(() => {
			const personFetches = fetchMock.mock.calls.filter((c) =>
				String(c[0]).includes(`/api/persons/${PERSON_ID}`)
			);
			expect(personFetches.length).toBeGreaterThanOrEqual(1);
		});
	});

	it('deduplicates fetches for the same personId across multiple mouseenter events (B15.5)', async () => {
		const fetchMock = vi.fn().mockResolvedValue({
			status: 404,
			ok: false,
			json: vi.fn()
		});
		vi.stubGlobal('fetch', fetchMock);

		// Two blocks both mention the same person
		const block2: TranscriptionBlockData = { ...mentionBlock, id: 'b2', annotationId: 'ann-2' };
		render(TranscriptionReadView, {
			blocks: [mentionBlock, block2],
			onParagraphClick: () => {}
		});

		const links = document.querySelectorAll('a.person-mention');
		links.forEach((link) => link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })));
		// Plus a re-hover on the first
		links[0].dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));

		await vi.waitFor(() => {
			const personFetches = fetchMock.mock.calls.filter(
				(c) => String(c[0]) === `/api/persons/${PERSON_ID}`
			);
			expect(personFetches.length).toBe(1);
		});
	});

	it('mounts the hover card on mouseenter when the fetch loads', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockImplementation((url: string) => {
				if (url.endsWith('/relationships')) {
					return Promise.resolve({ status: 200, ok: true, json: () => Promise.resolve([]) });
				}
				return Promise.resolve({
					status: 200,
					ok: true,
					json: () =>
						Promise.resolve({
							id: PERSON_ID,
							firstName: 'Auguste',
							lastName: 'Raddatz',
							displayName: 'Auguste Raddatz',
							personType: 'PERSON',
							familyMember: true,
							birthDate: '1882-01-01',
							birthDatePrecision: 'YEAR',
							deathDate: '1944-01-01',
							deathDatePrecision: 'YEAR'
						})
				});
			})
		);

		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));

		await vi.waitFor(() => {
			const card = document.querySelector('[data-testid="person-hover-card"]');
			expect(card).not.toBeNull();
		});
	});

	it('unmounts the hover card on mouseleave', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
		link.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));

		await vi.waitFor(() => {
			const card = document.querySelector('[data-testid="person-hover-card"]');
			expect(card).toBeNull();
		});
	});

	it('mounts the hover card on focusin so keyboard users see the preview (WCAG 2.1.1)', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockImplementation((url: string) => {
				if (String(url).endsWith('/relationships')) {
					return Promise.resolve({ status: 200, ok: true, json: () => Promise.resolve([]) });
				}
				return Promise.resolve({
					status: 200,
					ok: true,
					json: () =>
						Promise.resolve({
							id: PERSON_ID,
							firstName: 'Auguste',
							lastName: 'Raddatz',
							displayName: 'Auguste Raddatz',
							personType: 'PERSON',
							familyMember: true
						})
				});
			})
		);

		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));

		await vi.waitFor(() => {
			const card = document.querySelector('[data-testid="person-hover-card"]');
			expect(card).not.toBeNull();
		});
	});

	it('unmounts the hover card on focusout', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
		await vi.waitFor(() => {
			// the card mounts even in 404 → loading → null path; assert it cleans up on blur
		});
		link.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));

		await vi.waitFor(() => {
			const card = document.querySelector('[data-testid="person-hover-card"]');
			expect(card).toBeNull();
		});
	});

	it('lets ctrl-click and meta-click fall through so users can open in a new tab', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')! as HTMLAnchorElement;

		// ctrl-click (Linux/Win "open in new tab")
		const ctrlClick = new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: true });
		const ctrlPrevented = !link.dispatchEvent(ctrlClick);
		expect(ctrlPrevented).toBe(false);

		// meta-click (macOS "open in new tab")
		const metaClick = new MouseEvent('click', { bubbles: true, cancelable: true, metaKey: true });
		const metaPrevented = !link.dispatchEvent(metaClick);
		expect(metaPrevented).toBe(false);
	});

	it('lets middle-click fall through so users can open in a background tab', async () => {
		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')! as HTMLAnchorElement;
		// button === 1 is middle mouse button
		const middleClick = new MouseEvent('click', { bubbles: true, cancelable: true, button: 1 });
		const prevented = !link.dispatchEvent(middleClick);
		expect(prevented).toBe(false);
	});

	it('degrades to plain unlinked text when the person fetch returns 404', async () => {
		vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ status: 404, ok: false, json: vi.fn() }));

		render(TranscriptionReadView, {
			blocks: [mentionBlock],
			onParagraphClick: () => {}
		});

		const link = document.querySelector('a.person-mention')!;
		link.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));

		await vi.waitFor(() => {
			// Anchor is marked as deleted so subsequent hovers/clicks treat it as plain text
			const stillLink = document.querySelector('a.person-mention')!;
			expect(stillLink.getAttribute('data-person-deleted')).toBe('true');
		});

		// 404 → no card mounted
		const card = document.querySelector('[data-testid="person-hover-card"]');
		expect(card).toBeNull();
	});
});
