import { GlButton, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
import createFlash from '~/flash';
import Modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createEntriesFromPaths } from '../../helpers';

jest.mock('~/flash');

const NEW_NAME = 'babar';

describe('new file modal component', () => {
  const showModal = jest.fn();
  const toggleModal = jest.fn();

  let store;
  let wrapper;

  const findForm = () => wrapper.findByTestId('file-name-form');
  const findGlModal = () => wrapper.findComponent(GlModal);
  const findInput = () => wrapper.findByTestId('file-name-field');
  const findTemplateButtons = () => wrapper.findAllComponents(GlButton);
  const findTemplateButtonsModel = () =>
    findTemplateButtons().wrappers.map((x) => ({
      text: x.text(),
      variant: x.props('variant'),
      category: x.props('category'),
    }));

  const open = (type, path) => {
    // TODO: This component can not be passed props
    // We have to interact with the open() method?
    wrapper.vm.open(type, path);
  };
  const triggerSubmitForm = () => {
    findForm().trigger('submit');
  };
  const triggerSubmitModal = () => {
    findGlModal().vm.$emit('primary');
  };
  const triggerCancel = () => {
    findGlModal().vm.$emit('cancel');
  };

  const mountComponent = () => {
    const GlModalStub = stubComponent(GlModal);
    jest.spyOn(GlModalStub.methods, 'show').mockImplementation(showModal);
    jest.spyOn(GlModalStub.methods, 'toggle').mockImplementation(toggleModal);

    wrapper = shallowMountExtended(Modal, {
      store,
      stubs: {
        GlModal: GlModalStub,
      },
      // We need to attach to document for "focus" to work
      attachTo: document.body,
    });
  };

  beforeEach(() => {
    store = createStore();

    Object.assign(
      store.state.entries,
      createEntriesFromPaths([
        'README.md',
        'src',
        'src/deleted.js',
        'src/parent_dir',
        'src/parent_dir/foo.js',
      ]),
    );
    Object.assign(store.state.entries['src/deleted.js'], { deleted: true });

    jest.spyOn(store, 'dispatch').mockImplementation();
  });

  afterEach(() => {
    store = null;
    wrapper.destroy();
    document.body.innerHTML = '';
  });

  describe('default', () => {
    beforeEach(async () => {
      mountComponent();

      // Not necessarily needed, but used to ensure that nothing extra is happening after the tick
      await nextTick();
    });

    it('renders modal', () => {
      expect(findGlModal().props()).toMatchObject({
        actionCancel: {
          attributes: [{ variant: 'default' }],
          text: 'Cancel',
        },
        actionPrimary: {
          attributes: [{ variant: 'confirm' }],
          text: 'Create file',
        },
        actionSecondary: null,
        size: 'lg',
        modalId: 'ide-new-entry',
        title: 'Create new file',
      });
    });

    it('renders name label', () => {
      expect(wrapper.find('label').text()).toBe('Name');
    });

    it('renders template buttons', () => {
      const actual = findTemplateButtonsModel();

      expect(actual.length).toBeGreaterThan(0);
      expect(actual).toEqual(
        store.getters['fileTemplates/templateTypes'].map((template) => ({
          category: 'secondary',
          text: template.name,
          variant: 'dashed',
        })),
      );
    });

    // These negative ".not.toHaveBeenCalled" assertions complement the positive "toHaveBeenCalled"
    // assertions that show up later in this spec. Without these, we're not guaranteed the "act"
    // actually caused the change in behavior.
    it('does not dispatch actions by default', () => {
      expect(store.dispatch).not.toHaveBeenCalled();
    });

    it('does not trigger modal by default', () => {
      expect(showModal).not.toHaveBeenCalled();
      expect(toggleModal).not.toHaveBeenCalled();
    });

    it('does not focus input by default', () => {
      expect(document.activeElement).toBe(document.body);
    });
  });

  describe.each`
    entryType | path         | modalTitle                | btnTitle              | showsFileTemplates | inputValue    | inputPlaceholder
    ${'tree'} | ${''}        | ${'Create new directory'} | ${'Create directory'} | ${false}           | ${''}         | ${'dir/'}
    ${'blob'} | ${''}        | ${'Create new file'}      | ${'Create file'}      | ${true}            | ${''}         | ${'dir/file_name'}
    ${'blob'} | ${'foo/bar'} | ${'Create new file'}      | ${'Create file'}      | ${true}            | ${'foo/bar/'} | ${'dir/file_name'}
  `(
    'when opened as $entryType with path "$path"',
    ({
      entryType,
      path,
      modalTitle,
      btnTitle,
      showsFileTemplates,
      inputValue,
      inputPlaceholder,
    }) => {
      beforeEach(async () => {
        mountComponent();

        open(entryType, path);

        await nextTick();
      });

      it('sets modal props', () => {
        expect(findGlModal().props()).toMatchObject({
          title: modalTitle,
          actionPrimary: {
            attributes: [{ variant: 'confirm' }],
            text: btnTitle,
          },
        });
      });

      it('sets input attributes', () => {
        expect(findInput().element.value).toBe(inputValue);
        expect(findInput().attributes('placeholder')).toBe(inputPlaceholder);
      });

      it(`shows file templates: ${showsFileTemplates}`, () => {
        const actual = findTemplateButtonsModel().length > 0;

        expect(actual).toBe(showsFileTemplates);
      });

      it('shows modal', () => {
        expect(showModal).toHaveBeenCalled();
      });

      it('focus on input', () => {
        expect(document.activeElement).toBe(findInput().element);
      });

      it('resets when canceled', async () => {
        triggerCancel();

        await nextTick();

        // Resets input value
        expect(findInput().element.value).toBe('');
        // Resets to blob mode
        expect(findGlModal().props('title')).toBe('Create new file');
      });
    },
  );

  describe.each`
    modalType | name             | expectedName
    ${'blob'} | ${'foo/bar.js'}  | ${'foo/bar.js'}
    ${'blob'} | ${'foo /bar.js'} | ${'foo/bar.js'}
    ${'tree'} | ${'foo/dir'}     | ${'foo/dir'}
    ${'tree'} | ${'foo /dir'}    | ${'foo/dir'}
  `('when submitting as $modalType with "$name"', ({ modalType, name, expectedName }) => {
    describe('when using the modal primary button', () => {
      beforeEach(async () => {
        mountComponent();

        open(modalType, '');
        await nextTick();

        findInput().setValue(name);
        triggerSubmitModal();
      });

      it('triggers createTempEntry action', () => {
        expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
          name: expectedName,
          type: modalType,
        });
      });
    });

    describe('when triggering form submit (pressing enter)', () => {
      beforeEach(async () => {
        mountComponent();

        open(modalType, '');
        await nextTick();

        findInput().setValue(name);
        triggerSubmitForm();
      });

      it('triggers createTempEntry action', () => {
        expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
          name: expectedName,
          type: modalType,
        });
      });
    });
  });

  describe('when creating from template type', () => {
    beforeEach(async () => {
      mountComponent();

      open('blob', 'some_dir');

      await nextTick();

      // Set input, then trigger button
      findInput().setValue('some_dir/foo.js');
      findTemplateButtons().at(1).vm.$emit('click');
    });

    it('triggers createTempEntry action', () => {
      const { name: expectedName } = store.getters['fileTemplates/templateTypes'][1];

      expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
        name: `some_dir/${expectedName}`,
        type: 'blob',
      });
    });

    it('toggles modal', () => {
      expect(toggleModal).toHaveBeenCalled();
    });
  });

  describe.each`
    origPath            | title              | inputValue          | inputSelectionStart
    ${'src/parent_dir'} | ${'Rename folder'} | ${'src/parent_dir'} | ${'src/'.length}
    ${'README.md'}      | ${'Rename file'}   | ${'README.md'}      | ${0}
  `('when renaming for $origPath', ({ origPath, title, inputValue, inputSelectionStart }) => {
    beforeEach(async () => {
      mountComponent();

      open('rename', origPath);

      await nextTick();
    });

    it('sets modal props for renaming', () => {
      expect(findGlModal().props()).toMatchObject({
        title,
        actionPrimary: {
          attributes: [{ variant: 'confirm' }],
          text: title,
        },
      });
    });

    it('sets input value', () => {
      expect(findInput().element.value).toBe(inputValue);
    });

    it(`does not show file templates`, () => {
      expect(findTemplateButtonsModel()).toHaveLength(0);
    });

    it('shows modal when renaming', () => {
      expect(showModal).toHaveBeenCalled();
    });

    it('focus on input when renaming', () => {
      expect(document.activeElement).toBe(findInput().element);
    });

    it('selects name part of the input', () => {
      expect(findInput().element.selectionStart).toBe(inputSelectionStart);
      expect(findInput().element.selectionEnd).toBe(origPath.length);
    });

    describe('when renames is submitted successfully', () => {
      describe('when using the modal primary button', () => {
        beforeEach(() => {
          findInput().setValue(NEW_NAME);
          triggerSubmitModal();
        });

        it('dispatches renameEntry event', () => {
          expect(store.dispatch).toHaveBeenCalledWith('renameEntry', {
            path: origPath,
            parentPath: '',
            name: NEW_NAME,
          });
        });

        it('does not trigger flash', () => {
          expect(createFlash).not.toHaveBeenCalled();
        });
      });

      describe('when triggering form submit (pressing enter)', () => {
        beforeEach(() => {
          findInput().setValue(NEW_NAME);
          triggerSubmitForm();
        });

        it('dispatches renameEntry event', () => {
          expect(store.dispatch).toHaveBeenCalledWith('renameEntry', {
            path: origPath,
            parentPath: '',
            name: NEW_NAME,
          });
        });

        it('does not trigger flash', () => {
          expect(createFlash).not.toHaveBeenCalled();
        });
      });
    });
  });

  describe('when renaming and file already exists', () => {
    beforeEach(async () => {
      mountComponent();

      open('rename', 'src/parent_dir');

      await nextTick();

      // Set to something that already exists!
      findInput().setValue('src');
      triggerSubmitModal();
    });

    it('creates flash', () => {
      expect(createFlash).toHaveBeenCalledWith({
        message: 'The name "src" is already taken in this directory.',
        fadeTransition: false,
        addBodyClass: true,
      });
    });

    it('does not dispatch event', () => {
      expect(store.dispatch).not.toHaveBeenCalled();
    });
  });

  describe('when renaming and file has been deleted', () => {
    beforeEach(async () => {
      mountComponent();

      open('rename', 'src/parent_dir/foo.js');

      await nextTick();

      findInput().setValue('src/deleted.js');
      triggerSubmitModal();
    });

    it('does not create flash', () => {
      expect(createFlash).not.toHaveBeenCalled();
    });

    it('dispatches event', () => {
      expect(store.dispatch).toHaveBeenCalledWith('renameEntry', {
        path: 'src/parent_dir/foo.js',
        name: 'deleted.js',
        parentPath: 'src',
      });
    });
  });
});