'use strict';

/* eslint-disable max-nested-callbacks */

const mockData = [
	{id: '123', bpid: '123', name: 'Test University', city: 'London', country: 'GB'},
	{id: '456', bpid: '456', name: 'Example College', city: 'Paris', country: 'FR'},
	{id: '789', bpid: '789', name: 'Sample Institute', city: 'Berlin', country: 'DE'}
];

const fixture = `
	<div class="eds-c-autocomplete" data-eds-c-autocomplete>
		<input type="text" id="test-input" name="test" />
		<div class="eds-c-autocomplete__dropdown" id="test-dropdown"></div>
	</div>
`;

describe('EDS Component Autocomplete JavaScript Functionality', () => {
	let wrapper;
	let autocomplete;
	let EdsCAutocomplete;

	beforeAll(async () => {
		const module = await import('./eds-c-autocomplete.js');
		EdsCAutocomplete = module.default;
	});

	beforeEach(() => {
		document.body.innerHTML = fixture;
		wrapper = document.querySelector('[data-eds-c-autocomplete]');
		
		// Mock fetch globally
		global.fetch = jest.fn();
		
		// Mock scrollIntoView (not supported in jsdom)
		Element.prototype.scrollIntoView = jest.fn();
	});

	afterEach(() => {
		if (autocomplete && autocomplete.destroy) {
			autocomplete.destroy();
		}
		document.body.innerHTML = '';
		jest.clearAllMocks();
		jest.restoreAllMocks();
	});

	describe('Initialization', () => {
		test('should initialize with default options', () => {
			autocomplete = new EdsCAutocomplete(wrapper);
			
			expect(autocomplete.wrapper).toBe(wrapper);
			expect(autocomplete.minChars).toBe(2);
			expect(autocomplete.debounceDelay).toBe(300);
			expect(autocomplete.apiMethod).toBe('GET');
		});

		test('should initialize with custom options', () => {
			autocomplete = new EdsCAutocomplete(wrapper, {
				minChars: 3,
				debounceDelay: 500,
				apiUrl: '/api/search',
				apiMethod: 'POST'
			});
			
			expect(autocomplete.minChars).toBe(3);
			expect(autocomplete.debounceDelay).toBe(500);
			expect(autocomplete.apiUrl).toBe('/api/search');
			expect(autocomplete.apiMethod).toBe('POST');
		});

		test('should set ARIA attributes on input', () => {
			autocomplete = new EdsCAutocomplete(wrapper);
			const input = wrapper.querySelector('input');
			
			expect(input.getAttribute('role')).toBe('combobox');
			expect(input.getAttribute('aria-autocomplete')).toBe('list');
			expect(input.getAttribute('aria-expanded')).toBe('false');
			expect(input.getAttribute('aria-controls')).toBeTruthy();
		});

		test('should set ARIA attributes on dropdown', () => {
			autocomplete = new EdsCAutocomplete(wrapper);
			const dropdown = wrapper.querySelector('.eds-c-autocomplete__dropdown');
			
			expect(dropdown.getAttribute('role')).toBe('listbox');
			expect(dropdown.getAttribute('aria-label')).toBe('Search results');
		});
	});

	describe('API Integration', () => {
		beforeEach(() => {
			jest.useFakeTimers();
		});

		afterEach(() => {
			jest.runOnlyPendingTimers();
			jest.useRealTimers();
		});

		test('should fetch results from API with GET method', async () => {
			global.fetch.mockResolvedValueOnce({
				ok: true,
				json: async () => mockData
			});

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				minChars: 2
			});

			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			// Fast-forwards all pending timers (like setTimeout, setInterval, debounce delays) that have been queued
			jest.runAllTimers();
			// Waits for the next tick of the event loop. Ensures Promise resolution happens before the test continues
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(global.fetch).toHaveBeenCalledWith(
				'/api/autocomplete?q=test',
				expect.objectContaining({
					method: 'GET',
					credentials: 'same-origin'
				})
			);
		});

		test('should fetch results from API with POST method', async () => {
			global.fetch.mockResolvedValueOnce({
				ok: true,
				json: async () => mockData
			});

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				apiMethod: 'POST',
				minChars: 2
			});

			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(global.fetch).toHaveBeenCalledWith(
				'/api/autocomplete',
				expect.objectContaining({
					method: 'POST',
					body: JSON.stringify({q: 'test'})
				})
			);
		});

		test('should send POST request with urlencoded body when apiBodyFormat is urlencoded', async () => {
			global.fetch.mockResolvedValueOnce({
				ok: true,
				json: async () => mockData
			});

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				apiMethod: 'POST',
				apiBodyFormat: 'urlencoded',
				minChars: 2
			});

			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(global.fetch).toHaveBeenCalledWith(
				'/api/autocomplete',
				expect.objectContaining({
					method: 'POST',
					headers: expect.objectContaining({
						'Content-Type': 'application/x-www-form-urlencoded'
					}),
					body: 'q=test'
				})
			);
		});

		test('should transform API response', async () => {
			const apiResponse = {
				results: [{id: '1', title: 'Test', location: {city: 'London', country: 'GB'}}]
			};

			global.fetch.mockResolvedValueOnce({
				ok: true,
				json: async () => apiResponse
			});

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				minChars: 2,
				transformResponse: (data) => {
					return data.results.map(item => ({
						bpid: item.id,
						name: item.title,
						city: item.location.city,
						country: item.location.country
					}));
				}
			});

			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(autocomplete.filteredResults).toHaveLength(1);
			expect(autocomplete.filteredResults[0]).toEqual({
				bpid: '1',
				name: 'Test',
				city: 'London',
				country: 'GB'
			});
		});

		test('should handle API errors gracefully', async () => {
			global.fetch.mockRejectedValueOnce(new Error('Network error'));
			const consoleSpy = jest.spyOn(console, 'error').mockImplementation();

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				minChars: 2
			});

			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(consoleSpy).toHaveBeenCalled();
			expect(autocomplete.filteredResults).toHaveLength(0);
			
			// Wait for setTimeout in _renderNoResults
			jest.advanceTimersByTime(100);
			
			const dropdown = wrapper.querySelector('.eds-c-autocomplete__dropdown');
			expect(dropdown.innerHTML).toContain('No results found');
			
			consoleSpy.mockRestore();
		});

		test('should validate query length', async () => {
			const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();

			autocomplete = new EdsCAutocomplete(wrapper, {
				apiUrl: '/api/autocomplete',
				maxQueryLength: 10,
				minChars: 2
			});

			const input = wrapper.querySelector('input');
			input.value = 'a'.repeat(20); // Exceeds maxQueryLength
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			expect(consoleSpy).toHaveBeenCalledWith('Query exceeds maximum length');
			expect(global.fetch).not.toHaveBeenCalled();
			
			consoleSpy.mockRestore();
		});
	});

	describe('Dropdown behavior', () => {
		beforeEach(() => {
			jest.useFakeTimers();
			global.fetch.mockResolvedValue({ ok: true, json: async () => mockData });
			autocomplete = new EdsCAutocomplete(wrapper, {minChars: 2, apiUrl: '/api/autocomplete'});
		});

		afterEach(() => {
			jest.runOnlyPendingTimers();
			jest.useRealTimers();
		});

		test('should not open dropdown if query is less than minChars', () => {
			const input = wrapper.querySelector('input');
			input.value = 'a';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();

			expect(autocomplete.isOpen).toBe(false);
		});

		test('should open dropdown when query meets minChars', () => {
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();

			expect(autocomplete.isOpen).toBe(true);
			expect(input.getAttribute('aria-expanded')).toBe('true');
		});

		test('should not auto-focus first item when dropdown opens', async () => {
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			const firstOption = wrapper.querySelector('[role="option"]');
			expect(input.hasAttribute('aria-activedescendant')).toBe(false);
			expect(firstOption.getAttribute('aria-selected')).toBe('false');
			expect(firstOption.classList.contains('eds-c-autocomplete__item--active')).toBe(false);
		});

		test('should close dropdown on Escape key', () => {
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));
			jest.runAllTimers();

			const escapeEvent = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true});
			input.dispatchEvent(escapeEvent);

			expect(autocomplete.isOpen).toBe(false);
			expect(input.getAttribute('aria-expanded')).toBe('false');
		});

		test('should close dropdown on Tab key', () => {
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));
			jest.runAllTimers();

			const tabEvent = new KeyboardEvent('keydown', {key: 'Tab', bubbles: true});
			input.dispatchEvent(tabEvent);

			expect(autocomplete.isOpen).toBe(false);
		});

		test('should close dropdown when clicking outside', () => {
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));
			jest.runAllTimers();

			expect(autocomplete.isOpen).toBe(true);

			document.body.click();

			expect(autocomplete.isOpen).toBe(false);
		});

		test('should render no results message when no matches found', async () => {
			const input = wrapper.querySelector('input');
			global.fetch.mockResolvedValueOnce({ ok: true, json: async () => [] });
			input.value = 'zzzzz';
			input.dispatchEvent(new Event('input', {bubbles: true}));

			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);

			// Wait for setTimeout in _renderNoResults
			jest.advanceTimersByTime(100);

			const dropdown = wrapper.querySelector('.eds-c-autocomplete__dropdown');
			expect(dropdown.innerHTML).toContain('No results found');
		});
	});

	describe('Keyboard navigation', () => {
		beforeEach(async () => {
			jest.useFakeTimers();
			global.fetch.mockResolvedValue({ ok: true, json: async () => mockData });
			autocomplete = new EdsCAutocomplete(wrapper, {minChars: 1, apiUrl: '/api/autocomplete'});
			
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));
			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);
		});

		afterEach(() => {
			jest.runOnlyPendingTimers();
			jest.useRealTimers();
		});

		test('should move focus down with ArrowDown', () => {
			const input = wrapper.querySelector('input');
			const options = wrapper.querySelectorAll('[role="option"]');
			
			// No option should be focused initially
			expect(options[0].classList.contains('eds-c-autocomplete__item--active')).toBe(false);
			expect(input.hasAttribute('aria-activedescendant')).toBe(false);
			
			// Press ArrowDown to focus first option
			const arrowDownEvent = new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true});
			input.dispatchEvent(arrowDownEvent);
			
			// First option should now be active
			expect(options[0].classList.contains('eds-c-autocomplete__item--active')).toBe(true);
			expect(options[0].getAttribute('aria-selected')).toBe('true');
			expect(input.getAttribute('aria-activedescendant')).toBe(options[0].id);
		});

		test('should move focus up with ArrowUp', () => {
			const input = wrapper.querySelector('input');
			const options = wrapper.querySelectorAll('[role="option"]');
			
			// Move down to first option, then to second option
			input.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true}));
			input.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true}));
			
			// Second option should be active (index 1)
			expect(options[1].classList.contains('eds-c-autocomplete__item--active')).toBe(true);
			
			// Press ArrowUp to move back to first option
			input.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowUp', bubbles: true}));
			
			// First option should now be active
			expect(options[0].classList.contains('eds-c-autocomplete__item--active')).toBe(true);
			expect(options[0].getAttribute('aria-selected')).toBe('true');
		});
	});

	describe('Item selection', () => {
		beforeEach(async () => {
			jest.useFakeTimers();
			global.fetch.mockResolvedValue({ ok: true, json: async () => mockData });
			autocomplete = new EdsCAutocomplete(wrapper, {minChars: 1, apiUrl: '/api/autocomplete'});
			
			const input = wrapper.querySelector('input');
			input.value = 'test';
			input.dispatchEvent(new Event('input', {bubbles: true}));
			jest.runAllTimers();
			await new Promise(jest.requireActual('timers').setImmediate);
		});

		afterEach(() => {
			jest.runOnlyPendingTimers();
			jest.useRealTimers();
		});

		test('should select item with Enter key', () => {
			const input = wrapper.querySelector('input');
			let selectedDetail = null;

			wrapper.addEventListener('autocomplete:select', (e) => {
				selectedDetail = e.detail;
			});

			// Press ArrowDown to focus first item, then Enter to select it
			input.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true}));
			input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true}));

			expect(selectedDetail).toBeTruthy();
			expect(selectedDetail.value).toBe('123');
			expect(input.value).toBe('Test University');
		});

		test('should select item on click', () => {
			let selectedDetail = null;

			wrapper.addEventListener('autocomplete:select', (e) => {
				selectedDetail = e.detail;
			});

			const firstOption = wrapper.querySelector('[role="option"]');
			firstOption.click();

			expect(selectedDetail).toBeTruthy();
			expect(selectedDetail.value).toBe('123');
			expect(selectedDetail.displayText).toBe('Test University');
			expect(selectedDetail.item).toEqual(mockData[0]);
		});

		test('should update input value on selection', () => {
			const input = wrapper.querySelector('input');
			const firstOption = wrapper.querySelector('[role="option"]');
			
			firstOption.click();

			expect(input.value).toBe('Test University');
		});

		test('should dispatch selection event and update input', () => {
			const input = wrapper.querySelector('input');
			const firstOption = wrapper.querySelector('[role="option"]');
			
			let eventDispatched = false;
			wrapper.addEventListener('autocomplete:select', () => {
				eventDispatched = true;
			});
			
			firstOption.click();

			expect(eventDispatched).toBe(true);
			expect(input.value).toBe('Test University');
		});

		test('should highlight matching text', () => {
			const firstOption = wrapper.querySelector('[role="option"]');
			const nameElement = firstOption.querySelector('.eds-c-autocomplete__item-name');
			
			expect(nameElement.innerHTML).toContain('<strong>');
		});
	});

	describe('destroy()', () => {
		test('should remove event listeners', () => {
			autocomplete = new EdsCAutocomplete(wrapper);
			const input = wrapper.querySelector('input');
			
			const removeEventListenerSpy = jest.spyOn(input, 'removeEventListener');
			
			autocomplete.destroy();
			
			expect(removeEventListenerSpy).toHaveBeenCalledWith('input', expect.any(Function));
			expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
			expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function));
		});

		test('should remove ARIA attributes', () => {
			autocomplete = new EdsCAutocomplete(wrapper);
			const input = wrapper.querySelector('input');
			
			autocomplete.destroy();
			
			expect(input.hasAttribute('role')).toBe(false);
			expect(input.hasAttribute('aria-autocomplete')).toBe(false);
			expect(input.hasAttribute('aria-expanded')).toBe(false);
			expect(input.hasAttribute('aria-controls')).toBe(false);
		});
	});
});
