import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent, screen, waitFor } from '@testing-library/svelte';
import { tick } from 'svelte';
import OptionListbox from './OptionListbox.svelte';
const baseOptions = [
    { label: 'Option A', value: 'a' },
    { label: 'Option B', value: 'b' },
    { label: 'Option C', value: 'c', disabled: true },
];
const sectionOptions = [
    { label: 'Group 1', value: 'group1', type: 'section' },
    { label: 'Option A', value: 'a' },
    { label: 'Option B', value: 'b' },
    { label: 'Group 2', value: 'group2', type: 'section' },
    { label: 'Option C', value: 'c' },
];
function renderListbox(props = {}) {
    return render(OptionListbox, {
        props: {
            id: 'test-listbox',
            options: baseOptions,
            value: null,
            activeindex: -1,
            open: true,
            ...props,
        },
    });
}
describe('Rendering', () => {
    it('renders all options', () => {
        renderListbox();
        expect(screen.getByText('Option A')).toBeTruthy();
        expect(screen.getByText('Option B')).toBeTruthy();
        expect(screen.getByText('Option C')).toBeTruthy();
    });
    it('renders the search input when search=true', () => {
        renderListbox({ search: true });
        expect(screen.getByPlaceholderText('Find an option...')).toBeTruthy();
    });
    it('does not render the search input when search=false', () => {
        renderListbox({ search: false });
        expect(screen.queryByPlaceholderText('Find an option...')).toBeNull();
    });
    it('renders Select all / Clear buttons when multiple + actions', () => {
        renderListbox({ multiple: true, actions: true, value: [] });
        expect(screen.getByText('Select all')).toBeTruthy();
        expect(screen.getByText('Clear')).toBeTruthy();
    });
    it('does not render action buttons when actions=false', () => {
        renderListbox({ multiple: true, actions: false, value: [] });
        expect(screen.queryByText('Select all')).toBeNull();
    });
    it('uses custom labels for search placeholder, select and clear', () => {
        renderListbox({
            multiple: true,
            actions: true,
            value: [],
            search: true,
            searchplaceholder: 'Rechercher...',
            selectlabel: 'Tout sélectionner',
            clearlabel: 'Effacer',
        });
        expect(screen.getByPlaceholderText('Rechercher...')).toBeTruthy();
        expect(screen.getByText('Tout sélectionner')).toBeTruthy();
        expect(screen.getByText('Effacer')).toBeTruthy();
    });
    it('renders section headers with role="presentation" when checkablesections=false', () => {
        const { container } = renderListbox({ options: sectionOptions });
        const sections = container.querySelectorAll('[role="presentation"]');
        expect(sections.length).toBeGreaterThan(0);
    });
    it('renders section headers with role="option" when checkablesections=true and multiple=true', () => {
        const { container } = renderListbox({
            options: sectionOptions,
            checkablesections: true,
            multiple: true,
            value: [],
        });
        const listItems = container.querySelectorAll('[role="option"]');
        expect(listItems.length).toBe(sectionOptions.length);
    });
    it('marks disabled options with aria-disabled', () => {
        const { container } = renderListbox();
        const disabledOption = container.querySelector('[aria-disabled="true"]');
        expect(disabledOption).toBeTruthy();
    });
    it('renders the listbox with role="listbox"', () => {
        renderListbox();
        expect(screen.getByRole('listbox')).toBeTruthy();
    });
    it('sets aria-multiselectable when multiple=true', () => {
        renderListbox({ multiple: true, value: [] });
        const listbox = screen.getByRole('listbox');
        expect(listbox.getAttribute('aria-multiselectable')).toBe('true');
    });
    it('renders item content (secondary text) when provided', () => {
        const options = [{ label: 'Option A', value: 'a', content: 'Extra info' }];
        renderListbox({ options });
        expect(screen.getByText('Extra info')).toBeTruthy();
    });
});
describe('Single value', () => {
    it('selects an option on click', async () => {
        renderListbox({ value: null });
        const optionA = screen.getByText('Option A').closest('li');
        await fireEvent.click(optionA);
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
    it('deselects an already selected option on click', async () => {
        renderListbox({ value: 'a' });
        const optionA = screen.getByText('Option A').closest('li');
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        await fireEvent.click(optionA);
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('does not select disabled options', async () => {
        renderListbox({ value: null });
        const optionC = screen.getByText('Option C').closest('li');
        await fireEvent.click(optionC);
        expect(optionC.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
});
describe('Multiple values', () => {
    it('selects multiple options by clicking', async () => {
        renderListbox({ multiple: true, value: [] });
        const optionA = screen.getByText('Option A').closest('li');
        const optionB = screen.getByText('Option B').closest('li');
        await fireEvent.click(optionA);
        await fireEvent.click(optionB);
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
    it('deselects an option in multiple mode', async () => {
        renderListbox({ multiple: true, value: ['a', 'b'] });
        const optionA = screen.getByText('Option A').closest('li');
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        await fireEvent.click(optionA);
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('selects all options when "Select all" is clicked', async () => {
        renderListbox({ multiple: true, actions: true, value: [] });
        await fireEvent.click(screen.getByText('Select all'));
        const items = screen.getAllByRole('option').filter((el) => !el.getAttribute('aria-disabled'));
        items.forEach((item) => {
            expect(item.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        });
    });
    it('clears all selections when "Clear" is clicked', async () => {
        renderListbox({ multiple: true, actions: true, value: ['a', 'b'] });
        await fireEvent.click(screen.getByText('Clear'));
        const selected = screen
            .getAllByRole('option')
            .filter((el) => el.classList.contains('mc-option-listbox__item--selected'));
        expect(selected.length).toBe(0);
    });
});
describe('Checkable sections', () => {
    function renderWithSections(extra = {}) {
        return renderListbox({
            options: sectionOptions,
            checkablesections: true,
            multiple: true,
            value: [],
            ...extra,
        });
    }
    it('selects all items in a section when the section header is clicked', async () => {
        renderWithSections();
        const sectionHeader = screen.getByText('Group 1').closest('li');
        await fireEvent.click(sectionHeader);
        const optionA = screen.getByText('Option A').closest('li');
        const optionB = screen.getByText('Option B').closest('li');
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
    it('deselects all items in a section when the fully-selected header is clicked again', async () => {
        renderWithSections({ value: ['a', 'b'] });
        const sectionHeader = screen.getByText('Group 1').closest('li');
        expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        await fireEvent.click(sectionHeader);
        const optionA = screen.getByText('Option A').closest('li');
        const optionB = screen.getByText('Option B').closest('li');
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(false);
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('shows indeterminate state when only some items in a section are selected', async () => {
        renderWithSections({ value: ['a'] });
        const sectionHeader = screen.getByText('Group 1').closest('li');
        expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        const optionB = screen.getByText('Option B').closest('li');
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('is not indeterminate when no items in the section are selected', () => {
        renderWithSections({ value: [] });
        const sectionHeader = screen.getByText('Group 1').closest('li');
        expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('is not indeterminate when all items in the section are selected', () => {
        renderWithSections({ value: ['a', 'b'] });
        const sectionHeader = screen.getByText('Group 1').closest('li');
        expect(sectionHeader.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        const optionB = screen.getByText('Option B').closest('li');
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
    it('clicking an indeterminate section header selects all its items', async () => {
        renderWithSections({ value: ['a'] });
        const sectionHeader = screen.getByText('Group 1').closest('li');
        await fireEvent.click(sectionHeader);
        const optionA = screen.getByText('Option A').closest('li');
        const optionB = screen.getByText('Option B').closest('li');
        expect(optionA.classList.contains('mc-option-listbox__item--selected')).toBe(true);
        expect(optionB.classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
});
describe('Search', () => {
    it('filters options as the user types', async () => {
        renderListbox({ search: true });
        const input = screen.getByPlaceholderText('Find an option...');
        await fireEvent.input(input, { target: { value: 'Option A' } });
        await waitFor(() => {
            expect(screen.queryByText('Option B')).toBeNull();
            expect(screen.getByText('Option A')).toBeTruthy();
        }, { timeout: 500 });
    });
    it('shows all options when the search is cleared', async () => {
        renderListbox({ search: true });
        const input = screen.getByPlaceholderText('Find an option...');
        await fireEvent.input(input, { target: { value: 'Option A' } });
        await fireEvent.input(input, { target: { value: '' } });
        await waitFor(() => {
            expect(screen.getByText('Option A')).toBeTruthy();
            expect(screen.getByText('Option B')).toBeTruthy();
        }, { timeout: 500 });
    });
});
async function fireOnInput(key, opts = {}) {
    const input = screen.getByRole('combobox');
    await fireEvent.keyDown(input, { key, ...opts });
    await tick();
}
async function callKeydown(component, key, opts = {}) {
    const event = new KeyboardEvent('keydown', { key, cancelable: true, bubbles: true, ...opts });
    component.handleKeydown(event);
    await tick();
}
describe('Keyboard navigation', () => {
    it('moves active index forward with ArrowDown', async () => {
        const { component } = renderListbox({ open: true, activeindex: 0 });
        await callKeydown(component, 'ArrowDown');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
    });
    it('moves active index forward with ArrowDown', async () => {
        renderListbox({ open: true, activeindex: 0, search: true });
        await fireOnInput('ArrowDown');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
    });
    it('moves active index backward with ArrowUp (via exported method)', async () => {
        const { component } = renderListbox({ open: true, activeindex: 1 });
        await callKeydown(component, 'ArrowUp');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
    });
    it('wraps to last selectable item when ArrowUp is pressed from index 0', async () => {
        const { component } = renderListbox({ open: true, activeindex: 0 });
        await callKeydown(component, 'ArrowUp');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[1].classList.contains('mc-option-listbox__item--active')).toBe(true);
    });
    it('wraps to first selectable item when ArrowDown is pressed from last selectable', async () => {
        const { component } = renderListbox({ open: true, activeindex: 1 });
        await callKeydown(component, 'ArrowDown');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
    });
    it('selects the active item when Enter is pressed', async () => {
        const { component } = renderListbox({ open: true, activeindex: 0 });
        await callKeydown(component, 'Enter');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[0].classList.contains('mc-option-listbox__item--selected')).toBe(true);
    });
    it('does not select a disabled item when Enter is pressed on it', async () => {
        const { component } = renderListbox({ open: true, activeindex: 2 });
        await callKeydown(component, 'Enter');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[2].classList.contains('mc-option-listbox__item--selected')).toBe(false);
    });
    it('calls onclose when Escape is pressed (via exported method)', async () => {
        const onclose = vi.fn();
        const { component } = renderListbox({ open: true, onclose });
        await callKeydown(component, 'Escape');
        expect(onclose).toHaveBeenCalledOnce();
    });
    it('calls onclose when Escape is pressed (via search input)', async () => {
        const onclose = vi.fn();
        renderListbox({ open: true, search: true, onclose });
        await fireOnInput('Escape');
        expect(onclose).toHaveBeenCalledOnce();
    });
    it('calls onopen when ArrowDown is pressed while the listbox is closed', async () => {
        const onopen = vi.fn();
        const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
        await callKeydown(component, 'ArrowDown');
        expect(onopen).toHaveBeenCalledOnce();
    });
    it('calls onopen when ArrowUp is pressed while the listbox is closed', async () => {
        const onopen = vi.fn();
        const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
        await callKeydown(component, 'ArrowUp');
        expect(onopen).toHaveBeenCalledOnce();
    });
    it('calls onopen when Enter is pressed while the listbox is closed', async () => {
        const onopen = vi.fn();
        const { component } = renderListbox({ open: false, onopen, activeindex: -1 });
        await callKeydown(component, 'Enter');
        expect(onopen).toHaveBeenCalledOnce();
    });
    it('skips disabled options during downward keyboard navigation', async () => {
        const { component } = renderListbox({ open: true, activeindex: 1 });
        await callKeydown(component, 'ArrowDown');
        const items = screen.getByRole('listbox').querySelectorAll('li');
        expect(items[0].classList.contains('mc-option-listbox__item--active')).toBe(true);
        expect(items[2].classList.contains('mc-option-listbox__item--active')).toBe(false);
    });
});
describe('Readonly', () => {
    it('applies readonly class to all items when readonly=true', () => {
        const { container } = renderListbox({ readonly: true });
        const items = container.querySelectorAll('.mc-option-listbox__item--readonly');
        expect(items.length).toBe(baseOptions.length);
    });
});
describe('ARIA', () => {
    it('sets aria-activedescendant on the search input when open', () => {
        renderListbox({ search: true, activeindex: 0 });
        const input = screen.getByRole('combobox');
        expect(input.getAttribute('aria-activedescendant')).toBe('option-test-listbox-0');
    });
    it('sets aria-expanded on the search combobox', () => {
        renderListbox({ search: true, open: true });
        const input = screen.getByRole('combobox');
        expect(input.getAttribute('aria-expanded')).toBe('true');
    });
    it('sets correct id on each option element', () => {
        const { container } = renderListbox();
        const firstOption = container.querySelector('#option-test-listbox-0');
        expect(firstOption).toBeTruthy();
    });
});
