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

afterEach(cleanup);

describe('PersonTypeahead', () => {
	let fetchSpy: ReturnType<typeof vi.spyOn>;

	beforeEach(() => {
		fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
			return new Response(
				JSON.stringify([
					{ id: 'p-1', displayName: 'Anna Schmidt', firstName: 'Anna', lastName: 'Schmidt' },
					{ id: 'p-2', displayName: 'Bertha Müller', firstName: 'Bertha', lastName: 'Müller' }
				]),
				{ status: 200, headers: { 'Content-Type': 'application/json' } }
			);
		});
	});

	afterEach(() => fetchSpy?.mockRestore());

	it('renders the label and the search input', async () => {
		render(PersonTypeahead, { props: { name: 'sender', label: 'Absender' } });

		const label = document.querySelector('label[for="sender-search"]');
		expect(label?.textContent).toContain('Absender');
		const input = document.querySelector('input#sender-search');
		expect(input).not.toBeNull();
	});

	it('appends an asterisk when required is true', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', required: true }
		});

		const label = document.querySelector('label[for="s-search"]');
		expect(label?.textContent).toContain('*');
	});

	it('does not append an asterisk when required is false', async () => {
		render(PersonTypeahead, { props: { name: 's', label: 'Absender' } });

		const label = document.querySelector('label[for="s-search"]');
		expect(label?.textContent).not.toContain('*');
	});

	it('uses the placeholder prop when provided', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', placeholder: 'Tippe los…' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.placeholder).toBe('Tippe los…');
	});

	it('seeds the searchTerm from initialName', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', initialName: 'Anna Schmidt' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.value).toBe('Anna Schmidt');
	});

	it('exposes the value via a hidden input', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', value: 'p-1' }
		});

		const hidden = document.querySelector('input[name="s"][type="hidden"]') as HTMLInputElement;
		expect(hidden.value).toBe('p-1');
	});

	it('uses the large class set when large=true', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', large: true }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.className).toContain('h-14');
	});

	it('uses the compact class set when compact=true', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', compact: true }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.className).toContain('h-9');
	});

	it('does not render the listbox initially', async () => {
		render(PersonTypeahead, { props: { name: 's', label: 'Absender' } });

		const listbox = document.querySelector('[role="listbox"]');
		expect(listbox).toBeNull();
	});

	it('opens the listbox on focus when restrictToCorrespondentsOf is set', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		input.dispatchEvent(new Event('focus', { bubbles: true }));

		await vi.waitFor(() => {
			expect(document.querySelector('[role="listbox"]')).not.toBeNull();
		});
	});

	it('updates aria-expanded when the dropdown opens', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.getAttribute('aria-expanded')).toBe('false');

		input.dispatchEvent(new Event('focus', { bubbles: true }));
		await vi.waitFor(() => {
			expect(input.getAttribute('aria-expanded')).toBe('true');
		});
	});

	it('Escape key on a closed dropdown is a no-op (no listbox appears)', async () => {
		render(PersonTypeahead, { props: { name: 's', label: 'Absender' } });

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));

		expect(document.querySelector('[role="listbox"]')).toBeNull();
	});

	it('keeps the input usable when fetch rejects on focus (no error UI, no crash)', async () => {
		fetchSpy.mockRejectedValueOnce(new Error('boom'));
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		input.dispatchEvent(new Event('focus', { bubbles: true }));

		// Graceful failure: no listbox surfaces but the input stays mounted and interactive.
		await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled());
		expect(document.querySelector('input#s-search')).not.toBeNull();
	});

	it('keeps the input usable when fetch returns a non-OK response on focus', async () => {
		fetchSpy.mockResolvedValueOnce(new Response('error', { status: 500 }));
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', restrictToCorrespondentsOf: 'parent-id' }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		input.dispatchEvent(new Event('focus', { bubbles: true }));

		await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled());
		expect(document.querySelector('input#s-search')).not.toBeNull();
	});

	it('renders the FieldLabelBadge when badge is provided', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', badge: 'replace' }
		});

		const label = document.querySelector('label[for="s-search"]');
		expect(label?.children.length).toBeGreaterThan(0);
	});

	it('does not render the FieldLabelBadge when badge is undefined', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender' }
		});

		const label = document.querySelector('label[for="s-search"]');
		expect(label?.querySelector('[class*="badge"]')).toBeNull();
	});

	it('honours the autofocus prop', async () => {
		render(PersonTypeahead, {
			props: { name: 's', label: 'Absender', autofocus: true }
		});

		const input = document.querySelector('input#s-search') as HTMLInputElement;
		expect(input.hasAttribute('autofocus')).toBe(true);
	});
});
