import React from 'react';
import { fireEvent, render as renderDom, screen } from '@testing-library/react';
import { testComponent } from 'lib/test-utils/common';
import {
    clickApplyButton,
    findOption,
    getGroup,
    getInput,
    getInputDisplayValues,
    getInputTag,
    getInputWrapper,
    getListTag,
    getOption,
    openMultiSelect,
    queryInputTag,
    queryListTag,
    queryOption,
    querySelectMenu,
    selectOption,
    triggerInputKeyDown,
} from 'lib/test-utils/select';
import { ListViewComponent } from '../../listView';
import { MultiSelect } from '../multiSelect';
import {
    MultiSelectProps,
    SelectDropdownIndicatorComponent,
    SelectInputValueComponent,
    SelectInputComponent,
    SelectOption,
} from '../types';
import { t } from '../../../i18n';

describe('<MultiSelect>', () => {
    const option1 = { label: 'Option1', value: '1', data: { id: 1 } };
    const option2 = { label: 'Option2', value: '2', data: { id: 2 } };
    const option3 = { label: 'Option3', value: '3', data: { id: 3 } };

    const options = [option1, option2];

    const render = (props?: Partial<MultiSelectProps>) =>
        renderDom(<MultiSelect options={options} {...props} />);

    testComponent(MultiSelect, { options }, 'SuiteMultiSelect');

    test('renders dropdown with options', async () => {
        render();

        await openMultiSelect();

        expect(getOption(option1)).toBeInTheDocument();
        expect(getOption(option2)).toBeInTheDocument();
    });

    test('accepts grouped options', async () => {
        const group1 = { label: 'group1', options: [option1, option2] };
        const group2 = { label: 'group2', options: [option3] };

        render({ options: [group1, group2] });

        await openMultiSelect();

        expect(getGroup(group1)).toBeInTheDocument();
        expect(getGroup(group2)).toBeInTheDocument();

        expect(getOption(option1)).toBeInTheDocument();
        expect(getOption(option2)).toBeInTheDocument();
        expect(getOption(option3)).toBeInTheDocument();
    });

    test('renders placeholder', () => {
        const placeholder = 'PLACEHOLDER TEXT';
        const { getByText } = render({ placeholder, options: [] });

        expect(getByText(placeholder)).toBeVisible();
    });

    test('invokes callback when option selected', async () => {
        const onChange = vi.fn();

        render({ onChange });

        await openMultiSelect();

        selectOption(option1);

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith([option1]);
        expect(getInput()).toHaveValue(option1.value);
        expect(getInputDisplayValues()).toEqual([option1.label]);

        selectOption(option2);

        expect(onChange).toHaveBeenCalledTimes(2);
        expect(onChange).toHaveBeenCalledWith([option1, option2]);

        expect(getInputDisplayValues()).toEqual([option1.label, option2.label]);
        expect(getInput()).toHaveValue([option1.value, option2.value].join(','));
    });

    test('removes selected options from the list and adds them as tags', async () => {
        render();

        await openMultiSelect();

        expect(queryInputTag(option1)).toBeNull();
        expect(queryListTag(option1)).toBeNull();

        selectOption(option1);

        expect(getInputTag(option1)).toBeInTheDocument();
        expect(getListTag(option1)).toBeInTheDocument();
        expect(queryOption(option1)).toBeNull();
    });

    test('invokes callback when cleared', async () => {
        const onChange = vi.fn();

        const { getByTestId } = render({
            onChange,
            initialValue: [option2],
        });

        const clearButton = getByTestId('SuiteListViewClearButton');
        fireEvent.click(clearButton);

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith([]);
    });

    test('cannot select disabled options', async () => {
        const onChange = vi.fn();
        const disabledOption = {
            label: 'Option1',
            value: '1',
            data: { id: 1 },
            disabled: true,
        };

        render({
            onChange,
            options: [disabledOption],
        });

        await openMultiSelect();

        selectOption(disabledOption);

        expect(onChange).not.toHaveBeenCalled();
    });

    test('selected values is controllable with the "selectedValue" property', () => {
        const props = {
            selectedValue: undefined,
        };

        const { rerender } = render(props);

        expect(queryInputTag(option1)).toBeNull();

        rerender(<MultiSelect {...props} selectedValue={[option1]} />);

        expect(getInputTag(option1)).toBeVisible();

        rerender(<MultiSelect {...props} selectedValue={undefined} />);

        expect(queryInputTag(option1)).toBeNull();
    });

    test('selected values can be a an array of strings', () => {
        render({
            selectedValue: [option1.value],
        });

        expect(getInputTag(option1)).toBeInTheDocument();
    });

    test('initial values can be a an array of strings', () => {
        render({
            initialValue: [option1.value],
        });

        expect(getInputTag(option1)).toBeInTheDocument();
    });

    test('opens dropdown menu on arrow down', async () => {
        render();

        triggerInputKeyDown('ArrowDown');

        expect(await findOption(option1)).toBeInTheDocument();
    });

    describe('with "disabled" true', () => {
        test('disables the control', async () => {
            render({ disabled: true });

            const input = getInputWrapper();

            expect(input).toHaveClass('disabled');

            fireEvent.click(input);

            expect(querySelectMenu()).toBeNull();
        });
    });

    describe('with "error" true', () => {
        test('applies the invalid styling', () => {
            render({ error: true });

            const input = getInputWrapper();

            expect(input).toHaveClass('is-invalid');
            expect(input).toHaveAttribute('aria-invalid', 'true');
        });

        test('omits invalid attributes by default', () => {
            render();

            const input = getInputWrapper();

            expect(input).not.toHaveClass('is-invalid');
            expect(input).not.toHaveAttribute('aria-invalid');
        });
    });

    describe('with "requireApply" true', () => {
        test('requires clicking the "Apply" button to save changes', async () => {
            render({
                requireApply: true,
            });

            await openMultiSelect();

            selectOption(option1);

            expect(getListTag(option1)).toBeInTheDocument();
            expect(queryInputTag(option1)).toBeNull();

            clickApplyButton();

            expect(getInputTag(option1)).toBeInTheDocument();
            expect(queryListTag(option1)).toBeNull();

            expect(querySelectMenu()).toBeNull();
        });
    });

    describe('with "recentSelections"', () => {
        test('shows recent selection section', async () => {
            const recentOption = {
                label: 'recent test item',
                options: [option1],
            };

            render({
                recentSelections: [recentOption],
            });

            await openMultiSelect();

            selectOption(recentOption);

            expect(queryListTag(option1)).toBeInTheDocument();
            expect(queryInputTag(option1)).toBeInTheDocument();
        });
    });

    describe('with "quickSelections"', () => {
        test('shows quick selection section', async () => {
            const quickOption = {
                label: 'quick test item',
                options: [option1],
            };

            render({
                quickSelections: [quickOption],
            });

            await openMultiSelect();

            selectOption(quickOption);

            expect(queryListTag(option1)).toBeInTheDocument();
            expect(queryInputTag(option1)).toBeInTheDocument();
        });
    });

    test('applies given input "controlId"', () => {
        const controlId = 'test-id';

        const { getByTestId } = render({ controlId });

        const input = getByTestId('SuiteSelectInputValue');
        expect(input.id).toEqual(controlId);
    });

    test('invokes "onSerializeValue" callback with value', async () => {
        const onSerializeValue = (item: SelectOption) => `value:${item.value}`;

        const { getByTestId } = render({
            onSerializeValue,
            selectedValue: [option1.value],
        });

        const input = getByTestId('SuiteSelectInputValue');
        expect(input).toHaveValue('value:1');
    });

    describe('with controlled "open" property', () => {
        test('if open=true forces open state', async () => {
            render({ open: true });

            expect(await screen.findByTestId('SuiteListView')).toBeVisible();
            expect(getOption(option1)).toBeInTheDocument();
            expect(getOption(option2)).toBeInTheDocument();
        });

        test('if open=false forces closed state', async () => {
            const { queryByTestId } = render({ open: false });

            const input = getInput();
            fireEvent.click(input);

            expect(queryByTestId('SuiteListView')).toBeNull();
        });
    });

    describe('with custom components', () => {
        describe('"List"', () => {
            test('renders given component', async () => {
                const List: ListViewComponent<SelectOption, true> = (props) => (
                    <div data-testid="CustomMenuList">{props.options?.length}</div>
                );

                const { getByTestId, queryByTestId } = render({
                    options,
                    components: { List },
                });

                await openMultiSelect();

                expect(getByTestId('CustomMenuList')).toBeInTheDocument();
                expect(queryByTestId('SuiteListViewList')).toBeNull();
            });
        });

        describe('"DropdownIndicator"', () => {
            test('renders given component', () => {
                const DropdownIndicator: SelectDropdownIndicatorComponent = (props) => (
                    <div data-testid="CustomDropdownIndicator">{props.menuOpen}</div>
                );

                const { getByTestId, queryByTestId } = render({
                    components: { DropdownIndicator },
                });

                expect(getByTestId('CustomDropdownIndicator')).toBeInTheDocument();
                expect(queryByTestId('SuiteListViewExpandButton')).toBeNull();
            });
        });

        describe('"InputValue"', () => {
            test('renders given component', () => {
                const InputValue: SelectInputValueComponent<SelectOption, true> = (props) => (
                    <div data-testid="CustomInputValue">{props.value.length}</div>
                );

                const { getByTestId, queryByTestId } = render({
                    components: { InputValue },
                    selectedValue: ['test'],
                });

                expect(getByTestId('CustomInputValue')).toBeInTheDocument();
                expect(queryByTestId('SuiteSelectInputMultiValue')).toBeNull();
                expect(queryByTestId('SuiteSelectInputSingleValue')).toBeNull();
            });
        });

        describe('"SelectInput"', () => {
            test('renders given component', () => {
                const SelectInput: SelectInputComponent<SelectOption, true> = (props) => (
                    <div data-testid="CustomInput">{props.value?.length}</div>
                );

                const { getByTestId, queryByTestId } = render({
                    components: { SelectInput },
                    selectedValue: ['test'],
                });

                expect(getByTestId('CustomInput')).toBeInTheDocument();
                expect(queryByTestId('SuiteSelectInput')).toBeNull();
            });
        });
    });

    describe('with exclude mode', () => {
        test('shows label', async () => {
            const { getByText } = render({ excludeMode: true });
            expect(getByText(t('exclude'))).toBeInTheDocument();
        });

        test('calls callback when exclude is toggled', async () => {
            const onExcludeModeChange = vi.fn();
            const { getByText } = render({ showExcludeModeToggle: true, onExcludeModeChange });
            await openMultiSelect();

            const toggle = getByText(t('exclude'));
            fireEvent.click(toggle);

            expect(onExcludeModeChange).toHaveBeenCalledWith(true);
        });
    });

    describe('disabled options', () => {
        const group1Options = [
            { label: 'Option 1.1', value: 'g1-opt1' },
            { label: 'Option 1.2', value: 'g1-opt2' },
            { label: 'Option 1.3', value: 'g1-opt3', disabled: true },
        ];

        const group2Options = [
            { label: 'Option 2.1', value: 'g2-opt1', disabled: true },
            { label: 'Option 2.2', value: 'g2-opt2', disabled: true },
            { label: 'Option 2.3', value: 'g2-opt3' },
        ];

        const group3Options = [
            { label: 'Option 3.1', value: 'g3-opt1' },
            { label: 'Option 3.2', value: 'g3-opt2' },
            { label: 'Option 3.3', value: 'g3-opt3', disabled: true },
        ];

        const groupedOptions = [
            { label: 'Group 1', options: group1Options, showSelectAll: true },
            { label: 'Group 2', options: group2Options, showSelectAll: true },
            { label: 'Group 3', options: group3Options, showSelectAll: true },
        ];

        test('clicking SELECT ALL only selects enabled options from all groups', async () => {
            const onChange = vi.fn();
            render({ options: groupedOptions, onChange });

            await openMultiSelect();

            // Click Select All button
            const selectAllButton = screen.getByText('Select all');
            fireEvent.click(selectAllButton);

            // onChange should be called with only enabled options
            expect(onChange).toHaveBeenCalled();
            const selected = onChange.mock.calls[0][0];

            // Should have 5 selected options (disabled options excluded):
            // Group 1: g1-opt1, g1-opt2 (g1-opt3 disabled)
            // Group 2: g2-opt3 (g2-opt1, g2-opt2 disabled)
            // Group 3: g3-opt1, g3-opt2 (g3-opt3 disabled)
            expect(selected).toHaveLength(5);
            expect(selected.map((opt: SelectOption) => opt.value)).toEqual([
                'g1-opt1',
                'g1-opt2',
                'g2-opt3',
                'g3-opt1',
                'g3-opt2',
            ]);

            // Verify none of the disabled options were included
            const selectedValues = selected.map((opt: SelectOption) => opt.value);
            expect(selectedValues).not.toContain('g1-opt3'); // disabled
            expect(selectedValues).not.toContain('g2-opt1'); // disabled
            expect(selectedValues).not.toContain('g2-opt2'); // disabled
            expect(selectedValues).not.toContain('g3-opt3'); // disabled
        });
    });
});
