import '@testing-library/jest-dom/vitest';
import { fireEvent, render, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
import { ref } from 'vue';
import { UiAutocomplete, UiAutocompleteItem } from '../index';

const options = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'orange', label: 'Orange' },
  { value: 'grape', label: 'Grape' },
];

describe('UiAutocomplete', () => {
  test('renders an input with placeholder', () => {
    const { getByRole } = render(UiAutocomplete, {
      props: {
        options,
        placeholder: 'Search fruit',
      },
    });

    const input = getByRole('textbox', { name: 'Search fruit' });
    expect(input).toHaveAttribute('placeholder', 'Search fruit');
  });

  test('uses the input surface when no value is selected', () => {
    const { container } = render(UiAutocomplete, {
      props: {
        options,
        placeholder: 'Search fruit',
      },
    });

    const trigger = container.querySelector(
      '[data-slot="autocomplete-trigger"]',
    );
    expect(trigger).toHaveAttribute('data-placeholder');
    expect(trigger).toHaveClass(
      'data-[placeholder]:bg-background-input-default',
    );
  });

  test('uses the filled select surface when a value is selected', () => {
    const { container } = render(UiAutocomplete, {
      props: {
        options,
        defaultValue: 'apple',
        placeholder: 'Search fruit',
      },
    });

    const trigger = container.querySelector(
      '[data-slot="autocomplete-trigger"]',
    );
    expect(trigger).not.toHaveAttribute('data-placeholder');
    expect(trigger).toHaveClass('bg-accent');
    expect(trigger).toHaveClass(
      '[&:not([data-placeholder])]:border-border-surface-focus-ring',
    );
  });

  test('opens suggestions and filters by input text', async () => {
    const user = userEvent.setup();
    const { getByRole, queryByRole } = render(UiAutocomplete, {
      props: {
        options,
        placeholder: 'Search fruit',
      },
    });

    await user.type(getByRole('textbox'), 'ban');

    await waitFor(() => {
      expect(getByRole('listbox')).toBeInTheDocument();
      expect(getByRole('option', { name: 'Banana' })).toBeInTheDocument();
      expect(queryByRole('option', { name: 'Apple' })).not.toBeInTheDocument();
    });
  });

  test('selects a single option and emits its value', async () => {
    const user = userEvent.setup();
    const onUpdate = vi.fn();
    const onSelect = vi.fn();
    const onOpen = vi.fn();
    const { getByRole } = render(UiAutocomplete, {
      props: {
        options,
        placeholder: 'Search fruit',
        'onUpdate:modelValue': onUpdate,
        onSelect,
        'onUpdate:open': onOpen,
      },
    });

    await user.click(getByRole('textbox'));
    await waitFor(() => expect(getByRole('listbox')).toBeInTheDocument());
    await user.click(getByRole('option', { name: 'Orange' }));

    expect(onUpdate).toHaveBeenCalledWith('orange');
    expect(onSelect).toHaveBeenCalledWith('orange');
    expect(onOpen).toHaveBeenLastCalledWith(false);
    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Orange');
    });
  });

  test('supports keyboard navigation through internal options', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete },
      template: `
        <UiAutocomplete v-model="value" :options="options" placeholder="Search fruit" />
      `,
      setup() {
        const value = ref('');
        return { value, options };
      },
    });

    await user.click(getByRole('textbox'));
    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Apple' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).not.toBeInTheDocument();
    });

    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Banana' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).not.toBeInTheDocument();
    });

    await user.keyboard('{Enter}');

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Banana');
    });
  });

  test('starts keyboard navigation from the selected internal option when open', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete },
      template: `
        <UiAutocomplete v-model="value" :options="options" placeholder="Search fruit" />
      `,
      setup() {
        const value = ref('banana');
        return { value, options };
      },
    });

    await user.click(getByRole('textbox'));
    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Orange' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Banana');
    });

    await user.keyboard('{Enter}');

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Orange');
    });
  });

  test('supports keyboard navigation through external items', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete, UiAutocompleteItem },
      template: `
        <UiAutocomplete v-model="value" placeholder="Search fruit">
          <UiAutocompleteItem value="apple">Apple</UiAutocompleteItem>
          <UiAutocompleteItem value="banana">Banana</UiAutocompleteItem>
        </UiAutocomplete>
      `,
      setup() {
        const value = ref('');
        return { value };
      },
    });

    await user.click(getByRole('textbox'));
    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Apple' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).not.toBeInTheDocument();
    });

    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Banana' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).not.toBeInTheDocument();
    });

    await user.keyboard('{Enter}');

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Banana');
    });
  });

  test('starts keyboard navigation from the selected external item when open', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete, UiAutocompleteItem },
      template: `
        <UiAutocomplete v-model="value" placeholder="Search fruit">
          <UiAutocompleteItem value="apple">Apple</UiAutocompleteItem>
          <UiAutocompleteItem value="banana">Banana</UiAutocompleteItem>
          <UiAutocompleteItem value="orange">Orange</UiAutocompleteItem>
        </UiAutocomplete>
      `,
      setup() {
        const value = ref('banana');
        return { value };
      },
    });

    await user.click(getByRole('textbox'));
    await user.keyboard('{ArrowDown}');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Orange' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Banana');
    });

    await user.keyboard('{Enter}');

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Orange');
    });
  });

  test('hovering an option highlights it without changing the selected value', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete },
      template: `
        <UiAutocomplete v-model="value" :options="options" placeholder="Search fruit" />
      `,
      setup() {
        const value = ref('apple');
        return { value, options };
      },
    });

    await user.click(getByRole('textbox'));
    await waitFor(() => expect(getByRole('listbox')).toBeInTheDocument());

    await fireEvent.pointerMove(getByRole('option', { name: 'Banana' }));

    await waitFor(() => {
      expect(getByRole('option', { name: 'Banana' })).toHaveAttribute(
        'data-highlighted',
        '',
      );
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Apple');
    });
  });

  test('selects multiple options and keeps selected labels truncated in one value span', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete },
      template: `
        <UiAutocomplete
          v-model="value"
          multiple
          :options="options"
          placeholder="Search fruits"
          class="w-[180px]"
        />
      `,
      setup() {
        const value = ref<string[]>([]);
        return { value, options };
      },
    });

    await user.click(getByRole('textbox'));
    await waitFor(() => expect(getByRole('listbox')).toBeInTheDocument());

    await user.click(getByRole('option', { name: 'Apple' }));
    await user.click(getByRole('option', { name: 'Banana' }));
    await user.click(getByRole('option', { name: 'Orange' }));

    await waitFor(() => {
      const value = document.querySelector('[data-slot="autocomplete-value"]');
      expect(value).toHaveTextContent('Apple, Banana, Orange');
      expect(value).toHaveClass('truncate');
    });
  });

  test('renders custom input and item slots', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete },
      template: `
        <UiAutocomplete :options="options" placeholder="Search">
          <template #input="{ value, setInputValue, placeholder, ariaLabel }">
            <input
              :value="value"
              :placeholder="placeholder"
              :aria-label="ariaLabel"
              @input="setInputValue($event.target.value)"
            />
          </template>

          <template #item="{ option }">
            <strong>{{ option.label }}</strong>
          </template>
        </UiAutocomplete>
      `,
      setup() {
        return { options };
      },
    });

    await user.type(getByRole('textbox', { name: 'Search' }), 'app');

    await waitFor(() => {
      expect(getByRole('option', { name: 'Apple' })).toBeInTheDocument();
    });
  });

  test('renders the optional startIcon prop', () => {
    const { container } = render(UiAutocomplete, {
      props: {
        options,
        startIcon: 'search',
      },
    });

    expect(container.querySelector('[data-slot="icon"]')).toBeInTheDocument();
  });

  test('supports externally slotted autocomplete items', async () => {
    const user = userEvent.setup();
    const { getByRole } = render({
      components: { UiAutocomplete, UiAutocompleteItem },
      template: `
        <UiAutocomplete v-model="value" placeholder="Search fruit">
          <UiAutocompleteItem value="apple">Apple</UiAutocompleteItem>
          <UiAutocompleteItem value="banana">Banana</UiAutocompleteItem>
        </UiAutocomplete>
      `,
      setup() {
        const value = ref('');
        return { value };
      },
    });

    await user.click(getByRole('textbox'));
    await waitFor(() => expect(getByRole('listbox')).toBeInTheDocument());
    await user.click(getByRole('option', { name: 'Banana' }));

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Banana');
    });
  });

  test('forwards input focus, blur, keydown, and input events', async () => {
    const user = userEvent.setup();
    const onFocus = vi.fn();
    const onBlur = vi.fn();
    const onKeydown = vi.fn();
    const onInput = vi.fn();
    const { getByRole } = render(UiAutocomplete, {
      props: {
        options,
        onFocus,
        onBlur,
        onKeydown,
        onInput,
      },
    });

    const input = getByRole('textbox');
    await user.click(input);
    await user.keyboard('a');
    input.blur();

    expect(onFocus).toHaveBeenCalled();
    expect(onInput).toHaveBeenCalled();
    expect(onKeydown).toHaveBeenCalled();
    expect(onBlur).toHaveBeenCalled();
  });

  test('clicking the input still focuses it when a value is selected', async () => {
    const user = userEvent.setup();
    const { getByRole } = render(UiAutocomplete, {
      props: {
        options,
        defaultValue: 'apple',
      },
    });

    await waitFor(() => {
      expect(
        document.querySelector('[data-slot="autocomplete-value"]'),
      ).toHaveTextContent('Apple');
    });

    await user.click(getByRole('textbox'));

    expect(getByRole('textbox')).toHaveFocus();
  });

  test('renders one hidden input per selected value when name is set', async () => {
    const { container } = render(UiAutocomplete, {
      props: {
        options,
        name: 'fruits',
        multiple: true,
        defaultValue: ['apple', 'banana'],
      },
    });

    const hidden = container.querySelectorAll<HTMLInputElement>(
      'input[type="hidden"][name="fruits"]',
    );
    expect(Array.from(hidden, (input) => input.value).sort()).toEqual([
      'apple',
      'banana',
    ]);
  });
});
