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

const waitForFetch = () => new Promise((r) => setTimeout(r, 50));
const tick = () => new Promise((r) => setTimeout(r, 0));

function mockFetchWithTagObjects(tags: Tag[]) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue(tags)
		})
	);
}

function mockFetchWithTags(tagNames: string[]) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue(tagNames.map((name) => ({ name })))
		})
	);
}

function mockFetchEmpty() {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
	);
}

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

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

describe('TagInput – rendering', () => {
	it('shows creation placeholder when allowCreation=true and no tags', async () => {
		render(TagInput, { tags: [], allowCreation: true });
		await expect.element(page.getByPlaceholder('Schlagworte hinzufügen...')).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/tag-input-empty.png' });
	});

	it('shows filter placeholder when allowCreation=false', async () => {
		render(TagInput, { tags: [], allowCreation: false });
		await expect.element(page.getByPlaceholder('Nach Schlagworten filtern...')).toBeInTheDocument();
	});

	it('renders existing tags as chips', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }, { name: 'Krieg' }], allowCreation: true });
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
		await expect.element(page.getByText('Krieg')).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/tag-input-with-chips.png' });
	});

	it('hides input placeholder once tags exist', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }], allowCreation: true });
		const input = page.getByRole('textbox');
		await expect.element(input).toHaveAttribute('placeholder', '');
	});

	it('shows the "Enter" hint when allowCreation=true', async () => {
		render(TagInput, { tags: [], allowCreation: true });
		await expect.element(page.getByText(/Enter drücken/i)).toBeInTheDocument();
	});

	it('hides the "Enter" hint when allowCreation=false', async () => {
		render(TagInput, { tags: [], allowCreation: false });
		await expect.element(page.getByText(/Enter drücken/i)).not.toBeInTheDocument();
	});

	it('renders a color dot on chips that have a color', async () => {
		render(TagInput, { tags: [{ name: 'Familie', color: 'sage' }], allowCreation: true });
		const dot = page.getByTestId('tag-color-dot');
		await expect.element(dot).toBeInTheDocument();
		await expect.element(dot).toHaveAttribute('data-color', 'sage');
	});

	it('does not render a color dot on chips without a color', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }], allowCreation: true });
		await expect.element(page.getByTestId('tag-color-dot')).not.toBeInTheDocument();
	});
});

// ─── Adding tags ──────────────────────────────────────────────────────────────

describe('TagInput – adding tags', () => {
	it('adds a tag on Enter and clears the input', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Urlaubsreise');
		await userEvent.keyboard('{Enter}');
		await expect.element(page.getByText('Urlaubsreise')).toBeInTheDocument();
		await expect.element(input).toHaveValue('');
	});

	it('trims whitespace from the new tag', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('  Leerzeichen  ');
		await userEvent.keyboard('{Enter}');
		await expect.element(page.getByText('Leerzeichen')).toBeInTheDocument();
	});

	it('does not add a duplicate tag', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [{ name: 'Familie' }], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Familie');
		await userEvent.keyboard('{Enter}');
		await expect.element(input).toHaveValue('');
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
	});

	it('does not add an arbitrary tag when allowCreation=false', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [], allowCreation: false });
		const input = page.getByRole('textbox');
		await input.fill('UnbekannterTag');
		await userEvent.keyboard('{Enter}');
		await expect.element(page.getByText('UnbekannterTag')).not.toBeInTheDocument();
	});
});

// ─── Removing tags ────────────────────────────────────────────────────────────

describe('TagInput – removing tags', () => {
	it('removes a chip when its × button is clicked', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }, { name: 'Krieg' }], allowCreation: true });
		// The × buttons have aria-label="Schlagwort entfernen"
		document.querySelector<HTMLElement>('button[aria-label="Schlagwort entfernen"]')!.click();
		await tick();
		await expect.element(page.getByText('Familie')).not.toBeInTheDocument();
		await expect.element(page.getByText('Krieg')).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/tag-input-after-remove.png' });
	});

	it('removes the last tag on Backspace when the input is empty', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }, { name: 'Krieg' }], allowCreation: true });
		(document.querySelector('input[type="text"]') as HTMLInputElement).focus();
		await userEvent.keyboard('{Backspace}');
		await expect.element(page.getByText('Krieg')).not.toBeInTheDocument();
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
	});

	it('does not remove a tag on Backspace when the input has text', async () => {
		render(TagInput, { tags: [{ name: 'Familie' }], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('x');
		await userEvent.keyboard('{Backspace}');
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
	});
});

// ─── Autocomplete ─────────────────────────────────────────────────────────────

describe('TagInput – autocomplete', () => {
	it('shows suggestions after typing 2+ characters', async () => {
		mockFetchWithTags(['Familie', 'Freunde']);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		await expect.element(page.getByRole('option', { name: 'Familie' })).toBeInTheDocument();
		await expect.element(page.getByRole('option', { name: 'Freunde' })).toBeInTheDocument();
		await page.screenshot({ path: 'test-results/screenshots/tag-input-suggestions.png' });
	});

	it('does not call fetch for fewer than 2 characters', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('F');
		await waitForFetch();
		expect(fetch).not.toHaveBeenCalled();
	});

	it('filters already-selected tags out of suggestions', async () => {
		mockFetchWithTags(['Familie', 'Freunde']);
		render(TagInput, { tags: [{ name: 'Familie' }], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fr');
		await waitForFetch();
		await expect.element(page.getByRole('option', { name: 'Familie' })).not.toBeInTheDocument();
		await expect.element(page.getByRole('option', { name: 'Freunde' })).toBeInTheDocument();
	});

	it('selects a suggestion on click and adds it as a chip', async () => {
		mockFetchWithTags(['Familie', 'Freunde']);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		document.querySelector<HTMLElement>('[role="option"]')!.click();
		await tick();
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
		await expect.element(input).toHaveValue('');
		await page.screenshot({ path: 'test-results/screenshots/tag-input-suggestion-selected.png' });
	});

	it('navigates suggestions with ArrowDown and selects with Enter', async () => {
		mockFetchWithTags(['Aachen', 'Berlin', 'Celle']);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('__');
		await waitForFetch();
		await userEvent.keyboard('{ArrowDown}'); // index 0 → Aachen
		await userEvent.keyboard('{ArrowDown}'); // index 1 → Berlin
		await userEvent.keyboard('{Enter}');
		await expect.element(page.getByText('Berlin')).toBeInTheDocument();
	});

	it('hides the dropdown when clicking outside the component', async () => {
		mockFetchWithTags(['Familie']);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		await expect.element(page.getByRole('option', { name: 'Familie' })).toBeInTheDocument();
		document.body.click();
		await tick();
		await expect.element(page.getByRole('option', { name: 'Familie' })).not.toBeInTheDocument();
	});

	it('shows child suggestion after its parent when both are in results', async () => {
		mockFetchWithTagObjects([
			{ id: 'p1', name: 'Eltern' },
			{ id: 'c1', name: 'Kind', parentId: 'p1' }
		]);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('El');
		await waitForFetch();
		const options = Array.from(document.querySelectorAll('[role="option"]'));
		const texts = options.map((el) => el.textContent?.replace(/›/g, '').trim() ?? '');
		expect(texts.indexOf('Eltern')).toBeLessThan(texts.indexOf('Kind'));
		// direct match (Eltern contains 'El') has font-medium; context child does not
		const elternEl = options.find((el) => el.textContent?.replace(/›/g, '').trim() === 'Eltern');
		expect(elternEl?.classList.contains('font-medium')).toBe(true);
	});

	it('dropdown has role listbox', async () => {
		mockFetchWithTags(['Familie']);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		await expect.element(page.getByRole('listbox')).toBeInTheDocument();
	});

	it('selects a suggestion via Enter when allowCreation=false', async () => {
		mockFetchWithTags(['Familie']);
		render(TagInput, { tags: [], allowCreation: false });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		await userEvent.keyboard('{ArrowDown}'); // index 0 → Familie
		await userEvent.keyboard('{Enter}');
		await expect.element(page.getByText('Familie')).toBeInTheDocument();
		await expect.element(input).toHaveValue('');
	});
});

// ─── Tree-aware ordering ──────────────────────────────────────────────────────

describe('TagInput – tree-aware ordering', () => {
	it('shows children below match when root tag matches in DFS order', async () => {
		mockFetchWithTagObjects([
			{ id: 'r1', name: 'Briefe' },
			{ id: 'c1', name: 'Familienbriefe', parentId: 'r1' },
			{ id: 'g1', name: 'Weihnachtsbriefe', parentId: 'c1' }
		]);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Brief');
		await waitForFetch();
		const options = Array.from(document.querySelectorAll('[role="option"]'));
		const texts = options.map((el) => el.textContent?.replace(/›/g, '').trim() ?? '');
		expect(texts).toContain('Briefe');
		expect(texts).toContain('Familienbriefe');
		expect(texts).toContain('Weihnachtsbriefe');
		expect(texts.indexOf('Briefe')).toBeLessThan(texts.indexOf('Familienbriefe'));
		expect(texts.indexOf('Familienbriefe')).toBeLessThan(texts.indexOf('Weihnachtsbriefe'));
	});

	it('shows ancestor above match when child tag matches', async () => {
		mockFetchWithTagObjects([
			{ id: 'r1', name: 'Briefe' },
			{ id: 'c1', name: 'Hochzeit', parentId: 'r1' }
		]);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Hochzeit');
		await waitForFetch();
		const options = Array.from(document.querySelectorAll('[role="option"]'));
		const texts = options.map((el) => el.textContent?.replace(/›/g, '').trim() ?? '');
		expect(texts.indexOf('Briefe')).toBeLessThan(texts.indexOf('Hochzeit'));
		const hochzeitEl = options.find(
			(el) => el.textContent?.replace(/›/g, '').trim() === 'Hochzeit'
		);
		expect(hochzeitEl?.classList.contains('font-medium')).toBe(true);
	});

	it('keyboard Enter on context node adds it as a tag', async () => {
		mockFetchWithTagObjects([
			{ id: 'r1', name: 'Briefe' },
			{ id: 'c1', name: 'Hochzeit', parentId: 'r1' }
		]);
		render(TagInput, { tags: [], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Hochzeit');
		await waitForFetch();
		await userEvent.keyboard('{ArrowDown}'); // first item: Briefe (context ancestor)
		await userEvent.keyboard('{Enter}');
		// successful add clears the input; if .tag unwrap is missing, addTag returns early
		await expect.element(input).toHaveValue('');
	});

	it('shows orphaned children at depth 0 when their parent is already selected', async () => {
		mockFetchWithTagObjects([
			{ id: 'p1', name: 'Briefe' },
			{ id: 'c1', name: 'Familienbriefe', parentId: 'p1' }
		]);
		render(TagInput, { tags: [{ id: 'p1', name: 'Briefe' }], allowCreation: true });
		const input = page.getByRole('textbox');
		await input.fill('Fa');
		await waitForFetch();
		await expect.element(page.getByRole('option', { name: 'Familienbriefe' })).toBeInTheDocument();
		// the already-selected parent must not appear as a suggestion
		const optionNames = Array.from(document.querySelectorAll('[role="option"]')).map(
			(el) => el.textContent?.replace(/›/g, '').trim() ?? ''
		);
		expect(optionNames).not.toContain('Briefe');
	});
});

// ─── onTextInput callback ──────────────────────────────────────────────────────

describe('TagInput – onTextInput callback', () => {
	it('calls onTextInput with the current value on every input event', async () => {
		mockFetchEmpty();
		const onTextInput = vi.fn();
		render(TagInput, { tags: [], allowCreation: false, onTextInput });
		const input = page.getByRole('textbox');
		await input.fill('fa');
		await expect.poll(() => onTextInput.mock.calls.length).toBeGreaterThan(0);
		expect(onTextInput).toHaveBeenCalledWith('fa');
	});

	it('does not throw when onTextInput is not provided', async () => {
		mockFetchEmpty();
		render(TagInput, { tags: [], allowCreation: false });
		const input = page.getByRole('textbox');
		await expect(input.fill('fa')).resolves.not.toThrow();
	});

	it('calls onTextInput with empty string when a tag chip is added', async () => {
		mockFetchWithTags(['Kaufvertrag']);
		const onTextInput = vi.fn();
		render(TagInput, { tags: [], allowCreation: false, onTextInput });
		const input = page.getByRole('textbox');
		await input.fill('Ka');
		await waitForFetch();
		((await page.getByRole('option', { name: 'Kaufvertrag' }).element()) as HTMLElement).click();
		await expect.poll(() => onTextInput.mock.calls.at(-1)).toEqual(['']);
	});
});
