import { field } from '@/plugins/field';
import { mount } from '@vue/test-utils';
import { computed, h, nextTick } from 'vue';
import BmsCombobox, { SELECT_ALL_VALUE, SelectProps } from './BmsCombobox.vue';
import { Check, Heart, HeartOff, Minus, X } from 'lucide-vue-next';
import BmsTag from './BmsTag.vue';

interface Props extends SelectProps {
  modelValue?: any | any[];
}

vi.mock('lucide-vue-next', async (importOriginal) => {
  const originalModule = await importOriginal();

  return {
    ...(originalModule as any[]),
    Heart: '<mocked-heart-icon></mocked-heart-icon>',
    HeartOff: '<mocked-heart-off-icon></mocked-heart-off-icon>',
  };
});

vi.mock('@vueuse/core', async (importOriginal) => {
  const originalModule = (await importOriginal()) as any[];
  return { ...originalModule, useDebounceFn: (fn: Function) => fn };
});

const factory = (props: Props) => {
  const wrapper = mount(BmsCombobox, {
    props,
    global: { plugins: [field] },
    attachTo: document.body,
  });

  const inputElement = wrapper.get('input');
  const iconElement = wrapper.find('.option__icon');
  const datalistElement = wrapper.find('[data-testid="select-options"]');
  const optionElements = computed(() => wrapper.findAll('.combobox-option'));
  const optionsTextContent = computed(() =>
    optionElements.value.map((o) => o.element.textContent?.trim()),
  );

  const getTagDismissButton = (index: number) =>
    wrapper.findAllComponents(BmsTag)[index].find('.dismiss-button button');

  return {
    wrapper,
    inputElement,
    iconElement,
    datalistElement,
    optionElements,
    optionsTextContent,
    getTagDismissButton,
  };
};

describe('BmsCombobox', () => {
  describe('display value on init', () => {
    it('should display correct value on init', async () => {
      const { inputElement } = factory({
        modelValue: 'toto',
        options: ['titi', 'toto'],
      });
      await nextTick();

      expect(inputElement.element.value).toStrictEqual('toto');
    });

    it('should not display selected value if not present in options', async () => {
      const { inputElement } = factory({
        modelValue: 'toto',
        options: [],
      });
      await nextTick();

      expect(inputElement.element.value).not.toStrictEqual('toto');
    });

    it('should display selected value if present in object options', async () => {
      const { inputElement, iconElement } = factory({
        modelValue: 1,
        options: [
          { value: 1, label: 'titi', icon: Heart },
          { value: 2, label: 'toto', icon: HeartOff },
        ],
      });
      await nextTick();

      expect(inputElement.element.value).toStrictEqual('titi');
      expect(iconElement.element.innerHTML).toStrictEqual(
        '<mocked-heart-icon></mocked-heart-icon>',
      );
    });
  });

  describe('options', () => {
    test('should open datalist on click', async () => {
      const { inputElement, datalistElement } = factory({
        modelValue: 'toto',
        options: ['titi', 'toto'],
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeFalsy();
      await inputElement.trigger('click');
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should open datalist on key "down"', async () => {
      const { inputElement, datalistElement } = factory({
        modelValue: 'toto',
        options: ['titi', 'toto'],
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeFalsy();
      await inputElement.trigger('keyup.down');
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should display all options', async () => {
      const { optionsTextContent, datalistElement } = factory({
        options: ['titi', 'toto'],
        open: true,
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeTruthy();
      expect(optionsTextContent.value).toStrictEqual(['titi', 'toto']);
    });

    test('should display all options with object values', async () => {
      const { optionsTextContent } = factory({
        options: [
          { label: 'toto', value: 0, icon: Heart },
          { label: 'titi', value: 1, icon: HeartOff },
        ],
        open: true,
      });
      await nextTick();
      expect(optionsTextContent.value).toStrictEqual(['toto', 'titi']);
    });

    test('should filter options', async () => {
      const { optionsTextContent, inputElement } = factory({
        options: ['titi', 'toto'],
        open: true,
        autocomplete: true,
      });
      await inputElement.setValue('ti');
      expect(optionsTextContent.value).toStrictEqual(['titi']);
    });

    test('should filter options with object values', async () => {
      const { optionsTextContent, inputElement } = factory({
        options: [
          { label: 'toto', value: 0, icon: Heart },
          { label: 'titi', value: 1, icon: HeartOff },
        ],
        open: true,
        autocomplete: true,
      });
      await inputElement.setValue('ti');
      await nextTick();
      expect(optionsTextContent.value).toStrictEqual(['titi']);
    });

    test('should add new option', async () => {
      const { datalistElement, wrapper } = factory({
        options: [
          { label: 'toto', value: 0, icon: Heart },
          { label: 'titi', value: 1, icon: HeartOff },
        ],
        open: true,
        autocomplete: true,
        canAddNewOption: true,
        modelValue: 'tata',
      });
      await nextTick();
      const addNewOptionElement = wrapper.find(
        '[data-testid="add-new-option"]',
      );
      expect(datalistElement.text()).toContain('tata (nouvelle option)');
      addNewOptionElement.trigger('click');
      expect(wrapper?.emitted()['addNewOption']).toStrictEqual([['tata']]);
    });

    test('should display multiple selected options', async () => {
      const { wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        open: true,
        multiple: true,
      });
      await nextTick();

      //FIXME manière plus élégante de trouver si c'est checked ou pas ?
      expect(wrapper.findAllComponents(Check)).toHaveLength(2);
    });
  });

  describe('clear', () => {
    test('should not be able to clear if single & no autocomplete', async () => {
      const { wrapper, inputElement } = factory({
        options: ['titi', 'toto'],
        autocomplete: false,
        multiple: false,
        open: true,
      });
      await nextTick();
      await inputElement.setValue('search');
      expect(inputElement.element.value).toEqual('search');
      const clearIcon = wrapper.findAll('.combobox-clear-icon');
      expect(clearIcon).toHaveLength(0);
    });
    test('should be able to clear searching', async () => {
      const { wrapper, inputElement } = factory({
        options: ['titi', 'toto'],
        modelValue: 'typed input',
        autocomplete: true,
        canAddNewOption: true,
        open: true,
      });
      await nextTick();
      expect(inputElement.element.value).toEqual('typed input');
      const clearIcon = wrapper.find('.combobox-clear-icon');
      await clearIcon.trigger('click');
      expect(inputElement.element.value).toEqual('');
    });

    test('should be able to clear selectedValues', async () => {
      const { wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        open: true,
        multiple: true,
      });
      await nextTick();

      expect(wrapper.findAllComponents(BmsTag)).toHaveLength(2);
      const clearIcon = wrapper.find('.combobox-clear-icon');
      await clearIcon.trigger('click');
      expect(wrapper.findAllComponents(BmsTag)).toHaveLength(0);
    });

    test('should be able to remove one of multiple with click', async () => {
      const { wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        open: true,
        multiple: true,
      });
      await nextTick();
      const selectedValuesElements = wrapper.findAllComponents(BmsTag);
      expect(selectedValuesElements).toHaveLength(2);

      await selectedValuesElements[0]
        .find('.dismiss-button  button')
        .trigger('click');
      expect(wrapper.emitted()['update:modelValue']).toStrictEqual([
        [['toto']],
      ]);
    });
    test('should be able to remove one of multiple with backspace', async () => {
      const { wrapper, inputElement } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        open: true,
        multiple: true,
      });
      await nextTick();
      const selectedValuesElements = wrapper.findAllComponents(BmsTag);
      expect(selectedValuesElements).toHaveLength(2);

      await wrapper.trigger('keydown', { key: 'Backspace' });
      expect(wrapper.emitted()['update:modelValue']).toStrictEqual([
        [['titi']],
      ]);
    });
  });

  describe('select', () => {
    it('should be able to select on click', async () => {
      const { inputElement, optionElements, wrapper } = factory({
        options: ['titi', 'toto'],
      });
      await nextTick();

      expect(inputElement.element.value).toBe('');

      await optionElements.value[0].trigger('click');
      expect(inputElement.element.value).toBe('titi');
      expect(wrapper.emitted()['update:modelValue']).toStrictEqual([['titi']]);
    });

    it('should be able to remove selection on click with multiple', async () => {
      const { optionElements, wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi'],
        multiple: true,
      });
      await nextTick();
      expect(wrapper.findAllComponents(BmsTag)).toHaveLength(1);

      await optionElements.value[0].trigger('click');
      expect(wrapper.emitted()['update:modelValue']).toStrictEqual([[[]]]);
      expect(wrapper.findAllComponents(BmsTag)).toHaveLength(0);
    });
    it('should be able to select on click with multiple', async () => {
      const { optionElements, wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi'],
        multiple: true,
      });
      await nextTick();
      expect(wrapper.findAllComponents(BmsTag)).toHaveLength(1);

      await optionElements.value[1].trigger('click');
      await nextTick();
      expect(wrapper.emitted()['update:modelValue']).toStrictEqual([
        [['titi', 'toto']],
      ]);
    });
  });

  describe('focus & datalist after removal', () => {
    test('should focus input and open datalist after clear (autocomplete)', async () => {
      const { wrapper, inputElement, datalistElement } = factory({
        options: ['titi', 'toto'],
        modelValue: 'titi',
        autocomplete: true,
        open: false,
      });
      await nextTick();

      const clearIcon = wrapper.find('.combobox-clear-icon');
      await clearIcon.trigger('click');

      expect(document.activeElement).toBe(inputElement.element);
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should focus input and open datalist after clear (multiple)', async () => {
      const { wrapper, inputElement, datalistElement } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        multiple: true,
        open: false,
      });
      await nextTick();

      const clearIcon = wrapper.find('.combobox-clear-icon');
      await clearIcon.trigger('click');

      expect(document.activeElement).toBe(inputElement.element);
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should focus input and open datalist after tag dismiss', async () => {
      const { inputElement, datalistElement, getTagDismissButton } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        multiple: true,
        open: false,
      });
      await nextTick();

      await getTagDismissButton(0).trigger('click');

      expect(document.activeElement).toBe(inputElement.element);
      expect(datalistElement.isVisible()).toBeTruthy();
    });
  });

  describe('datalist open/close', () => {
    test('should close datalist on Escape', async () => {
      const { wrapper, datalistElement } = factory({
        options: ['titi', 'toto'],
        open: true,
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeTruthy();

      await wrapper.trigger('keyup', { key: 'Escape' });
      expect(datalistElement.isVisible()).toBeFalsy();
    });

    test('should close datalist on Tab', async () => {
      const { wrapper, datalistElement } = factory({
        options: ['titi', 'toto'],
        open: true,
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeTruthy();

      await wrapper.trigger('keyup', { key: 'Tab' });
      expect(datalistElement.isVisible()).toBeFalsy();
    });

    test('should not close datalist on click when autocomplete is true', async () => {
      const { inputElement, datalistElement } = factory({
        options: ['titi', 'toto'],
        open: true,
        autocomplete: true,
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeTruthy();

      await inputElement.trigger('click');
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should close datalist on click when autocomplete is false', async () => {
      const { inputElement, datalistElement } = factory({
        options: ['titi', 'toto'],
        open: true,
        autocomplete: false,
      });
      await nextTick();
      expect(datalistElement.isVisible()).toBeTruthy();

      await inputElement.trigger('click');
      expect(datalistElement.isVisible()).toBeFalsy();
    });
  });

  describe('backspace', () => {
    test('should not remove last value if searching is not empty', async () => {
      const { wrapper, inputElement } = factory({
        options: ['titi', 'toto'],
        modelValue: ['titi', 'toto'],
        multiple: true,
        open: true,
      });
      await nextTick();
      await inputElement.setValue('t');

      await wrapper.trigger('keydown', { key: 'Backspace' });
      expect(wrapper.emitted()['update:modelValue']).toBeUndefined();
    });

    test('should not remove last value if modelValue is empty', async () => {
      const { wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: [],
        multiple: true,
        open: true,
      });
      await nextTick();

      await wrapper.trigger('keydown', { key: 'Backspace' });
      expect(wrapper.emitted()['update:modelValue']).toBeUndefined();
    });
  });

  describe('select multiple behavior', () => {
    test('should keep datalist open after selecting an option', async () => {
      const { optionElements, datalistElement } = factory({
        options: ['titi', 'toto'],
        modelValue: [],
        multiple: true,
        open: true,
      });
      await nextTick();

      await optionElements.value[0].trigger('click');
      expect(datalistElement.isVisible()).toBeTruthy();
    });

    test('should reset searching to empty string after selecting an option', async () => {
      const { optionElements, inputElement } = factory({
        options: ['titi', 'toto'],
        modelValue: [],
        multiple: true,
        open: true,
        autocomplete: true,
      });
      await nextTick();
      await inputElement.setValue('ti');

      await optionElements.value[0].trigger('click');
      expect(inputElement.element.value).toBe('');
    });
  });

  describe('canAddNewOption (without request)', () => {
    test('should set modelValue to existing option value when input matches', async () => {
      const { wrapper, inputElement } = factory({
        options: [
          { label: 'titi', value: 1 },
          { label: 'toto', value: 2 },
        ],
        autocomplete: true,
        canAddNewOption: true,
      });
      await nextTick();

      await inputElement.setValue('titi');
      await nextTick();
      expect(wrapper.emitted()['update:modelValue']?.at(-1)).toStrictEqual([1]);
    });

    test('should set modelValue to typed text when option does not exist', async () => {
      const { wrapper, inputElement } = factory({
        options: [
          { label: 'titi', value: 1 },
          { label: 'toto', value: 2 },
        ],
        autocomplete: true,
        canAddNewOption: true,
      });
      await nextTick();

      await inputElement.setValue('newvalue');
      await nextTick();
      expect(wrapper.emitted()['update:modelValue']?.at(-1)).toStrictEqual([
        'newvalue',
      ]);
    });
  });

  describe('disabled', () => {
    test('should not open datalist on click when disabled', async () => {
      const { inputElement, datalistElement } = factory({
        options: ['titi', 'toto'],
        disabled: true,
      });
      await nextTick();

      await inputElement.trigger('click');
      expect(datalistElement.isVisible()).toBeFalsy();
    });

    test('should not focus input when disabled', async () => {
      const { inputElement, wrapper } = factory({
        options: ['titi', 'toto'],
        disabled: true,
      });
      await nextTick();

      await wrapper.find('.select-input').trigger('click');
      expect(document.activeElement).not.toBe(inputElement.element);
    });
  });

  describe('autocomplete filtering behavior', () => {
    test('should show all options on open when a value is already selected', async () => {
      const { optionsTextContent, inputElement } = factory({
        options: ['titi', 'toto'],
        modelValue: 'titi',
        autocomplete: true,
      });
      await nextTick();

      await inputElement.trigger('click');
      expect(optionsTextContent.value).toStrictEqual(['titi', 'toto']);
    });

    test('should filter options only after user types', async () => {
      const { inputElement, wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: 'titi',
        open: true,
        autocomplete: true,
      });
      const textContent = () =>
        wrapper
          .findAll('.combobox-option')
          .map((o) => o.element.textContent?.trim());
      await nextTick();

      expect(textContent()).toStrictEqual(['titi', 'toto']);

      await inputElement.setValue('ti');
      expect(textContent()).toStrictEqual(['titi']);
    });

    test('should reset filtering when menu is reopened', async () => {
      const { inputElement, wrapper } = factory({
        options: ['titi', 'toto'],
        modelValue: 'titi',
        open: true,
        autocomplete: true,
      });
      const textContent = () =>
        wrapper
          .findAll('.combobox-option')
          .map((o) => o.element.textContent?.trim());
      await nextTick();
      await inputElement.setValue('ti');
      expect(textContent()).toStrictEqual(['titi']);

      await wrapper.trigger('keyup', { key: 'Escape' });
      await inputElement.trigger('click');
      await nextTick();

      expect(textContent()).toStrictEqual(['titi', 'toto']);
    });
  });

  describe('selected option highlight', () => {
    test('should show check icon on the selected option in single mode', async () => {
      const { wrapper, optionElements } = factory({
        options: ['titi', 'toto'],
        modelValue: 'titi',
        open: true,
      });
      await nextTick();

      expect(wrapper.findAllComponents(Check)).toHaveLength(1);
      expect(
        optionElements.value[0].findComponent(Check).exists(),
      ).toBeTruthy();
      expect(optionElements.value[1].findComponent(Check).exists()).toBeFalsy();
    });

    test('should not show check icon when no value is selected', async () => {
      const { wrapper } = factory({
        options: ['titi', 'toto'],
        open: true,
      });
      await nextTick();

      expect(wrapper.findAllComponents(Check)).toHaveLength(0);
    });
  });

  describe('selectAll', () => {
    const options = ['titi', 'toto', 'tata'];

    test('should show select-all option first when selectAll is true', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
      });
      await nextTick();

      const allOptions = wrapper.findAll('.combobox-option');
      expect(allOptions[0].element.textContent?.trim()).toBe(
        'Tout sélectionner',
      );
    });

    test('should not show select-all option when selectAll is false', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: false,
      });
      await nextTick();

      const allOptions = wrapper.findAll('.combobox-option');
      expect(allOptions[0].element.textContent?.trim()).not.toBe(
        'Tout sélectionner',
      );
    });

    test('should use custom selectAllLabel', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        selectAllLabel: 'Select all',
      });
      await nextTick();

      const allOptions = wrapper.findAll('.combobox-option');
      expect(allOptions[0].element.textContent?.trim()).toBe('Select all');
    });

    test('should select all options when none are selected', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: [],
      });
      await nextTick();

      await wrapper.findAll('.combobox-option')[0].trigger('click');

      expect(wrapper.emitted()['update:modelValue']?.at(-1)).toStrictEqual([
        ['titi', 'toto', 'tata'],
      ]);
    });

    test('should select all options when selection is partial', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: ['titi'],
      });
      await nextTick();

      await wrapper.findAll('.combobox-option')[0].trigger('click');

      expect(wrapper.emitted()['update:modelValue']?.at(-1)).toStrictEqual([
        ['titi', 'toto', 'tata'],
      ]);
    });

    test('should deselect all options when all are selected', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: ['titi', 'toto', 'tata'],
      });
      await nextTick();

      await wrapper.findAll('.combobox-option')[0].trigger('click');

      expect(wrapper.emitted()['update:modelValue']?.at(-1)).toStrictEqual([
        [],
      ]);
    });

    test('should show Check icon on select-all when all options are selected', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: ['titi', 'toto', 'tata'],
      });
      await nextTick();

      const selectAllOption = wrapper.findAll('.combobox-option')[0];
      expect(selectAllOption.findComponent(Check).exists()).toBeTruthy();
      expect(selectAllOption.findComponent(Minus).exists()).toBeFalsy();
    });

    test('should show Minus icon on select-all when selection is partial', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: ['titi'],
      });
      await nextTick();

      const selectAllOption = wrapper.findAll('.combobox-option')[0];
      expect(selectAllOption.findComponent(Minus).exists()).toBeTruthy();
      expect(selectAllOption.findComponent(Check).exists()).toBeFalsy();
    });

    test('should show no icon on select-all when nothing is selected', async () => {
      const { wrapper } = factory({
        options,
        open: true,
        multiple: true,
        selectAll: true,
        modelValue: [],
      });
      await nextTick();

      const selectAllOption = wrapper.findAll('.combobox-option')[0];
      expect(selectAllOption.findComponent(Check).exists()).toBeFalsy();
      expect(selectAllOption.findComponent(Minus).exists()).toBeFalsy();
    });

    test('should warn in dev when selectAll is used without multiple', async () => {
      const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
      factory({ options, selectAll: true, multiple: false });
      await nextTick();

      expect(warnSpy).toHaveBeenCalledWith(
        '[BmsCombobox] "selectAll" requires "multiple" to be enabled.',
      );
      warnSpy.mockRestore();
    });
  });

  describe('with request', () => {
    test('should load initial values', async () => {
      const request = vi.fn();
      request.mockResolvedValue({
        data: [
          { label: 'titi', value: 'titi' },
          { label: 'toto', value: 'toto' },
        ],
      });
      const { optionsTextContent } = factory({
        request,
        open: true,
      });
      await nextTick();
      expect(request).toHaveBeenCalledOnce();
      await nextTick();
      expect(optionsTextContent.value).toStrictEqual(['titi', 'toto']);
    });

    test('should call request with search', async () => {
      const request = vi.fn();
      request.mockResolvedValue({
        data: [
          { label: 'titi', value: 'titi' },
          { label: 'toto', value: 'toto' },
        ],
      });
      const { optionsTextContent, inputElement } = factory({
        request,
        open: true,
        autocomplete: true,
      });
      await nextTick();
      expect(request).toHaveBeenCalledWith(new AbortController(), '');

      await inputElement.setValue('search');
      await nextTick();
      expect(request).toHaveBeenCalledWith(new AbortController(), 'search');
    });
  });
});
