import React, { ReactElement } from 'react';
import { fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';

import { Column, IdType, Row, SortingRule } from 'react-table';

import renderWithTheme from '../../../testUtils/renderWithTheme';

import Table, { hasFixedLayout } from '../Table';
import Input from '../../Input';
import { fromUndefinedable, getOrElse } from '../../../fp/Option';
import { pipe } from '../../../fp/function';

type RowShape = {
  age: number;
  name: string;
  status: string;
};

const columns: Column<RowShape>[] = [
  { Header: 'Name', accessor: 'name' },
  { Header: 'Age', accessor: 'age' },
  { Header: 'Status', accessor: 'status', disableSortBy: true },
];
const data = [
  { name: 'Hanh Le', age: 20, status: 'Complicated' },
  { name: 'Vinh Tony', age: 21, status: 'In relationship' },
  { name: 'Hau Dao', age: 19, status: 'Single' },
];

describe('rendering', () => {
  it('renders header & data', () => {
    const { getByText } = renderWithTheme(
      <Table data={data} columns={columns} />
    );

    expect(getByText('Name')).toBeInTheDocument();
    expect(getByText('Hanh Le')).toBeInTheDocument();
    expect(getByText('Vinh Tony')).toBeInTheDocument();
    expect(getByText('Hau Dao')).toBeInTheDocument();

    expect(getByText('Age')).toBeInTheDocument();
    expect(getByText('20')).toBeInTheDocument();
    expect(getByText('21')).toBeInTheDocument();
    expect(getByText('19')).toBeInTheDocument();

    expect(getByText('Status')).toBeInTheDocument();
    expect(getByText('Complicated')).toBeInTheDocument();
    expect(getByText('In relationship')).toBeInTheDocument();
    expect(getByText('Single')).toBeInTheDocument();
  });

  it('allows to custom cell rendering', () => {
    const Cell = ({ value }: { value: string }): ReactElement => (
      <div>{`Custom: ${value}`}</div>
    );

    const columnsWithCustomCell: Column<RowShape>[] = [
      { Header: 'Name', accessor: 'name' },
      { Header: 'Age', accessor: 'age' },
      {
        Header: 'Status',
        accessor: 'status',
        disableSortBy: true,
        Cell,
      },
    ];
    const { getByText } = renderWithTheme(
      <Table data={data} columns={columnsWithCustomCell} />
    );

    expect(getByText('Name')).toBeInTheDocument();
    expect(getByText('Hanh Le')).toBeInTheDocument();
    expect(getByText('Vinh Tony')).toBeInTheDocument();
    expect(getByText('Hau Dao')).toBeInTheDocument();

    expect(getByText('Age')).toBeInTheDocument();
    expect(getByText('20')).toBeInTheDocument();
    expect(getByText('21')).toBeInTheDocument();
    expect(getByText('19')).toBeInTheDocument();

    expect(getByText('Status')).toBeInTheDocument();
    expect(getByText('Custom: Complicated')).toBeInTheDocument();
    expect(getByText('Custom: In relationship')).toBeInTheDocument();
    expect(getByText('Custom: Single')).toBeInTheDocument();
  });

  it('renders empty interface when there is no data', () => {
    const { getByText } = renderWithTheme(
      <Table data={[]} columns={columns} noData="No data to display" />
    );
    expect(getByText('No data to display')).toBeInTheDocument();
  });
});

describe('interaction', () => {
  describe('sorting', () => {
    it('allows to sort on enabled sorting columns', () => {
      const MyTable = (): ReactElement => {
        const [sortBy, setSortBy] = React.useState<SortingRule<RowShape>[]>([]);
        return (
          <Table
            data={data}
            columns={columns}
            sortBy={sortBy}
            onSortByChange={setSortBy}
          />
        );
      };
      const { getByText, container } = renderWithTheme(<MyTable />);

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
        'Hau Dao',
        '19',
        'Single',
      ]);

      // 1st click: Sort by age asc
      fireEvent.click(getByText('Age'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Hau Dao',
        '19',
        'Single',
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
      ]);

      // 2nd click: Sort by age desc
      fireEvent.click(getByText('Age'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Vinh Tony',
        '21',
        'In relationship',
        'Hanh Le',
        '20',
        'Complicated',
        'Hau Dao',
        '19',
        'Single',
      ]);

      // 3rd click: Reset to original order
      fireEvent.click(getByText('Age'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
        'Hau Dao',
        '19',
        'Single',
      ]);
    });

    it('allows to sort on enabled sorting columns with icons', () => {
      const MyTable = (): ReactElement => {
        const [sortBy, setSortBy] = React.useState<SortingRule<RowShape>[]>([]);
        return (
          <Table
            data={data}
            columns={columns}
            sortBy={sortBy}
            onSortByChange={setSortBy}
          />
        );
      };
      const { getByTestId, container } = renderWithTheme(<MyTable />);

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
        'Hau Dao',
        '19',
        'Single',
      ]);

      // 1st click: Sort by age desc
      fireEvent.click(getByTestId('table__header_age__down'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Vinh Tony',
        '21',
        'In relationship',
        'Hanh Le',
        '20',
        'Complicated',
        'Hau Dao',
        '19',
        'Single',
      ]);

      // 2nd click: Sort by age asc
      fireEvent.click(getByTestId('table__header_age__up'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Hau Dao',
        '19',
        'Single',
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
      ]);

      // 3rd click: Sort by name desc
      fireEvent.click(getByTestId('table__header_name__down'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual([
        'Vinh Tony',
        '21',
        'In relationship',
        'Hau Dao',
        '19',
        'Single',
        'Hanh Le',
        '20',
        'Complicated',
      ]);
    });

    it('DOES NOT allow to sort on disabled sorting columns', () => {
      const MyTable = (): ReactElement => {
        const [sortBy, setSortBy] = React.useState<SortingRule<RowShape>[]>([]);
        return (
          <Table
            data={data}
            columns={columns}
            sortBy={sortBy}
            onSortByChange={setSortBy}
          />
        );
      };
      const { getByText, queryByTestId, container } = renderWithTheme(
        <MyTable />
      );

      const orginalOrder = [
        'Hanh Le',
        '20',
        'Complicated',
        'Vinh Tony',
        '21',
        'In relationship',
        'Hau Dao',
        '19',
        'Single',
      ];

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual(orginalOrder);

      // Click on disabled sort by column: Status
      fireEvent.click(getByText('Status'));

      expect(
        Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML)
      ).toEqual(orginalOrder);

      // Does not show sort icons
      expect(
        queryByTestId('table__header_status__down')
      ).not.toBeInTheDocument();
      expect(queryByTestId('table__header_status__up')).not.toBeInTheDocument();
    });
  });

  describe('pagination', () => {
    it('allows to control paginated data', () => {
      const paginatedData = [
        [
          { name: 'Vy Nguyen', age: 50, status: 'Single' },
          { name: 'Hau Dao', age: 51, status: 'Single' },
        ],
        [
          { name: 'Tuan Thieu', age: 99, status: 'In relationship' },
          { name: 'Son Trinh', age: 69, status: 'Separated' },
        ],
      ];
      const MyTable = (): ReactElement => {
        const [currentPage, setCurrentPage] = React.useState(1);
        return (
          <Table
            data={pipe(
              paginatedData[currentPage - 1],
              fromUndefinedable,
              getOrElse(() => [])
            )}
            columns={columns}
            pagination={{ current: currentPage, total: paginatedData.length }}
            onPaginationChange={setCurrentPage}
          />
        );
      };

      const { getByText, getByTestId } = renderWithTheme(<MyTable />);
      expect(getByTestId('table__cell_0_name')).toHaveTextContent('Vy Nguyen');
      expect(getByTestId('table__cell_0_age')).toHaveTextContent('50');
      expect(getByTestId('table__cell_0_status')).toHaveTextContent('Single');

      expect(getByTestId('table__cell_1_name')).toHaveTextContent('Hau Dao');
      expect(getByTestId('table__cell_1_age')).toHaveTextContent('51');
      expect(getByTestId('table__cell_1_status')).toHaveTextContent('Single');

      // Click on page 2
      fireEvent.click(getByText('2'));

      expect(getByTestId('table__cell_0_name')).toHaveTextContent('Tuan Thieu');
      expect(getByTestId('table__cell_0_age')).toHaveTextContent('99');
      expect(getByTestId('table__cell_0_status')).toHaveTextContent(
        'In relationship'
      );

      expect(getByTestId('table__cell_1_name')).toHaveTextContent('Son Trinh');
      expect(getByTestId('table__cell_1_age')).toHaveTextContent('69');
      expect(getByTestId('table__cell_1_status')).toHaveTextContent(
        'Separated'
      );
    });
  });

  describe('filtering', () => {
    it('allows to filter table data', async () => {
      const NameFilter = ({
        column: { filterValue, setFilter },
      }: {
        column: {
          filterValue?: string;
          setFilter: (value: string) => void;
        };
      }): ReactElement => (
        <Input
          placeholder="Search..."
          size="small"
          value={filterValue}
          onChange={(e): void => {
            setFilter(e.target.value);
          }}
        />
      );
      const columnsWithFilter: Column<RowShape>[] = [
        { Header: 'Name', accessor: 'name', Filter: NameFilter },
        { Header: 'Age', accessor: 'age' },
        {
          Header: 'Status',
          accessor: 'status',
        },
      ];
      const MyTable = (): ReactElement => {
        const [filters, setFilters] = React.useState([
          { id: 'name', value: '' },
        ]);
        const [nameFilter] = filters;
        const filteredData = React.useMemo(
          () =>
            data.filter(
              ({ name }) =>
                nameFilter === undefined ||
                nameFilter.value === '' ||
                name.includes(nameFilter.value)
            ),
          [nameFilter]
        );
        return (
          <Table
            data={filteredData}
            columns={columnsWithFilter}
            filters={filters}
            onFiltersChange={setFilters}
          />
        );
      };

      const {
        queryByText,
        getByTestId,
        getByPlaceholderText,
      } = renderWithTheme(<MyTable />);

      expect(queryByText('Hanh Le')).toBeInTheDocument();
      expect(queryByText('Vinh Tony')).toBeInTheDocument();
      expect(queryByText('Hau Dao')).toBeInTheDocument();

      // Open name filter dropdown
      await act(async () => {
        fireEvent.click(getByTestId('table__header_name__filter'));
      });

      fireEvent.change(getByPlaceholderText('Search...'), {
        target: { value: 'Vinh' },
      });

      expect(queryByText('Hanh Le')).not.toBeInTheDocument();
      expect(queryByText('Vinh Tony')).toBeInTheDocument();
      expect(queryByText('Hau Dao')).not.toBeInTheDocument();
    });
  });

  describe('row expansion', () => {
    it('allows to expand rows', () => {
      const expandedRowRenderer = (rowData: RowShape): ReactElement => (
        <tr>
          <td>{`${rowData.name} - ${rowData.age}`}</td>
        </tr>
      );
      const expansionConfig = {
        expandedRowRenderer,
        rowExpandable: (rowData: RowShape): boolean => rowData.age >= 18,
      };
      const MyTable = (): ReactElement => {
        const [expandedRows, setExpandedRows] = React.useState<
          Record<IdType<RowShape>, boolean>
        >({ 0: true });
        return (
          <Table
            data={data}
            columns={columns}
            expansion={expansionConfig}
            expandedRows={expandedRows}
            onExpandedRowsChange={setExpandedRows}
          />
        );
      };

      const { queryByText, container } = renderWithTheme(<MyTable />);
      expect(queryByText('Hanh Le')).toBeInTheDocument();
      expect(queryByText('20')).toBeInTheDocument();
      expect(queryByText('Hanh Le - 20')).toBeInTheDocument();

      expect(queryByText('Vinh Tony')).toBeInTheDocument();
      expect(queryByText('21')).toBeInTheDocument();
      expect(queryByText('Vinh Tony - 21')).not.toBeInTheDocument();

      expect(queryByText('Hau Dao')).toBeInTheDocument();
      expect(queryByText('19')).toBeInTheDocument();
      expect(queryByText('Hau Dao - 19')).not.toBeInTheDocument();

      fireEvent.click(container.querySelectorAll('.hero-icon-add').item(0));
      expect(queryByText('Vinh Tony - 21')).toBeInTheDocument();
    });
  });

  describe('row selection', () => {
    it('allows to select rows', () => {
      const mockSetSelectedRows = jest.fn();
      const MyTable = (): ReactElement => {
        const [selectedRows, setSelectedRows] = React.useState<
          Record<IdType<RowShape>, boolean>
        >({});
        return (
          <Table
            data={data}
            columns={columns}
            selectable
            selectedRows={selectedRows}
            onSelectedRowsChange={(value): void => {
              setSelectedRows(value);
              mockSetSelectedRows(value);
            }}
          />
        );
      };
      const { container } = renderWithTheme(<MyTable />);

      // Click on first checkbox will select all rows
      fireEvent.click(
        container.querySelectorAll('input[type=checkbox]').item(0)
      );
      expect(mockSetSelectedRows).toHaveBeenCalledWith({
        0: true,
        1: true,
        2: true,
      });

      mockSetSelectedRows.mockReset();

      // Click on first checkbox again will deselect all rows
      fireEvent.click(
        container.querySelectorAll('input[type=checkbox]').item(0)
      );
      expect(mockSetSelectedRows).toHaveBeenCalledWith({});

      mockSetSelectedRows.mockReset();

      // Click on second checkbox will select the first row
      fireEvent.click(
        container.querySelectorAll('input[type=checkbox]').item(1)
      );
      expect(mockSetSelectedRows).toHaveBeenCalledWith({
        0: true,
      });
    });
  });

  describe('row custom color', () => {
    it('allow to set custom background color', async () => {
      const rows = {
        generateClassName: (row: Row<RowShape>): string | undefined => {
          if (row.original.age > 19) {
            return 'custom-bg custom-hover-bg';
          }
          if (row.original.name === 'Hau Dao') {
            return 'custom-bg-yellow';
          }
          return undefined;
        },
      };
      const { getByTestId } = renderWithTheme(
        <Table rows={rows} data={data} columns={columns} />
      );
      expect(getByTestId('table__row_0')).toHaveClass(
        'custom-bg custom-hover-bg'
      );
      expect(getByTestId('table__row_1')).toHaveClass(
        'custom-bg custom-hover-bg'
      );
      expect(getByTestId('table__row_2')).toHaveClass('custom-bg-yellow');
      expect(getByTestId('table__row_2')).not.toHaveClass(
        'custom-bg custom-hover-bg'
      );
    });
  });
});

describe('hasFixedLayout', () => {
  it('returns true', () => {
    const headers = [
      { Header: 'Name', accessor: 'name', width: '50%' },
      { Header: 'Age', accessor: 'age' },
      { Header: 'Status', accessor: 'status' },
    ];

    expect(hasFixedLayout(headers)).toBe(true);
  });

  it('returns false', () => {
    const headers = [
      { Header: 'Name', accessor: 'name' },
      { Header: 'Age', accessor: 'age' },
      { Header: 'Status', accessor: 'status' },
    ];

    expect(hasFixedLayout(headers)).toBe(false);
  });
});
