import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import TranscriptionBlockHost from './TranscriptionBlock.test-fixture.svelte';
import type { ConfirmService } from '$lib/shared/services/confirm.svelte.js';

afterEach(cleanup);

const BASE_PROPS = {
	blockId: 'block-1',
	documentId: 'doc-1',
	blockNumber: 3,
	text: 'Liebe Mutter,',
	label: null,
	active: false,
	saveState: 'idle' as const,
	canComment: true,
	currentUserId: 'user-1',
	onTextChange: vi.fn(),
	onFocus: vi.fn(),
	onDeleteClick: vi.fn(),
	onRetry: vi.fn()
};

// Renders TranscriptionBlock via the host, which provides ConfirmService context.
function renderBlock(overrides: Record<string, unknown> = {}) {
	return render(TranscriptionBlockHost, {
		...BASE_PROPS,
		onServiceReady: () => {},
		...overrides
	});
}

// ─── Rendering ───────────────────────────────────────────────────────────────

describe('TranscriptionBlock — rendering', () => {
	it('renders block number in turquoise badge', async () => {
		renderBlock();
		await expect.element(page.getByText('3')).toBeInTheDocument();
	});

	it('renders text in textarea', async () => {
		renderBlock();
		await expect.element(page.getByText('Liebe Mutter,')).toBeInTheDocument();
	});

	it('renders optional label when provided', async () => {
		renderBlock({ label: 'Anrede' });
		await expect.element(page.getByText('Anrede')).toBeInTheDocument();
	});

	it('does not render label when null', async () => {
		renderBlock({ label: null });
		const label = page.getByText('Anrede');
		await expect.element(label).not.toBeInTheDocument();
	});
});

// ─── Save states ─────────────────────────────────────────────────────────────

describe('TranscriptionBlock — save states', () => {
	it('shows nothing in idle state', async () => {
		renderBlock({ saveState: 'idle' });
		const saving = page.getByText('Speichere...');
		await expect.element(saving).not.toBeInTheDocument();
	});

	it('shows "Speichere..." in saving state', async () => {
		renderBlock({ saveState: 'saving' });
		await expect.element(page.getByText('Speichere...')).toBeInTheDocument();
	});

	it('shows "Gespeichert" in saved state', async () => {
		renderBlock({ saveState: 'saved' });
		await expect.element(page.getByText(/Gespeichert/)).toBeInTheDocument();
	});

	it('shows error with retry button in error state', async () => {
		const onRetry = vi.fn();
		renderBlock({ saveState: 'error', onRetry });
		await expect.element(page.getByText('Nicht gespeichert')).toBeInTheDocument();
		const retryBtn = page.getByText('Erneut versuchen');
		await expect.element(retryBtn).toBeInTheDocument();
	});
});

// ─── Active state ────────────────────────────────────────────────────────────

describe('TranscriptionBlock — active border', () => {
	it('has turquoise left border when active', async () => {
		renderBlock({ active: true });
		await expect.element(page.getByRole('textbox')).toBeInTheDocument();
		const block = document.querySelector('[data-block-id="block-1"]')!;
		expect(block.className).toContain('border-turquoise');
	});

	it('has error left border when save failed', async () => {
		renderBlock({ saveState: 'error' });
		await expect.element(page.getByRole('textbox')).toBeInTheDocument();
		const block = document.querySelector('[data-block-id="block-1"]')!;
		expect(block.className).toContain('border-error');
	});
});

// ─── Interactions ────────────────────────────────────────────────────────────

describe('TranscriptionBlock — interactions', () => {
	it('calls onTextChange when typing in textarea', async () => {
		const onTextChange = vi.fn();
		renderBlock({ onTextChange });
		const textarea = page.getByRole('textbox');
		await textarea.fill('Neue Zeile');
		expect(onTextChange).toHaveBeenCalled();
	});

	it('calls onFocus when textarea is focused', async () => {
		const onFocus = vi.fn();
		renderBlock({ onFocus });
		const textboxEl = (await page.getByRole('textbox').element()) as HTMLElement;
		textboxEl.dispatchEvent(new FocusEvent('focus', { bubbles: false }));
		expect(onFocus).toHaveBeenCalled();
	});

	it('shows Kommentieren button when no comments exist', async () => {
		renderBlock();
		const btn = page.getByText('Kommentieren');
		await expect.element(btn).toBeInTheDocument();
	});
});

// ─── Reorder controls ────────────────────────────────────────────────────────

describe('TranscriptionBlock — reorder controls', () => {
	it('shows a drag handle element', async () => {
		renderBlock();
		const handle = document.querySelector('[data-drag-handle]');
		expect(handle).not.toBeNull();
	});

	it('disables move-up button when isFirst', async () => {
		renderBlock({ isFirst: true });
		const btn = page.getByRole('button', { name: 'Nach oben' });
		await expect.element(btn).toBeDisabled();
	});

	it('disables move-down button when isLast', async () => {
		renderBlock({ isLast: true });
		const btn = page.getByRole('button', { name: 'Nach unten' });
		await expect.element(btn).toBeDisabled();
	});

	it('calls onMoveUp when up arrow clicked', async () => {
		const onMoveUp = vi.fn();
		renderBlock({ onMoveUp, isFirst: false });
		const btnEl = (await page
			.getByRole('button', { name: 'Nach oben' })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		expect(onMoveUp).toHaveBeenCalled();
	});

	it('calls onMoveDown when down arrow clicked', async () => {
		const onMoveDown = vi.fn();
		renderBlock({ onMoveDown, isLast: false });
		const btnEl = (await page
			.getByRole('button', { name: 'Nach unten' })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		expect(onMoveDown).toHaveBeenCalled();
	});
});

// ─── Touch targets (WCAG 2.2 §2.5.8) ──────────────────────────────────────────

// After #722 removed the on-canvas delete button, the panel footer's icon-only
// actions are the only touch-reachable controls for those operations. They must
// meet the 44×44px minimum target size in both dimensions.
//
// We assert the sizing utility classes rather than measuring getBoundingClientRect:
// the component-test browser env (src/test-setup.ts) loads no Tailwind stylesheet,
// so class-based layout has no effect there and a rendered element collapses to its
// icon (16px). The `min-h/min-w-[44px]` classes are the exact mechanism that yields
// the WCAG target size in the real app; the compiled px size is covered by e2e.
describe('TranscriptionBlock — footer action touch targets', () => {
	it('delete button carries the 44px minimum-target-size classes', async () => {
		renderBlock();
		const btn = (await page.getByRole('button', { name: 'Löschen' }).element()) as HTMLElement;
		expect(btn.className).toContain('min-h-[44px]');
		expect(btn.className).toContain('min-w-[44px]');
	});

	it('review toggle button carries the 44px minimum-target-size classes', async () => {
		renderBlock({ reviewed: false });
		const btn = (await page
			.getByRole('button', { name: 'Als geprüft markieren' })
			.element()) as HTMLElement;
		expect(btn.className).toContain('min-h-[44px]');
		expect(btn.className).toContain('min-w-[44px]');
	});
});

// ─── Delete confirmation ──────────────────────────────────────────────────────

function renderBlockWithService(overrides: Record<string, unknown> = {}) {
	let service!: ConfirmService;
	render(TranscriptionBlockHost, {
		blockId: 'block-1',
		documentId: 'doc-1',
		blockNumber: 3,
		text: 'Liebe Mutter,',
		label: null,
		active: false,
		saveState: 'idle' as const,
		canComment: true,
		currentUserId: 'user-1',
		onTextChange: vi.fn(),
		onFocus: vi.fn(),
		onDeleteClick: vi.fn(),
		onRetry: vi.fn(),
		onServiceReady: (s: ConfirmService) => {
			service = s;
		},
		...overrides
	});
	return { service };
}

describe('TranscriptionBlock — delete confirmation', () => {
	it('does not call onDeleteClick when user cancels via confirm service', async () => {
		const onDeleteClick = vi.fn();
		const { service } = renderBlockWithService({ onDeleteClick });

		// Use native DOM click so the async handler starts but yields at the await,
		// letting the test observe service.options and settle the promise.
		const deleteBtn = document.querySelector('button[aria-label="Löschen"]') as HTMLButtonElement;
		deleteBtn.click();
		await vi.waitFor(() => expect(service.options).not.toBeNull());
		service.settle(false);
		await vi.waitFor(() => expect(service.options).toBeNull());

		expect(onDeleteClick).not.toHaveBeenCalled();
	});

	it('calls onDeleteClick when user confirms via confirm service', async () => {
		const onDeleteClick = vi.fn();
		const { service } = renderBlockWithService({ onDeleteClick });

		const deleteBtn = document.querySelector('button[aria-label="Löschen"]') as HTMLButtonElement;
		deleteBtn.click();
		await vi.waitFor(() => expect(service.options).not.toBeNull());
		service.settle(true);
		await vi.waitFor(() => expect(service.options).toBeNull());

		expect(onDeleteClick).toHaveBeenCalledOnce();
	});
});

// ─── Quote selection ─────────────────────────────────────────────────────────

describe('TranscriptionBlock — quote selection', () => {
	it('shows quote hint after text is selected in the editor', async () => {
		renderBlock({ text: 'Breslau, den 12. August' });
		// Native .focus() activates ProseMirror's DOMObserver so it listens for selectionchange.
		const editorEl = (await page.getByRole('textbox').element()) as HTMLElement;
		editorEl.focus();
		// Let ProseMirror's focus handler complete before we overwrite the selection.
		await new Promise((r) => setTimeout(r, 0));
		const range = document.createRange();
		range.selectNodeContents(editorEl);
		const sel = window.getSelection()!;
		sel.removeAllRanges();
		sel.addRange(range);
		document.dispatchEvent(new Event('selectionchange'));
		await expect.element(page.getByText(/Zitat/)).toBeInTheDocument();
	});
});

// ─── Fading state ────────────────────────────────────────────────────────────

describe('TranscriptionBlock — fading save state', () => {
	it('shows Gespeichert text in fading state (opacity-0 fade-out)', async () => {
		renderBlock({ saveState: 'fading' });
		const indicator = page.getByText(/Gespeichert/);
		await expect.element(indicator).toBeInTheDocument();
		// The fading class sets opacity-0
		const el = document.querySelector('.opacity-0');
		expect(el).not.toBeNull();
	});
});
