import React from 'react';
import { fireEvent, render as renderDom } from '@testing-library/react';
import { testComponent } from 'lib/test-utils/common';
import { ListView } from '../listView';
import { ListViewItem, ListViewComponent, ListViewProps } from '../types';

describe('<ListView', () => {
    const option1 = { label: 'Option1', value: '1', data: { id: 1 } };
    const option2 = { label: 'Option2', value: '2', data: { id: 2 } };
    const options = [option1, option2];
    const noOptionsText = 'no options';
    const noMatchText = 'no match';
    const noRemainingOptionsText = 'no more options';

    const defaultProps = {
        emptyStateText: noOptionsText,
        noMatchText: noMatchText,
        noRemainingOptionsText: noRemainingOptionsText,
        filterPlaceholder: 'type to filter',
        highlightedOption: { index: 0, scrollToRow: false },
        width: 100,
        onFilterChange: vi.fn(),
        onFilterKeyDown: vi.fn(),
        onSelect: vi.fn(),
        onRemoveAll: vi.fn(),
        onSelectAll: vi.fn(),
        onClear: vi.fn(),
        onRemove: vi.fn(),
        onApply: vi.fn(),
        onOptionMouseOver: vi.fn(),
        options: [],
        unfilteredOptions: [],
        rowHeight: 25,
        height: 100,
        totalCount: 10,
    };

    const renderSingleValue = (props: Partial<ListViewProps<ListViewItem, false>> = {}) =>
        renderDom(
            <ListView<ListViewItem, false> selectedValue={undefined} {...defaultProps} {...props} />
        );

    const renderMultiValue = (props: Partial<ListViewProps<ListViewItem, true>> = {}) =>
        renderDom(<ListView<ListViewItem, true> selectedValue={[]} {...defaultProps} {...props} />);

    testComponent(ListView, defaultProps, 'SuiteListView');

    test('applies given height', () => {
        const height = 200;
        const { getByTestId } = renderSingleValue({ options, height });

        const list = getByTestId('SuiteListView');

        expect(list).toHaveStyle({
            height: `${height}px`,
        });
    });

    test('applies given max-height', () => {
        const maxHeight = 200;
        const { getByTestId } = renderSingleValue({ options, maxHeight });

        const list = getByTestId('SuiteListView');

        expect(list).toHaveStyle({
            maxHeight: `${maxHeight}px`,
        });
    });

    test('applies given width', () => {
        const width = 200;
        const { getByTestId } = renderSingleValue({ options, width });

        const list = getByTestId('SuiteListView');

        expect(list).toHaveStyle({
            width: `${width}px`,
        });
    });

    test('invokes "onFilterKeyDown" callback on key down events', () => {
        const onFilterKeyDown = vi.fn();

        const { getByTestId } = renderSingleValue({ onFilterKeyDown });

        const input = getByTestId('SuiteListViewFilterInput');

        fireEvent.keyDown(input);

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

    test('invokes "onFilterChange" callback when filter changes', () => {
        const onFilterChange = vi.fn();

        const { getByTestId } = renderSingleValue({ onFilterChange });

        const input = getByTestId('SuiteListViewFilterInput');

        const value = 'Test value ';
        fireEvent.change(input, { target: { value } });

        expect(onFilterChange).toHaveBeenCalledTimes(1);
        expect(onFilterChange).toHaveBeenCalledWith(value.trim().toLowerCase());
    });

    test('invokes "onOptionMouseOver" callback on mouse over option', () => {
        const onOptionMouseOver = vi.fn();

        const { getByText, getByTestId } = renderSingleValue({
            onOptionMouseOver,
            options,
        });

        const row2 = getByText(options[1].label);
        const list = getByTestId('SuiteListViewList');

        fireEvent.mouseMove(list);
        fireEvent.mouseOver(row2);

        expect(onOptionMouseOver).toHaveBeenCalledTimes(1);
        expect(onOptionMouseOver).toHaveBeenCalledWith(1);
    });

    test('renders filter placeholder', () => {
        const filterPlaceholder = 'type something';
        const { getByPlaceholderText } = renderSingleValue({
            filterPlaceholder,
        });

        const input = getByPlaceholderText(filterPlaceholder);

        expect(input).toBeInTheDocument();
    });

    test('renders empty state if list is empty', () => {
        const { getByText } = renderSingleValue({ options: [], totalCount: 0 });

        const emptyState = getByText(noOptionsText);

        expect(emptyState).toBeInTheDocument();
    });

    test('does not render empty state if list is not empty', () => {
        const { queryByText } = renderSingleValue({ options });

        const emptyState = queryByText(noOptionsText);

        expect(emptyState).toBeNull();
    });

    test('renders no match state if filtered list is empty', () => {
        const { getByText } = renderSingleValue({
            options: [],
            filter: 'test',
        });

        const emptyState = getByText(noMatchText);

        expect(emptyState).toBeInTheDocument();
    });

    test('does not render no match state if filtered options is not empty', () => {
        const { queryByText } = renderSingleValue({ options, filter: 'test' });

        const emptyState = queryByText(noMatchText);

        expect(emptyState).toBeNull();
    });

    test('does not render filter input if "hideFilter" is true', () => {
        const { queryByTestId } = renderSingleValue({ hideFilter: true });

        const input = queryByTestId('SuiteListViewFilterInput');

        expect(input).toBeNull();
    });

    test('does not render list mode buttons by default', () => {
        const { queryAllByTestId } = renderSingleValue();

        const buttons = queryAllByTestId('SuiteListViewModeButton');

        expect(buttons).toHaveLength(0);
    });

    describe('single-select', () => {
        test('does not render footer', () => {
            const { queryByText } = renderSingleValue({
                totalCount: options.length,
                options,
            });

            expect(queryByText('Select all')).toBeNull();
            expect(queryByText('Clear all')).toBeNull();
            expect(queryByText('Apply')).toBeNull();
        });
    });

    describe('when multi-select', () => {
        test('renders no remaining options text if all options are selected', () => {
            const { getByText } = renderMultiValue({
                options,
                totalCount: options.length,
                selectedValue: options,
            });

            const emptyState = getByText(noRemainingOptionsText);

            expect(emptyState).toBeInTheDocument();
        });

        describe('with no selected values', () => {
            test('renders "Select all" enabled and "Clear all" disabled', () => {
                const { getByText } = renderMultiValue({
                    selectedValue: [],
                    options: [option1],
                });

                const selectAllButton = getByText('Select all');
                const clearAllButton = getByText('Clear all');

                expect(selectAllButton).toBeEnabled();
                expect(clearAllButton).toBeDisabled();
            });
        });

        describe('with some selected values', () => {
            test('renders "Clear all" and "Select all" enabled', () => {
                const { getByText } = renderMultiValue({
                    selectedValue: [option1],
                    options: [option1, option2],
                });

                const selectAllButton = getByText('Select all');
                const clearAllButton = getByText('Clear all');

                expect(selectAllButton).toBeEnabled();
                expect(clearAllButton).toBeEnabled();
            });
        });

        describe('with all selected values', () => {
            test('renders "Select all" disabled and "Clear all" enabled', () => {
                const { getByText } = renderMultiValue({
                    totalCount: 2,
                    selectedValue: [option1, option2],
                    options: [option1, option2],
                });

                const selectAllButton = getByText('Select all');
                const clearAllButton = getByText('Clear all');

                expect(selectAllButton).toBeDisabled();
                expect(clearAllButton).toBeEnabled();
            });
        });

        describe('without "requireApply" defined"', () => {
            test('does not render "Apply" button', () => {
                const { queryByText } = renderMultiValue({
                    requireApply: false,
                });

                expect(queryByText('Apply')).toBeNull();
            });
        });

        describe('with "requireApply" defined"', () => {
            test('renders "Apply" button', () => {
                const { getByText } = renderMultiValue({
                    options,
                    requireApply: true,
                    onApply: vi.fn(),
                });

                expect(getByText('Apply')).toBeEnabled();
            });

            test('invokes the callback when clicked', () => {
                const onApply = vi.fn();

                const { getByText } = renderMultiValue({
                    options,
                    requireApply: true,
                    onApply,
                });

                const button = getByText('Apply');
                fireEvent.click(button);

                expect(onApply).toHaveBeenCalledTimes(1);
                expect(onApply).toHaveBeenCalledWith();
            });
        });

        describe('without "recentSelections""', () => {
            test('does not render section', () => {
                const { queryByText } = renderMultiValue({
                    options,
                    recentSelections: undefined,
                });

                expect(queryByText('Recent selections')).toBeNull();
            });
        });

        describe('without "quickSelections""', () => {
            test('does not render section', () => {
                const { queryByText } = renderMultiValue({
                    options,
                    quickSelections: undefined,
                });

                expect(queryByText('Quick selections')).toBeNull();
            });
        });

        describe('with "recentSelections""', () => {
            test('renders section items with clock glyph', () => {
                const { getByText, getByTestId } = renderMultiValue({
                    options,
                    recentSelections: [{ label: 'test', options: [option1] }],
                });

                expect(getByText('Recent selections')).toBeInTheDocument();
                expect(getByTestId('ClockGlyphIcon')).toBeInTheDocument();
            });

            describe('clicking on a item', () => {
                test('selects the item options', async () => {
                    const itemLabel = 'recent selection item';
                    const onSelect = vi.fn();

                    const { getByText } = renderMultiValue({
                        onSelect,
                        options: [option1, option2],
                        selectedValue: [],
                        recentSelections: [{ label: itemLabel, options: [option2] }],
                    });

                    const item = getByText(itemLabel);
                    fireEvent.click(item);

                    expect(onSelect).toHaveBeenCalledTimes(1);
                    expect(onSelect).toHaveBeenCalledWith([option2], {
                        replace: true,
                    });
                });
            });
        });

        describe('with "quickSelections""', () => {
            test('renders section', () => {
                const { getByText } = renderMultiValue({
                    options,
                    quickSelections: [{ label: 'test', options: [] }],
                });

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

            describe('clicking on a item', () => {
                test('selects the item options', async () => {
                    const itemLabel = 'quick selection item';
                    const onSelect = vi.fn();

                    const { getByText } = renderMultiValue({
                        onSelect,
                        options: [option1, option2],
                        selectedValue: [],
                        quickSelections: [{ label: itemLabel, options: [option2] }],
                    });

                    const item = getByText(itemLabel);
                    fireEvent.click(item);

                    expect(onSelect).toHaveBeenCalledTimes(1);
                    expect(onSelect).toHaveBeenCalledWith([option2], {
                        replace: true,
                    });
                });
            });
        });

        describe('with "showListModes" true', () => {
            test('shows mode buttons', () => {
                const { getAllByTestId } = renderMultiValue({
                    showListModes: true,
                });

                const buttons = getAllByTestId('SuiteListViewModeButton');

                expect(buttons).toHaveLength(2);
            });

            test('clicking on mode buttons changes the list type', () => {
                const { getAllByTestId, getByTestId, queryByTestId } = renderMultiValue({
                    showListModes: true,
                });

                const [defaultMode, sectionedMode] = getAllByTestId('SuiteListViewModeButton');

                let defaultList: HTMLElement | null = getByTestId('SuiteListViewList');
                let sectionedList = queryByTestId('SuiteListViewSectionedList');

                expect(defaultList).toBeInTheDocument();
                expect(sectionedList).toBeNull();

                fireEvent.click(sectionedMode);

                defaultList = queryByTestId('SuiteListViewList');
                sectionedList = getByTestId('SuiteListViewSectionedList');

                expect(defaultList).toBeNull();
                expect(sectionedList).toBeInTheDocument();

                fireEvent.click(defaultMode);

                defaultList = getByTestId('SuiteListViewList');
                sectionedList = queryByTestId('SuiteListViewSectionedList');

                expect(defaultList).toBeInTheDocument();
                expect(sectionedList).toBeNull();
            });

            test('invokes the "onListModeChange" callback when mode changes', () => {
                const onListModeChange = vi.fn();

                const { getAllByTestId } = renderMultiValue({
                    showListModes: true,
                    onListModeChange,
                });

                const [defaultMode, sectionedMode] = getAllByTestId('SuiteListViewModeButton');

                fireEvent.click(sectionedMode);

                expect(onListModeChange).toHaveBeenCalledTimes(1);
                expect(onListModeChange).toHaveBeenCalledWith('sectioned');

                fireEvent.click(defaultMode);

                expect(onListModeChange).toHaveBeenCalledTimes(2);
                expect(onListModeChange).toHaveBeenLastCalledWith('consolidated');
            });
        });

        describe('with "listMode"', () => {
            test('"listMode" is controllable', () => {
                const { getByTestId, queryByTestId, rerender } = renderMultiValue({});

                expect(getByTestId('SuiteListViewList')).toBeInTheDocument();
                expect(queryByTestId('SuiteListViewSectionedList')).toBeNull();

                rerender(
                    <ListView<ListViewItem, true>
                        selectedValue={[]}
                        {...defaultProps}
                        listMode="sectioned"
                    />
                );

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

            describe('when "listMode" == "sectioned"', () => {
                test('sets the initial list to "sectioned"', () => {
                    const { getByTestId, queryByTestId } = renderMultiValue({
                        listMode: 'sectioned',
                    });

                    const defaultList = queryByTestId('SuiteListViewList');
                    const sectionedList = getByTestId('SuiteListViewSectionedList');

                    expect(defaultList).toBeNull();
                    expect(sectionedList).toBeInTheDocument();
                });
            });
        });
    });

    describe('with "ListContainer" component', () => {
        test('renders given component', () => {
            const ListContainer: ListViewComponent<ListViewItem, false> = (props) => (
                <div data-testid="CustomListContainer">{props.options?.length}</div>
            );

            const { getByTestId, queryByTestId } = renderSingleValue({
                options,
                components: { ListContainer },
            });

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

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

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

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

    describe('with "Aside" component', () => {
        test('renders given component', () => {
            const Aside: ListViewComponent<ListViewItem, false> = (props) => (
                <div data-testid="CustomAside">{props.options?.length}</div>
            );

            const { getByTestId, queryByTestId } = renderSingleValue({
                options,
                components: { Aside },
            });

            expect(getByTestId('CustomAside')).toBeInTheDocument();
            expect(queryByTestId('SuiteListViewAside')).toBeNull();
        });
    });

    describe('with "Header" component', () => {
        test('renders given component', () => {
            const Header: ListViewComponent<ListViewItem, false> = (props) => (
                <div data-testid="CustomHeader">{props.options?.length}</div>
            );

            const { getByTestId, queryByTestId } = renderSingleValue({
                options,
                components: { Header },
            });

            expect(getByTestId('CustomHeader')).toBeInTheDocument();
            expect(queryByTestId('SuiteListViewHeader')).toBeNull();
        });
    });

    describe('with "Footer" component', () => {
        test('renders given component', () => {
            const Footer: ListViewComponent<ListViewItem, false> = (props) => (
                <div data-testid="CustomFooter">{props.options?.length}</div>
            );

            const { getByTestId, queryByTestId } = renderSingleValue({
                options,
                components: { Footer },
            });

            expect(getByTestId('CustomFooter')).toBeInTheDocument();
            expect(queryByTestId('SuiteListViewFooter')).toBeNull();
        });
    });

    describe('with "excludeModeToggle"', () => {
        describe('without "requireApply"', () => {
            test('clicks invoke "onExcludeModeChange"', () => {
                const onExcludeModeChange = vi.fn();
                const { getByText } = renderSingleValue({
                    options,
                    requireApply: false,
                    showExcludeModeToggle: true,
                    excludeMode: true,
                    onExcludeModeChange,
                });

                fireEvent.click(getByText('Exclude'));
                expect(onExcludeModeChange).toHaveBeenCalledWith(false);
            });
        });

        describe('with "requireApply"', () => {
            test('clicking apply invokes "onExcludeModeChange"', () => {
                const onExcludeModeChange = vi.fn();
                const { getByText } = renderSingleValue({
                    options,
                    requireApply: true,
                    showExcludeModeToggle: true,
                    excludeMode: true,
                    onExcludeModeChange,
                });

                fireEvent.click(getByText('Exclude'));
                expect(onExcludeModeChange).not.toHaveBeenCalled();

                const button = getByText('Apply');
                fireEvent.click(button);

                expect(onExcludeModeChange).toHaveBeenCalledTimes(1);
                expect(onExcludeModeChange).toHaveBeenCalledWith(false);
            });
        });
    });

    describe('with "static" variant', () => {
        test('renders static list', () => {
            const { container } = renderSingleValue({
                options,
                variant: 'static',
            });

            const staticElement = container.querySelector('.static');
            expect(staticElement).toBeInTheDocument();
        });
    });

    describe('onPaste callback', () => {
        test('allows native paste behavior when onPaste is not provided', () => {
            const onFilterChange = vi.fn();
            const { getByTestId } = renderSingleValue({ options, onFilterChange });

            const input = getByTestId('SuiteListViewFilterInput');
            fireEvent.paste(input, {
                clipboardData: {
                    getData: () => 'pasted text',
                },
            });

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

        test('invokes onPaste callback and applies modification when provided', () => {
            const onPaste = vi.fn(() => 'MODIFIED');
            const onFilterChange = vi.fn();
            const { getByTestId } = renderSingleValue({
                options,
                onPaste,
                onFilterChange,
            });

            const input = getByTestId('SuiteListViewFilterInput');
            fireEvent.paste(input, {
                clipboardData: {
                    getData: () => 'original text',
                },
            });

            expect(onPaste).toHaveBeenCalledTimes(1);
            expect(onPaste).toHaveBeenCalledWith('original text');
            expect(input).toHaveValue('MODIFIED');
            expect(onFilterChange).toHaveBeenCalledWith('modified');
        });
    });

    describe('with "filterMessage"', () => {
        test('does not render filter message when not provided', () => {
            const { queryByTestId } = renderSingleValue({ options });

            const filterMessage = queryByTestId('SuiteListView-filter-message');

            expect(filterMessage).toBeNull();
        });

        test('renders error filter message with string', () => {
            const message = 'This is an error message';
            const { getByTestId, getByText } = renderSingleValue({
                options,
                filterMessage: { type: 'error', message },
            });

            const filterMessage = getByTestId('SuiteListView-filter-message');

            expect(filterMessage).toBeInTheDocument();
            expect(filterMessage).toHaveClass('error');
            expect(getByText(message)).toBeInTheDocument();
        });

        test('renders warning filter message with string', () => {
            const message = 'This is a warning message';
            const { getByTestId, getByText } = renderSingleValue({
                options,
                filterMessage: { type: 'warning', message },
            });

            const filterMessage = getByTestId('SuiteListView-filter-message');

            expect(filterMessage).toBeInTheDocument();
            expect(filterMessage).toHaveClass('warning');
            expect(getByText(message)).toBeInTheDocument();
        });

        test('renders warning icon for warning type', () => {
            const message = 'This is a warning message';
            const { getByTestId } = renderSingleValue({
                options,
                filterMessage: { type: 'warning', message },
            });

            const warningIcon = getByTestId('WarningGlyphIcon');

            expect(warningIcon).toBeInTheDocument();
        });

        test('does not render warning icon for error type', () => {
            const message = 'This is an error message';
            const { queryByTestId } = renderSingleValue({
                options,
                filterMessage: { type: 'error', message },
            });

            const warningIcon = queryByTestId('WarningGlyphIcon');

            expect(warningIcon).toBeNull();
        });

        test('renders filter message with JSX element', () => {
            const message = (
                <span>
                    This is a <strong>formatted</strong> message
                </span>
            );
            const { getByTestId, getByText } = renderSingleValue({
                options,
                filterMessage: { type: 'error', message },
            });

            const filterMessage = getByTestId('SuiteListView-filter-message');

            expect(filterMessage).toBeInTheDocument();
            expect(getByText('formatted')).toBeInTheDocument();
        });
    });

    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 on a disabled option does not get it selected', () => {
            const onSelect = vi.fn();
            const r = renderMultiValue({
                options: groupedOptions,
                onSelect,
            });

            const opt = r.getByText('Option 1.3');
            fireEvent.click(opt);

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

        test('group SELECT only includes enabled options from that group', () => {
            const onSelect = vi.fn();
            const r = renderMultiValue({
                options: groupedOptions,
                onSelect,
            });

            const group2Label = r.getByText('Group 2');
            const group2Container = group2Label.closest('.SuiteListView-group');

            expect(group2Container).toBeInTheDocument();
            if (!group2Container) throw new Error('Group 2 container not found');

            // Hover over the group to make the SELECT button visible
            fireEvent.mouseOver(group2Container);

            const selectButton = group2Container.querySelector('.SuiteListView-group-select');
            expect(selectButton).toBeInTheDocument();

            fireEvent.click(selectButton!);

            // Should only select Option 2.3 (the only enabled option in Group 2)
            expect(onSelect).toHaveBeenCalledTimes(1);
            const selectedOptions = onSelect.mock.calls[0][0];
            expect(selectedOptions).toHaveLength(1);
            expect(selectedOptions[0].value).toBe('g2-opt3');
        });
    });
});
