import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { config } from 'react-transition-group';
import { describe, expect, it, vi } from 'vitest';
import { wait } from '../../../helpers/storybook';
import { CustomTags } from './CustomTags';

describe('CustomTags', () => {
  // Disable Animations for testing
  config.disabled = true;

  const getTagsFromDOM = (container: HTMLElement): string[] => {
    const tags = container.querySelectorAll('span > span');
    return Array.from(tags).map((node) => node.textContent || '');
  };

  it('renders the component without crashing', () => {
    const { container } = render(<CustomTags name={'test-name'} />);

    expect(container).toBeTruthy();
  });

  it('displays a label', () => {
    const mockLabel = 'mockLabel';
    const { container } = render(
      <CustomTags name={'test-name'} label={mockLabel} />,
    );

    const label = container.querySelector('label');

    expect(label?.textContent).toBe(mockLabel);
  });

  it(`each Button component should have type of 'button'`, () => {
    const mockCurrentTags = ['1', '2', '3'];
    const { container } = render(
      <CustomTags name={'test-name'} value={mockCurrentTags} />,
    );

    const buttons = container.querySelectorAll('button');

    buttons.forEach((button) => {
      expect((button as HTMLButtonElement).type).toBe('button');
    });
  });

  it('should render one plus button', () => {
    const { container } = render(<CustomTags name="test-name" />);
    const plusButton = container.querySelector('.plusButton');

    expect(plusButton).toBeInTheDocument();
  });

  it('should disable plus button if there is no value in input field', () => {
    const { container } = render(<CustomTags name="test-name" />);
    const plusButton = container.querySelector(
      '.plusButton',
    ) as HTMLButtonElement;

    expect(plusButton.disabled).toBeTruthy();
  });

  it('displays a tag for each currently selected tag', () => {
    const mockCurrentTags = ['1', '2', '3'];
    const { container } = render(
      <CustomTags name={'test-name'} value={mockCurrentTags} />,
    );

    const tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);
    expect(tags).toEqual(mockCurrentTags);
  });

  it(`input attribute 'autocomplete' should be set to 'off'`, () => {
    const { container } = render(<CustomTags name={'test-name'} />);

    const input = container.querySelector('input') as HTMLInputElement;

    expect(input.autocomplete).toBe('off');
  });

  it('raises onChange when the "Enter" key is pressed with the new value, updates custom tags with entered tag, and resets input value', async () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const mockTag = '4';
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, { target: { value: mockTag } });

    await act(async () => {
      fireEvent.keyDown(input, { key: 'Enter' });
    });

    tags = getTagsFromDOM(container);
    expect(tags).toEqual([...mockCurrentTags, mockTag]);
    expect(tags).toHaveLength(4);
    expect(onChangeSpy).toHaveBeenCalledTimes(1);
    expect(input.value).toBe('');
  });

  it('raises onChange when the "+" button is pressed with the new value, updates custom tags with entered tag, and resets input value', async () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const mockTag = '4';
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, { target: { value: mockTag } });

    const plusButton = container.querySelector(
      '.plusButton',
    ) as HTMLButtonElement;
    fireEvent.click(plusButton);

    tags = getTagsFromDOM(container);
    expect(tags).toEqual([...mockCurrentTags, mockTag]);
    expect(tags).toHaveLength(4);
    expect(onChangeSpy).toHaveBeenCalledTimes(1);
    expect(input.value).toBe('');
  });

  it('raises onChange when removing a tag along with the new value', async () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    const removeButtons = container.querySelectorAll('svg');
    const lastRemoveButton = removeButtons[
      removeButtons.length - 1
    ] as SVGElement;

    await act(async () => {
      fireEvent.click(lastRemoveButton);
    });

    expect(onChangeSpy).toHaveBeenCalledTimes(1);
  });

  it('raises onChange and updates custom tags when removing a tag', async () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const removeButtons = container.querySelectorAll('svg');
    const lastRemoveButton = removeButtons[
      removeButtons.length - 1
    ] as SVGElement;

    await act(async () => {
      fireEvent.click(lastRemoveButton);
    });

    tags = getTagsFromDOM(container);

    expect(tags).toEqual(['1', '2']);
    expect(tags).toHaveLength(2);
    expect(onChangeSpy).toHaveBeenCalledTimes(1);
  });

  it('raises onFocus event', () => {
    const focusSpy = vi.fn();
    const { container } = render(
      <CustomTags name={'test-name'} onFocus={focusSpy} />,
    );

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.focus(input);
    expect(focusSpy).toHaveBeenCalledTimes(1);
  });

  describe('onBlur', () => {
    it('raises onBlur event', () => {
      const blurSpy = vi.fn();
      const { container } = render(
        <CustomTags name={'test-name'} onBlur={blurSpy} />,
      );

      const input = container.querySelector('input') as HTMLInputElement;

      fireEvent.blur(input);
      expect(blurSpy).toHaveBeenCalledTimes(1);
    });

    it('adds a valid value to current tags state', async () => {
      const blurSpy = vi.fn();
      const mockCurrentTags = ['1', '2', '3'];
      const mockTag = '4';
      const { container } = render(
        <CustomTags
          name={'test-name'}
          onBlur={blurSpy}
          value={mockCurrentTags}
        />,
      );

      let tags = getTagsFromDOM(container);

      expect(tags).toHaveLength(3);

      const input = container.querySelector('input') as HTMLInputElement;

      fireEvent.change(input, { target: { value: mockTag } });
      fireEvent.blur(input);

      tags = getTagsFromDOM(container);
      expect(tags).toEqual([...mockCurrentTags, mockTag]);
      expect(tags).toHaveLength(4);
      expect(blurSpy).toHaveBeenCalledTimes(1);
      expect(input.value).toBe('');
    });

    it(`doesn't add value if value isn't valid`, async () => {
      const blurSpy = vi.fn();
      const mockCurrentTags = ['1', '2', '3'];
      const mockInvalidTag = '';
      const { container } = render(
        <CustomTags
          name={'test-name'}
          onBlur={blurSpy}
          value={mockCurrentTags}
        />,
      );

      let tags = getTagsFromDOM(container);

      expect(tags).toHaveLength(3);

      const input = container.querySelector('input') as HTMLInputElement;

      fireEvent.change(input, { target: { value: mockInvalidTag } });
      fireEvent.blur(input);

      tags = getTagsFromDOM(container);
      expect(tags).toEqual([...mockCurrentTags]);
      expect(tags).toHaveLength(3);
      expect(blurSpy).toHaveBeenCalledTimes(1);
      expect(input.value).toBe('');
    });

    it(`resets input and add button if value is an empty space(s)`, async () => {
      const blurSpy = vi.fn();
      const mockCurrentTags = ['1', '2', '3'];
      const mockInvalidTag = '    ';
      const { container } = render(
        <CustomTags
          name={'test-name'}
          onBlur={blurSpy}
          value={mockCurrentTags}
        />,
      );

      let tags = getTagsFromDOM(container);

      expect(tags).toHaveLength(3);

      const input = container.querySelector('input') as HTMLInputElement;

      fireEvent.change(input, { target: { value: mockInvalidTag } });
      fireEvent.blur(input);

      tags = getTagsFromDOM(container);
      expect(tags).toEqual([...mockCurrentTags]);
      expect(tags).toHaveLength(3);
      expect(blurSpy).toHaveBeenCalledTimes(1);
      expect(input.value).toBe('');
    });
  });

  it('display tags inline', () => {
    const { container } = render(<CustomTags name={'test-name'} />);
    const tagsWrapper = container.querySelector('.tagsWrapper') as HTMLElement;
    expect(tagsWrapper.style.flexDirection).toBe('row');
  });

  it('display tags as rows', () => {
    const { container } = render(
      <CustomTags name={'test-name'} displayAsRows={true} />,
    );
    const tagsWrapper = container.querySelector('.tagsWrapper') as HTMLElement;
    expect(tagsWrapper.style.flexDirection).toBe('column');
  });

  it('should not raise onChange nor update current tags if the value is an empty string', () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const mockTag = '';
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, { target: { value: mockTag } });
    fireEvent.keyDown(input, { key: 'Enter' });

    tags = getTagsFromDOM(container);

    expect(tags).toEqual(mockCurrentTags);
    expect(tags).toHaveLength(3);
    expect(onChangeSpy).not.toHaveBeenCalled();
  });

  it('should not raise onChange nor update current tags if the value is a duplicate', () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['1', '2', '3'];
    const mockTag = '2';
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, { target: { value: mockTag } });
    fireEvent.keyDown(input, { key: 'Enter' });

    tags = getTagsFromDOM(container);

    expect(tags).toEqual(mockCurrentTags);
    expect(tags).toHaveLength(3);
    expect(onChangeSpy).not.toHaveBeenCalled();
  });

  it('trims the entered value', () => {
    const mockTag = ' TagWithSpace';
    const { container } = render(<CustomTags name={'test-name'} />);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, { target: { value: mockTag } });
    fireEvent.keyDown(input, { key: 'Enter' });

    const tag = container.querySelector('span > span');

    expect(tag?.textContent).toEqual(mockTag.trim());
  });

  describe('Error handling', () => {
    it('applies error styling and renders error message when an error is passed', () => {
      const mockErrorMessage = 'test-error-message';
      const { container } = render(
        <CustomTags name={'test-name'} error={mockErrorMessage} />,
      );
      const errorMsg = container.querySelector('small');
      const errorStyling = container.querySelector('input');
      expect(errorMsg?.textContent).toBe(mockErrorMessage);
      expect(errorStyling?.classList.contains('hasError')).toBe(true);
    });

    it(`renders a 'Duplicate value' error message when the entered value already exists`, () => {
      const onChangeSpy = vi.fn();
      const mockCurrentTags = ['1', '2', '3', '4'];
      const mockNewTag = '4';
      const { container } = render(
        <CustomTags
          name={'test-name'}
          onChange={onChangeSpy}
          value={mockCurrentTags}
        />,
      );

      let errorMsg = container.querySelector('small');
      let errorStyling = container.querySelector('input');
      expect(errorMsg).not.toBeInTheDocument();
      expect(errorStyling?.classList.contains('hasError')).toBe(false);

      const input = container.querySelector('input') as HTMLInputElement;

      act(() => {
        fireEvent.change(input, {
          target: { value: mockNewTag },
        });
      });

      errorMsg = container.querySelector('small');
      errorStyling = container.querySelector('input');

      expect(errorMsg?.textContent).toBe('Duplicate value');
      expect(errorStyling?.classList.contains('hasError')).toBe(true);
      expect(onChangeSpy).not.toHaveBeenCalled();
    });
  });

  describe('Live Suggestions', () => {
    it(`'liveSuggestionsResolver' is undefined by default`, () => {
      const { container } = render(<CustomTags name={'test-name'} />);

      // liveSuggestionsResolver is a prop, not a DOM attribute, so we just verify component renders
      expect(container.querySelector('.container')).toBeInTheDocument();
    });

    it(`sets 'liveSuggestionsDelay'`, () => {
      const mockDelay = 500;
      const { container } = render(
        <CustomTags name={'test-name'} liveSuggestionsDelay={mockDelay} />,
      );

      // liveSuggestionsDelay is a prop, not a DOM attribute, so we just verify component renders
      expect(container.querySelector('.container')).toBeInTheDocument();
    });

    it(`emits 'liveSuggestionsResolver' with value`, async () => {
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['test-value', 'test-value2'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      expect(suggestionSpy).toHaveBeenCalledTimes(1);
      expect(suggestionSpy).toHaveBeenCalledWith(mockTag);
    });

    it(`renders a list of suggestions if 'liveSuggestionsResolver' returns an array which contains values`, async () => {
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['test-value', 'test-value2'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      let suggestionsWrapper = container.querySelector('ul');
      let suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionSpy).toHaveBeenCalledTimes(1);
      expect(suggestionSpy).toHaveBeenCalledWith(mockTag);
      expect(suggestionsWrapper).toBeInTheDocument();
      expect(suggestions).toHaveLength(mockSuggestions.length);
    });

    it(`renders only the suggestions that match the currently entered value`, async () => {
      const suggestionDelay = 50;
      const firstSuggestionHandlerWait = 1000;
      const mockSuggestions1: string[] = ['test-value'];
      const mockSuggestions2: string[] = ['test-value2', 'test-value3'];
      const suggestionSpy = vi
        .fn()
        .mockImplementationOnce(async () => {
          await wait(1000);
          return mockSuggestions1;
        })
        .mockImplementationOnce(async () => {
          await wait(2000);
          return mockSuggestions2;
        });
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={[]}
          liveSuggestionsResolver={suggestionSpy}
          liveSuggestionsDelay={suggestionDelay}
        />,
      );

      let suggestionsWrapper = container.querySelector('ul');
      let suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: 'value' },
        });
      });

      // Advance time for first debounce to happen and trigger the resolver
      await act(async () => {
        vi.advanceTimersByTime(suggestionDelay);
      });

      // Change the value, so first resolver should be rejected
      await act(async () => {
        fireEvent.change(input, {
          target: { value: 'changed' },
        });
      });

      // Advance time for second debounce to happen as well as the first resolver call to resolve
      await act(async () => {
        vi.advanceTimersByTime(firstSuggestionHandlerWait);
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionSpy).toHaveBeenCalledTimes(2);
      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      // Advance time for the second resolver to resolve
      await act(async () => {
        vi.runAllTimers();
        // It's so nice, we do it twice!
        // (if we do it only once, the test randomly fails sometimes...)
        vi.runAllTimers();
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionSpy).toHaveBeenCalledTimes(2);
      expect(suggestionsWrapper).toBeInTheDocument();
      expect(suggestions.length).toBeGreaterThan(0);
      expect(suggestions).toHaveLength(mockSuggestions2.length);
    });

    it(`values that already exist are filtered out of suggestions`, async () => {
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['1', '2', '3', '4', '5'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      const suggestionElements = container.querySelectorAll('li');
      const suggestions = Array.from(suggestionElements).map(
        (node) => node.textContent || '',
      );

      const expectedSuggestions: string[] = mockSuggestions.filter(
        (item) => !mockCurrentTags.includes(item),
      );

      expect(suggestions).toEqual(expectedSuggestions);
    });

    it(`does not render suggestions if 'liveSuggestionsResolver' returns an empty array`, async () => {
      const mockTag = 'test-';
      const mockSuggestions: string[] = [];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      let suggestionsWrapper = container.querySelector('ul');
      let suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionSpy).toHaveBeenCalledTimes(1);
      expect(suggestionSpy).toHaveBeenCalledWith(mockTag);
      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);
    });

    it(`does not render suggestions if 'input' value is an empty string`, async () => {
      const mockTag = '';
      const mockSuggestions: string[] = ['1', '2', '3', '4', '5'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      let suggestionsWrapper = container.querySelector('ul');
      let suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionSpy).not.toHaveBeenCalled();
      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);
    });

    it(`does not render suggestions if 'liveSuggestionsResolver' is not set`, async () => {
      const mockTag = 'test-';

      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags name={'test-name'} value={mockCurrentTags} />,
      );

      let suggestionsWrapper = container.querySelector('ul');
      let suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      suggestionsWrapper = container.querySelector('ul');
      suggestions = container.querySelectorAll('li');

      expect(suggestionsWrapper).not.toBeInTheDocument();
      expect(suggestions).toHaveLength(0);
    });

    it(`adds new tag when suggestion is clicked and sets focus on input`, async () => {
      const onChangespy = vi.fn();
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['test-value', 'test-value2'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          onChange={onChangespy}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      const suggestions = container.querySelectorAll('li');

      fireEvent.mouseDown(suggestions[0]);

      const tags = getTagsFromDOM(container);

      const [firstSuggestion] = mockSuggestions;

      expect(tags).toEqual([...mockCurrentTags, firstSuggestion]);
      expect(suggestionSpy).toHaveBeenCalledTimes(1);
      expect(suggestionSpy).toHaveBeenCalledWith(mockTag);
      expect(onChangespy).toHaveBeenCalledTimes(1);
      expect(document.activeElement).toBe(input);
    });

    it(`selects right item when up and down arrows are pressed`, async () => {
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['test-value', 'test-value2'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      // select first element in suggestions with down key
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowDown',
        });
      });
      let suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(true);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(false);

      // select second element in suggestions with down key
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowDown',
        });
      });
      suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(false);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(true);

      // attempt to select third element which doesn't exist and should leave the last element selected
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowDown',
        });
      });
      suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(false);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(true);

      // select the first element again with the up key
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowUp',
        });
      });
      suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(true);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(false);

      // pressing up key again when the first element is selected should deselect all
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowUp',
        });
      });
      suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(false);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(false);
    });

    it(`adds item selected via arrow key when the 'Enter' key is pressed`, async () => {
      const onChangespy = vi.fn();
      const mockTag = 'test-';
      const mockSuggestions: string[] = ['test-value', 'test-value2'];
      const suggestionSpy = vi.fn().mockResolvedValue(mockSuggestions);
      const mockCurrentTags: string[] = ['1', '2', '3'];
      const { container } = render(
        <CustomTags
          name={'test-name'}
          value={mockCurrentTags}
          onChange={onChangespy}
          liveSuggestionsResolver={suggestionSpy}
        />,
      );

      let tags = getTagsFromDOM(container);

      expect(tags).toHaveLength(3);

      vi.useFakeTimers();

      const input = container.querySelector('input') as HTMLInputElement;

      await act(async () => {
        fireEvent.change(input, {
          target: { value: mockTag },
        });
      });

      await act(async () => {
        vi.runAllTimers();
      });

      // select first element
      act(() => {
        fireEvent.keyDown(input, {
          key: 'ArrowDown',
        });
      });
      const suggestions = container.querySelectorAll('li');
      expect(suggestions[0].classList.contains('selected')).toBe(true);
      expect(
        suggestions[suggestions.length - 1].classList.contains('selected'),
      ).toBe(false);

      // enter key pressed
      await act(async () => {
        fireEvent.keyDown(input, {
          key: 'Enter',
        });
      });

      await waitFor(() => {
        tags = getTagsFromDOM(container);
        expect(tags).toHaveLength(4);
      });

      const [firstSuggestion] = mockSuggestions;

      expect(tags).toEqual([...mockCurrentTags, firstSuggestion]);
      expect(onChangespy).toHaveBeenCalledTimes(1);
    });
  });

  it('manages tags sorted', async () => {
    const onChangeSpy = vi.fn();
    const mockCurrentTags = ['4', '2', '1'];
    const mockTag = '3';
    const { container } = render(
      <CustomTags
        name={'test-name'}
        onChange={onChangeSpy}
        value={mockCurrentTags}
      />,
    );

    let tags = getTagsFromDOM(container);

    expect(tags).toHaveLength(3);

    const input = container.querySelector('input') as HTMLInputElement;

    fireEvent.change(input, {
      target: { value: mockTag },
    });

    const plusButton = container.querySelector(
      '.plusButton',
    ) as HTMLButtonElement;
    fireEvent.click(plusButton);

    tags = getTagsFromDOM(container);
    expect(tags).toEqual(['1', '2', '3', '4']);
    expect(tags).toHaveLength(4);
    expect(onChangeSpy).toHaveBeenCalledTimes(1);
    expect(input.value).toBe('');
  });
});
