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

const PERSON_ID = '11111111-0000-0000-0000-000000000001';

const block: TranscriptionBlockData = {
	id: 'b1',
	annotationId: 'a1',
	documentId: 'd1',
	text: '@Auguste',
	label: null,
	sortOrder: 0,
	version: 1,
	source: 'MANUAL',
	reviewed: false,
	mentionedPersons: [{ personId: PERSON_ID, displayName: 'Auguste' }]
};

function mockPersonFetch() {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockImplementation((url: string) => {
			if (url.includes('/relationships')) {
				return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
			}
			return Promise.resolve({
				ok: true,
				status: 200,
				json: () =>
					Promise.resolve({
						id: PERSON_ID,
						firstName: 'Auguste',
						lastName: 'Raddatz',
						displayName: 'Auguste Raddatz'
					})
			});
		})
	);
}

function getMentionLink(): HTMLAnchorElement {
	return document.querySelector(
		`a.person-mention[data-person-id="${PERSON_ID}"]`
	) as HTMLAnchorElement;
}

function getHoverCard(): HTMLElement | null {
	return document.querySelector('[data-testid="person-hover-card"]');
}

/** Hover a mention and wait until the loaded card content is in the DOM. */
async function showCard(): Promise<void> {
	getMentionLink().dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
	await vi.waitFor(() => {
		expect(document.querySelector('[data-testid="person-hover-card-content"]')).not.toBeNull();
	});
}

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

// ─── Mouse timer behavior ──────────────────────────────────────────────────────

describe('TranscriptionReadView — hover card mouse timer', () => {
	it('keeps the card open when mouse moves from mention to card within 150ms', async () => {
		mockPersonFetch();
		render(TranscriptionReadView, { blocks: [block], onParagraphClick: () => {} });

		await showCard();

		// Leave mention — starts 150ms close timer
		getMentionLink().dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));

		// Enter card before 150ms — cancels timer
		getHoverCard()!.dispatchEvent(new MouseEvent('mouseenter'));

		// Wait past the original 150ms window
		await new Promise((r) => setTimeout(r, 200));

		expect(getHoverCard()).not.toBeNull();
	});

	it('closes the card immediately when mouse leaves the card (no timer)', async () => {
		mockPersonFetch();
		render(TranscriptionReadView, { blocks: [block], onParagraphClick: () => {} });

		await showCard();

		// Leave card — activeCard = null immediately, no timer
		getHoverCard()!.dispatchEvent(new MouseEvent('mouseleave'));

		await vi.waitFor(() => {
			expect(getHoverCard()).toBeNull();
		});
	});

	it('cancels a pending close when mouse re-enters a mention', async () => {
		mockPersonFetch();
		render(TranscriptionReadView, { blocks: [block], onParagraphClick: () => {} });

		await showCard();

		// Leave mention — starts 150ms close timer
		getMentionLink().dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));

		// Re-enter same mention before 150ms — cancels timer
		getMentionLink().dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));

		// Wait past the original 150ms window
		await new Promise((r) => setTimeout(r, 200));

		expect(getHoverCard()).not.toBeNull();
	});
});

// ─── Keyboard focus behavior ───────────────────────────────────────────────────

describe('TranscriptionReadView — hover card keyboard focus', () => {
	it('keeps the card open when keyboard focus moves from mention into card', async () => {
		mockPersonFetch();
		render(TranscriptionReadView, { blocks: [block], onParagraphClick: () => {} });

		// Show card via keyboard focusin on mention
		getMentionLink().dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
		await vi.waitFor(() => {
			expect(document.querySelector('[data-testid="person-hover-card-content"]')).not.toBeNull();
		});

		// Focus leaves mention — starts 150ms close timer
		getMentionLink().dispatchEvent(new FocusEvent('focusout', { bubbles: true }));

		// Focus enters card — should cancel the close timer
		getHoverCard()!.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));

		// Wait past the 150ms window
		await new Promise((r) => setTimeout(r, 200));

		expect(getHoverCard()).not.toBeNull();
	});

	it('closes the card when keyboard focus leaves the card entirely', async () => {
		mockPersonFetch();
		render(TranscriptionReadView, { blocks: [block], onParagraphClick: () => {} });

		// Show card via keyboard focusin
		getMentionLink().dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
		await vi.waitFor(() => {
			expect(document.querySelector('[data-testid="person-hover-card-content"]')).not.toBeNull();
		});

		// Focus leaves mention — 150ms timer starts
		getMentionLink().dispatchEvent(new FocusEvent('focusout', { bubbles: true }));

		// Focus enters card — cancels timer
		getHoverCard()!.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));

		// Focus leaves card entirely (relatedTarget = null means focus left the page)
		getHoverCard()!.dispatchEvent(
			new FocusEvent('focusout', { bubbles: true, relatedTarget: null })
		);

		await vi.waitFor(() => {
			expect(getHoverCard()).toBeNull();
		});
	});
});
