import { describe, expect, it, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import PersonTypeahead from './PersonTypeahead.svelte';

const waitForDebounce = () => new Promise((r) => setTimeout(r, 350));
const tick = () => new Promise((r) => setTimeout(r, 0));

const PERSONS = [
	{
		id: '1',
		firstName: 'Max',
		lastName: 'Mustermann',
		displayName: 'Max Mustermann',
		personType: 'PERSON'
	},
	{
		id: '2',
		firstName: 'Anna',
		lastName: 'Musterfrau',
		displayName: 'Anna Musterfrau',
		personType: 'PERSON'
	}
];

function mockFetchWithPersons(persons = PERSONS) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue(persons)
		})
	);
}

function mockFetchFailure() {
	vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
}

function hiddenInput(name: string) {
	return document.querySelector<HTMLInputElement>(`input[type="hidden"][name="${name}"]`);
}

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

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

describe('PersonTypeahead – rendering', () => {
	it('renders the label and text input', async () => {
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		await expect.element(page.getByText('Absender')).toBeInTheDocument();
		await expect.element(page.getByPlaceholder('Namen tippen...')).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/person-typeahead-empty.png' });
	});

	it('pre-fills the visible input from initialName', async () => {
		render(PersonTypeahead, {
			name: 'senderId',
			label: 'Absender',
			initialName: 'Max Mustermann'
		});
		// The $effect that syncs initialName runs after mount — poll until the value appears
		await expect.element(page.getByPlaceholder('Namen tippen...')).toHaveValue('Max Mustermann');
	});

	it('renders a hidden input with the correct name attribute', async () => {
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		await tick();
		expect(hiddenInput('senderId')).toBeTruthy();
	});

	it('hidden input starts with the provided value', async () => {
		render(PersonTypeahead, { name: 'senderId', label: 'Absender', value: '42' });
		await tick();
		expect(hiddenInput('senderId')?.value).toBe('42');
	});
});

// ─── Search ───────────────────────────────────────────────────────────────────

describe('PersonTypeahead – search', () => {
	it('opens the dropdown with results after typing', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
		await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/person-typeahead-open.png' });
	});

	it('shows loading indicator while fetching', async () => {
		vi.stubGlobal('fetch', vi.fn().mockReturnValue(new Promise(() => {})));
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Ma');
		await waitForDebounce();
		await expect.element(page.getByText('Suche...')).toBeInTheDocument();
	});

	it('shows no dropdown when the search returns empty results', async () => {
		mockFetchWithPersons([]);
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('XYZ');
		await waitForDebounce();
		await expect.element(page.getByText('Suche...')).not.toBeInTheDocument();
	});

	it('shows no results when fetch fails', async () => {
		mockFetchFailure();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Ma');
		await waitForDebounce();
		await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument();
	});
});

// ─── Selection ────────────────────────────────────────────────────────────────

describe('PersonTypeahead – selection', () => {
	it('fills the visible input and closes dropdown on click', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		await expect.element(input).toHaveValue('Max Mustermann');
		await expect
			.element(page.getByRole('option', { name: 'Max Mustermann' }))
			.not.toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/person-typeahead-selected.png' });
	});

	it('sets the hidden input value to the selected person id', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		await tick();
		expect(hiddenInput('senderId')?.value).toBe('1');
	});

	it('calls onchange with the person id on selection', async () => {
		mockFetchWithPersons();
		const onchange = vi.fn();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender', onchange });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		expect(onchange).toHaveBeenCalledWith('1');
	});

	it('selects a result with Enter key', async () => {
		mockFetchWithPersons([
			{
				id: '1',
				firstName: 'Max',
				lastName: 'Mustermann',
				displayName: 'Max Mustermann',
				personType: 'PERSON'
			}
		]);
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Ma');
		await waitForDebounce();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		await expect.element(input).toHaveValue('Max Mustermann');
	});
});

// ─── Clearing ─────────────────────────────────────────────────────────────────

describe('PersonTypeahead – clearing a selection', () => {
	it('clears the hidden value when user edits the visible input after a selection', async () => {
		mockFetchWithPersons();
		const onchange = vi.fn();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender', onchange });
		const input = page.getByPlaceholder('Namen tippen...');

		await input.fill('Mu');
		await waitForDebounce();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		expect(onchange).toHaveBeenCalledWith('1');
		onchange.mockClear();

		await input.fill('x');
		await waitForDebounce();
		expect(onchange).toHaveBeenCalledWith('');
		await tick();
		expect(hiddenInput('senderId')?.value).toBe('');
	});
});

// ─── Correspondent mode ───────────────────────────────────────────────────────

describe('PersonTypeahead – correspondent mode', () => {
	it('fetches correspondents immediately on focus when restrictToCorrespondentsOf is set', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, {
			name: 'receiverId',
			label: 'Empfänger',
			restrictToCorrespondentsOf: 'person-a-id'
		});

		(document.querySelector('input[placeholder="Namen tippen..."]') as HTMLInputElement).focus();
		await waitForDebounce();

		const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
		expect(fetchMock).toHaveBeenCalledWith(
			expect.stringContaining('/api/persons/person-a-id/correspondents')
		);
	});

	it('shows correspondent results immediately on focus without typing', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, {
			name: 'receiverId',
			label: 'Empfänger',
			restrictToCorrespondentsOf: 'person-a-id'
		});

		(document.querySelector('input[placeholder="Namen tippen..."]') as HTMLInputElement).focus();
		await waitForDebounce();

		await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
	});

	it('uses correspondents endpoint with q param when typing', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, {
			name: 'receiverId',
			label: 'Empfänger',
			restrictToCorrespondentsOf: 'person-a-id'
		});

		await page.getByPlaceholder('Namen tippen...').fill('Anna');
		await waitForDebounce();

		const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
		expect(fetchMock).toHaveBeenCalledWith(
			expect.stringContaining('/api/persons/person-a-id/correspondents?q=Anna')
		);
	});

	it('uses normal persons endpoint when restrictToCorrespondentsOf is not set', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });

		await page.getByPlaceholder('Namen tippen...').fill('Anna');
		await waitForDebounce();

		const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
		expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/persons?q=Anna'));
	});
});

// ─── Click outside ────────────────────────────────────────────────────────────

describe('PersonTypeahead – click outside', () => {
	it('closes the dropdown when clicking outside the component', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
		document.body.click();
		await tick();
		await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument();
	});
});

// ─── ARIA roles ───────────────────────────────────────────────────────────────

describe('PersonTypeahead – ARIA roles', () => {
	it('dropdown uses role="listbox" container and role="option" items', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		// Container must be a listbox
		await expect.element(page.getByRole('listbox')).toBeInTheDocument();

		// Items must be options, not buttons
		const options = page.getByRole('option');
		await expect.element(options.first()).toBeInTheDocument();
		await expect.element(page.getByRole('option', { name: 'Max Mustermann' })).toBeInTheDocument();
		await expect.element(page.getByRole('option', { name: 'Anna Musterfrau' })).toBeInTheDocument();
	});

	it('input has aria-expanded="false" when dropdown is closed', async () => {
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await expect.element(input).toHaveAttribute('aria-expanded', 'false');
	});

	it('input has aria-expanded="true" when dropdown is open', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		await expect.element(input).toHaveAttribute('aria-expanded', 'true');
	});

	it('input has aria-controls pointing to the listbox id', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		const ariaControls = await input.element().getAttribute('aria-controls');
		expect(ariaControls).toBeTruthy();

		// The listbox with that id must exist
		const listbox = document.getElementById(ariaControls!);
		expect(listbox).not.toBeNull();
		expect(listbox!.getAttribute('role')).toBe('listbox');
	});

	it('input has aria-haspopup="listbox"', async () => {
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await expect.element(input).toHaveAttribute('aria-haspopup', 'listbox');
	});
});

// ─── Keyboard navigation ──────────────────────────────────────────────────────

describe('PersonTypeahead – keyboard navigation', () => {
	it('ArrowDown moves highlight to the first option', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		await input.click();
		await userEvent.keyboard('{ArrowDown}');
		await tick();

		// First option should be highlighted (aria-selected="true")
		const firstOption = page.getByRole('option', { name: 'Max Mustermann' });
		await expect.element(firstOption).toHaveAttribute('aria-selected', 'true');
	});

	it('ArrowDown then ArrowDown moves highlight to the second option', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		await input.click();
		await userEvent.keyboard('{ArrowDown}');
		await tick();
		await userEvent.keyboard('{ArrowDown}');
		await tick();

		const secondOption = page.getByRole('option', { name: 'Anna Musterfrau' });
		await expect.element(secondOption).toHaveAttribute('aria-selected', 'true');
	});

	it('ArrowUp from first wraps to last option', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		await input.click();
		await userEvent.keyboard('{ArrowDown}'); // highlight first
		await tick();
		await userEvent.keyboard('{ArrowUp}'); // wrap to last
		await tick();

		const lastOption = page.getByRole('option', { name: 'Anna Musterfrau' });
		await expect.element(lastOption).toHaveAttribute('aria-selected', 'true');
	});

	it('ArrowDown from last wraps to first option', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		await input.click();
		await userEvent.keyboard('{ArrowDown}'); // highlight first (index 0)
		await tick();
		await userEvent.keyboard('{ArrowDown}'); // highlight second (index 1 = last)
		await tick();
		await userEvent.keyboard('{ArrowDown}'); // wrap to first (index 0)
		await tick();

		const firstOption = page.getByRole('option', { name: 'Max Mustermann' });
		await expect.element(firstOption).toHaveAttribute('aria-selected', 'true');
	});

	it('Enter selects the highlighted option', async () => {
		mockFetchWithPersons([
			{
				id: '1',
				firstName: 'Max',
				lastName: 'Mustermann',
				displayName: 'Max Mustermann',
				personType: 'PERSON'
			}
		]);
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Ma');
		await waitForDebounce();

		await input.click();
		await userEvent.keyboard('{ArrowDown}');
		await tick();
		await userEvent.keyboard('{Enter}');
		await tick();

		await expect.element(input).toHaveValue('Max Mustermann');
		await expect.element(page.getByRole('listbox')).not.toBeInTheDocument();
	});

	it('Escape closes the dropdown without selecting', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();
		await expect.element(page.getByRole('listbox')).toBeInTheDocument();

		await input.click();
		await userEvent.keyboard('{Escape}');
		await tick();
		await expect.element(page.getByRole('listbox')).not.toBeInTheDocument();
		// Value unchanged — nothing was selected
		await expect.element(input).toHaveValue('Mu');
	});

	it('active option id is set as aria-activedescendant on the input', async () => {
		mockFetchWithPersons();
		render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
		const input = page.getByPlaceholder('Namen tippen...');
		await input.fill('Mu');
		await waitForDebounce();

		// No active option before pressing ArrowDown
		const beforeNav = await input.element().getAttribute('aria-activedescendant');
		expect(beforeNav).toBeFalsy();

		await input.click();
		await userEvent.keyboard('{ArrowDown}');
		await tick();

		const afterNav = await input.element().getAttribute('aria-activedescendant');
		expect(afterNav).toBeTruthy();
	});
});
