import React from 'react'
import { ListGrid } from '.'
import type {
    Column,
    GridActionsMenuFullProps,
    GridActionsMenuProps,
    GridRowMeta,
} from '../../types'
import { render, snapshot, waitFor, within } from '../../utils/test-utils'
import userEvent from '@testing-library/user-event'
import { GridCellDefault, GridCellDropdownMenu } from '../../components'
import { EmptyState, ListItem, ListItemInfo, Menu } from '@planview/pv-uikit'
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 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] }],
])

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

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

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

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

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

        snapshot(
            'should support flexColumnId to specify which column should flex',
            <ListGrid columns={columns} rows={rows} flexColumnId="lastName" />
        )

        snapshot(
            'should default flexColumnId to first column',
            <ListGrid columns={columns} rows={rows} />
        )
    })

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

    it('should render a list grid without headers', () => {
        const { container } = render(<ListGrid columns={columns} rows={rows} />)

        // ListGrid should hide headers but show them to screen readers
        expect(container.querySelector('thead')).toHaveStyle(
            'clip: rect(0 0 0 0)'
        )
    })

    it('should render a list grid without selection checkboxes', () => {
        const { queryByRole } = render(
            <ListGrid columns={columns} rows={rows} />
        )

        // ListGrid forces selectionMode="none"
        expect(queryByRole('checkbox')).not.toBeInTheDocument()
    })

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

        // Should render more rows with larger container
        expect(queryAllByRole('row').length).toBeGreaterThan(25)
    })

    it('should render a tree structure', async () => {
        const treeColumns = columns.map((c) => {
            if (c.id === 'firstName') {
                return {
                    ...c,
                    tree: true,
                }
            }
            return c
        })

        const { queryByText, getByRole } = render(
            <ListGrid
                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 handle both rows', () => {
        const { getByRole } = render(<ListGrid columns={columns} rows={rows} />)
        expect(getByRole('gridcell', { name: /Joe/ })).toBeInTheDocument()
        expect(getByRole('gridcell', { name: /Fran/ })).toBeInTheDocument()
    })

    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(
                        <ListGrid
                            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(
                        <ListGrid
                            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(
                        <ListGrid
                            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 support conditional rendering', async () => {
                    const { getAllByRole, queryByRole } = render(
                        <ListGrid
                            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('should navigate to second column, first row', async () => {
            const { getByText } = render(
                <ListGrid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{ArrowRight}')
            expect(getByText(/Doe/)).toHaveFocus()
        })

        it('should navigate to last column on keyboard End', async () => {
            const { getByRole } = render(
                <ListGrid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{End}')
            await waitFor(() =>
                expect(
                    getByRole('button', {
                        name: /Active/,
                        expanded: false,
                    })
                ).toHaveFocus()
            )
        })

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

            await waitFor(() => expect(getByText(/Joe/)).toHaveFocus())
        })

        it('should freeze navigation on opening dropdown', async () => {
            const { getByRole, getByText } = render(
                <ListGrid columns={columns} rows={rows} />
            )
            await userEvent.keyboard('{Tab}{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 { getAllByRole, getByRole } = render(
                <ListGrid columns={columns} rows={rows} />
            )
            const buttons = getAllByRole('button', {
                name: /Active/,
                expanded: false,
            })
            await userEvent.click(buttons[0])

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