import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { GlyphIcon } from '@theorchard/suite-icons';
import { testComponent } from 'lib/test-utils/common';
import { map } from 'lodash-es';
import { GridTable } from '../table';
import { ColumnDefinition, TableProps } from '../types';

interface TestData {
    name: string | null;
    value: number;
}

const testDefaultTable = (data: TestData[]) => {
    const columnHeaderTextSelector = (index: number) =>
        `.GridTable-table > .GridTable-header-row > .GridTable-column:nth-child(${index}) .GridTable-column-text`;

    const cellTextSelector = (index: number) =>
        `.GridTable-table > .GridTable-body > .GridTable-row:nth-child(${index}) > .GridTable-cell > .GridTable-cell-text`;

    test('matches snapshot', () => {
        expect(document.body.firstChild).toMatchSnapshot();
    });

    test('renders with default class names', () => {
        const tableElement = screen.getByTestId('GridTable');

        expect(tableElement).toHaveClass('GridTable');
        expect(tableElement).not.toHaveClass('bordered');
        expect(tableElement).not.toHaveClass('loading');
        expect(tableElement).not.toHaveClass('updating');
        expect(tableElement).not.toHaveClass('stickyColumnHeaders');
    });

    test('renders rows with data', () => {
        data.forEach(({ name, value }, index) => {
            const selector = cellTextSelector(index + 1);

            const nameCell = screen.getByText(name!, { selector });
            const valueCell = screen.getByText(value, { selector });

            expect(nameCell).toBeInTheDocument();
            expect(valueCell).toBeInTheDocument();
        });
    });

    test('renders column headers', () => {
        const nameHeader = screen.getByText('name', {
            selector: columnHeaderTextSelector(1),
        });

        const valueHeader = screen.getByText('value', {
            selector: columnHeaderTextSelector(2),
        });

        expect(nameHeader).toBeInTheDocument();
        expect(valueHeader).toBeInTheDocument();
    });

    test('does not render empty state', () => {
        const tableElement = screen.getByTestId('GridTable');

        const emptyStateElement = tableElement.querySelector('.GridTable-empty');

        expect(emptyStateElement).toBeNull();
    });
};

describe('<Table>', () => {
    const data: TestData[] = [
        { name: 'test1', value: 2 },
        { name: 'test2', value: 1 },
    ];

    const defaultProps: TableProps<TestData> = {
        data: [],
    };

    const renderTable = (props: Partial<TableProps<TestData>> = {}) =>
        render(<GridTable {...defaultProps} {...props} />);

    testComponent(GridTable, defaultProps);

    describe('with data', () => {
        describe('no column definitions', () => {
            beforeEach(() => {
                renderTable({ data });
            });

            testDefaultTable(data);
        });

        describe('with column definitions', () => {
            const columnDefs = [{ name: 'name' }, { name: 'value' }];

            beforeEach(() => {
                renderTable({ data, columnDefs });
            });

            testDefaultTable(data);
        });

        describe('with child Column elements', () => {
            beforeEach(() => {
                render(
                    <GridTable data={data}>
                        <GridTable.Column name="name" />
                        <GridTable.Column name="value" />
                    </GridTable>
                );
            });

            testDefaultTable(data);
        });
    });

    describe('without data', () => {
        const emptyTextSelector = '.GridTable-empty .InfoMessage-title';

        beforeEach(() => {
            renderTable({ data: [] });
        });

        test('matches snapshot', () => {
            expect(document.body.firstChild).toMatchSnapshot();
        });

        test('renders empty state', () => {
            const emptyStateElement = screen.getByText('data_empty', {
                selector: emptyTextSelector,
            });

            expect(emptyStateElement).toBeInTheDocument();
        });

        test('renders given empty state component', () => {
            const emptyMessage = 'Empty Message New';
            const emptyStateComponent = <div>{emptyMessage}</div>;

            const { getByText } = renderTable({ emptyStateComponent });

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

        test('renders given empty state title and body', () => {
            const emptyStateTitle = 'No data';
            const emptyStateBody = 'Because we had no data';
            const { getByText } = renderTable({
                emptyStateTitle,
                emptyStateBody,
            });

            expect(getByText(emptyStateTitle)).toBeVisible();
            expect(getByText(emptyStateBody)).toBeVisible();
        });

        test('renders given empty state ComponentType icon', () => {
            const { getByTestId } = renderTable({
                emptyStateIcon: <GlyphIcon name="search" size={16} />,
            });

            expect(getByTestId('SearchGlyphIcon')).toBeVisible();
        });

        test('renders given empty state ReactNode icon', () => {
            const { getByTestId } = renderTable({
                emptyStateIcon: <GlyphIcon name="search" size={24} />,
            });

            expect(getByTestId('SearchGlyphIcon')).toBeVisible();
        });
    });

    describe('outer header', () => {
        beforeEach(() => {
            renderTable({ data });
        });

        test('is not rendered by default', () => {
            const tableElement = screen.getByTestId('GridTable');

            const headerElement = tableElement.querySelector('.GridTable-header');
            expect(headerElement).toBeNull();
        });

        test('is rendered when defined', () => {
            const headerText = 'outer header';
            renderTable({ header: <>{headerText}</> });

            const headerElement = screen.getByText(headerText, {
                selector: '.GridTable-header',
            });

            expect(headerElement).toBeInTheDocument();
        });
    });

    describe('CSS Grid', () => {
        test('defaults to "minmax(min-content, 1fr)" grid column width', () => {
            render(
                <GridTable data={data}>
                    <GridTable.Column name="name" />
                    <GridTable.Column name="value" />
                </GridTable>
            );

            const table = document.querySelector('.GridTable-table');
            const tableStyle = window.getComputedStyle(table!);

            expect(tableStyle.getPropertyValue('grid-template-columns')).toBe(
                'minmax(min-content, 1fr) minmax(min-content, 1fr)'
            );
        });

        test('applies defined column widths', () => {
            render(
                <GridTable data={data}>
                    <GridTable.Column name="name" minWidth="50px" maxWidth="100px" />
                    <GridTable.Column name="value" minWidth="min-content" maxWidth="max-content" />
                </GridTable>
            );

            const table = document.querySelector('.GridTable-table');
            const tableStyle = window.getComputedStyle(table!);

            expect(tableStyle.getPropertyValue('grid-template-columns')).toBe(
                'minmax(50px, 100px) minmax(min-content, max-content)'
            );
        });
    });

    describe('with property "bordered" enabled', () => {
        test('adds table "bordered" class', () => {
            renderTable({ bordered: true });

            const tableElement = screen.getByTestId('GridTable');

            expect(tableElement).toHaveClass('bordered');
        });
    });

    describe('with property "loading" enabled', () => {
        test('adds table "loading" class', () => {
            renderTable({ loading: true, data: [] });

            const tableElement = screen.getByTestId('GridTable');

            expect(tableElement).toHaveClass('loading');
            expect(tableElement).not.toHaveClass('updating');
        });
    });

    describe('with property "loading" enabled and has data', () => {
        test('adds table "updating" class', () => {
            renderTable({ loading: true, data });

            const tableElement = screen.getByTestId('GridTable');

            expect(tableElement).toHaveClass('loading');
            expect(tableElement).toHaveClass('updating');
        });
    });

    describe('with property "loading" enabled and has no data', () => {
        test('adds table "loadingEmpty" class', () => {
            renderTable({ loading: true, data: [] });

            const tableElement = screen.getByTestId('GridTable');

            expect(tableElement).toHaveClass('loading');
            expect(tableElement).toHaveClass('loading-empty');
        });
    });

    describe('with property "noColumnHeaders" enabled', () => {
        test('does not render table column headers', () => {
            renderTable({ hideColumnHeaders: true });

            const tableElement = screen.getByTestId('GridTable');
            const headElement = tableElement.querySelector('.GridTable-header-row');

            expect(headElement).toBeNull();
        });
    });

    describe('with property "noEmptyState" enabled', () => {
        test('does not render empty state', () => {
            renderTable({ data: [], hideEmptyState: true });

            const emptyStateElement = screen.queryByText('data_empty');

            expect(emptyStateElement).not.toBeInTheDocument();
        });
    });

    describe('with property "stickyColumnHeaders" enabled', () => {
        test('adds table "stickyColumnHeaders" class', () => {
            renderTable({ data: [], stickyColumnHeaders: true });

            const tableElement = screen.getByTestId('GridTable');

            expect(tableElement).toHaveClass('with-sticky-header');
        });
    });

    describe('with property "sortBy"', () => {
        const VAL1 = 'VAL-1';
        const VAL2 = 'VAL-2';
        const VAL3 = 'VAL-3';
        const VAL4 = null;

        const getCellValues = (container: HTMLElement, col: keyof TestData) => {
            const cells = container.querySelectorAll(`.cell-${col} > .GridTable-cell-text`);
            return map(cells, (cell) => cell.innerHTML);
        };

        const props: TableProps<TestData> = {
            sortBy: { key: 'name', direction: 'asc' },
            data: [
                { value: 2, name: VAL2 },
                { value: 3, name: VAL3 },
                { value: 1, name: VAL1 },
                { value: 4, name: VAL4 },
            ],
        };

        test('renders sorted table body', () => {
            const { container } = renderTable(props);

            expect(getCellValues(container, 'name')).toEqual([VAL1, VAL2, VAL3, '-']);
        });

        describe('with "direction" set to "desc"', () => {
            test('renders table in descending order', () => {
                const { container } = renderTable({
                    ...props,
                    sortBy: { key: 'name', direction: 'desc' },
                });
                expect(getCellValues(container, 'name')).toEqual(['-', VAL3, VAL2, VAL1]);
            });
        });

        describe('sortBy values updated', () => {
            test('renders table with new sort configuration', () => {
                const { container, rerender } = renderTable({
                    ...props,
                    sortBy: { key: 'name', direction: 'desc' },
                });

                expect(getCellValues(container, 'name')).toEqual(['-', VAL3, VAL2, VAL1]);

                rerender(<GridTable {...props} sortBy={{ key: 'value', direction: 'asc' }} />);

                expect(getCellValues(container, 'value')).toEqual(['1', '2', '3', '4']);
            });
        });

        describe('with "sortBy.nullsLast"', () => {
            test('renders nulls last for asc direction', () => {
                const { container } = renderTable({
                    ...props,
                    sortBy: { key: 'name', direction: 'asc', nullsLast: true },
                });

                expect(getCellValues(container, 'name')).toEqual([VAL1, VAL2, VAL3, '-']);
            });

            test('renders nulls last for desc direction', () => {
                const { container } = renderTable({
                    ...props,
                    sortBy: { key: 'name', direction: 'desc', nullsLast: true },
                });

                expect(getCellValues(container, 'name')).toEqual([VAL3, VAL2, VAL1, '-']);
            });
        });
    });

    describe('on sortable column header is clicked', () => {
        const getNameColumnHeader = () =>
            screen.getByText('name', {
                selector: '.GridTable-column-text',
            });

        const getColumnCellValues = () => {
            const tableElement = screen.getByTestId('GridTable');
            const cellElements = tableElement.querySelectorAll(
                '.GridTable-cell.cell-name > .GridTable-cell-text'
            );

            return map(cellElements, (cellElement) => cellElement.innerHTML);
        };

        const columnDefs: ColumnDefinition<TestData>[] = [
            {
                name: 'name',
                sortable: true,
                defaultSortDirection: 'desc',
            },
            { name: 'value', sortable: true },
        ];

        test('sorts the rows by column name', () => {
            renderTable({
                data,
                columnDefs,
            });

            const nameHeader = getNameColumnHeader();

            expect(getColumnCellValues()).toEqual(['test1', 'test2']);

            fireEvent.click(nameHeader);

            expect(getColumnCellValues()).toEqual(['test2', 'test1']);

            fireEvent.click(nameHeader);

            expect(getColumnCellValues()).toEqual(['test1', 'test2']);
        });

        test('sorts the rows by column "sortKey"', () => {
            renderTable({
                data,
                columnDefs: [
                    {
                        name: 'name',
                        sortKey: 'value',
                        sortable: true,
                        defaultSortDirection: 'desc',
                    },
                    { name: 'value' },
                ],
            });

            const nameHeader = getNameColumnHeader();

            expect(getColumnCellValues()).toEqual(['test1', 'test2']);

            fireEvent.click(nameHeader);

            expect(getColumnCellValues()).toEqual(['test1', 'test2']);

            fireEvent.click(nameHeader);

            expect(getColumnCellValues()).toEqual(['test2', 'test1']);
        });

        test('column sorting indicator changes', () => {
            renderTable({
                data,
                columnDefs,
            });

            const nameHeader = getNameColumnHeader();

            const getSortingIndicator = (direction: 'Asc' | 'Desc') => {
                const sName = `.Arrow${direction === 'Asc' ? 'Up' : 'Down'}GlyphIcon`;
                return nameHeader.parentElement!.querySelector(sName);
            };

            expect(getSortingIndicator('Desc')).toBeInTheDocument();
            expect(getSortingIndicator('Asc')).toBeNull();

            fireEvent.click(nameHeader);

            expect(getSortingIndicator('Desc')).toBeInTheDocument();
            expect(getSortingIndicator('Asc')).toBeNull();

            fireEvent.click(nameHeader);

            expect(getSortingIndicator('Desc')).toBeNull();
            expect(getSortingIndicator('Asc')).toBeInTheDocument();

            const valueHeader = screen.getByText('value', {
                selector: '.GridTable-column-text',
            });

            fireEvent.click(valueHeader);

            expect(getSortingIndicator('Desc')).toBeInTheDocument();
            expect(getSortingIndicator('Asc')).toBeNull();
        });

        describe('when "onSort" prop supplied', () => {
            test('onSort is called with column name and direction', () => {
                const onSort = vi.fn();
                renderTable({ onSort, data, columnDefs });

                const nameHeader = getNameColumnHeader();

                fireEvent.click(nameHeader);

                expect(onSort).toHaveBeenCalledWith([{ direction: 'desc', key: 'name' }]);
            });
        });
    });

    describe('with "variant"', () => {
        test('sets className', () => {
            renderTable({ variant: 'zebra' });
            const tableElement = screen.getByTestId('GridTable');
            expect(tableElement).toHaveClass('zebra-variant');
        });
    });
});
