import { screen, waitFor, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';

import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
import { Field } from '../field/Field';

mockMatchMedia();
mockResizeObserver();

describe('SelectInput', () => {
  it('renders placeholder', () => {
    render(
      <SelectInput
        placeholder="Currency"
        items={[
          { type: 'option', value: 'USD' },
          { type: 'option', value: 'EUR' },
        ]}
      />,
    );

    expect(screen.getByText('Currency')).toBeInTheDocument();
  });

  it('renders footer', async () => {
    render(
      <SelectInput
        items={[
          { type: 'option', value: 'USD' },
          { type: 'option', value: 'EUR' },
        ]}
        renderFooter={({ queryNormalized: normalizedQuery }) =>
          normalizedQuery != null ? (
            <>Showing results for ‘{normalizedQuery}’</>
          ) : (
            <>All items shown</>
          )
        }
        filterable
      />,
    );

    await userEvent.tab();
    await userEvent.keyboard(' ');

    const footer = screen.getByText('All items shown');
    expect(footer).toBeInTheDocument();

    await userEvent.keyboard('u');
    expect(footer).toHaveTextContent(/‘u’$/);

    await userEvent.keyboard('r');
    expect(footer).toHaveTextContent(/‘ur’$/);

    await userEvent.keyboard('x');
    expect(footer).toHaveTextContent(/‘urx’$/);

    await userEvent.keyboard('{Backspace}');
    expect(footer).toHaveTextContent(/‘ur’$/);
  });

  it('shows item selected via mouse', async () => {
    const handleClose = jest.fn();

    render(
      <SelectInput
        items={[
          { type: 'option', value: 'USD' },
          { type: 'option', value: 'EUR' },
        ]}
        onClose={handleClose}
      />,
    );

    expect(screen.queryByText('EUR')).not.toBeInTheDocument();

    const trigger = screen.getByRole('combobox');
    await userEvent.click(trigger);

    expect(handleClose).not.toHaveBeenCalled();

    const listbox = screen.getByRole('listbox');
    const option = within(listbox).getByRole('option', { name: 'EUR' });
    await userEvent.click(option);

    expect(handleClose).toHaveBeenCalledTimes(1);
    expect(trigger).toHaveTextContent('EUR');
  });

  it('filters items via keyboard', async () => {
    const handleClose = jest.fn();

    render(
      <SelectInput
        items={[
          {
            type: 'group',
            label: 'Popular currencies',
            options: [
              { type: 'option', value: 'USD' },
              { type: 'option', value: 'EUR' },
              { type: 'option', value: 'GBP' },
            ],
          },
        ]}
        filterable
        onClose={handleClose}
      />,
    );

    const trigger = screen.getByRole('combobox');
    await userEvent.tab();
    await userEvent.keyboard(' ');

    expect(handleClose).not.toHaveBeenCalled();

    const listbox = screen.getByRole('listbox');
    expect(within(listbox).getAllByRole('option')).toHaveLength(3);

    await userEvent.keyboard('u');
    expect(within(listbox).getAllByRole('option')).toHaveLength(2);

    await userEvent.keyboard('r');
    expect(within(listbox).getByRole('option')).toBeInTheDocument();

    await userEvent.keyboard('x');
    expect(within(listbox).queryByRole('option')).not.toBeInTheDocument();

    await userEvent.keyboard('{Backspace}');
    expect(within(listbox).getByRole('option')).toBeInTheDocument();

    const option = within(listbox).getAllByRole('option')[0];
    await userEvent.click(option);

    expect(handleClose).toHaveBeenCalledTimes(1);
    expect(trigger).toHaveTextContent('EUR');
  });

  it('clears filter query on close', async () => {
    const handleFilterChange = jest.fn();

    render(
      <SelectInput
        items={[
          { type: 'option', value: 'USD' },
          { type: 'option', value: 'EUR' },
        ]}
        filterable
        onFilterChange={handleFilterChange}
      />,
    );

    const trigger = screen.getByRole('combobox');
    await userEvent.tab();
    await userEvent.keyboard(' ');

    expect(handleFilterChange).not.toHaveBeenCalled();

    await userEvent.keyboard(' x');
    expect(handleFilterChange).toHaveBeenLastCalledWith({
      query: ' x',
      queryNormalized: 'x',
    });

    await userEvent.keyboard('{Escape}');
    await waitFor(() => {
      expect(handleFilterChange).toHaveBeenLastCalledWith({
        query: '',
        queryNormalized: null,
      });
    });

    await userEvent.click(trigger);

    const listbox = screen.getByRole('listbox');
    expect(within(listbox).getAllByRole('option')).toHaveLength(2);
  });

  it('selects multiple options', async () => {
    render(
      <SelectInput
        multiple
        items={[
          { type: 'option', value: 'USD' },
          { type: 'option', value: 'EUR' },
        ]}
      />,
    );

    const trigger = screen.getByRole('combobox');
    await userEvent.click(trigger);

    const listbox = screen.getByRole('listbox');
    const options = within(listbox).getAllByRole('option');
    for (const option of options) {
      await userEvent.click(option);
    }

    expect(trigger).toHaveTextContent('USD, EUR');
  });

  it('supports custom `id` attribute', () => {
    render(<SelectInput id="custom" items={[]} />);

    const trigger = screen.getByRole('combobox');
    expect(trigger).toHaveAttribute('id', 'custom');
  });

  it('supports `Field` for labeling', () => {
    render(
      <Field label="Currency">
        <SelectInput items={[{ type: 'option', value: 'USD' }]} value="USD" />
      </Field>,
    );
    expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
  });

  describe('listbox label', () => {
    const fieldLabel = 'Fruits';
    const triggerLabel = 'Select fruit';
    const options: SelectInputOptionItem[] = [
      { type: 'option', value: 'Banana' },
      { type: 'option', value: 'Orange' },
      { type: 'option', value: 'Olive' },
    ];
    const requiredTriggerButtonProps = {
      id: undefined,
      'aria-labelledby': undefined,
      'aria-describedby': undefined,
      'aria-invalid': undefined,
      'aria-label': undefined,
    };

    const renderSelectInput = (props: Omit<SelectInputProps<string | null>, 'items'> = {}) =>
      render(
        <Field label={fieldLabel} id="selectId">
          <SelectInput {...props} items={options} />
        </Field>,
      );

    it("should propagate trigger's label if nothing is selected", async () => {
      renderSelectInput({
        UNSAFE_triggerButtonProps: {
          ...requiredTriggerButtonProps,
          'aria-label': triggerLabel,
        },
      });
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
    });

    it("should propagate trigger's label if an option is selected", async () => {
      renderSelectInput({
        UNSAFE_triggerButtonProps: {
          ...requiredTriggerButtonProps,
          'aria-label': triggerLabel,
        },
        value: options[1].value,
      });
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
    });

    it("should propagate trigger's label by id", async () => {
      const customLabelId = 'customLabelId';
      renderSelectInput({
        UNSAFE_triggerButtonProps: {
          ...requiredTriggerButtonProps,
          'aria-labelledby': customLabelId,
        },
      });
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      expect(screen.getByRole('listbox')).toHaveAttribute('aria-labelledby', customLabelId);
    });

    it("should propagate input's label by id", async () => {
      renderSelectInput();
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      expect(screen.getByRole('listbox', { name: fieldLabel })).toBeInTheDocument();
    });

    it('should prefer explicit label over label ids', async () => {
      const customLabelId = 'customLabelId';
      renderSelectInput({
        UNSAFE_triggerButtonProps: {
          ...requiredTriggerButtonProps,
          'aria-labelledby': customLabelId,
          'aria-label': triggerLabel,
        },
      });
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
      expect(screen.getByRole('listbox')).not.toHaveAttribute('aria-labelledby');
    });

    it('should have no label if none of the above are provided', async () => {
      render(<SelectInput items={options} />);
      const trigger = screen.getByRole('combobox');
      await userEvent.click(trigger);
      const listBox = screen.getByRole('listbox');
      expect(listBox).not.toHaveAttribute('aria-label');
      expect(listBox).not.toHaveAttribute('aria-labelledby');
    });
  });
});
