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

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

function mockFetchWithTags(tags: { id: string; name: string; parentId?: string }[]) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue(tags)
		})
	);
}

beforeEach(() => {
	vi.useFakeTimers();
});

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

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

describe('TagParentPicker – rendering', () => {
	it('renders the text input', async () => {
		render(TagParentPicker, { name: 'parentId' });
		await expect.element(page.getByRole('combobox')).toBeInTheDocument();
	});

	it('renders hidden input with correct name', async () => {
		render(TagParentPicker, { name: 'parentId' });
		await vi.advanceTimersByTimeAsync(0);
		expect(hiddenInput('parentId')).toBeTruthy();
	});

	it('uses custom placeholder text when provided', async () => {
		render(TagParentPicker, { name: 'target', placeholder: 'Ziel-Schlagwort suchen …' });
		const input = await page.getByRole('combobox').element();
		expect(input.getAttribute('placeholder')).toBe('Ziel-Schlagwort suchen …');
	});
});

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

describe('TagParentPicker – search', () => {
	it('typing shows dropdown results', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

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

	it('filters excludeIds from results', async () => {
		mockFetchWithTags([
			{ id: 't1', name: 'Haus' },
			{ id: 't2', name: 'Garten' }
		]);
		render(TagParentPicker, { name: 'parentId', excludeIds: ['t1'] });

		const input = page.getByRole('combobox');
		await input.fill('a');
		await vi.advanceTimersByTimeAsync(300);

		await expect.element(page.getByText('Haus')).not.toBeInTheDocument();
		await expect.element(page.getByText('Garten')).toBeInTheDocument();
	});
});

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

describe('TagParentPicker – selection', () => {
	it('selecting an option sets the hidden input value', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		await page.getByRole('option', { name: 'Haus' }).click();
		await vi.advanceTimersByTimeAsync(0);

		expect(hiddenInput('parentId')?.value).toBe('t1');
	});

	it('clear button appears when value is set', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		await page.getByRole('option', { name: 'Haus' }).click();
		await vi.advanceTimersByTimeAsync(0);

		await expect
			.element(page.getByRole('button', { name: 'Auswahl entfernen' }))
			.toBeInTheDocument();
	});

	it('clear button resets value', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		await page.getByRole('option', { name: 'Haus' }).click();
		await vi.advanceTimersByTimeAsync(0);

		expect(hiddenInput('parentId')?.value).toBe('t1');

		await page.getByRole('button', { name: 'Auswahl entfernen' }).click();
		await vi.advanceTimersByTimeAsync(0);

		expect(hiddenInput('parentId')?.value).toBe('');
	});
});

// ─── ARIA combobox ────────────────────────────────────────────────────────────

describe('TagParentPicker – ARIA combobox', () => {
	it('ArrowDown moves aria-activedescendant to first option', async () => {
		mockFetchWithTags([
			{ id: 't1', name: 'Haus' },
			{ id: 't2', name: 'Garten' }
		]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('a');
		await vi.advanceTimersByTimeAsync(300);

		// Dropdown is open — arrow down should highlight first option
		const el = await input.element();
		el.dispatchEvent(
			new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, cancelable: true })
		);
		await vi.advanceTimersByTimeAsync(0);

		expect(el.getAttribute('aria-activedescendant')).toBe('parentId-option-0');
	});

	it('listbox items have role="option" with ids', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		await expect.element(page.getByRole('option', { name: 'Haus' })).toBeInTheDocument();
		const option = await page.getByRole('option', { name: 'Haus' }).element();
		expect(option.id).toBe('parentId-option-0');
	});
});

// ─── Parent name resolution ───────────────────────────────────────────────────

describe('TagParentPicker – parent name subtitle', () => {
	it('shows parent name instead of UUID when allTags is provided', async () => {
		mockFetchWithTags([{ id: 't2', name: 'Keller', parentId: 't1' }]);
		const allTags = [
			{ id: 't1', name: 'Haus', documentCount: 5 },
			{ id: 't2', name: 'Keller', parentId: 't1', documentCount: 2 }
		];
		render(TagParentPicker, { name: 'parentId', allTags });

		const input = page.getByRole('combobox');
		await input.fill('K');
		await vi.advanceTimersByTimeAsync(300);

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

	it('shows nothing as subtitle when tag has no parentId', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		const allTags = [{ id: 't1', name: 'Haus', documentCount: 5 }];
		render(TagParentPicker, { name: 'parentId', allTags });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		// Only the tag name should appear (no subtitle)
		await expect.element(page.getByRole('option', { name: 'Haus' })).toBeInTheDocument();
	});

	it('shows the parentId as subtitle when allTags omits the parent', async () => {
		mockFetchWithTags([{ id: 't2', name: 'Keller', parentId: 'unknown-parent-id' }]);
		render(TagParentPicker, { name: 'parentId', allTags: [] });

		const input = page.getByRole('combobox');
		await input.fill('K');
		await vi.advanceTimersByTimeAsync(300);

		// When parent not found in allTags, fallback shows the parentId itself
		await expect.element(page.getByText('unknown-parent-id')).toBeInTheDocument();
	});
});

describe('TagParentPicker – keyboard navigation', () => {
	it('ArrowUp wraps around to last option', async () => {
		mockFetchWithTags([
			{ id: 't1', name: 'Haus' },
			{ id: 't2', name: 'Garten' }
		]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('a');
		await vi.advanceTimersByTimeAsync(300);

		const el = await input.element();
		// Without prior arrow-down, ArrowUp from -1 wraps via modular arithmetic
		el.dispatchEvent(
			new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true })
		);
		await vi.advanceTimersByTimeAsync(0);

		expect(el.getAttribute('aria-activedescendant')).toBeTruthy();
	});

	it('Escape closes the dropdown', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		const el = await input.element();
		el.dispatchEvent(
			new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
		);
		await vi.advanceTimersByTimeAsync(0);

		// Listbox should be gone
		expect(document.querySelector('[role="listbox"]')).toBeNull();
	});

	it('Enter without active selection does nothing', async () => {
		mockFetchWithTags([{ id: 't1', name: 'Haus' }]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('H');
		await vi.advanceTimersByTimeAsync(300);

		const el = await input.element();
		el.dispatchEvent(
			new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
		);
		await vi.advanceTimersByTimeAsync(0);

		// Hidden input should still be empty
		expect(hiddenInput('parentId')?.value).toBe('');
	});

	it('keydown with no active dropdown is a no-op', async () => {
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		const el = await input.element();
		expect(() =>
			el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
		).not.toThrow();
	});

	it('Enter with active selection selects the highlighted tag', async () => {
		mockFetchWithTags([
			{ id: 't1', name: 'Haus' },
			{ id: 't2', name: 'Garten' }
		]);
		render(TagParentPicker, { name: 'parentId' });

		const input = page.getByRole('combobox');
		await input.fill('a');
		await vi.advanceTimersByTimeAsync(300);

		const el = await input.element();
		el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
		await vi.advanceTimersByTimeAsync(0);
		el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
		await vi.advanceTimersByTimeAsync(0);

		// Some tag selected
		expect(hiddenInput('parentId')?.value).toBeTruthy();
	});
});

describe('TagParentPicker – excludeIds filter', () => {
	it('filters out tags whose id is in excludeIds', async () => {
		mockFetchWithTags([
			{ id: 't1', name: 'Haus' },
			{ id: 't2', name: 'Keller' }
		]);
		render(TagParentPicker, { name: 'parentId', excludeIds: ['t1'] });

		const input = page.getByRole('combobox');
		await input.fill('a');
		await vi.advanceTimersByTimeAsync(300);

		// Only Keller should be visible
		const options = document.querySelectorAll('[role="option"]');
		expect(options.length).toBe(1);
		expect(options[0].textContent).toContain('Keller');
	});
});
