import { Breakpoint } from '../common';
import { render, screen, userEvent, waitFor, mockMatchMedia } from '../test-utils';

import Select from '.';
import { SelectItemWithPlaceholder } from './Select';

mockMatchMedia();

function enableDesktopScreen() {
  window.innerWidth = Breakpoint.LARGE;
}

function enableMobileScreen() {
  window.innerWidth = Breakpoint.EXTRA_SMALL;
}

const mockScrollIntoView = jest.fn();

const originalRAF = global.requestAnimationFrame;

describe('Select', () => {
  const props = {
    onChange: jest.fn(),
    options: [
      { value: 0, label: 'yo' },
      { value: 1, label: 'dawg' },
      { value: 2, label: 'boi' },
    ],
  };

  beforeEach(() => {
    global.requestAnimationFrame = (callback) => setTimeout(callback, 16);

    Element.prototype.scrollIntoView = mockScrollIntoView;

    enableDesktopScreen();

    jest.clearAllMocks();
  });

  afterEach(() => {
    global.requestAnimationFrame = originalRAF;
  });

  const openSelect = async (container: HTMLElement) => {
    const button = screen.getByRole('button');
    await userEvent.click(button);
  };

  describe('search property', () => {
    it('should focus input when dropdown is open', async () => {
      const { container } = render(<Select {...props} search />);

      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');

      expect(input).toHaveFocus();
    });

    it('should pass id to search box', async () => {
      const { container } = render(<Select {...props} search id="dummy" />);

      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');

      expect(input).toHaveAttribute('id', 'dummy-searchbox');
    });

    it('should filter the options with the default filter function', async () => {
      const { container } = render(<Select {...props} search />);

      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'yo');

      // Not ideal, this higlights accessibility issues.
      expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
        container.querySelector('.np-dropdown-item'),
      );
      expect(screen.getByText('yo')).toBeInTheDocument();
    });

    describe('when options contain searchables', () => {
      let input: HTMLElement;

      const searchableOptions = [
        {
          value: 0,
          label: 'first_label',
          note: 'first_note',
          secondary: 'first_secondary',
          currency: 'gbp',
          searchStrings: ['first_search_string'],
        },
        {
          value: 1,
          label: 'second_label',
          note: 'second_note',
          secondary: 'second_secondary',
          currency: 'usd',
          searchStrings: ['second_search_string'],
        },
        {
          value: 2,
          label: 'third_label',
          note: 'third_note',
          secondary: 'third_secondary',
          currency: 'eur',
          searchStrings: ['third_search_string'],
        },
      ];

      beforeEach(async () => {
        const { container } = render(<Select {...props} options={searchableOptions} search />);

        await openSelect(container);

        input = screen.getByPlaceholderText('Search...');
      });

      describe('when searching by label', () => {
        it('should display the search result', async () => {
          await userEvent.type(input, 'second_label');

          expect(screen.queryByText('first_label')).not.toBeInTheDocument();
          expect(screen.getByText('second_label')).toBeInTheDocument();
          expect(screen.queryByText('third_label')).not.toBeInTheDocument();
        });
      });

      describe('when searching by note', () => {
        it('should display the search result', async () => {
          await userEvent.type(input, 'third_note');

          expect(screen.queryByText('first_label')).not.toBeInTheDocument();
          expect(screen.queryByText('second_label')).not.toBeInTheDocument();
          expect(screen.getByText('third_label')).toBeInTheDocument();
        });
      });

      describe('when searching by secondary', () => {
        it('should display the search result', async () => {
          await userEvent.type(input, 'first_secondary');

          expect(screen.getByText('first_label')).toBeInTheDocument();
          expect(screen.queryByText('second_label')).not.toBeInTheDocument();
          expect(screen.queryByText('third_label')).not.toBeInTheDocument();
        });
      });

      describe('when searching by currency', () => {
        it('should display the search result', async () => {
          await userEvent.type(input, 'usd');

          expect(screen.queryByText('first_label')).not.toBeInTheDocument();
          expect(screen.getByText('second_label')).toBeInTheDocument();
          expect(screen.queryByText('third_label')).not.toBeInTheDocument();
        });
      });

      describe('when searching by searchStrings', () => {
        it('should display the search result', async () => {
          await userEvent.type(input, 'third_search_string');

          expect(screen.queryByText('first_label')).not.toBeInTheDocument();
          expect(screen.queryByText('second_label')).not.toBeInTheDocument();
          expect(screen.getByText('third_label')).toBeInTheDocument();
        });
      });
    });

    it('should be able to search disabled options', async () => {
      const { container } = render(
        <Select
          {...props}
          search
          options={[
            { value: 0, label: 'Enabled Option' },
            { value: 1, label: 'Disabled Option', disabled: true },
          ]}
        />,
      );
      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'Disabled');
      expect(screen.queryByText('Enabled Option')).not.toBeInTheDocument();
      expect(screen.getByText('Disabled Option')).toBeInTheDocument();
    });

    it('should filter the options with the default filter function in their currency attribute', async () => {
      const { container } = render(
        <Select
          {...props}
          search
          options={[
            { value: 0, label: 'Hungarian forint', currency: 'HUF' },
            { value: 1, label: 'British pound', currency: 'GBP' },
          ]}
        />,
      );

      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'HUF');

      expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
        container.querySelector('.np-dropdown-item'),
      );
      expect(
        screen.getByText('Hungarian forint').parentElement?.parentElement?.parentElement
          ?.parentElement,
      ).toHaveClass('np-dropdown-item clickable np-dropdown-item--focused');
    });

    it('should include searchable strings in option search if present', async () => {
      const { container } = render(
        <Select
          {...props}
          search
          options={[
            { value: 0, label: 'Estonia', searchStrings: ['EE', 'Tallinn'] },
            { value: 1, label: 'United States of America', searchStrings: ['US', 'USA', 'DC'] },
          ]}
        />,
      );

      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'Tallinn');

      expect(container.querySelector('.np-dropdown-item.np-dropdown-item--focused')).toStrictEqual(
        container.querySelector('.np-dropdown-item'),
      );
      expect(screen.getByText('Estonia').parentElement?.parentElement).toHaveClass(
        'np-dropdown-item clickable np-dropdown-item--focused',
      );
    });

    it('should filter the options with a custom search function', async () => {
      const searchFunction = jest.fn();
      const { container } = render(<Select {...props} search={searchFunction} />);
      await openSelect(container);
      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'o');

      expect(searchFunction).toHaveBeenCalledTimes(3);
    });

    it('should filter the options with a custom search function with 3 results', async () => {
      const searchFunction = (option: SelectItemWithPlaceholder, searchValue: string) =>
        typeof option.label === 'string' && option.label.includes(searchValue);
      const { container } = render(<Select {...props} search={searchFunction} />);
      await openSelect(container);

      const input = screen.getByPlaceholderText('Search...');
      await userEvent.type(input, 'o');

      expect(screen.getByRole('listbox').children).toHaveLength(3);
    });
  });

  it('show list of options in dropdown (Panel) on desktop', async () => {
    const { container } = render(<Select {...props} />);
    await openSelect(container);

    const drawer = document.querySelector('.np-panel');
    expect(drawer).toBeInTheDocument();
  });

  it('show list of options in Bottom Sheet when on mobile', async () => {
    enableMobileScreen();
    const { container } = render(<Select {...props} />);
    await openSelect(container);

    const bottomSheet = await screen.findByRole('dialog');

    expect(bottomSheet).toBeInTheDocument();
  });

  it('show list of options in Drawer when on mobile and search in enabled', async () => {
    enableMobileScreen();
    const { container } = render(<Select {...props} search />);
    await openSelect(container);

    await waitFor(() => {
      const drawer = document.querySelector('.np-drawer');
      expect(drawer).toBeInTheDocument();
    });
  });

  it('focuses dropdown options when clicked', async () => {
    const { container } = render(<Select {...props} />);
    await openSelect(container);

    const dropdown = screen.getByRole('listbox');
    expect(dropdown).toHaveFocus();
  });

  it("on touch device doesn't focus on the search box once opened", async () => {
    enableMobileScreen();
    const { container } = render(<Select {...props} onSearchChange={jest.fn()} />);

    await openSelect(container);
    const input = await screen.findByPlaceholderText('Search...');

    expect(input).not.toHaveFocus();
  });

  it('on not touch device focuses on the search box once opened', async () => {
    const { container } = render(<Select {...props} search onSearchChange={jest.fn()} />);

    await openSelect(container);
    const input = screen.getByPlaceholderText('Search...');

    expect(input).toHaveFocus();
  });

  it('renders controls with fallback id', async () => {
    const { container } = render(<Select {...props} />);
    await openSelect(container);

    const button = screen.getByRole('button');
    const options = screen.getByRole('listbox');

    expect(button).toHaveAttribute('id');
    expect(options).toHaveAttribute('id');
  });

  it('renders controls with passed id', async () => {
    const { container } = render(<Select id="my-select-component" {...props} />);
    await openSelect(container);

    const button = screen.getByRole('button');
    const options = screen.getByRole('listbox');

    expect(button).toHaveAttribute('id', 'my-select-component');
    expect(options).toHaveAttribute('id', 'my-select-component-listbox');
  });

  it('renders button with correct aria-controls attribute', async () => {
    const { container } = render(<Select {...props} search onSearchChange={jest.fn()} />);
    await openSelect(container);

    const button = screen.getByRole('button');
    const options = screen.getByRole('listbox');

    const ariaControlsId = button.getAttribute('aria-controls');
    const optionsId = options.getAttribute('id');

    expect(ariaControlsId).toBe(optionsId);
  });

  describe('keyboard navigation on desktop', () => {
    it('navigates to the next item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      expect(screen.queryByRole('listbox')).toHaveFocus();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });
    });

    it('skips disabled items when navigating to the next item', async () => {
      const { container } = render(
        <Select
          {...props}
          options={[props.options[0], { ...props.options[1], disabled: true }, props.options[2]]}
        />,
      );

      await openSelect(container);

      expect(screen.queryByRole('listbox')).toHaveFocus();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });
    });

    it('cannot navigate past the first list item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      expect(screen.queryByRole('listbox')).toHaveFocus();

      await userEvent.keyboard('[ArrowUp]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowUp]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });
    });

    it('cannot navigate past the last list item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      expect(screen.queryByRole('listbox')).toHaveFocus();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'dawg' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });
    });

    describe('with search enabled', () => {
      it('maintains focus on search but visually indicates which item is focused with keyboard navigation and selects it on enter', async () => {
        const { container } = render(<Select {...props} search />);

        await openSelect(container);

        expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();

        await userEvent.keyboard('[ArrowDown]');

        await waitFor(() => {
          expect(screen.queryByRole('option', { name: 'yo' })).toHaveClass(
            'np-dropdown-item--focused',
            { exact: false },
          );
        });

        expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();

        await userEvent.keyboard('[ArrowDown]');

        await waitFor(() => {
          expect(screen.queryByRole('option', { name: 'dawg' })).toHaveClass(
            'np-dropdown-item--focused',
            { exact: false },
          );
        });

        expect(screen.queryByPlaceholderText('Search...')).toHaveFocus();

        await userEvent.keyboard('[Enter]');

        expect(props.onChange).toHaveBeenCalledWith({ label: 'dawg', value: 1 });
      });
    });
  });

  describe('keyboard navigation on mobile', () => {
    beforeEach(() => {
      enableMobileScreen();
    });

    it('navigates to the next item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });
    });

    it('skips disabled items when navigating to the next item', async () => {
      const { container } = render(
        <Select
          {...props}
          options={[props.options[0], { ...props.options[1], disabled: true }, props.options[2]]}
        />,
      );

      await openSelect(container);

      await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });
    });

    it('cannot navigate past the first list item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();

      await userEvent.keyboard('[ArrowUp]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowUp]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });
    });

    it('cannot navigate past the last list item', async () => {
      const { container } = render(<Select {...props} />);

      await openSelect(container);

      await expect(screen.findByRole('dialog')).resolves.toBeInTheDocument();

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'yo' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'dawg' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });

      await userEvent.keyboard('[ArrowDown]');

      await waitFor(() => {
        expect(screen.queryByRole('option', { name: 'boi' })).toHaveFocus();
      });
    });

    describe('with search enabled', () => {
      beforeEach(() => {
        enableMobileScreen();
      });

      it('visually indicates which item is focused with keyboard navigation and selects it on enter', async () => {
        const { container } = render(<Select {...props} search />);

        await openSelect(container);

        await waitFor(() => {
          expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();
        });

        await userEvent.keyboard('[ArrowDown]');

        await waitFor(() => {
          expect(screen.queryByRole('option', { name: 'yo' })).toHaveClass(
            'np-dropdown-item--focused',
            { exact: false },
          );
        });

        expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();

        await userEvent.keyboard('[ArrowDown]');

        await waitFor(() => {
          expect(screen.queryByRole('option', { name: 'dawg' })).toHaveClass(
            'np-dropdown-item--focused',
            { exact: false },
          );
        });

        expect(screen.queryByPlaceholderText('Search...')).not.toHaveFocus();

        await userEvent.keyboard('[Enter]');

        expect(props.onChange).toHaveBeenCalledWith({ label: 'dawg', value: 1 });
      });
    });
  });
});
