import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

const { createTypeahead } = await import('./useTypeahead.svelte');

describe('createTypeahead', () => {
	beforeEach(() => {
		vi.useFakeTimers();
	});
	afterEach(() => {
		vi.useRealTimers();
	});

	it('starts with empty query and closed dropdown', () => {
		const ta = createTypeahead({ fetchUrl: vi.fn().mockResolvedValue([]) });
		expect(ta.query).toBe('');
		expect(ta.isOpen).toBe(false);
		expect(ta.results).toEqual([]);
		expect(ta.loading).toBe(false);
	});

	it('setQuery updates query and opens dropdown', async () => {
		const fetchUrl = vi.fn().mockResolvedValue([{ id: '1', name: 'Foo' }]);
		const ta = createTypeahead({ fetchUrl });
		ta.setQuery('foo');
		expect(ta.query).toBe('foo');
		expect(ta.isOpen).toBe(true);
	});

	it('setQuery triggers debounced fetch and populates results', async () => {
		const fetchUrl = vi.fn().mockResolvedValue([{ id: '1', name: 'Foo' }]);
		const ta = createTypeahead({ fetchUrl, debounceMs: 300 });
		ta.setQuery('foo');
		expect(fetchUrl).not.toHaveBeenCalled();
		await vi.advanceTimersByTimeAsync(300);
		expect(fetchUrl).toHaveBeenCalledWith('foo');
		expect(ta.results).toEqual([{ id: '1', name: 'Foo' }]);
	});

	it('close() resets isOpen', () => {
		const ta = createTypeahead({ fetchUrl: vi.fn().mockResolvedValue([]) });
		ta.setQuery('foo');
		expect(ta.isOpen).toBe(true);
		ta.close();
		expect(ta.isOpen).toBe(false);
	});

	it('select(item) calls onSelect and closes dropdown', () => {
		const onSelect = vi.fn();
		const ta = createTypeahead({
			fetchUrl: vi.fn().mockResolvedValue([]),
			onSelect
		});
		ta.setQuery('foo');
		ta.select({ id: '1', name: 'Foo' });
		expect(onSelect).toHaveBeenCalledWith({ id: '1', name: 'Foo' });
		expect(ta.isOpen).toBe(false);
	});

	it('debounce coalesces rapid setQuery calls', async () => {
		const fetchUrl = vi.fn().mockResolvedValue([]);
		const ta = createTypeahead({ fetchUrl, debounceMs: 300 });
		ta.setQuery('f');
		ta.setQuery('fo');
		ta.setQuery('foo');
		await vi.advanceTimersByTimeAsync(300);
		expect(fetchUrl).toHaveBeenCalledTimes(1);
		expect(fetchUrl).toHaveBeenCalledWith('foo');
	});

	it('fetch error logs to console.error and sets results to empty', async () => {
		const fetchUrl = vi.fn().mockRejectedValue(new Error('network error'));
		const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
		const ta = createTypeahead({ fetchUrl, debounceMs: 0 });
		ta.setQuery('foo');
		await vi.advanceTimersByTimeAsync(0);
		expect(errorSpy).toHaveBeenCalled();
		expect(ta.results).toEqual([]);
		errorSpy.mockRestore();
	});

	it('fetch error sets the error flag so callers can render a failure state', async () => {
		const fetchUrl = vi.fn().mockRejectedValue(new Error('500'));
		const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
		const ta = createTypeahead({ fetchUrl, debounceMs: 0 });
		expect(ta.error).toBe(false);
		ta.setQuery('foo');
		await vi.advanceTimersByTimeAsync(0);
		expect(ta.error).toBe(true);
		errorSpy.mockRestore();
	});

	it('error flag clears on the next successful fetch', async () => {
		const fetchUrl = vi
			.fn()
			.mockRejectedValueOnce(new Error('500'))
			.mockResolvedValueOnce([{ id: '1' }]);
		const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
		const ta = createTypeahead({ fetchUrl, debounceMs: 0 });
		ta.setQuery('foo');
		await vi.advanceTimersByTimeAsync(0);
		expect(ta.error).toBe(true);
		ta.setQuery('foob');
		await vi.advanceTimersByTimeAsync(0);
		expect(ta.error).toBe(false);
		expect(ta.results).toEqual([{ id: '1' }]);
		errorSpy.mockRestore();
	});

	it('sets loading immediately on setQuery so empty results read as pending, not "no results"', async () => {
		const fetchUrl = vi.fn().mockResolvedValue([]);
		const ta = createTypeahead({ fetchUrl, debounceMs: 300 });
		ta.setQuery('foo');
		// During the debounce window no fetch has run yet — callers must be able to
		// distinguish "still searching" from "searched, zero hits".
		expect(ta.loading).toBe(true);
		await vi.advanceTimersByTimeAsync(300);
		expect(ta.loading).toBe(false);
	});

	it('close() cancels the pending debounce — no stale fetch fires, loading resets', async () => {
		const fetchUrl = vi.fn().mockResolvedValue([{ id: '1' }]);
		const ta = createTypeahead({ fetchUrl, debounceMs: 300 });
		ta.setQuery('foo');
		expect(ta.loading).toBe(true);
		ta.close();
		expect(ta.loading).toBe(false);
		await vi.advanceTimersByTimeAsync(300);
		expect(fetchUrl).not.toHaveBeenCalled();
	});

	it('setActiveIndex updates activeIndex', () => {
		const ta = createTypeahead({ fetchUrl: vi.fn().mockResolvedValue([]) });
		expect(ta.activeIndex).toBe(-1);
		ta.setActiveIndex(2);
		expect(ta.activeIndex).toBe(2);
	});
});
