import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import {
  GlIcon,
  GlLoadingIcon,
  GlDropdown,
  GlDropdownForm,
  GlDropdownItem,
  GlSearchBoxByType,
  GlButton,
} from '@gitlab/ui';

import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';

const mockProjects = [
  {
    id: 2,
    name_with_namespace: 'Gitlab Org / Gitlab Shell',
    full_path: 'gitlab-org/gitlab-shell',
  },
  {
    id: 3,
    name_with_namespace: 'Gnuwget / Wget2',
    full_path: 'gnuwget/wget2',
  },
  {
    id: 4,
    name_with_namespace: 'Commit451 / Lab Coat',
    full_path: 'Commit451/lab-coat',
  },
];

const mockProps = {
  projectsFetchPath: '/-/autocomplete/projects?project_id=1',
  dropdownButtonTitle: 'Move issuable',
  dropdownHeaderTitle: 'Move issuable',
  moveInProgress: false,
};

const mockEvent = {
  stopPropagation: jest.fn(),
  preventDefault: jest.fn(),
};

const createComponent = (propsData = mockProps) =>
  shallowMount(IssuableMoveDropdown, {
    propsData,
  });

describe('IssuableMoveDropdown', () => {
  let mock;
  let wrapper;

  beforeEach(() => {
    mock = new MockAdapter(axios);
    wrapper = createComponent();
    wrapper.vm.$refs.dropdown.hide = jest.fn();
    wrapper.vm.$refs.searchInput.focusInput = jest.fn();
  });

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

  describe('watch', () => {
    describe('searchKey', () => {
      it('calls `fetchProjects` with value of the prop', async () => {
        jest.spyOn(wrapper.vm, 'fetchProjects');
        wrapper.setData({
          searchKey: 'foo',
        });

        await wrapper.vm.$nextTick();

        expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
      });
    });
  });

  describe('methods', () => {
    describe('fetchProjects', () => {
      it('sets projectsListLoading to true and projectsListLoadFailed to false', () => {
        wrapper.vm.fetchProjects();

        expect(wrapper.vm.projectsListLoading).toBe(true);
        expect(wrapper.vm.projectsListLoadFailed).toBe(false);
      });

      it('calls `axios.get` with `projectsFetchPath` and query param `search`', () => {
        jest.spyOn(axios, 'get').mockResolvedValue({
          data: mockProjects,
        });

        wrapper.vm.fetchProjects('foo');

        expect(axios.get).toHaveBeenCalledWith(
          mockProps.projectsFetchPath,
          expect.objectContaining({
            params: {
              search: 'foo',
            },
          }),
        );
      });

      it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
        jest.spyOn(axios, 'get').mockResolvedValue({
          data: mockProjects,
        });

        await wrapper.vm.fetchProjects('foo');

        expect(wrapper.vm.projects).toBe(mockProjects);
        expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
      });

      it('sets projectsListLoadFailed to true when request fails', async () => {
        jest.spyOn(axios, 'get').mockRejectedValue({});

        await wrapper.vm.fetchProjects('foo');

        expect(wrapper.vm.projectsListLoadFailed).toBe(true);
      });

      it('sets projectsListLoading to false when request completes', async () => {
        jest.spyOn(axios, 'get').mockResolvedValue({
          data: mockProjects,
        });

        await wrapper.vm.fetchProjects('foo');

        expect(wrapper.vm.projectsListLoading).toBe(false);
      });
    });

    describe('isSelectedProject', () => {
      it.each`
        project            | selectedProject    | title                       | returnValue
        ${mockProjects[0]} | ${mockProjects[0]} | ${'are same projects'}      | ${true}
        ${mockProjects[0]} | ${mockProjects[1]} | ${'are different projects'} | ${false}
      `(
        'returns $returnValue when selectedProject and provided project param $title',
        async ({ project, selectedProject, returnValue }) => {
          wrapper.setData({
            selectedProject,
          });

          await wrapper.vm.$nextTick();

          expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
        },
      );

      it('returns false when selectedProject is null', async () => {
        wrapper.setData({
          selectedProject: null,
        });

        await wrapper.vm.$nextTick();

        expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
      });
    });
  });

  describe('template', () => {
    const findDropdownEl = () => wrapper.find(GlDropdown);

    it('renders collapsed state element with icon', () => {
      const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');

      expect(collapsedEl.exists()).toBe(true);
      expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
      expect(collapsedEl.find(GlIcon).exists()).toBe(true);
      expect(collapsedEl.find(GlIcon).props('name')).toBe('arrow-right');
    });

    describe('gl-dropdown component', () => {
      it('renders component container element', () => {
        expect(findDropdownEl().exists()).toBe(true);
        expect(findDropdownEl().props('block')).toBe(true);
      });

      it('renders gl-dropdown-form component', () => {
        expect(
          findDropdownEl()
            .find(GlDropdownForm)
            .exists(),
        ).toBe(true);
      });

      it('renders header element', () => {
        const headerEl = findDropdownEl().find('[data-testid="header"]');

        expect(headerEl.exists()).toBe(true);
        expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
        expect(headerEl.find(GlButton).props('icon')).toBe('close');
      });

      it('renders gl-search-box-by-type component', () => {
        const searchEl = findDropdownEl().find(GlSearchBoxByType);

        expect(searchEl.exists()).toBe(true);
        expect(searchEl.attributes()).toMatchObject({
          placeholder: 'Search project',
          debounce: '300',
        });
      });

      it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
        wrapper.setData({
          projectsListLoading: true,
        });

        await wrapper.vm.$nextTick();

        expect(
          findDropdownEl()
            .find(GlLoadingIcon)
            .exists(),
        ).toBe(true);
      });

      it('renders gl-dropdown-item components for available projects', async () => {
        wrapper.setData({
          projects: mockProjects,
          selectedProject: mockProjects[0],
        });

        await wrapper.vm.$nextTick();

        const dropdownItems = wrapper.findAll(GlDropdownItem);

        expect(dropdownItems).toHaveLength(mockProjects.length);
        expect(dropdownItems.at(0).props()).toMatchObject({
          isCheckItem: true,
          isChecked: true,
        });
        expect(dropdownItems.at(0).text()).toBe(mockProjects[0].name_with_namespace);
      });

      it('renders string "No matching results" when search does not yield any matches', async () => {
        wrapper.setData({
          searchKey: 'foo',
        });

        // Wait for `searchKey` watcher to run.
        await wrapper.vm.$nextTick();

        wrapper.setData({
          projects: [],
          projectsListLoading: false,
        });

        await wrapper.vm.$nextTick();

        const dropdownContentEl = wrapper.find('[data-testid="content"]');

        expect(dropdownContentEl.text()).toContain('No matching results');
      });

      it('renders string "Failed to load projects" when loading projects list fails', async () => {
        wrapper.setData({
          projects: [],
          projectsListLoading: false,
          projectsListLoadFailed: true,
        });

        await wrapper.vm.$nextTick();

        const dropdownContentEl = wrapper.find('[data-testid="content"]');

        expect(dropdownContentEl.text()).toContain('Failed to load projects');
      });

      it('renders gl-button within footer', async () => {
        const moveButtonEl = wrapper.find('[data-testid="footer"]').find(GlButton);

        expect(moveButtonEl.text()).toBe('Move');
        expect(moveButtonEl.attributes('disabled')).toBe('true');

        wrapper.setData({
          selectedProject: mockProjects[0],
        });

        await wrapper.vm.$nextTick();

        expect(
          wrapper
            .find('[data-testid="footer"]')
            .find(GlButton)
            .attributes('disabled'),
        ).not.toBeDefined();
      });
    });

    describe('events', () => {
      it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
        wrapper.find('[data-testid="move-collapsed"]').trigger('click');

        expect(wrapper.emitted('toggle-collapse')).toBeTruthy();
      });

      it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
        jest.spyOn(axios, 'get').mockResolvedValue({
          data: mockProjects,
        });

        findDropdownEl().vm.$emit('shown');

        expect(axios.get).toHaveBeenCalled();
      });

      it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
        wrapper.setData({
          projectItemClick: true,
        });

        findDropdownEl().vm.$emit('hide', mockEvent);

        expect(mockEvent.preventDefault).toHaveBeenCalled();
        expect(wrapper.vm.projectItemClick).toBe(false);
      });

      it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
        findDropdownEl().vm.$emit('hide');

        expect(wrapper.emitted('dropdown-close')).toBeTruthy();
      });

      it('close icon in dropdown header closes the dropdown when clicked', () => {
        wrapper
          .find('[data-testid="header"]')
          .find(GlButton)
          .vm.$emit('click', mockEvent);

        expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
      });

      it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
        wrapper.setData({
          projects: mockProjects,
        });

        await wrapper.vm.$nextTick();

        wrapper
          .findAll(GlDropdownItem)
          .at(0)
          .vm.$emit('click', mockEvent);

        expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
      });

      it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
        wrapper.setData({
          selectedProject: mockProjects[0],
        });

        await wrapper.vm.$nextTick();

        wrapper
          .find('[data-testid="footer"]')
          .find(GlButton)
          .vm.$emit('click');

        expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
        expect(wrapper.emitted('move-issuable')).toBeTruthy();
        expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
      });
    });
  });
});
