import React from 'react';
import { fireEvent, render as renderDom, screen } from '@testing-library/react';
import { testComponent } from 'lib/test-utils/common';
import {
    findOption,
    getGroup,
    getInput,
    getInputDisplayValue,
    getInputWrapper,
    getOption,
    openSelect,
    querySelectMenu,
    selectOption,
    triggerFilterChange,
    triggerFilterKeyDown,
    triggerInputKeyDown,
    waitForOptionToBeHighlighted,
} from 'lib/test-utils/select';
import { ListViewComponent, ListViewOptionSubtitle } from '../../listView';
import { Select } from '../select';
import {
    SelectDropdownIndicatorComponent,
    SelectInputValueComponent,
    SelectInputComponent,
    SelectOption,
    SelectProps,
} from '../types';
import { t } from '.././../../i18n';

describe('<Select>', () => {
    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<SelectProps>) =>
        renderDom(<Select options={options} {...props} />);

    testComponent(Select, { options }, 'SuiteSelect');

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

        await openSelect();

        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 openSelect();

        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 openSelect();

        selectOption(option1);

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith(option1);
        expect(getInputDisplayValue()).toEqual(option1.label);
        expect(getInput()).toHaveValue(option1.value);

        await openSelect();

        selectOption(option2);

        expect(onChange).toHaveBeenCalledTimes(2);
        expect(onChange).toHaveBeenCalledWith(option2);
        expect(getInputDisplayValue()).toEqual(option2.label);
        expect(getInput()).toHaveValue(option2.value);
    });

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

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

        expect(getInputDisplayValue()).toEqual(option2.label);

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

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith(undefined);
        expect(getInputDisplayValue()).toBeUndefined();
    });

    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 openSelect();

        selectOption(disabledOption);

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

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

        const { rerender } = render(props);

        expect(getInputDisplayValue()).toBeUndefined();

        rerender(<Select {...props} selectedValue={option1} />);

        expect(getInputDisplayValue()).toEqual(option1.label);

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

        expect(getInputDisplayValue()).toBeUndefined();
    });

    test('selected value can be a string', () => {
        render({
            selectedValue: option1.value,
        });

        expect(getInputDisplayValue()).toEqual(option1.label);
    });

    test('default value can be a string', () => {
        render({
            defaultValue: option1.value,
        });

        expect(getInputDisplayValue()).toEqual(option1.label);
    });

    test('initial value can be a string', () => {
        render({
            initialValue: option1.value,
        });

        expect(getInputDisplayValue()).toEqual(option1.label);
    });

    test('highlights next option when pressing down arrow', async () => {
        render({
            selectedValue: option1,
        });

        await openSelect();

        triggerFilterKeyDown('ArrowDown');

        expect(getOption(option2)).toHaveClass('highlight');
        expect(getOption(option1)).not.toHaveClass('highlight');
    });

    test('highlights prev option when pressing up arrow', async () => {
        render({
            selectedValue: option2,
        });

        await openSelect();

        triggerFilterKeyDown('ArrowUp');

        expect(getOption(option1)).toHaveClass('highlight');
        expect(getOption(option2)).not.toHaveClass('highlight');
    });

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

        triggerInputKeyDown('ArrowDown');

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

    test('selects highlighted option on enter', async () => {
        const onChange = vi.fn();
        render({ onChange });

        await openSelect();

        triggerFilterKeyDown('ArrowDown');
        triggerFilterKeyDown('ArrowDown');
        triggerFilterKeyDown('Enter');

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith(option2);
    });

    describe('with "hideFilter" true', () => {
        test('selects highlighted option on enter', async () => {
            const onChange = vi.fn();
            render({
                onChange,
                hideFilter: true,
            });

            await openSelect();

            triggerInputKeyDown('ArrowDown');
            triggerInputKeyDown('ArrowDown');
            triggerInputKeyDown('Enter');

            expect(onChange).toHaveBeenCalledTimes(1);
            expect(onChange).toHaveBeenCalledWith(option2);
        });
    });

    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');
        });
    });

    test('filtering highlights first match', async () => {
        const onChange = vi.fn();
        render({ onChange });

        await openSelect();

        triggerFilterChange('option1');

        await waitForOptionToBeHighlighted(option1);

        triggerFilterKeyDown('Enter');

        expect(onChange).toHaveBeenCalledTimes(1);
        expect(onChange).toHaveBeenCalledWith(option1);

        await openSelect();

        triggerFilterChange('option2');

        await waitForOptionToBeHighlighted(option2);

        triggerFilterKeyDown('Enter');

        expect(onChange).toHaveBeenCalledTimes(2);
        expect(onChange).toHaveBeenCalledWith(option2);
    });

    describe('with "quickSelections"', () => {
        test('shows quick selection section', async () => {
            const option = { label: 'quick option', value: 'test-quick' };

            const { getByText } = render({
                quickSelections: [option],
            });

            await openSelect();

            expect(getByText('Quick selections')).toBeInTheDocument();

            selectOption(option);

            expect(getInputDisplayValue()).toEqual(option.label);
        });
    });

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

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

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

    test('passes down and invokes "onFocus" callback on inner SelectInput', () => {
        const onFocus = vi.fn();

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

        const input = getByTestId('SuiteSelectInputValue');
        fireEvent.focus(input);

        expect(onFocus).toHaveBeenCalledTimes(1);
    });

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

        const { getByTestId } = render({
            onSerializeValue,
            selectedValue: 'test',
        });

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

    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)).toBeVisible();
            expect(getOption(option2)).toBeVisible();
        });

        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, false> = (props) => (
                    <div data-testid="CustomMenuList">{props.options?.length}</div>
                );

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

                await openSelect();

                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, false> = (props) => (
                    <div data-testid="CustomInputValue">{props.value?.value}</div>
                );

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

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

        describe('"option components"', () => {
            test('renders option label with custom JSX', async () => {
                const { getByTestId, getByText } = render({
                    options: [option1],
                    components: {
                        OptionLabel: ({ option }) => (
                            <div data-testid="CustomJSXOption">{option.label}</div>
                        ),
                    },
                });

                await openSelect();

                expect(getByTestId('CustomJSXOption')).toBeInTheDocument();
                expect(getByText('Option1')).toBeInTheDocument();
            });

            test('renders option subtitle with extra data using ListViewOptionSubtitle', async () => {
                const customOptions = [
                    { ...option1, customProp: 'customPropValue1' },
                    { ...option2, customProp: 'customPropValue2' },
                ];

                const { getByTestId, getByText } = render({
                    options: customOptions,
                    components: {
                        OptionSubtitle: ({ option }) => (
                            <ListViewOptionSubtitle
                                testId={`CustomListViewOptionSubtitle${option.data.id}`}
                            >
                                {option.customProp}
                            </ListViewOptionSubtitle>
                        ),
                    },
                });

                await openSelect();

                expect(getByTestId('CustomListViewOptionSubtitle1')).toBeInTheDocument();
                expect(getByText('Option1')).toBeInTheDocument();
                expect(getByText('customPropValue1')).toBeInTheDocument();
            });
        });

        describe('"SelectInput"', () => {
            test('renders given component', () => {
                const SelectInput: SelectInputComponent<SelectOption, false> = (props) => (
                    <div data-testid="CustomInput">{props.value?.value}</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 openSelect();

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

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