/**
 * Tests for components/custom_dropdown.js
 *
 * Tests the custom dropdown component's core behavior:
 * setup, filtering, selection, keyboard navigation, and cleanup.
 */

import '@js/security/xss-protection.js';
import '@js/components/custom_dropdown.js';

const setupCustomDropdown = window.setupCustomDropdown;
const updateDropdownOptions = window.updateDropdownOptions;

describe('setupCustomDropdown', () => {
    let input, hiddenInput, dropdownList, onSelect, options;

    beforeEach(() => {
        input = document.createElement('input');
        input.type = 'text';
        input.id = 'test-dropdown-input';

        hiddenInput = document.createElement('input');
        hiddenInput.type = 'hidden';
        hiddenInput.id = 'test-dropdown-input_hidden';

        dropdownList = document.createElement('div');
        dropdownList.id = 'test-dropdown-list';

        const wrapper = document.createElement('div');
        wrapper.appendChild(input);
        wrapper.appendChild(hiddenInput);
        wrapper.appendChild(dropdownList);
        document.body.appendChild(wrapper);

        onSelect = vi.fn();

        options = [
            { value: 'gpt4', label: 'GPT-4' },
            { value: 'claude', label: 'Claude' },
            { value: 'llama', label: 'Llama 3' },
            { value: 'mistral', label: 'Mistral' },
        ];
    });

    afterEach(() => {
        const wrapper = input.closest('div');
        if (wrapper && wrapper.parentNode) {
            wrapper.remove();
        }
        // Clean up any detached dropdown lists
        const detached = document.getElementById('test-dropdown-list');
        if (detached) detached.remove();
    });

    it('returns control functions', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        expect(dd.updateDropdown).toBeTypeOf('function');
        expect(dd.showDropdown).toBeTypeOf('function');
        expect(dd.hideDropdown).toBeTypeOf('function');
        expect(dd.destroy).toBeTypeOf('function');
        expect(dd.setValue).toBeTypeOf('function');
        dd.destroy();
    });

    it('initially hides the dropdown', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        expect(dropdownList.style.display).toBe('none');
        dd.destroy();
    });

    it('shows dropdown on input click', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        expect(dropdownList.style.display).toBe('block');
        dd.destroy();
    });

    it('populates dropdown with all options on click', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        expect(items.length).toBe(4);
        dd.destroy();
    });

    it('filters options when typing', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.value = 'Cl';
        input.dispatchEvent(new Event('input'));
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        expect(items.length).toBe(1);
        expect(items[0].getAttribute('data-value')).toBe('claude');
        dd.destroy();
    });

    it('shows "no results" when nothing matches', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.value = 'zzzzz';
        input.dispatchEvent(new Event('input'));
        expect(dropdownList.querySelector('.ldr-custom-dropdown-no-results')).not.toBeNull();
        dd.destroy();
    });

    it('calls onSelect when an option is clicked', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        items[1].click(); // Click "Claude"
        expect(onSelect).toHaveBeenCalledWith('claude', options[1]);
        dd.destroy();
    });

    it('updates input value on selection', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        items[0].click(); // Click "GPT-4"
        expect(input.value).toBe('GPT-4');
        dd.destroy();
    });

    it('updates hidden input on selection', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        items[0].click();
        expect(hiddenInput.value).toBe('gpt4');
        dd.destroy();
    });

    it('hides dropdown after selection', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        items[0].click();
        expect(dropdownList.style.display).toBe('none');
        dd.destroy();
    });

    it('hides dropdown on Escape key', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        input.click();
        expect(dropdownList.style.display).toBe('block');
        input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
        expect(dropdownList.style.display).toBe('none');
        dd.destroy();
    });

    it('sets ARIA attributes for accessibility', () => {
        const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
        // Initially closed
        expect(input.getAttribute('aria-expanded')).toBe('false');
        // Open
        input.click();
        expect(input.getAttribute('aria-expanded')).toBe('true');

        // Items have role="option"
        const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
        items.forEach(item => {
            expect(item.getAttribute('role')).toBe('option');
        });
        dd.destroy();
    });

    describe('setValue', () => {
        it('sets value by matching option value', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            dd.setValue('mistral');
            expect(input.value).toBe('Mistral');
            expect(hiddenInput.value).toBe('mistral');
            dd.destroy();
        });

        it('calls onSelect callback', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            dd.setValue('claude');
            expect(onSelect).toHaveBeenCalledWith('claude', options[1]);
            dd.destroy();
        });

        it('clears input for unknown value when custom values disallowed', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect, false);
            dd.setValue('unknown-model');
            expect(input.value).toBe('');
            dd.destroy();
        });

        it('sets raw value when custom values are allowed', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect, true);
            dd.setValue('custom-model');
            expect(input.value).toBe('custom-model');
            dd.destroy();
        });
    });

    describe('destroy', () => {
        it('cleans up event listeners and registry', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            dd.destroy();
            // After destroy, input events should not trigger dropdown
            input.click();
            // Dropdown should stay hidden (no listener to open it)
            expect(dropdownList.style.display).toBe('none');
        });
    });

    describe('keyboard navigation', () => {
        it('ArrowDown opens dropdown and selects first item', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
            expect(dropdownList.style.display).toBe('block');
            const active = dropdownList.querySelector('.active');
            expect(active).not.toBeNull();
            dd.destroy();
        });

        it('Enter selects highlighted item', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            // Open and select first
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
            // Select it
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
            expect(onSelect).toHaveBeenCalled();
            dd.destroy();
        });
    });

    describe('group headers', () => {
        const grouped = [
            { value: 'fav1', label: 'Pinned One', group_label: 'Favorites' },
            { value: 'arxiv', label: 'ArXiv', group_label: 'Academic' },
            { value: 'pubmed', label: 'PubMed', group_label: 'Academic' },
            { value: 'tavily', label: 'Tavily', group_label: 'API key' },
        ];

        it('renders one non-selectable header per band, in order', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.click();
            const headers = dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header');
            expect(Array.from(headers).map(h => h.textContent)).toEqual([
                'Favorites', 'Academic', 'API key',
            ]);
            headers.forEach(h =>
                expect(h.classList.contains('ldr-custom-dropdown-item')).toBe(false)
            );
            dd.destroy();
        });

        it('keeps the selectable option count equal to items (headers excluded)', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.click();
            const items = dropdownList.querySelectorAll('.ldr-custom-dropdown-item');
            expect(items.length).toBe(4);
            dd.destroy();
        });

        it('shows a band header once even when it has multiple items', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.click();
            const academic = Array.from(
                dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header')
            ).filter(h => h.textContent === 'Academic');
            expect(academic.length).toBe(1);
            dd.destroy();
        });

        it('hides a band header when filtering removes all its items', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.value = 'arxiv';
            input.dispatchEvent(new Event('input'));
            const headerTexts = Array.from(
                dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header')
            ).map(h => h.textContent);
            expect(headerTexts).toEqual(['Academic']);
            dd.destroy();
        });

        it('renders no headers when options have no group_label', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => options, onSelect);
            input.click();
            const headers = dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header');
            expect(headers.length).toBe(0);
            dd.destroy();
        });

        it('marks headers as presentational so assistive tech skips them', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.click();
            const header = dropdownList.querySelector('.ldr-custom-dropdown-group-header');
            expect(header.getAttribute('role')).toBe('presentation');
            expect(header.getAttribute('aria-hidden')).toBe('true');
            dd.destroy();
        });

        it('keyboard navigation lands on an item, never a header', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            // First ArrowDown must select the first ITEM (fav1), not the
            // 'Favorites' header that precedes it.
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
            const active = dropdownList.querySelector('.active');
            expect(active.classList.contains('ldr-custom-dropdown-item')).toBe(true);
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
            expect(onSelect).toHaveBeenCalledWith('fav1', expect.objectContaining({ value: 'fav1' }));
            dd.destroy();
        });

        it('renders both favorite stars and band headers when both apply', () => {
            const onFavoriteToggle = vi.fn();
            const dd = setupCustomDropdown(
                input, dropdownList, () => grouped, onSelect, false, 'No results found.', onFavoriteToggle
            );
            input.click();
            expect(dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header').length).toBe(3);
            expect(dropdownList.querySelectorAll('.ldr-dropdown-favorite-star').length).toBe(4);
            dd.destroy();
        });

        it('updateDropdownOptions re-renders band headers for the open list', () => {
            const dd = setupCustomDropdown(input, dropdownList, () => grouped, onSelect);
            input.click();
            const newOptions = [
                { value: 'wiki', label: 'Wikipedia', group_label: 'No API key' },
                { value: 'serper', label: 'Serper', group_label: 'API key' },
            ];
            updateDropdownOptions(input, newOptions);
            const headerTexts = Array.from(
                dropdownList.querySelectorAll('.ldr-custom-dropdown-group-header')
            ).map(h => h.textContent);
            expect(headerTexts).toEqual(['No API key', 'API key']);
            expect(dropdownList.querySelectorAll('.ldr-custom-dropdown-item').length).toBe(2);
            dd.destroy();
        });
    });
});

describe('updateDropdownOptions', () => {
    it('does nothing for null input', () => {
        expect(() => updateDropdownOptions(null, [])).not.toThrow();
    });
});
