import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';

jest.mock('prosemirror-tables');

describe('content/components/wrappers/table_cell_base', () => {
  let wrapper;
  let editor;
  let getPos;

  const createWrapper = async (propsData = { cellType: 'td' }) => {
    wrapper = shallowMountExtended(TableCellBaseWrapper, {
      propsData: {
        editor,
        getPos,
        ...propsData,
      },
    });
  };

  const findDropdown = () => wrapper.findComponent(GlDropdown);
  const findDropdownItemWithLabel = (name) =>
    wrapper
      .findAllComponents(GlDropdownItem)
      .filter((dropdownItem) => dropdownItem.text().includes(name))
      .at(0);
  const findDropdownItemWithLabelExists = (name) =>
    wrapper
      .findAllComponents(GlDropdownItem)
      .filter((dropdownItem) => dropdownItem.text().includes(name)).length > 0;
  const setCurrentPositionInCell = () => {
    const { $cursor } = editor.state.selection;

    getPos.mockReturnValue($cursor.pos - $cursor.parentOffset - 1);
  };
  const mockDropdownHide = () => {
    /*
     * TODO: Replace this method with using the scoped hide function
     * provided by BootstrapVue https://bootstrap-vue.org/docs/components/dropdown.
     * GitLab UI is not exposing it in the default scope
     */
    findDropdown().vm.hide = jest.fn();
  };

  beforeEach(() => {
    getPos = jest.fn();
    editor = createTestEditor({});
  });

  afterEach(() => {
    wrapper.destroy();
  });

  it('renders a td node-view-wrapper with relative position', () => {
    createWrapper();
    expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
    expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('td');
  });

  it('displays dropdown when selection cursor is on the cell', async () => {
    setCurrentPositionInCell();
    createWrapper();

    await nextTick();

    expect(findDropdown().props()).toMatchObject({
      category: 'tertiary',
      icon: 'chevron-down',
      size: 'small',
      split: false,
    });
    expect(findDropdown().attributes()).toMatchObject({
      boundary: 'viewport',
      'no-caret': '',
    });
  });

  it('does not display dropdown when selection cursor is not on the cell', async () => {
    createWrapper();

    await nextTick();

    expect(findDropdown().exists()).toBe(false);
  });

  describe('when dropdown is visible', () => {
    beforeEach(async () => {
      setCurrentPositionInCell();
      getSelectedRect.mockReturnValue({
        map: {
          height: 1,
          width: 1,
        },
      });

      createWrapper();
      await nextTick();

      mockDropdownHide();
    });

    it.each`
      dropdownItemLabel         | commandName
      ${'Insert column before'} | ${'addColumnBefore'}
      ${'Insert column after'}  | ${'addColumnAfter'}
      ${'Insert row before'}    | ${'addRowBefore'}
      ${'Insert row after'}     | ${'addRowAfter'}
      ${'Delete table'}         | ${'deleteTable'}
    `(
      'executes $commandName when $dropdownItemLabel button is clicked',
      ({ commandName, dropdownItemLabel }) => {
        const mocks = mockChainedCommands(editor, [commandName, 'run']);

        findDropdownItemWithLabel(dropdownItemLabel).vm.$emit('click');

        expect(mocks[commandName]).toHaveBeenCalled();
      },
    );

    it('does not allow deleting rows and columns', async () => {
      expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
      expect(findDropdownItemWithLabelExists('Delete column')).toBe(false);
    });

    it('allows deleting rows when there are more than 2 rows in the table', async () => {
      const mocks = mockChainedCommands(editor, ['deleteRow', 'run']);

      getSelectedRect.mockReturnValue({
        map: {
          height: 3,
        },
      });

      emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });

      await nextTick();

      findDropdownItemWithLabel('Delete row').vm.$emit('click');

      expect(mocks.deleteRow).toHaveBeenCalled();
    });

    it('allows deleting columns when there are more than 1 column in the table', async () => {
      const mocks = mockChainedCommands(editor, ['deleteColumn', 'run']);

      getSelectedRect.mockReturnValue({
        map: {
          width: 2,
        },
      });

      emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });

      await nextTick();

      findDropdownItemWithLabel('Delete column').vm.$emit('click');

      expect(mocks.deleteColumn).toHaveBeenCalled();
    });

    describe('when current row is the table’s header', () => {
      beforeEach(async () => {
        // Remove 2 rows condition
        getSelectedRect.mockReturnValue({
          map: {
            height: 3,
          },
        });

        createWrapper({ cellType: 'th' });

        await nextTick();
      });

      it('does not allow adding a row before the header', async () => {
        expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
      });

      it('does not allow removing the header row', async () => {
        createWrapper({ cellType: 'th' });

        await nextTick();

        expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
      });
    });
  });
});
