import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import TranscriptionEditView from './TranscriptionEditView.svelte';
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
import { m } from '$lib/paraglide/messages.js';

afterEach(cleanup);

const block1 = {
	id: 'b1',
	annotationId: 'a1',
	documentId: 'doc-1',
	text: 'Block eins',
	label: null,
	sortOrder: 0,
	version: 0,
	source: 'MANUAL' as const,
	reviewed: false,
	mentionedPersons: []
};
const block2 = {
	id: 'b2',
	annotationId: 'a2',
	documentId: 'doc-1',
	text: 'Block zwei',
	label: null,
	sortOrder: 1,
	version: 0,
	source: 'OCR' as const,
	reviewed: true,
	mentionedPersons: []
};

function renderView(overrides: Record<string, unknown> = {}, service = createConfirmService()) {
	return {
		...render(TranscriptionEditView, {
			props: {
				documentId: 'doc-1',
				blocks: [block1, block2],
				canComment: true,
				currentUserId: 'user-1',
				onBlockFocus: vi.fn(),
				onSaveBlock: vi.fn(),
				onDeleteBlock: vi.fn(),
				onReviewToggle: vi.fn(),
				...overrides
			},
			context: new Map([[CONFIRM_KEY, service]])
		}),
		service
	};
}

const unreviewedBlock1 = { ...block1, reviewed: false };
const unreviewedBlock2 = { ...block2, reviewed: false };
const reviewedBlock1 = { ...block1, reviewed: true };
const reviewedBlock2 = { ...block2, reviewed: true };

describe('TranscriptionEditView — rendering', () => {
	it('renders blocks in sort order', async () => {
		renderView();
		const textareas = page.getByRole('textbox').all();
		expect(textareas.length).toBeGreaterThanOrEqual(2);
	});

	it('shows next-block CTA after block list', async () => {
		renderView();
		await expect.element(page.getByText(/Block 3/)).toBeInTheDocument();
	});

	it('shows coach card when no blocks', async () => {
		renderView({ blocks: [] });
		await expect
			.element(page.getByRole('heading', { level: 2 }))
			.toHaveTextContent('Erste Transkription?');
	});

	it('hides training footer when no blocks', async () => {
		renderView({ blocks: [], canWrite: true });
		await expect.element(page.getByText('Für Training vormerken')).not.toBeInTheDocument();
	});

	it('shows training footer when blocks exist', async () => {
		renderView({ blocks: [block1], canWrite: true });
		await expect.element(page.getByText('Für Training vormerken')).toBeInTheDocument();
	});
});

describe('TranscriptionEditView — annotation sync', () => {
	it('activates block matching activeAnnotationId', async () => {
		renderView({ activeAnnotationId: 'a2' });
		// Block 2 (annotation a2) should have turquoise border
		const block = document.querySelector('[data-block-id="b2"]')!;
		expect(block.className).toContain('border-turquoise');
	});

	it('does not activate any block when activeAnnotationId is null', async () => {
		renderView({ activeAnnotationId: null });
		const block1 = document.querySelector('[data-block-id="b1"]')!;
		const block2 = document.querySelector('[data-block-id="b2"]')!;
		expect(block1.className).not.toContain('border-turquoise');
		expect(block2.className).not.toContain('border-turquoise');
	});
});

describe('TranscriptionEditView — reorder', () => {
	it('renders move-up button disabled on first block', async () => {
		renderView();
		const upButtons = page.getByRole('button', { name: 'Nach oben' }).all();
		// First block's up button should be disabled
		await expect.element(upButtons[0]).toBeDisabled();
	});

	it('renders move-down button disabled on last block', async () => {
		renderView();
		const downButtons = page.getByRole('button', { name: 'Nach unten' }).all();
		// Last block's down button should be disabled
		await expect.element(downButtons[downButtons.length - 1]).toBeDisabled();
	});

	it('has a drag handle on each block', async () => {
		renderView();
		const handles = document.querySelectorAll('[data-drag-handle]');
		expect(handles.length).toBe(2);
	});
});

// ─── Auto-save debounce ───────────────────────────────────────────────────────

describe('TranscriptionEditView — auto-save debounce', () => {
	it('calls onSaveBlock after 1500ms debounce when text changes', async () => {
		vi.useFakeTimers();
		const onSaveBlock = vi.fn().mockResolvedValue(undefined);
		renderView({ onSaveBlock });

		const textarea = page.getByRole('textbox').first();
		await textarea.fill('Neue Zeile');

		// Not called immediately
		expect(onSaveBlock).not.toHaveBeenCalled();

		// Advance past debounce
		vi.advanceTimersByTime(1500);
		await vi.runAllTimersAsync();

		expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Neue Zeile', []);
		vi.useRealTimers();
	});

	it('passes the block mentionedPersons array as the 3rd save argument', async () => {
		const onSaveBlock = vi.fn().mockResolvedValue(undefined);
		const blockWithMention = {
			...block1,
			// text must contain the @displayName token so deserialize() creates a mention node;
			// fill() replaces the whole content with plain text and would destroy the node
			text: '@Auguste Raddatz',
			mentionedPersons: [{ personId: 'p-aug', displayName: 'Auguste Raddatz' }]
		};
		renderView({ blocks: [blockWithMention], onSaveBlock });

		// type() focuses the element (cursor at position 0) then inserts without replacing the
		// existing mention node.  Fake timers interfere with keyboard CDP so use real timers
		// + vi.waitFor to catch the 1500 ms debounce.
		await userEvent.type(page.getByRole('textbox').first(), 'Hallo ');

		await vi.waitFor(
			() =>
				expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Hallo @Auguste Raddatz', [
					{ personId: 'p-aug', displayName: 'Auguste Raddatz' }
				]),
			{ timeout: 3000 }
		);
	});

	it('resets debounce timer on rapid successive changes', async () => {
		vi.useFakeTimers();
		const onSaveBlock = vi.fn().mockResolvedValue(undefined);
		renderView({ onSaveBlock });

		const textarea = page.getByRole('textbox').first();
		await textarea.fill('First');
		vi.advanceTimersByTime(500);

		await textarea.fill('Second');
		vi.advanceTimersByTime(500);

		// 1000ms elapsed since first change — should not have saved yet
		expect(onSaveBlock).not.toHaveBeenCalled();

		vi.advanceTimersByTime(1000);
		await vi.runAllTimersAsync();

		// Only one save with the final value
		expect(onSaveBlock).toHaveBeenCalledTimes(1);
		expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Second', []);
		vi.useRealTimers();
	});
});

// ─── Save state transitions ───────────────────────────────────────────────────

describe('TranscriptionEditView — save state indicators', () => {
	it('shows saving indicator while onSaveBlock is in-flight', async () => {
		vi.useFakeTimers();
		let resolveSave!: () => void;
		const onSaveBlock = vi.fn().mockReturnValue(new Promise<void>((r) => (resolveSave = r)));
		renderView({ onSaveBlock });

		await page.getByRole('textbox').first().fill('Hello');
		vi.advanceTimersByTime(1500);
		await vi.runAllTimersAsync();

		await expect.element(page.getByText('Speichere...')).toBeInTheDocument();

		resolveSave();
		vi.useRealTimers();
	});

	it('shows error state when onSaveBlock rejects', async () => {
		vi.useFakeTimers();
		const onSaveBlock = vi.fn().mockRejectedValue(new Error('network'));
		renderView({ onSaveBlock });

		await page.getByRole('textbox').first().fill('Fails');
		vi.advanceTimersByTime(1500);
		await vi.runAllTimersAsync();

		await expect.element(page.getByText('Nicht gespeichert')).toBeInTheDocument();
		await expect.element(page.getByText('Erneut versuchen')).toBeInTheDocument();
		vi.useRealTimers();
	});
});

// ─── Flush on blur ────────────────────────────────────────────────────────────

describe('TranscriptionEditView — flush on blur', () => {
	it('flushes pending save immediately on textarea blur before debounce expires', async () => {
		vi.useFakeTimers();
		const onSaveBlock = vi.fn().mockResolvedValue(undefined);
		renderView({ onSaveBlock });

		const textarea = page.getByRole('textbox').first();
		await textarea.fill('Blur text');

		// Blur before 1500ms debounce fires — locator.blur() not available, use native DOM.
		// PersonMentionEditor uses a contenteditable div (role=textbox), not a <textarea>.
		const el = document.querySelector('[role="textbox"]') as HTMLElement;
		el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));

		await vi.runAllTimersAsync();
		expect(onSaveBlock).toHaveBeenCalledWith('b1', 'Blur text', []);
		vi.useRealTimers();
	});
});

// ─── onDeleteBlock callback ───────────────────────────────────────────────────

describe('TranscriptionEditView — delete block', () => {
	it('calls onDeleteBlock with correct blockId when delete is confirmed', async () => {
		const onDeleteBlock = vi.fn().mockResolvedValue(undefined);
		const { service } = renderView({ onDeleteBlock });

		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(onDeleteBlock).toHaveBeenCalledWith('b1');
	});

	it('does not call onDeleteBlock when deletion is cancelled', async () => {
		const onDeleteBlock = vi.fn();
		const { service } = renderView({ onDeleteBlock });

		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(onDeleteBlock).not.toHaveBeenCalled();
	});
});

// ─── Review progress counter ──────────────────────────────────────────────────

describe('TranscriptionEditView — review progress counter', () => {
	it('shows reviewed count and total when blocks exist', async () => {
		// block1: reviewed=false, block2: reviewed=true → "1 / 2 geprüft"
		renderView();
		await expect.element(page.getByText(/1 \/ 2 geprüft/)).toBeInTheDocument();
	});

	it('shows 0 reviewed when no blocks are reviewed', async () => {
		renderView({ blocks: [block1] }); // block1.reviewed = false
		await expect.element(page.getByText(/0 \/ 1 geprüft/)).toBeInTheDocument();
	});

	it('does not show progress counter when there are no blocks', async () => {
		renderView({ blocks: [] });
		await expect.element(page.getByText(/geprüft/)).not.toBeInTheDocument();
	});
});

// ─── Bulk mark all as reviewed ────────────────────────────────────────────────

describe('TranscriptionEditView — mark all reviewed', () => {
	it('shows "Alle als fertig markieren" button when onMarkAllReviewed is provided and blocks are unreviewed', async () => {
		renderView({
			blocks: [unreviewedBlock1, unreviewedBlock2],
			onMarkAllReviewed: vi.fn().mockResolvedValue(undefined)
		});
		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.toBeInTheDocument();
	});

	it('does not show "Alle als fertig markieren" button when onMarkAllReviewed is not provided', async () => {
		renderView({ blocks: [unreviewedBlock1, unreviewedBlock2] });
		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.not.toBeInTheDocument();
	});

	it('disables button when all blocks are already reviewed', async () => {
		renderView({
			blocks: [reviewedBlock1, reviewedBlock2],
			onMarkAllReviewed: vi.fn().mockResolvedValue(undefined)
		});
		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.toBeDisabled();
	});

	it('calls onMarkAllReviewed exactly once when button is clicked', async () => {
		const onMarkAllReviewed = vi.fn().mockResolvedValue(undefined);
		renderView({
			blocks: [unreviewedBlock1, unreviewedBlock2],
			onMarkAllReviewed
		});

		// userEvent.click() via Playwright CDP doesn't reliably trigger Svelte 5 onclick
		// handlers when a TipTap editor is mounted in the same component tree.
		const btn = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		await vi.waitFor(() => expect(onMarkAllReviewed).toHaveBeenCalledTimes(1));
	});

	it('disables button while operation is in-flight', async () => {
		let resolveMarkAll!: () => void;
		const onMarkAllReviewed = vi
			.fn()
			.mockReturnValue(new Promise<void>((r) => (resolveMarkAll = r)));
		renderView({
			blocks: [unreviewedBlock1, unreviewedBlock2],
			onMarkAllReviewed
		});

		// Same CDP click workaround: dispatch from browser JS to reliably fire Svelte 5 onclick
		const btnEl = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.toBeDisabled();
		resolveMarkAll();
	});

	it('shows error message when onMarkAllReviewed callback rejects', async () => {
		const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR'));
		renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed });

		const btnEl = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));

		await expect.element(page.getByRole('alert')).toBeInTheDocument();
		await expect
			.element(page.getByRole('alert'))
			.toHaveTextContent(m.transcription_mark_all_reviewed_error());
	});

	it('clears error when dismiss button is clicked', async () => {
		const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR'));
		renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed });

		const btnEl = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		await expect.element(page.getByRole('alert')).toBeInTheDocument();

		const dismissEl = (await page
			.getByRole('button', { name: m.comp_dismiss() })
			.element()) as HTMLButtonElement;
		dismissEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));

		await expect.element(page.getByRole('alert')).not.toBeInTheDocument();
	});

	it('clears error on next successful markAllReviewed call', async () => {
		const onMarkAllReviewed = vi
			.fn()
			.mockRejectedValueOnce(new Error('INTERNAL_ERROR'))
			.mockResolvedValue(undefined);
		renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed });

		const btnEl = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		await expect.element(page.getByRole('alert')).toBeInTheDocument();
		// Wait for the button to be re-enabled before the second click — ensures the first
		// async rejection has fully settled and Svelte has flushed state changes
		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.not.toBeDisabled();

		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));

		await expect.element(page.getByRole('alert')).not.toBeInTheDocument();
	});

	it('re-enables button after markAllReviewed failure', async () => {
		const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR'));
		renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed });

		const btnEl = (await page
			.getByRole('button', { name: m.transcription_mark_all_reviewed() })
			.element()) as HTMLButtonElement;
		btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
		await expect.element(page.getByRole('alert')).toBeInTheDocument();

		await expect
			.element(page.getByRole('button', { name: m.transcription_mark_all_reviewed() }))
			.not.toBeDisabled();
	});
});
