import React from 'react'
import { Grid } from '.'
import type {
    Column,
    GridActionsMenuFullProps,
    GridActionsMenuProps,
    GridRowData,
    GridRowMeta,
} from '../../types'
import {
    render,
    screen,
    snapshot,
    waitFor,
    within,
} from '../../utils/test-utils'
import userEvent from '@testing-library/user-event'
import {
    GridCellBase,
    GridCellDefault,
    GridCellDropdownMenu,
} from '../../components'
import {
    Chip,
    EmptyState,
    ListItem,
    ListItemInfo,
    Menu,
} from '@planview/pv-uikit'
import {
    GridEditorInput,
    GridEditorSwitch,
    GridEditorCombobox,
} from '../../editors'
import { TestingProvider } from '@planview/pv-utilities'

/* Just ignore setPointerCapture calls, JSDOM doesn't support it */
Object.defineProperty(global.HTMLElement.prototype, 'setPointerCapture', {
    value: () => {},
})

type User = {
    id: number
    firstName: string
    lastName: string
    status: 'Active' | 'Pending'
}

const columns: Column<User>[] = [
    {
        id: 'firstName',
        label: 'First Name',
    },
    {
        id: 'lastName',
        label: 'Last Name',
    },
    {
        id: 'status',
        label: 'Status',
        cell: {
            Renderer({ tabIndex, value }) {
                return (
                    <GridCellDropdownMenu tabIndex={tabIndex} label={value}>
                        <ListItem label={'Active'} />
                        <ListItem label={'Pending'} />
                    </GridCellDropdownMenu>
                )
            },
        },
    },
]

const treeColumns = columns.map((c) => {
    if (c.id === 'firstName') {
        return {
            ...c,
            tree: true,
        }
    }
    return c
})

const columnsWithEditors: Column<User>[] = [
    {
        id: 'firstName',
        label: 'First Name',
        cell: {
            editable: true,
        },
    },
    {
        id: 'lastName',
        label: 'Last Name',
        cell: {
            editable: true,
            Editor(props) {
                return (
                    <GridEditorInput
                        {...props}
                        placeholder="Custom placeholder"
                    />
                )
            },
        },
    },
    {
        id: 'status',
        label: 'Status',
        cell: {
            editable: true,
            value({ row }) {
                return row.status === 'Active'
            },
            hybridEditor: true,
            Editor: GridEditorSwitch,
        },
    },
    {
        id: 'statusDisplay',
        label: 'Status Detail',
        cell: {
            value({ row }) {
                return row.status
            },
        },
    },
]

const rows: User[] = [
    { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' },
    { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' },
]

const nestedRows = new Map<number, User>([
    [3, { id: 3, firstName: 'Mr', lastName: 'Manager', status: 'Active' }],
    [1, { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' }],
    [2, { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' }],
])

const treeMeta = new Map<number, GridRowMeta<User>>([
    [3, { type: 'tree', children: [1, 2] }],
])

const groupMeta = new Map<number, GridRowMeta<User>>([
    [3, { type: 'group', children: [1, 2] }],
])

describe('Grid', () => {
    describe('snapshots', () => {
        snapshot(
            'should render a simple grid',
            <Grid columns={columns} rows={rows} />
        )

        snapshot(
            'should render a grid with empty content',
            <Grid
                columns={columns}
                rows={[]}
                emptyContent={<EmptyState header="no rows" />}
            />
        )

        snapshot(
            'should render an editable grid',
            <Grid columns={columnsWithEditors} rows={rows} />
        )

        snapshot(
            'should render a grid without columns or rows',
            <Grid columns={[]} rows={[]} />
        )

        snapshot(
            'should render a grid in loading mode without columns or rows',
            <Grid columns={[]} rows={[]} loading={3} />
        )

        snapshot(
            'should render a grid without columns',
            <Grid columns={[]} rows={rows} />
        )
    })

    it('should support an accessible label', () => {
        const { queryByRole } = render(
            <Grid columns={columns} rows={rows} label="Users grid" />
        )
        expect(queryByRole('grid', { name: 'Users grid' })).toBeInTheDocument()
    })

    it('should not render empty state when changing from loading to rows provided', () => {
        let emptyRendered = false

        const EmptyComponent = () => {
            emptyRendered = true
            return <div>No data</div>
        }

        const { queryAllByRole, rerender } = render(
            <Grid
                columns={columns}
                rows={[]}
                loading
                emptyContent={<EmptyComponent />}
            />
        )

        rerender(
            <Grid
                columns={columns}
                rows={rows}
                loading={false}
                emptyContent={<EmptyComponent />}
            />
        )

        expect(emptyRendered).toBe(false)
        expect(queryAllByRole('row')).toHaveLength(3)
    })

    it('should render a grid with 25 rows (1 header row) and 14 columns by default', () => {
        const { queryAllByRole } = render(
            <Grid
                columns={Array.from({ length: 100 }, (_, i) => ({
                    id: `column-${i}`,
                    label: `${i}`,
                    width: 100,
                }))}
                rows={Array.from({ length: 100 }, (_, i) => ({
                    id: i,
                    title: `${i}`,
                }))}
            />
        )

        expect(queryAllByRole('row')).toHaveLength(25)
        expect(queryAllByRole('row')[1].querySelectorAll('td')).toHaveLength(14)
    })

    it('should render a grid with more rows and columns when used with TestingProvider', () => {
        const { queryAllByRole } = render(
            <TestingProvider clientWidth={1000} clientHeight={800}>
                <Grid
                    columns={Array.from({ length: 100 }, (_, i) => ({
                        id: `column-${i}`,
                        label: `${i}`,
                        width: 100,
                    }))}
                    rows={Array.from({ length: 100 }, (_, i) => ({
                        id: i,
                        title: `${i}`,
                    }))}
                />
            </TestingProvider>
        )

        expect(queryAllByRole('row')).toHaveLength(29)
        expect(queryAllByRole('row')[1].querySelectorAll('td')).toHaveLength(16)
    })

    it('should render a grid without a selection column with selectionMode=multi-hidden', () => {
        const { queryAllByRole } = render(
            <Grid columns={columns} rows={rows} selectionMode="multi-hidden" />
        )

        expect(queryAllByRole('checkbox')).toHaveLength(0)
    })

    it('should render a grid in loading with filteredIds without crashing', () => {
        expect(() =>
            render(
                <Grid
                    columns={columns}
                    loading
                    filteredIds={[1, 2]}
                    rows={rows}
                />
            )
        ).not.toThrow()
    })

    it('should render a tree', async () => {
        const { queryByText, getByRole } = render(
            <Grid
                columns={treeColumns}
                rows={{ ids: [3], data: nestedRows, meta: treeMeta }}
            />
        )

        const treegrid = getByRole('treegrid')
        expect(treegrid).toBeInTheDocument()
        expect(treegrid).toHaveAttribute('aria-rowcount', '4')

        expect(queryByText('Joe')).not.toBeInTheDocument()

        await userEvent.click(getByRole('button', { name: 'Expand row' }))

        expect(queryByText('Joe')).toBeInTheDocument()
    })

    it('should set correct setsize and posinset values on tree', async () => {
        const { getAllByRole } = render(
            <Grid
                columns={treeColumns}
                expandedRows={new Set([3])}
                rows={{ ids: [3], data: nestedRows, meta: treeMeta }}
            />
        )

        const [, firstRow, secondRow, thirdRow] = getAllByRole('row')
        expect(firstRow).toHaveAttribute('aria-setsize', '1')
        expect(firstRow).toHaveAttribute('aria-posinset', '1')

        expect(secondRow).toHaveAttribute('aria-setsize', '2')
        expect(secondRow).toHaveAttribute('aria-posinset', '1')

        expect(thirdRow).toHaveAttribute('aria-setsize', '2')
        expect(thirdRow).toHaveAttribute('aria-posinset', '2')
    })

    it('should not render setsize and posinset values on flat grid', async () => {
        const { getAllByRole } = render(<Grid columns={columns} rows={rows} />)

        const [, firstRow] = getAllByRole('row')
        expect(firstRow).not.toHaveAttribute('aria-setsize')
        expect(firstRow).not.toHaveAttribute('aria-posinset')
    })

    it('should render a tree with correct role on expandable rows', async () => {
        const { getByRole, queryByRole, queryAllByRole } = render(
            <Grid
                columns={treeColumns}
                rows={{ ids: [3], data: nestedRows, meta: treeMeta }}
            />
        )

        expect(getByRole('treegrid')).toBeInTheDocument()

        expect(queryByRole('row', { expanded: false })).toBeInTheDocument()

        await userEvent.click(getByRole('button', { name: 'Expand row' }))

        expect(queryByRole('row', { expanded: true })).toBeInTheDocument()
        expect(queryByRole('row', { expanded: false })).not.toBeInTheDocument()
        expect(
            queryAllByRole('row').map((el) => el.getAttribute('aria-level'))
        ).toEqual([null, '1', '2', '2'])
    })

    it('should render footer when column has aggregation defined', () => {
        const { getByRole } = render(
            <Grid
                columns={[
                    {
                        id: 'firstName',
                        label: 'First Name',
                    },
                    {
                        id: 'lastName',
                        label: 'Last Name',
                        footer: {
                            aggregation: 'size',
                            label: (v) => `Number of rows: ${v?.toString()}`,
                        },
                    },
                ]}
                rows={rows}
            />
        )

        expect(
            getByRole('gridcell', { name: 'Number of rows: 2' })
        ).toBeInTheDocument()
    })
    it('should render footer when column has label defined', () => {
        const { getByRole } = render(
            <Grid
                columns={[
                    {
                        id: 'firstName',
                        label: 'First Name',
                    },
                    {
                        id: 'lastName',
                        label: 'Last Name',
                        footer: {
                            label: () => `Custom label`,
                        },
                    },
                ]}
                rows={rows}
            />
        )

        expect(
            getByRole('gridcell', { name: 'Custom label' })
        ).toBeInTheDocument()
    })

    it('should support selecting a tree parent independently from a child', async () => {
        const { getAllByRole } = render(
            <Grid
                expandedRows={new Set([3])}
                columns={treeColumns}
                rows={{ ids: [3], data: nestedRows, meta: treeMeta }}
            />
        )

        await userEvent.click(
            getAllByRole('checkbox', { name: 'Toggle row selection' })[0]
        )

        expect(getAllByRole('row', { selected: true })).toHaveLength(1)
    })

    it('should support selecting a group parent which selects all children', async () => {
        const { getAllByRole } = render(
            <Grid
                expandedRows={new Set([3])}
                columns={treeColumns}
                rows={{ ids: [3], data: nestedRows, meta: groupMeta }}
            />
        )

        await userEvent.click(
            getAllByRole('checkbox', { name: 'Toggle row selection' })[0]
        )

        expect(getAllByRole('row', { selected: true })).toHaveLength(3)
    })

    it('should set aria-readonly when no cell is possibly editable', () => {
        const { queryByRole } = render(<Grid columns={columns} rows={rows} />)
        expect(queryByRole('grid')).toHaveAttribute('aria-readonly', 'true')
    })

    it('should have both rows', () => {
        const { getByRole } = render(<Grid columns={columns} rows={rows} />)
        expect(getByRole('gridcell', { name: /Joe/ })).toBeInTheDocument()
        expect(getByRole('gridcell', { name: /Fran/ })).toBeInTheDocument()
    })

    it('should render indeterminate state for select all checkbox if some items selected', () => {
        const { getByRole } = render(
            <Grid selection={new Set([1])} columns={columns} rows={rows} />
        )
        expect(
            getByRole('checkbox', {
                name: 'Select all rows',
            })
            // eslint-disable-next-line jest-dom/prefer-checked
        ).toHaveAttribute('aria-checked', 'mixed')
    })

    it('should not coerce falsy values in user data', () => {
        const rows = [
            { id: '1', enabled: true, count: 5, related: 'Joe' },
            { id: '2', enabled: false, count: 0, related: undefined },
        ]
        const columns: Column<(typeof rows)[0]>[] = [
            {
                id: 'enabled',
                label: 'Enabled',
            },
            {
                id: 'count',
                label: 'Count',
            },
            {
                id: 'related',
                label: 'Related',
                cell: {
                    label({ value }) {
                        return typeof value
                    },
                },
            },
        ]

        const { getAllByRole } = render(
            <Grid selectionMode="none" columns={columns} rows={rows} />
        )

        const row1Content = within(getAllByRole('row')[1])
            .getAllByRole('gridcell')
            .map((d) => d.textContent)
        const row2Content = within(getAllByRole('row')[2])
            .getAllByRole('gridcell')
            .map((d) => d.textContent)

        expect(row1Content).toEqual(['true', '5', 'string'])
        expect(row2Content).toEqual(['false', '0', 'undefined'])
    })

    it('should make metadata available to every column', () => {
        const data: Record<string, GridRowData> = {
            1: { id: '1' },
            2: { id: '2' },
        }

        const meta: Record<string, GridRowMeta> = {
            1: { type: 'tree' },
            2: { type: 'group' },
        }

        const columns: Column<GridRowData, GridRowMeta>[] = [
            {
                id: 'id',
                label: 'Id',
                tree: true,
                cell: {
                    value({ rowMeta }) {
                        return `Column 1 ${rowMeta.type}`
                    },
                },
            },
            {
                id: 'alt',
                label: 'Alt',
                cell: {
                    value({ rowMeta }) {
                        return `Column 2 ${rowMeta.type}`
                    },
                },
            },
        ]

        const { getByRole } = render(
            <Grid columns={columns} rows={{ ids: ['1', '2'], data, meta }} />
        )

        expect(
            getByRole('gridcell', { name: 'Column 1 tree' })
        ).toBeInTheDocument()
        expect(
            getByRole('gridcell', { name: 'Column 2 tree' })
        ).toBeInTheDocument()
        expect(
            getByRole('gridcell', { name: 'Column 1 group' })
        ).toBeInTheDocument()
        expect(
            getByRole('gridcell', { name: 'Column 2 group' })
        ).toBeInTheDocument()
    })

    describe('Header interaction', () => {
        it('should focus on first column header checkbox', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}')
            expect(
                getByRole('checkbox', {
                    name: 'Select all rows',
                })
            ).toHaveFocus()
        })
        it('header checkbox should toggle rows selected using space key', async () => {
            const { queryAllByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(0)
            await userEvent.keyboard(' ')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(2)
        })
        it('header checkbox should toggle rows selected using enter key', async () => {
            const { queryAllByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(0)
            await userEvent.keyboard('{Enter}')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(2)
        })
        it('should move focus to second column and update sorting using space key', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}{ArrowRight}')
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveFocus()
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'none')
            await userEvent.keyboard(' ')
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'ascending')
        })
        it('should move focus to second column and update sorting using enter key', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}{ArrowRight}')
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveFocus()
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'none')
            await userEvent.keyboard('{Enter}')
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'ascending')
        })
        it('should update sorting using click', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'none')
            await userEvent.click(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            )
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveAttribute('aria-sort', 'ascending')
        })
        it('should update focus state on click on header cell', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            const headerCell = getByRole('columnheader', {
                name: 'First Name',
            })
            await userEvent.click(headerCell)
            expect(headerCell).toHaveAttribute('tabindex', '0')
        })
    })
    describe('Body interaction', () => {
        it('should update focus state on click on cell', async () => {
            const { getByLabelText, getByText } = render(
                <Grid columns={columns} rows={rows} />
            )
            expect(getByLabelText('Select all rows')).toHaveAttribute(
                'tabindex',
                '0'
            )
            const label = getByText(/Fran/)
            await userEvent.click(label)
            expect(label).toHaveAttribute('tabindex', '0')
        })
        it('should toggle row selected using space key', async () => {
            const { queryAllByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            expect(queryAllByRole('row', { selected: true })).toHaveLength(0)
            await userEvent.keyboard('{Tab}{ArrowDown}')
            await userEvent.keyboard(' ')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(1)
        })
        it('should toggle row selected using enter key', async () => {
            const { queryAllByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            expect(queryAllByRole('row', { selected: true })).toHaveLength(0)
            await userEvent.keyboard('{Tab}{ArrowDown}')
            await userEvent.keyboard('{Enter}')
            expect(queryAllByRole('row', { selected: true })).toHaveLength(1)
        })
    })

    describe('Focus handling', () => {
        it('should update focus state on click on header cell and navigate down to next row with arrow down', async () => {
            const { getByRole, getByText } = render(
                <Grid columns={columns} rows={rows} />
            )
            const headerCell = getByRole('columnheader', {
                name: 'First Name',
            })
            await userEvent.click(headerCell)
            expect(headerCell).toHaveAttribute('tabindex', '0')
            await userEvent.keyboard('{ArrowDown}')
            expect(getByText(/Fran/)).toHaveFocus()
        })

        it('should treat click on select all as focus event as well and navigate to next column with arrow right', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.click(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            )
            await userEvent.click(
                getByRole('checkbox', {
                    name: 'Select all rows',
                })
            )
            await userEvent.keyboard('{ArrowRight}')
            expect(
                getByRole('columnheader', {
                    name: 'First Name',
                })
            ).toHaveFocus()
        })

        it('should treat click on row select as focus event as well and navigate to next column with arrow right', async () => {
            const { getAllByRole, getByText } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.click(
                getAllByRole('checkbox', {
                    name: 'Toggle row selection',
                })[0]
            )
            await userEvent.keyboard('{ArrowRight}')
            expect(getByText(/Joe/)).toHaveFocus()
        })

        it('should treat click on cell as focus event and navigate to the next column with arrow right', async () => {
            const { getByText, getByRole } = render(
                <Grid columns={columns} rows={rows} />
            )

            const cell = getByRole('gridcell', { name: /Joe/ })
            await userEvent.click(cell)

            await userEvent.keyboard('{ArrowRight}')
            expect(getByText(/Doe/)).toHaveFocus()
        })

        it('should not get stuck on custom content with tabIndex=0', async () => {
            const { getByText } = render(
                <Grid
                    selectionMode="single"
                    rows={[
                        { id: '1', text: 'One' },
                        { id: '2', text: 'Two' },
                        { id: '3', text: 'Three' },
                    ]}
                    columns={[
                        { id: 'text', label: 'Text' },
                        {
                            id: 'chip',
                            label: 'Chip',
                            cell: {
                                Renderer({ tabIndex }) {
                                    return (
                                        <GridCellBase tabIndex={tabIndex}>
                                            <Chip
                                                label="Chip"
                                                removeButtonLabel="Remove"
                                            />
                                        </GridCellBase>
                                    )
                                },
                            },
                        },
                    ]}
                />
            )

            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowDown}')

            expect(getByText('Three')).toHaveFocus()
        })

        it('should navigate through a merged cell without changing the index', async () => {
            const { getByRole, getByText } = render(
                <Grid
                    columns={[
                        {
                            id: 'one',
                            label: 'One',
                            cell: {
                                colSpan({ row }) {
                                    return row.id === 2
                                        ? ['one', 'two', 'three']
                                        : []
                                },
                            },
                        },
                        {
                            id: 'two',
                            label: 'Two',
                        },
                        {
                            id: 'three',
                            label: 'Three',
                        },
                    ]}
                    rows={[
                        { id: 1, one: '1.1', two: '1.2', three: '1.3' },
                        { id: 2, one: '2.1', two: '2.2', three: '2.3' },
                        { id: 3, one: '3.1', two: '3.2', three: '3.3' },
                    ]}
                />
            )

            await userEvent.click(getByRole('gridcell', { name: '1.2' }))

            await userEvent.keyboard('{ArrowDown}')

            expect(getByText('2.1')).toHaveFocus()

            await userEvent.keyboard('{ArrowDown}')

            expect(getByText('3.2')).toHaveFocus()
        })
    })

    describe('onRowClick', () => {
        it('should trigger onRowClick when clicking on a cell', async () => {
            const rowClickSpy = jest.fn()
            const { getByRole } = render(
                <Grid columns={columns} rows={rows} onRowClick={rowClickSpy} />
            )

            await userEvent.click(getByRole('gridcell', { name: /Joe/ }))

            expect(rowClickSpy).toHaveBeenCalledTimes(1)
            expect(rowClickSpy).toHaveBeenCalledWith(1)
        })
        it('should trigger onRowClick when clicking selection checkbox', async () => {
            const rowClickSpy = jest.fn()
            const { getAllByRole } = render(
                <Grid columns={columns} rows={rows} onRowClick={rowClickSpy} />
            )

            await userEvent.click(getAllByRole('checkbox')[1])

            expect(rowClickSpy).toHaveBeenCalledTimes(1)
            expect(rowClickSpy).toHaveBeenCalledWith(1)
        })
        it('should trigger onRowClick when hitting shift+space', async () => {
            const rowClickSpy = jest.fn()
            render(
                <Grid columns={columns} rows={rows} onRowClick={rowClickSpy} />
            )

            await userEvent.keyboard(
                '{Tab}{ArrowDown}{ArrowRight}{Shift>} {/Shift}'
            )

            expect(rowClickSpy).toHaveBeenCalledTimes(1)
            expect(rowClickSpy).toHaveBeenCalledWith(1)
        })
        it('should not trigger onRowClick when clicking on a dropdown menu', async () => {
            const rowClickSpy = jest.fn()
            const { getAllByRole } = render(
                <Grid columns={columns} rows={rows} onRowClick={rowClickSpy} />
            )

            await userEvent.click(
                getAllByRole('button', { expanded: false })[0]
            )

            expect(rowClickSpy).not.toHaveBeenCalled()
        })
        it('should not trigger onRowClick when clicking to edit a cell', async () => {
            const rowClickSpy = jest.fn()
            const { getByRole } = render(
                <Grid
                    columns={columnsWithEditors}
                    rows={rows}
                    onRowClick={rowClickSpy}
                />
            )

            await userEvent.click(getByRole('gridcell', { name: /Joe/ }))

            expect(rowClickSpy).not.toHaveBeenCalled()
        })
        describe('range selection', () => {
            it('should select range of rows', async () => {
                const spy = jest.fn()
                const { getAllByRole } = render(
                    <Grid
                        columns={columnsWithEditors}
                        rows={rows}
                        onSelectionChange={spy}
                    />
                )
                const user = userEvent.setup()
                await user.click(getAllByRole('row')[1])
                expect(spy).toHaveBeenCalledWith(new Set([1]))
                await user.keyboard('[ShiftLeft>]')
                await user.click(getAllByRole('row')[2])
                expect(spy).toHaveBeenCalledWith(new Set([1, 2]))
            })
        })
    })

    describe('ActionsMenu', () => {
        const ActionsMenuItems = ({ row }: GridActionsMenuProps<User>) => (
            <>
                <ListItem label="Action one" />
                <ListItemInfo>
                    {row.firstName} {row.lastName}
                </ListItemInfo>
            </>
        )

        const ActionsMenu = ({
            row,
            menuProps,
        }: GridActionsMenuFullProps<User>) => (
            <Menu {...menuProps}>
                <ListItem label="Action one" />
                <ListItemInfo>
                    {row.firstName} {row.lastName}
                </ListItemInfo>
            </Menu>
        )

        ;[
            {
                label: 'MenuItems',
                description: 'with a MenuItems component',
                actionsMenu: ActionsMenuItems,
            },
            {
                label: 'Menu',
                description: 'with a Menu component',
                actionsMenu: { Menu: ActionsMenu },
            },
        ].forEach(({ label, description, actionsMenu }) => {
            describe(`${description}`, () => {
                it('should open the menu if a user right clicks on a row', async () => {
                    const { getAllByRole, getByRole } = render(
                        <Grid
                            columns={columns}
                            rows={rows}
                            actionsMenu={actionsMenu}
                        />
                    )

                    const renderedRows = getAllByRole('row')

                    await userEvent.pointer({
                        target: renderedRows[1],
                        keys: '[MouseRight]',
                    })

                    expect(
                        getByRole('menuitemradio', { name: 'Action one' })
                    ).toBeInTheDocument()

                    expect(
                        within(getByRole('menu')).getByText('Joe Doe')
                    ).toBeInTheDocument()
                })

                it('should open the menu if a user clicks on the actions button', async () => {
                    const { getAllByRole, getByRole } = render(
                        <Grid
                            columns={columns}
                            rows={rows}
                            actionsMenu={actionsMenu}
                        />
                    )

                    const buttons = getAllByRole('button', {
                        name: /More actions/,
                        expanded: false,
                    })

                    await userEvent.click(buttons[0])

                    expect(
                        getByRole('menuitemradio', { name: 'Action one' })
                    ).toBeInTheDocument()

                    expect(
                        within(getByRole('menu')).getByText('Joe Doe')
                    ).toBeInTheDocument()
                })

                it('should not open a menu if a user right clicks on a link', async () => {
                    const { getByRole, queryByRole } = render(
                        <Grid
                            columns={[
                                {
                                    id: 'firstName',
                                    label: 'First Name',
                                    cell: {
                                        Renderer({ label, tabIndex }) {
                                            return (
                                                <GridCellDefault
                                                    href="about:blank"
                                                    label={label}
                                                    tabIndex={tabIndex}
                                                />
                                            )
                                        },
                                    },
                                },
                            ]}
                            rows={rows}
                            actionsMenu={actionsMenu}
                        />
                    )

                    const link = getByRole('link', {
                        name: /Joe/,
                    })

                    await userEvent.pointer({
                        target: link,
                        keys: '[MouseRight]',
                    })

                    expect(
                        queryByRole('menuitemradio', { name: 'Action one' })
                    ).not.toBeInTheDocument()
                })

                it('should not open an extra menu if a user right clicks in a dropdown menu', async () => {
                    const { getAllByRole, getByRole } = render(
                        <Grid
                            columns={columns}
                            rows={rows}
                            actionsMenu={actionsMenu}
                        />
                    )

                    const buttons = getAllByRole('button', {
                        name: /More actions/,
                        expanded: false,
                    })

                    await userEvent.click(buttons[0])

                    await userEvent.pointer({
                        target: getByRole('menuitemradio', {
                            name: 'Action one',
                        }),
                        keys: '[MouseRight]',
                    })

                    expect(
                        getAllByRole('menuitemradio', { name: 'Action one' })
                    ).toHaveLength(1)
                })

                if (label === 'MenuItems') {
                    it('should support conditional rendering (deprecated support)', async () => {
                        const { getAllByRole, queryByRole } = render(
                            <Grid
                                columns={columns}
                                rows={rows}
                                actionsMenu={{
                                    Component: ActionsMenuItems,
                                    visible({ row }) {
                                        return row.id === 1
                                    },
                                }}
                            />
                        )

                        const renderedRows = getAllByRole('row')

                        // Button rendered on first row
                        expect(
                            within(renderedRows[1]).queryByRole('button', {
                                name: /More actions/,
                                expanded: false,
                            })
                        ).toBeInTheDocument()

                        // Button not rendered on second row
                        expect(
                            within(renderedRows[2]).queryByRole('button', {
                                name: /More actions/,
                                expanded: false,
                            })
                        ).not.toBeInTheDocument()

                        // Right click on second row, no menu should be shown
                        await userEvent.pointer({
                            target: renderedRows[2],
                            keys: '[MouseRight]',
                        })

                        expect(
                            queryByRole('menuitemradio', { name: 'Action one' })
                        ).not.toBeInTheDocument()

                        // Right click on first row, menu should be shown
                        await userEvent.pointer({
                            target: renderedRows[1],
                            keys: '[MouseRight]',
                        })

                        expect(
                            queryByRole('menuitemradio', { name: 'Action one' })
                        ).toBeInTheDocument()
                    })
                }

                it('should support conditional rendering', async () => {
                    const { getAllByRole, queryByRole } = render(
                        <Grid
                            columns={columns}
                            rows={rows}
                            actionsMenu={
                                label === 'MenuItems'
                                    ? {
                                          MenuItems: ActionsMenuItems,
                                          visible({ row }) {
                                              return row.id === 1
                                          },
                                      }
                                    : {
                                          Menu: ActionsMenu,
                                          visible({ row }) {
                                              return row.id === 1
                                          },
                                      }
                            }
                        />
                    )

                    const renderedRows = getAllByRole('row')

                    // Button rendered on first row
                    expect(
                        within(renderedRows[1]).queryByRole('button', {
                            name: /More actions/,
                            expanded: false,
                        })
                    ).toBeInTheDocument()

                    // Button not rendered on second row
                    expect(
                        within(renderedRows[2]).queryByRole('button', {
                            name: /More actions/,
                            expanded: false,
                        })
                    ).not.toBeInTheDocument()

                    // Right click on second row, no menu should be shown
                    await userEvent.pointer({
                        target: renderedRows[2],
                        keys: '[MouseRight]',
                    })

                    expect(
                        queryByRole('menuitemradio', { name: 'Action one' })
                    ).not.toBeInTheDocument()

                    // Right click on first row, menu should be shown
                    await userEvent.pointer({
                        target: renderedRows[1],
                        keys: '[MouseRight]',
                    })

                    expect(
                        queryByRole('menuitemradio', { name: 'Action one' })
                    ).toBeInTheDocument()
                })
            })
        })
    })

    describe('Navigation', () => {
        it('if last row is focused when removed,it should move focus up to the header', async () => {
            const { getByRole, rerender } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            rerender(<Grid columns={columns} rows={[rows[0]]} />)
            expect(
                getByRole('columnheader', {
                    name: /First Name/,
                })
            ).toHaveFocus()
        })
        it('if all rows are removed, it should move focus up to the header', async () => {
            const { getByRole, rerender } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            rerender(<Grid columns={columns} rows={[]} />)
            expect(
                getByRole('columnheader', {
                    name: /First Name/,
                })
            ).toHaveFocus()
        })
        it('if last column is focused when removed, it should move focus to first column', async () => {
            const { getAllByRole, rerender } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard(
                '{Tab}{ArrowDown}{ArrowDown}{ArrowRight}{ArrowRight}{ArrowRight}'
            )
            rerender(<Grid columns={columns.slice(0, 2)} rows={rows} />)
            expect(
                getAllByRole('checkbox', {
                    name: 'Toggle row selection',
                })[1]
            ).toHaveFocus()
        })
        it('should navigate to second role, second row', async () => {
            const { getByText } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            expect(getByText(/Fran/)).toHaveFocus()
        })

        it('should navigate to last column on keyboard End', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}{End}')
            await waitFor(() =>
                expect(
                    getByRole('columnheader', {
                        name: /Status/,
                    })
                ).toHaveFocus()
            )
        })
        it('should navigate to last column in last row on keyboard Control + End', async () => {
            const { getByRole } = render(<Grid columns={columns} rows={rows} />)
            await userEvent.keyboard('{Tab}{Control>}{End}{/Control}')
            await waitFor(() =>
                expect(
                    getByRole('button', {
                        name: /Pending/,
                        expanded: false,
                    })
                ).toHaveFocus()
            )
        })

        it('should navigate to first column on keyboard Home', async () => {
            const { getByText, getAllByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            expect(getByText(/Fran/)).toHaveFocus()
            await userEvent.keyboard('{Home}')

            await waitFor(() =>
                expect(
                    getAllByRole('checkbox', {
                        name: 'Toggle row selection',
                    })[1]
                ).toHaveFocus()
            )
        })

        it('should navigate to first column in header row on keyboard Control + Home', async () => {
            const { getByText, getByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            expect(getByText(/Fran/)).toHaveFocus()
            await userEvent.keyboard('{Control>}{Home}{/Control}')
            await waitFor(() =>
                expect(
                    getByRole('checkbox', {
                        name: 'Select all rows',
                    })
                ).toHaveFocus()
            )
        })

        it('should navigate to last item on page down', async () => {
            const { getAllByRole } = render(
                <TestingProvider clientHeight={400} clientWidth={500}>
                    <Grid columns={columns} rows={rows} />
                </TestingProvider>
            )
            await userEvent.keyboard('{Tab}{PageDown}')
            await waitFor(() =>
                expect(
                    getAllByRole('checkbox', {
                        name: 'Toggle row selection',
                    })[1]
                ).toHaveFocus()
            )
        })
        it('should navigate to header on page up', async () => {
            const { getByText, getByRole } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowDown}{ArrowDown}{ArrowRight}')
            expect(getByText(/Fran/)).toHaveFocus()
            await userEvent.keyboard('{PageUp}')
            await waitFor(() =>
                expect(
                    getByRole('columnheader', {
                        name: /First Name/,
                    })
                ).toHaveFocus()
            )
        })

        it('should freeze navigation on opening dropdown', async () => {
            const { getByRole, getByText } = render(
                <Grid columns={columns} rows={rows} />
            )
            await userEvent.keyboard(
                '{Tab}{ArrowDown}{ArrowRight}{ArrowRight}{ArrowRight}'
            )
            expect(
                getByRole('button', {
                    name: /Active/,
                    expanded: false,
                })
            ).toHaveFocus()
            await userEvent.keyboard(' ')

            await userEvent.keyboard('{ArrowLeft}{ArrowDown}{ArrowLeft}')

            expect(getByRole('menu')).toHaveFocus()

            await userEvent.keyboard('{Escape}')

            await userEvent.keyboard('{ArrowLeft}')

            expect(getByText(/Doe/)).toHaveFocus()
        })

        it('should not select the row when clicking on a dropdown', async () => {
            const spy = jest.fn()
            const { getAllByRole, getByRole } = render(
                <Grid columns={columns} rows={rows} onSelectionChange={spy} />
            )
            const buttons = getAllByRole('button', {
                name: /Active/,
                expanded: false,
            })
            await userEvent.click(buttons[0])

            expect(getByRole('menu')).toHaveFocus()

            expect(spy).not.toHaveBeenCalled()
        })

        it('should focus back on the cell when starting by interacting with a click on dropdown menu', async () => {
            const spy = jest.fn()
            const { getAllByRole, getByRole, getByText } = render(
                <Grid columns={columns} rows={rows} onSelectionChange={spy} />
            )
            const buttons = getAllByRole('button', {
                name: /Active/,
                expanded: false,
            })
            await userEvent.click(buttons[0])

            expect(getByRole('menu')).toHaveFocus()

            await userEvent.click(
                getByRole('menuitemradio', { name: /Active/ })
            )

            await userEvent.keyboard('{ArrowLeft}')

            expect(getByText(/Doe/)).toHaveFocus()
        })
    })

    describe('Editing', () => {
        describe('aria-readonly', () => {
            beforeEach(() => {
                render(<Grid columns={columnsWithEditors} rows={rows} />)
            })

            it('should not set aria-readonly on grid when a cell is possibly editable', () => {
                expect(screen.queryByRole('grid')).not.toHaveAttribute(
                    'aria-readonly'
                )
            })

            it('should not mark editable cells as read only', () => {
                expect(
                    screen.queryByRole('gridcell', { name: /Joe/ })
                ).not.toHaveAttribute('aria-readonly')
            })

            it('should mark non-editable cells as read only', () => {
                expect(
                    screen.queryByRole('gridcell', { name: /Active/ })
                ).toHaveAttribute('aria-readonly', 'true')
            })
        })

        it('should not set aria-readonly on grid when a cell is possibly editable', () => {
            const { queryByRole } = render(
                <Grid columns={columnsWithEditors} rows={rows} />
            )
            expect(queryByRole('grid')).not.toHaveAttribute('aria-readonly')
        })

        describe('mouse interaction', () => {
            it('should support editing a single cell', async () => {
                const spy = jest.fn()
                const { getByRole, getByText } = render(
                    <Grid
                        columns={columnsWithEditors}
                        rows={rows}
                        onCellChange={spy}
                    />
                )

                await userEvent.click(getByRole('gridcell', { name: /Joe/ }))

                await userEvent.type(getByRole('textbox'), 'Jane', {
                    initialSelectionStart: 0,
                    initialSelectionEnd: 3,
                })

                await userEvent.keyboard('{Enter}')

                expect(getByText(/Joe/)).toHaveFocus()
                expect(spy).toHaveBeenCalledWith({
                    previousValue: 'Joe',
                    nextValue: 'Jane',
                    columnId: 'firstName',
                    rowId: 1,
                })
            })

            it('should support editing multiple cells', async () => {
                const spy = jest.fn()
                const { getByRole, getAllByRole } = render(
                    <Grid
                        columns={columnsWithEditors}
                        rows={rows}
                        onCellChange={spy}
                    />
                )

                await userEvent.click(getByRole('gridcell', { name: /Joe/ }))

                await userEvent.type(getByRole('textbox'), 'Jane', {
                    initialSelectionStart: 0,
                    initialSelectionEnd: 3,
                })

                await userEvent.click(getByRole('gridcell', { name: /Doe/ }))

                await userEvent.type(getByRole('textbox'), 'Green', {
                    initialSelectionStart: 0,
                    initialSelectionEnd: 3,
                })

                await userEvent.click(
                    getAllByRole('checkbox', { name: 'Status' })[0]
                )

                expect(spy).toHaveBeenCalledTimes(3)
                expect(spy).toHaveBeenCalledWith({
                    previousValue: 'Joe',
                    nextValue: 'Jane',
                    columnId: 'firstName',
                    rowId: 1,
                })
                expect(spy).toHaveBeenCalledWith({
                    previousValue: 'Doe',
                    nextValue: 'Green',
                    columnId: 'lastName',
                    rowId: 1,
                })
                expect(spy).toHaveBeenCalledWith({
                    previousValue: true,
                    nextValue: false,
                    columnId: 'status',
                    rowId: 1,
                })
            })
        })
        describe('GridEditorCombobox', () => {
            it('should call onCellChange on click', async () => {
                const spy = jest.fn()

                const options = [
                    {
                        label: 'option active',
                        value: 'active',
                    },
                    {
                        label: 'option inactive',
                        value: 'inactive',
                    },
                ]

                const { getByRole } = render(
                    <Grid
                        columns={[
                            {
                                id: 'status',
                                label: 'Status',
                                cell: {
                                    editable: true,
                                    Editor(props) {
                                        return (
                                            <GridEditorCombobox
                                                {...props}
                                                clearable={false}
                                                value={options.find(
                                                    (o) =>
                                                        o.value === props.value
                                                )}
                                                options={options}
                                            />
                                        )
                                    },
                                },
                            },
                        ]}
                        rows={[{ id: '1', status: 'active' }]}
                        onCellChange={spy}
                    />
                )

                await userEvent.click(getByRole('gridcell', { name: /active/ }))
                expect(spy).toHaveBeenCalledTimes(0)

                await userEvent.click(
                    getByRole('option', { name: /option inactive/ })
                )
                expect(spy).toHaveBeenCalledTimes(1)
            })
            it('should call onCellChange on Enter', async () => {
                const spy = jest.fn()
                const options = [
                    {
                        label: 'option active',
                        value: 'active',
                    },
                    {
                        label: 'option inactive',
                        value: 'inactive',
                    },
                ]
                const { getAllByRole, getByRole } = render(
                    <Grid
                        columns={[
                            {
                                id: 'status',
                                label: 'Status',
                                cell: {
                                    editable: true,
                                    Editor(props) {
                                        return (
                                            <GridEditorCombobox
                                                {...props}
                                                clearable={false}
                                                value={options.find(
                                                    (o) =>
                                                        o.value === props.value
                                                )}
                                                options={options}
                                            />
                                        )
                                    },
                                },
                            },
                            {
                                id: 'firstName',
                                label: 'First Name',
                                cell: {
                                    editable: true,
                                },
                            },
                        ]}
                        rows={[
                            { id: '1', status: 'active', firstName: 'test' },
                        ]}
                        onCellChange={spy}
                    />
                )

                await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}')
                expect(getAllByRole('option')).toHaveLength(2)
                await waitFor(() => expect(getByRole('combobox')).toHaveFocus())
                await userEvent.keyboard('{ArrowDown}{Enter}')
                expect(spy).toHaveBeenCalledTimes(1)
            })
            it('should call onCellChange on tab', async () => {
                const spy = jest.fn()
                const options = [
                    {
                        label: 'option active',
                        value: 'active',
                    },
                    {
                        label: 'option inactive',
                        value: 'inactive',
                    },
                ]
                const { getByRole } = render(
                    <Grid
                        columns={[
                            {
                                id: 'status',
                                label: 'Status',
                                cell: {
                                    editable: true,
                                    Editor(props) {
                                        return (
                                            <GridEditorCombobox
                                                {...props}
                                                clearable={false}
                                                value={options.find(
                                                    (o) =>
                                                        o.value === props.value
                                                )}
                                                options={options}
                                            />
                                        )
                                    },
                                },
                            },
                            {
                                id: 'firstName',
                                label: 'First Name',
                                cell: {
                                    editable: true,
                                },
                            },
                        ]}
                        rows={[
                            { id: '1', status: 'active', firstName: 'test' },
                        ]}
                        onCellChange={spy}
                    />
                )

                await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}')
                await waitFor(() => expect(getByRole('combobox')).toHaveFocus())
                await userEvent.keyboard('{ArrowDown}{ArrowDown}{Tab}')
                expect(spy).toHaveBeenCalledTimes(1)
            })
            it('should call not onCellChange on Escape', async () => {
                const spy = jest.fn()
                const options = [
                    {
                        label: 'option active',
                        value: 'active',
                    },
                    {
                        label: 'option inactive',
                        value: 'inactive',
                    },
                ]
                const { getAllByRole } = render(
                    <Grid
                        columns={[
                            {
                                id: 'status',
                                label: 'Status',
                                cell: {
                                    editable: true,
                                    Editor(props) {
                                        return (
                                            <GridEditorCombobox
                                                {...props}
                                                clearable={false}
                                                value={options.find(
                                                    (o) =>
                                                        o.value === props.value
                                                )}
                                                options={options}
                                            />
                                        )
                                    },
                                },
                            },
                            {
                                id: 'firstName',
                                label: 'First Name',
                                cell: {
                                    editable: true,
                                },
                            },
                        ]}
                        rows={[
                            { id: '1', status: 'active', firstName: 'test' },
                        ]}
                        onCellChange={spy}
                    />
                )

                await userEvent.keyboard('{Tab}{ArrowDown}{ArrowRight}{Enter}')
                expect(getAllByRole('option')).toHaveLength(2)
                await userEvent.keyboard('{Escape}')
                expect(spy).toHaveBeenCalledTimes(0)
            })
        })
    })
})
