import {
  describe,
  it,
  expect,
  vi,
  beforeAll,
  afterAll,
  beforeEach,
  afterEach,
} from 'vitest';
import { mount, VueWrapper } from '@vue/test-utils';
import { nextTick } from 'vue';
import MOptionListbox from './MOptionListbox.vue';
import type { ListboxOption } from './MOptionListbox.vue';

const globalStubs = {
  MTextInput: {
    name: 'MTextInput',
    template: `
      <div>
        <slot name="icon" />
        <input
          :value="modelValue"
          :placeholder="placeholder"
          :role="role"
          :id="id"
          @input="$emit('update:modelValue', $event.target.value); $emit('input', $event)"
          @keydown="$emit('keydown', $event)"
        />
      </div>
    `,
    props: [
      'modelValue',
      'placeholder',
      'role',
      'id',
      'size',
      'autocomplete',
      'ariaExpanded',
      'ariaControls',
      'ariaAutocomplete',
      'ariaActivedescendant',
    ],
    emits: ['update:modelValue', 'input', 'keydown'],
    methods: {
      focus() {},
    },
  },
  MButton: {
    name: 'MButton',
    template: `<button @click="$emit('click')"><slot /></button>`,
    emits: ['click'],
  },
  Search24: { template: '<span class="icon-search" />' },
  Less20: { template: '<span class="icon-less" />' },
  Check20: { template: '<span class="icon-check" />' },
  CheckCircleFilled24: { template: '<span class="icon-check-circle" />' },
};

const baseOptions: ListboxOption[] = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
  { label: 'Cherry', value: 'cherry' },
];

const optionsWithDisabled: ListboxOption[] = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana', disabled: true },
  { label: 'Cherry', value: 'cherry' },
];

const optionsWithSections: ListboxOption[] = [
  { label: 'Fruits', value: 'fruits', type: 'section' },
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
  { label: 'Vegetables', value: 'vegetables', type: 'section' },
  { label: 'Carrot', value: 'carrot' },
];

let originalScrollIntoViewDescriptor: PropertyDescriptor | undefined;
const scrollIntoViewMock = vi.fn();

beforeAll(() => {
  originalScrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(
    HTMLElement.prototype,
    'scrollIntoView',
  );

  Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
    configurable: true,
    value: scrollIntoViewMock,
  });
});

afterAll(() => {
  if (originalScrollIntoViewDescriptor) {
    Object.defineProperty(
      HTMLElement.prototype,
      'scrollIntoView',
      originalScrollIntoViewDescriptor,
    );
  } else {
    delete (HTMLElement.prototype as { scrollIntoView?: unknown })
      .scrollIntoView;
  }
});

function mountListbox(props: Record<string, unknown> = {}): VueWrapper {
  return mount(MOptionListbox, {
    props: {
      id: 'test-listbox',
      modelValue: null,
      options: baseOptions,
      open: true,
      ...props,
    },
    global: { stubs: globalStubs },
  });
}

describe('MOptionListbox – rendering', () => {
  it('renders all options', () => {
    const wrapper = mountListbox();
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items).toHaveLength(baseOptions.length);
  });

  it('displays option labels', () => {
    const wrapper = mountListbox();
    const texts = wrapper
      .findAll('.mc-option-listbox__text')
      .map((w) => w.text());
    expect(texts).toEqual(['Apple', 'Banana', 'Cherry']);
  });

  it('displays additional content when provided', () => {
    const options: ListboxOption[] = [
      { label: 'Apple', value: 'apple', content: 'Extra info' },
    ];
    const wrapper = mountListbox({ options });
    expect(wrapper.find('.mc-option-listbox__additional').text()).toBe(
      'Extra info',
    );
  });

  it('does NOT render the search block by default', () => {
    const wrapper = mountListbox();
    expect(wrapper.find('.mc-option-listbox__search').exists()).toBe(false);
  });

  it('renders the search block when search=true', () => {
    const wrapper = mountListbox({ search: true });
    expect(wrapper.find('.mc-option-listbox__search').exists()).toBe(true);
  });

  it('does NOT render actions block by default', () => {
    const wrapper = mountListbox();
    expect(wrapper.find('.mc-option-listbox__actions').exists()).toBe(false);
  });

  it('renders actions block when multiple=true and actions=true', () => {
    const wrapper = mountListbox({
      multiple: true,
      actions: true,
      modelValue: [],
    });
    expect(wrapper.find('.mc-option-listbox__actions').exists()).toBe(true);
  });

  it('sets aria-multiselectable="true" when multiple=true', () => {
    const wrapper = mountListbox({ multiple: true, modelValue: [] });
    expect(
      wrapper.find('ul[role="listbox"]').attributes('aria-multiselectable'),
    ).toBe('true');
  });

  it('sets aria-multiselectable="false" when multiple=false', () => {
    const wrapper = mountListbox();
    expect(
      wrapper.find('ul[role="listbox"]').attributes('aria-multiselectable'),
    ).toBe('false');
  });

  it('marks a disabled item with the disabled class', () => {
    const wrapper = mountListbox({ options: optionsWithDisabled });
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[1].classes()).toContain('mc-option-listbox__item--disabled');
  });

  it('renders section titles with the correct class', () => {
    const wrapper = mountListbox({ options: optionsWithSections });
    const sectionTitles = wrapper.findAll('.mc-option-listbox__section-title');
    expect(sectionTitles).toHaveLength(2);
  });
});

describe('MOptionListbox – single selection', () => {
  it('marks the selected option with --selected class', () => {
    const wrapper = mountListbox({ modelValue: 'banana' });
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[1].classes()).toContain('mc-option-listbox__item--selected');
    expect(items[0].classes()).not.toContain(
      'mc-option-listbox__item--selected',
    );
  });

  it('emits update:modelValue with option value on click', async () => {
    const wrapper = mountListbox({ modelValue: null });
    await wrapper.findAll('.mc-option-listbox__item')[0].trigger('click');
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['apple']);
  });

  it('deselects (emits null) when the same option is clicked again', async () => {
    const wrapper = mountListbox({ modelValue: 'apple' });
    await wrapper.findAll('.mc-option-listbox__item')[0].trigger('click');
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([null]);
  });
});

describe('MOptionListbox – multiple selection', () => {
  it('adds a value to the array on click', async () => {
    const wrapper = mountListbox({ multiple: true, modelValue: [] });
    await wrapper.findAll('.mc-option-listbox__item')[0].trigger('click');
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['apple']]);
  });

  it('removes a value from the array when already selected', async () => {
    const wrapper = mountListbox({
      multiple: true,
      modelValue: ['apple', 'cherry'],
    });
    await wrapper.findAll('.mc-option-listbox__item')[0].trigger('click');
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['cherry']]);
  });

  it('selectAll emits all non-disabled, non-section values', async () => {
    const wrapper = mountListbox({
      multiple: true,
      actions: true,
      modelValue: [],
    });
    await wrapper
      .find('.mc-option-listbox__actions button:first-child')
      .trigger('click');
    const emitted = wrapper.emitted('update:modelValue')?.[0]?.[0] as string[];
    expect(emitted.sort()).toEqual(['apple', 'banana', 'cherry']);
  });

  it('clearSelection emits empty array', async () => {
    const wrapper = mountListbox({
      multiple: true,
      actions: true,
      modelValue: ['apple', 'cherry'],
    });
    const buttons = wrapper.findAll('.mc-option-listbox__actions button');
    await buttons[1].trigger('click');
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[]]);
  });

  it('renders Check20 icon (checkbox) for each selectable item in multiple mode', () => {
    const wrapper = mountListbox({ multiple: true, modelValue: [] });
    const checkboxes = wrapper.findAll('.mc-option-listbox__checkbox');
    expect(checkboxes.length).toBeGreaterThan(0);
  });

  it('renders CheckCircleFilled24 for single mode', () => {
    const wrapper = mountListbox({ modelValue: null });
    expect(wrapper.find('.mc-option-listbox__selection-icon').exists()).toBe(
      true,
    );
  });
});

describe('MOptionListbox – sections (checkableSections)', () => {
  it('section has role="presentation" when checkableSections=false', () => {
    const wrapper = mountListbox({ options: optionsWithSections });
    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    expect(sectionItem.attributes('role')).toBe('presentation');
  });

  it('section has role="option" when checkableSections=true and multiple=true', () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: [],
    });
    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    expect(sectionItem.attributes('role')).toBe('option');
  });

  it('toggleSection selects all items in the section', async () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: [],
    });
    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    await sectionItem.trigger('click');
    const emitted = wrapper.emitted('update:modelValue')?.[0]?.[0] as string[];
    expect(emitted.sort()).toEqual(['apple', 'banana']);
  });

  it('toggleSection deselects all items in the section when all are selected', async () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: ['apple', 'banana'],
    });
    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    await sectionItem.trigger('click');
    const emitted = wrapper.emitted('update:modelValue')?.[0]?.[0] as string[];
    expect(emitted).toEqual([]);
  });

  it('section is indeterminate when only some items are selected', async () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: ['apple'], // only one of the two "Fruits" items selected
    });

    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    // The --selected class is applied when selected OR indeterminate
    expect(sectionItem.classes()).toContain(
      'mc-option-listbox__item--selected',
    );
    // The Less20 icon (indeterminate) should be rendered instead of Check20
    expect(sectionItem.find('.icon-less').exists()).toBe(true);
    expect(sectionItem.find('.icon-check').exists()).toBe(false);
  });

  it('section is NOT indeterminate when no items are selected', async () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: [],
    });

    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    expect(sectionItem.classes()).not.toContain(
      'mc-option-listbox__item--selected',
    );
    expect(sectionItem.find('.icon-less').exists()).toBe(false);
  });

  it('section is NOT indeterminate when all items are selected', async () => {
    const wrapper = mountListbox({
      options: optionsWithSections,
      checkableSections: true,
      multiple: true,
      modelValue: ['apple', 'banana'],
    });

    const sectionItem = wrapper.findAll('.mc-option-listbox__item')[0];
    expect(sectionItem.find('.icon-less').exists()).toBe(false);
    expect(sectionItem.find('.icon-check').exists()).toBe(true);
  });
});

describe('MOptionListbox – search / filtering', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('filters options based on search text', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.setValue('ban');
    await input.trigger('input');

    await vi.runAllTimersAsync();
    await nextTick();
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items).toHaveLength(1);
    expect(items[0].text()).toContain('Banana');
  });

  it('shows all options when search text is cleared', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.setValue('ban');
    await input.trigger('input');
    await vi.runAllTimersAsync();
    await nextTick();

    await input.setValue('');
    await input.trigger('input');
    await vi.runAllTimersAsync();
    await nextTick();

    expect(wrapper.findAll('.mc-option-listbox__item')).toHaveLength(
      baseOptions.length,
    );
  });

  it('is case-insensitive', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.setValue('APPLE');
    await input.trigger('input');
    await vi.runAllTimersAsync();
    await nextTick();
    expect(wrapper.findAll('.mc-option-listbox__item')).toHaveLength(1);
  });

  it('shows no items when no match found', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.setValue('zzz');
    await input.trigger('input');
    await vi.runAllTimersAsync();
    await nextTick();
    expect(wrapper.findAll('.mc-option-listbox__item')).toHaveLength(0);
  });
});

describe('MOptionListbox – keyboard navigation', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    scrollIntoViewMock.mockClear();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('ArrowDown sets activeIndex to 0 when no item is active', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowDown' });
    await nextTick();
    expect(
      wrapper.findAll('.mc-option-listbox__item--active')[0],
    ).toBeDefined();
  });

  it('ArrowDown wraps around to first item from last', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');

    for (let i = 0; i < baseOptions.length; i++) {
      await input.trigger('keydown', { key: 'ArrowDown' });
    }

    await input.trigger('keydown', { key: 'ArrowDown' });
    await nextTick();
    const activeItems = wrapper.findAll('.mc-option-listbox__item--active');
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[0].classes()).toContain('mc-option-listbox__item--active');
    expect(activeItems).toHaveLength(1);
  });

  it('ArrowUp wraps to last item from first', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowDown' });
    await input.trigger('keydown', { key: 'ArrowUp' });
    await nextTick();
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[items.length - 1].classes()).toContain(
      'mc-option-listbox__item--active',
    );
  });

  it('Enter selects the active item', async () => {
    const wrapper = mountListbox({ search: true, modelValue: null });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowDown' });
    await input.trigger('keydown', { key: 'Enter' });
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['apple']);
  });

  it('Escape emits close', async () => {
    const wrapper = mountListbox({ search: true });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'Escape' });
    expect(wrapper.emitted('close')).toBeDefined();
  });

  it('ArrowDown when closed emits open and sets activeIndex to 0', async () => {
    const wrapper = mountListbox({ search: true, open: false });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowDown' });
    expect(wrapper.emitted('open')).toBeDefined();
  });

  it('ArrowDown when closed sets activeIndex to selected option', async () => {
    const wrapper = mountListbox({
      search: true,
      open: false,
      modelValue: 'banana',
    });
    const input = wrapper.find('input');

    await input.trigger('keydown', { key: 'ArrowDown' });
    await nextTick();

    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[1].classes()).toContain('mc-option-listbox__item--active');
  });

  it('ArrowUp when closed emits open and sets activeIndex to last item', async () => {
    const wrapper = mountListbox({ search: true, open: false });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowUp' });
    expect(wrapper.emitted('open')).toBeDefined();
  });

  it('skips disabled items during navigation', async () => {
    const wrapper = mountListbox({
      search: true,
      options: optionsWithDisabled,
    });
    const input = wrapper.find('input');
    await input.trigger('keydown', { key: 'ArrowDown' }); // activeIndex = 0 (Apple)
    await input.trigger('keydown', { key: 'ArrowDown' }); // skip Banana (disabled) → Cherry
    await nextTick();
    const items = wrapper.findAll('.mc-option-listbox__item');
    expect(items[2].classes()).toContain('mc-option-listbox__item--active');
  });

  it('scrolls the active option into view during ArrowDown navigation', async () => {
    const wrapper = mountListbox({ search: true, open: true });
    const input = wrapper.find('input');

    await input.trigger('keydown', { key: 'ArrowDown' });
    await nextTick();

    expect(scrollIntoViewMock).toHaveBeenCalledTimes(1);
    expect(scrollIntoViewMock).toHaveBeenCalledWith({
      block: 'nearest',
      inline: 'nearest',
    });
  });

  it('does not scroll active option when listbox is closed', async () => {
    const wrapper = mountListbox({ search: true, open: false });
    const input = wrapper.find('input');

    await input.trigger('keydown', { key: 'ArrowDown' });
    await nextTick();

    expect(scrollIntoViewMock).not.toHaveBeenCalled();
  });
});

describe('MOptionListbox – exposed API', () => {
  it('exposes handleKeydown, toggleValue, listboxEl, activeIndex', () => {
    const wrapper = mountListbox();
    const exposed = wrapper.vm as unknown as {
      handleKeydown: (e: KeyboardEvent) => void;
      toggleValue: (item: ListboxOption) => void;
      listboxEl: unknown;
      activeIndex: number;
    };
    expect(typeof exposed.handleKeydown).toBe('function');
    expect(typeof exposed.toggleValue).toBe('function');
    expect('listboxEl' in wrapper.vm).toBe(true);
    expect('activeIndex' in wrapper.vm).toBe(true);
  });
});

describe('MOptionListbox – accessibility', () => {
  it('list element has role="listbox"', () => {
    const wrapper = mountListbox();
    expect(wrapper.find('ul').attributes('role')).toBe('listbox');
  });

  it('selectable option items have role="option"', () => {
    const wrapper = mountListbox();
    const items = wrapper.findAll('[role="option"]');
    expect(items.length).toBe(baseOptions.length);
  });

  it('aria-selected is true for selected item', () => {
    const wrapper = mountListbox({ modelValue: 'cherry' });
    const items = wrapper.findAll('[role="option"]');
    const cherry = items.find((i) => i.text().includes('Cherry'));
    expect(cherry?.attributes('aria-selected')).toBe('true');
  });

  it('aria-selected is false for unselected items', () => {
    const wrapper = mountListbox({ modelValue: 'cherry' });
    const items = wrapper.findAll('[role="option"]');
    const apple = items.find((i) => i.text().includes('Apple'));
    expect(apple?.attributes('aria-selected')).toBe('false');
  });

  it('aria-disabled is set on disabled items', () => {
    const wrapper = mountListbox({ options: optionsWithDisabled });
    const items = wrapper.findAll('[role="option"]');
    const banana = items.find((i) => i.text().includes('Banana'));
    expect(banana?.attributes('aria-disabled')).toBe('true');
  });
});
