import React, { useState } from 'react';
import { waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import renderWithTheme from '../../../testUtils/renderWithTheme';

import SingleSelect from '..';
import Icon from '../../Icon';

describe('rendering', () => {
  it('renders input and not render list when disabled', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={jest.fn()}
        disabled
      />
    );

    await waitFor(() => {
      expect(getByPlaceholderText('Select an item')).toBeInTheDocument();
      expect(getByText('Item 1')).not.toBeVisible();
      expect(getByText('Item 2')).not.toBeVisible();
    });

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('Item 1')).not.toBeVisible();
      expect(getByText('Item 2')).not.toBeVisible();
    });
  });

  it('renders input and option list', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={jest.fn()}
      />
    );

    await waitFor(() => {
      expect(getByPlaceholderText('Select an item')).toBeInTheDocument();
      expect(getByText('Item 1')).not.toBeVisible();
      expect(getByText('Item 2')).not.toBeVisible();
    });

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('Item 1')).toBeVisible();
      expect(getByText('Item 2')).toBeVisible();
    });
  });

  it('renders input and option list with category names', async () => {
    const options = [
      {
        category: 'Teams',
        options: [{ value: 'team-1', text: 'Team 1', helpText: '5 members' }],
      },
      {
        category: 'Locations',
        options: [{ value: 'location-1', text: 'Location 1' }],
      },
      {
        category: 'Individual',
        options: [{ value: 'person-1', text: 'Person 1' }],
      },
    ];
    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={jest.fn()}
      />
    );

    await waitFor(() => {
      expect(getByPlaceholderText('Select an item')).toBeInTheDocument();
      expect(getByText('Teams')).not.toBeVisible();
      expect(getByText('Locations')).not.toBeVisible();
      expect(getByText('Individual')).not.toBeVisible();
      expect(getByText('Team 1')).not.toBeVisible();
      expect(getByText('Location 1')).not.toBeVisible();
      expect(getByText('Person 1')).not.toBeVisible();
      expect(getByText('5 members')).not.toBeVisible();
    });

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('Teams')).toBeVisible();
      expect(getByText('Locations')).toBeVisible();
      expect(getByText('Individual')).toBeVisible();
      expect(getByText('Team 1')).toBeVisible();
      expect(getByText('Location 1')).toBeVisible();
      expect(getByText('Person 1')).toBeVisible();
      expect(getByText('5 members')).toBeVisible();
    });
  });

  it('renders custom option renderer with additional option props', async () => {
    const options = [
      { value: 'item-1', text: 'Item 1', icon: 'add-person' } as const,
      { value: 'item-2', text: 'Item 2', icon: 'alignment' } as const,
    ];

    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        optionRenderer={({ option: { text, icon }, index }) => (
          <>
            <Icon icon={icon} />
            <span style={{ margin: 0, padding: 0 }}>{`${
              index + 1
            }: ${text}`}</span>
          </>
        )}
        onChange={jest.fn()}
        placeholder="Select an item"
      />
    );

    await waitFor(() => {
      expect(getByPlaceholderText('Select an item')).toBeInTheDocument();
    });

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('1: Item 1')).toBeVisible();
      expect(getByText('2: Item 2')).toBeVisible();
    });

    expect(
      getByText('1: Item 1').parentElement?.querySelector('i')
    ).toHaveClass('hero-icon-add-person');
    expect(
      getByText('2: Item 2').parentElement?.querySelector('i')
    ).toHaveClass('hero-icon-alignment');
  });
});

describe('interaction', () => {
  it('allows to select an item', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const onChange = jest.fn();
    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={onChange}
      />
    );

    fireEvent.click(getByPlaceholderText('Select an item'));
    fireEvent.click(getByText('Item 1'));

    await waitFor(() => {
      expect(onChange).toHaveBeenCalledTimes(1);
      expect(onChange).toHaveBeenCalledWith(1);
    });
  });

  it('allows to select an item from grouped options', async () => {
    const options = [
      {
        category: 'Teams',
        options: [
          { value: 'team-1', text: 'Team 1', helpText: '5 members' },
          { value: 'team-2', text: 'Team 2', helpText: '3 members' },
        ],
      },
      {
        category: 'Locations',
        options: [{ value: 'location-1', text: 'Location 1' }],
      },
      {
        category: 'Individual',
        options: [{ value: 'person-1', text: 'Person 1' }],
      },
    ];
    const onChange = jest.fn();
    const { getByText, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={onChange}
      />
    );

    fireEvent.click(getByPlaceholderText('Select an item'));
    fireEvent.click(getByText('Location 1'));

    await waitFor(() => {
      expect(onChange).toHaveBeenCalledTimes(1);
      expect(onChange).toHaveBeenCalledWith('location-1');
    });
  });

  it('allows to search for an item', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const onChange = jest.fn();
    const onQueryChange = jest.fn();
    const { getByText, getByPlaceholderText, queryByText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        query="1"
        onQueryChange={onQueryChange}
      />
    );

    await waitFor(() => {
      expect(getByText('Item 1')).toBeInTheDocument();
      expect(queryByText('Item 2')).not.toBeInTheDocument();
    });

    fireEvent.change(getByPlaceholderText('Select an item'), {
      target: { value: '2' },
    });

    await waitFor(() => {
      expect(onQueryChange).toHaveBeenCalledTimes(1);
      expect(onQueryChange).toHaveBeenCalledWith('2');
    });
  });

  it('allows to create new item', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const onChange = jest.fn();
    const onQueryChange = jest.fn();
    const onCreateNewOption = jest.fn();
    const { getByText, getByPlaceholderText, queryByText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        query="New Item"
        onQueryChange={onQueryChange}
        onCreateNewOption={onCreateNewOption}
      />
    );

    await waitFor(() => {
      expect(queryByText('Item 1')).not.toBeInTheDocument();
      expect(queryByText('Item 2')).not.toBeInTheDocument();
      expect(getByText('New Item')).not.toBeVisible();
    });

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('New Item')).toBeVisible();
    });

    fireEvent.click(getByText('New Item'));

    await waitFor(() => {
      expect(onCreateNewOption).toHaveBeenCalledTimes(1);
      expect(onCreateNewOption).toHaveBeenCalledWith('New Item');
    });
  });

  it('allows to call callback when scrolling to bottom of the list', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
      { value: 3, text: 'Item 3' },
      { value: 4, text: 'Item 4' },
      { value: 5, text: 'Item 5' },
      { value: 6, text: 'Item 6' },
      { value: 7, text: 'Item 7' },
      { value: 8, text: 'Item 8' },
      { value: 9, text: 'Item 9' },
      { value: 10, text: 'Item 10' },
    ];
    const onChange = jest.fn();
    const onScrollListToBottom = jest.fn();
    const { getByText, getByRole, getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        onScrollListToBottom={onScrollListToBottom}
      />
    );

    fireEvent.click(getByPlaceholderText('Select an item'));

    await waitFor(() => {
      expect(getByText('Item 10')).toBeVisible();
      expect(onScrollListToBottom).not.toHaveBeenCalled();
    });

    fireEvent.scroll(getByRole('listbox'), { y: 200 });

    await waitFor(() => {
      expect(onScrollListToBottom).toHaveBeenCalledTimes(1);
    });
  });

  it('allows to clear selected item', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];
    const onChange = jest.fn();
    const { getByTestId } = renderWithTheme(
      <SingleSelect
        value={1}
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        clearable
      />
    );

    userEvent.hover(getByTestId('query-input'));
    userEvent.click(getByTestId('remove-icon'));

    await waitFor(() => {
      expect(onChange).toHaveBeenCalledWith(undefined);
    });
  });

  it('only calls onQueryChange when query is not undefined', async () => {
    const options = [
      { value: 1, text: 'Item 1' },
      { value: 2, text: 'Item 2' },
    ];

    const onChange = jest.fn();
    const onQueryChange = jest.fn();

    const { getByText, findByText, getByTestId } = renderWithTheme(
      <SingleSelect
        value={1}
        options={options}
        placeholder="Select an item"
        selectedOptionRenderer={({ option }) => option.text.substring(4)}
        onChange={onChange}
        onQueryChange={onQueryChange}
        clearable
      />
    );

    const input = getByTestId('query-input');

    // open select dropdown
    userEvent.click(input);
    expect(onQueryChange).not.toHaveBeenCalled();

    // select item 1
    const itemOne = await findByText('Item 1');
    expect(itemOne).toBeVisible();
    userEvent.click(itemOne);
    expect(onQueryChange).not.toHaveBeenCalled();

    // open select dropdown again
    userEvent.click(input);
    expect(onQueryChange).not.toHaveBeenCalled();

    // select item 2
    const itemTwo = await findByText('Item 2');
    expect(itemTwo).toBeVisible();
    userEvent.click(itemTwo);
    expect(onQueryChange).not.toHaveBeenCalled();

    // open select dropdown again
    userEvent.click(input);

    // close dropdown by clicking outside Select
    userEvent.click(document.body);
    expect(getByText('Item 1')).not.toBeVisible();
    expect(getByText('Item 2')).not.toBeVisible();
    expect(onQueryChange).not.toHaveBeenCalled();

    // Change query
    await waitFor(() => {
      userEvent.type(input, 'Item');
    });
    expect(onQueryChange).toHaveBeenCalled();
    expect(onQueryChange).not.toHaveBeenCalledWith(undefined);
  });

  it('makes onQueryChange to have been called with undefined when onBlur is undefined', async () => {
    const options = [{ value: 1, text: 'Item 1' }];

    const onChange = jest.fn();
    const onFocus = jest.fn();
    const onQueryChange = jest.fn();

    const { getByPlaceholderText } = renderWithTheme(
      <SingleSelect
        value={1}
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        onBlur={undefined}
        onFocus={onFocus}
        onQueryChange={onQueryChange}
        query="2"
      />
    );

    const input = getByPlaceholderText('Select an item');

    // click on select input
    userEvent.click(input);

    await waitFor(() => {
      expect(onFocus).toHaveBeenCalledTimes(1);
    });

    // click outside
    userEvent.click(document.body);
    expect(onQueryChange).toHaveBeenCalledWith(undefined);
  });

  it('calls onBlur and onFocus when they are not undefined', async () => {
    const options = [{ value: 1, text: 'Item 1' }];

    const onChange = jest.fn();
    const onBlur = jest.fn();
    const onFocus = jest.fn();
    const onQueryChange = jest.fn();

    const { getByTestId } = renderWithTheme(
      <SingleSelect
        value={1}
        options={options}
        placeholder="Select an item"
        onChange={onChange}
        onBlur={onBlur}
        onFocus={onFocus}
        onQueryChange={undefined}
      />
    );

    const input = getByTestId('query-input');

    // click on select input
    userEvent.click(input);

    await waitFor(() => {
      expect(onFocus).toHaveBeenCalledTimes(1);
      expect(onBlur).not.toHaveBeenCalled();
    });

    // click outside
    userEvent.click(document.body);
    expect(onBlur).toHaveBeenCalledTimes(1);
    expect(onQueryChange).not.toHaveBeenCalledWith(undefined);
  });

  it('shows noResult text even when loading', async () => {
    const onChange = jest.fn();

    const { getByPlaceholderText, getByText } = renderWithTheme(
      <SingleSelect
        options={[]}
        placeholder="Select items"
        onChange={onChange}
        loading
        noResults="Not found"
      />
    );

    const input = getByPlaceholderText('Select items');

    // click on select input
    userEvent.click(input);

    await waitFor(() => {
      expect(getByText('Not found')).toBeInTheDocument();
    });
  });

  it('works well when value got changed outside', () => {
    const options = [
      { value: 'item-1', text: 'Item 1' },
      { value: 'item-2', text: 'Item 2' },
    ];

    const Example = () => {
      const [value, setValue] = useState<string | number | undefined>();

      return (
        <>
          <SingleSelect
            options={options}
            value={value}
            onChange={setValue}
            placeholder="Select item"
            data-test-id="select"
          />
          <input
            value={value}
            onChange={e => setValue(e.target.value)}
            data-test-id="input"
          />
        </>
      );
    };

    const { getByTestId, getByText } = renderWithTheme(<Example />);
    const select = getByTestId('select');

    userEvent.click(select);
    userEvent.click(getByText('Item 1'));
    expect(select).toSelectItem('Item 1');

    const input = getByTestId('input');

    userEvent.click(input);
    userEvent.clear(input);
    expect(select).toSelectItem('');

    userEvent.click(select);
    userEvent.click(getByText('Item 1'));
    expect(select).toSelectItem('Item 1');
  });

  it('shows spinner when loading', () => {
    const options = [
      { value: 'item-1', text: 'Item 1' },
      { value: 'item-2', text: 'Item 2' },
    ];

    const { getByTestId } = renderWithTheme(
      <SingleSelect
        options={options}
        value="item-1"
        onChange={jest.fn()}
        placeholder="Select item"
        loading
      />
    );
    expect(getByTestId('loading-icon')).toBeVisible();
  });
});
