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

const docItem = (overrides: Partial<{ note: string }> = {}) => ({
	id: 'item-1',
	position: 0,
	document: {
		id: 'doc-1',
		title: 'Brief von Karl',
		datePrecision: 'DAY' as const,
		receiverCount: 0
	},
	...overrides
});

const interludeItem = (note = 'Reise nach Wien') => ({
	id: 'item-2',
	position: 1,
	note
});

const defaultProps = (overrides = {}) => ({
	index: 0,
	total: 3,
	onMoveUp: vi.fn(),
	onMoveDown: vi.fn(),
	onRemove: vi.fn(),
	onNotePatch: vi.fn().mockResolvedValue(undefined),
	...overrides
});

afterEach(() => cleanup());

describe('JourneyItemRow — interlude label', () => {
	it('shows "Zwischentext" (not the add-button label) on interlude rows', async () => {
		render(JourneyItemRow, { item: interludeItem(), ...defaultProps() });

		await expect.element(page.getByText(m.journey_interlude_label())).toBeInTheDocument();
		await expect.element(page.getByText(m.journey_add_interlude())).not.toBeInTheDocument();
	});

	it('uses "Zwischentext" in the move button aria-labels', async () => {
		render(JourneyItemRow, { item: interludeItem(), ...defaultProps({ index: 1 }) });

		await expect
			.element(
				page.getByRole('button', {
					name: m.journey_move_up({ title: m.journey_interlude_label() })
				})
			)
			.toBeInTheDocument();
	});
});

describe('JourneyItemRow — note textarea', () => {
	it('opens note textarea on "Notiz hinzufügen" click', async () => {
		render(JourneyItemRow, { item: docItem(), ...defaultProps() });

		await userEvent.click(page.getByText(m.journey_note_add()));

		await expect
			.element(page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ }))
			.toBeInTheDocument();
	});

	it('blur without typing does not call onNotePatch and collapses the textarea', async () => {
		// '' (untouched draft) and undefined (no note) both mean "no note" — a
		// spurious PATCH {note: null} must not fire, and the empty textarea closes.
		const onNotePatch = vi.fn().mockResolvedValue(undefined);
		render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });

		await userEvent.click(page.getByText(m.journey_note_add()));
		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
		await textarea.element().dispatchEvent(new FocusEvent('blur'));

		expect(onNotePatch).not.toHaveBeenCalled();
		await expect.element(page.getByText(m.journey_note_add())).toBeInTheDocument();
	});

	it('moves focus into the textarea when "Notiz hinzufügen" opens it', async () => {
		render(JourneyItemRow, { item: docItem(), ...defaultProps() });

		const toggle = page.getByText(m.journey_note_add());
		expect(toggle.element().getAttribute('aria-expanded')).toBe('false');
		await userEvent.click(toggle);

		await vi.waitFor(() => {
			const textarea = page
				.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ })
				.element();
			expect(document.activeElement).toBe(textarea);
		});
	});

	it('calls onNotePatch on textarea blur with non-empty value', async () => {
		const onNotePatch = vi.fn().mockResolvedValue(undefined);
		render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });

		await userEvent.click(page.getByText(m.journey_note_add()));
		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
		await userEvent.fill(textarea, 'Eine neue Notiz');
		await textarea.element().dispatchEvent(new FocusEvent('blur'));

		expect(onNotePatch).toHaveBeenCalledWith('Eine neue Notiz');
	});

	it('limits the note textarea to 2000 characters', async () => {
		render(JourneyItemRow, { item: docItem({ note: 'Notiz' }), ...defaultProps() });

		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz/ });
		await expect.element(textarea).toHaveAttribute('maxlength', '2000');
	});
});

describe('JourneyItemRow — note error state', () => {
	it('shows role=alert error message when onNotePatch rejects', async () => {
		const onNotePatch = vi.fn().mockRejectedValue(new Error('server error'));
		render(JourneyItemRow, { item: docItem(), ...defaultProps({ onNotePatch }) });

		await userEvent.click(page.getByText(m.journey_note_add()));
		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz für Brief von Karl/ });
		await userEvent.fill(textarea, 'Eine Notiz');
		await textarea.element().dispatchEvent(new FocusEvent('blur'));

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

describe('JourneyItemRow — note remove error state', () => {
	it('restores note and shows error when onNotePatch rejects during remove', async () => {
		const onNotePatch = vi.fn().mockRejectedValue(new Error('server error'));
		render(JourneyItemRow, {
			item: docItem({ note: 'keep me' }),
			...defaultProps({ onNotePatch })
		});

		await userEvent.click(page.getByText(m.journey_note_remove()));

		// textarea should be visible again (showNote restored)
		await expect
			.element(page.getByRole('textbox', { name: /Kuratoren-Notiz/ }))
			.toBeInTheDocument();
		// error alert should be shown
		await expect.element(page.getByRole('alert')).toBeInTheDocument();
	});
});

describe('JourneyItemRow — interlude rules', () => {
	it('does not show "Notiz entfernen" for interlude items', async () => {
		render(JourneyItemRow, { item: interludeItem(), ...defaultProps() });

		// Note section should be visible (interlude always shows note)
		await expect
			.element(page.getByRole('textbox', { name: /Kuratoren-Notiz/ }))
			.toBeInTheDocument();
		// But "Notiz entfernen" must be absent
		await expect.element(page.getByText(m.journey_note_remove())).not.toBeInTheDocument();
	});

	it('blocks saving empty text on interlude note blur', async () => {
		const onNotePatch = vi.fn().mockResolvedValue(undefined);
		render(JourneyItemRow, {
			item: interludeItem('original text'),
			...defaultProps({ onNotePatch })
		});

		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz/ });
		await userEvent.clear(textarea);
		await textarea.element().dispatchEvent(new FocusEvent('blur'));

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

	it('restores the original note text after a blocked empty-clear blur', async () => {
		render(JourneyItemRow, {
			item: interludeItem('original text'),
			...defaultProps()
		});

		const textarea = page.getByRole('textbox', { name: /Kuratoren-Notiz/ });
		await userEvent.clear(textarea);
		await textarea.element().dispatchEvent(new FocusEvent('blur'));

		await expect.element(textarea).toHaveValue('original text');
	});
});

describe('JourneyItemRow — remove confirm', () => {
	it('shows inline confirm when removing a document item that has a note', async () => {
		render(JourneyItemRow, {
			item: docItem({ note: 'Wichtige Notiz' }),
			...defaultProps()
		});

		// Click remove (x button)
		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);

		await expect.element(page.getByText(m.journey_remove_confirm())).toBeInTheDocument();
		await expect
			.element(page.getByRole('button', { name: m.journey_remove_confirm_yes() }))
			.toBeInTheDocument();
	});

	it('clicking Bestätigen invokes onRemove (destructive path)', async () => {
		const onRemove = vi.fn();
		render(JourneyItemRow, {
			item: docItem({ note: 'Wichtige Notiz' }),
			...defaultProps({ onRemove })
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);
		await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_yes() }));

		expect(onRemove).toHaveBeenCalledTimes(1);
	});

	it('confirm cancel restores remove button without calling onRemove', async () => {
		const onRemove = vi.fn();
		render(JourneyItemRow, {
			item: docItem({ note: 'Notiz' }),
			...defaultProps({ onRemove })
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);
		await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_cancel() }));

		expect(onRemove).not.toHaveBeenCalled();
		// The remove button should be back
		await expect
			.element(
				page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
			)
			.toBeInTheDocument();
	});

	it('confirm cancel returns keyboard focus to the row remove button', async () => {
		render(JourneyItemRow, {
			item: docItem({ note: 'Notiz' }),
			...defaultProps()
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);
		await userEvent.click(page.getByRole('button', { name: m.journey_remove_confirm_cancel() }));

		await vi.waitFor(() => {
			const removeBtn = page
				.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
				.element();
			expect(document.activeElement).toBe(removeBtn);
		});
	});
});

describe('JourneyItemRow — remove confirm a11y', () => {
	it('confirm area is wrapped in role=group with an accessible label', async () => {
		render(JourneyItemRow, {
			item: docItem({ note: 'Wichtige Notiz' }),
			...defaultProps()
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);

		const group = document.querySelector('[role="group"]');
		expect(group).toBeTruthy();
		expect(group!.getAttribute('aria-label')).toBeTruthy();
	});

	it('keyboard focus moves to Cancel button when confirm appears', async () => {
		render(JourneyItemRow, {
			item: docItem({ note: 'Wichtige Notiz' }),
			...defaultProps()
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);

		await vi.waitFor(() => {
			const cancelBtn = page
				.getByRole('button', { name: m.journey_remove_confirm_cancel() })
				.element();
			expect(document.activeElement).toBe(cancelBtn);
		});
	});

	it('pressing Escape while confirm is open hides confirm and refocuses remove button', async () => {
		render(JourneyItemRow, {
			item: docItem({ note: 'Wichtige Notiz' }),
			...defaultProps()
		});

		await userEvent.click(
			page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
		);
		await vi.waitFor(() => {
			const cancelBtn = page
				.getByRole('button', { name: m.journey_remove_confirm_cancel() })
				.element();
			expect(document.activeElement).toBe(cancelBtn);
		});

		await userEvent.keyboard('{Escape}');

		await vi.waitFor(() => {
			const removeBtn = page
				.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
				.element();
			expect(document.activeElement).toBe(removeBtn);
		});
		await expect.element(page.getByText(m.journey_remove_confirm())).not.toBeInTheDocument();
	});
});

describe('JourneyItemRow — pending remove state', () => {
	it('renders dimmed with the pending text and without a remove button', async () => {
		render(JourneyItemRow, {
			item: docItem(),
			...defaultProps({ pendingRemove: true })
		});

		await expect.element(page.getByText(m.journey_item_pending_remove())).toBeInTheDocument();
		await expect
			.element(
				page.getByRole('button', { name: m.journey_remove_item_aria({ title: 'Brief von Karl' }) })
			)
			.not.toBeInTheDocument();

		const root = document.querySelector('[data-block-id="item-1"]')!;
		expect(root.className).toContain('opacity-60');
	});
});

describe('JourneyItemRow — drag handle', () => {
	it('is pointer-only: removed from tab order and hidden from the accessibility tree', async () => {
		render(JourneyItemRow, { item: docItem(), ...defaultProps() });

		const handle = document.querySelector('[data-drag-handle]')!;
		expect(handle.getAttribute('tabindex')).toBe('-1');
		expect(handle.getAttribute('aria-hidden')).toBe('true');
	});
});
