import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import {
  searchResponse,
  searchResponseOnMR,
  projectMembersResponse,
  participantsQueryResponse,
  mockUser1,
  mockUser2,
} from 'jest/sidebar/mock_data';

const assignee = {
  id: 'gid://gitlab/User/4',
  avatarUrl:
    'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
  name: 'Developer',
  username: 'dev',
  webUrl: '/dev',
  status: null,
};

const mockError = jest.fn().mockRejectedValue('Error!');

const waitForSearch = async () => {
  jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
  await nextTick();
  await waitForPromises();
};

Vue.use(VueApollo);

describe('User select dropdown', () => {
  let wrapper;
  let fakeApollo;

  const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
  const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
  const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
  const findSelectedParticipantByIndex = (index) =>
    findSelectedParticipants().at(index).findComponent(SidebarParticipant);
  const findUnselectedParticipants = () =>
    wrapper.findAll('[data-testid="unselected-participant"]');
  const findUnselectedParticipantByIndex = (index) =>
    findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
  const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
  const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
  const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
  const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');

  const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
  const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);

  const createComponent = ({
    props = {},
    searchQueryHandler = searchQueryHandlerSuccess,
    participantsQueryHandler = participantsQueryHandlerSuccess,
  } = {}) => {
    fakeApollo = createMockApollo([
      [searchUsersQuery, searchQueryHandler],
      [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
      [getIssueParticipantsQuery, participantsQueryHandler],
    ]);
    wrapper = shallowMount(UserSelect, {
      apolloProvider: fakeApollo,
      propsData: {
        headerText: 'test',
        text: 'test-text',
        fullPath: '/project',
        iid: '1',
        value: [],
        currentUser: {
          username: 'random',
          name: 'Mr. Random',
        },
        allowMultipleAssignees: false,
        ...props,
      },
      stubs: {
        GlDropdown: {
          template: `
            <div>
              <slot name="header"></slot>
              <slot></slot>
              <slot name="footer"></slot>
            </div>
          `,
          methods: {
            hide: jest.fn(),
          },
        },
      },
    });
  };

  afterEach(() => {
    fakeApollo = null;
  });

  it('renders a loading spinner if participants are loading', () => {
    createComponent();

    expect(findParticipantsLoading().exists()).toBe(true);
  });

  it('skips the queries if `isEditing` prop is false', () => {
    createComponent({ props: { isEditing: false } });

    expect(findParticipantsLoading().exists()).toBe(false);
    expect(searchQueryHandlerSuccess).not.toHaveBeenCalled();
    expect(participantsQueryHandlerSuccess).not.toHaveBeenCalled();
  });

  it('emits an `error` event if participants query was rejected', async () => {
    createComponent({ participantsQueryHandler: mockError });
    await waitForPromises();

    expect(wrapper.emitted('error')).toEqual([[]]);
  });

  it('emits an `error` event if search query was rejected', async () => {
    createComponent({ searchQueryHandler: mockError });
    await waitForSearch();

    expect(wrapper.emitted('error')).toEqual([[]]);
  });

  it('renders current user if they are not in participants or assignees', async () => {
    createComponent();
    await waitForPromises();

    expect(findCurrentUser().exists()).toBe(true);
  });

  it('does not render current user if user is not logged in', async () => {
    createComponent({
      props: {
        currentUser: {},
      },
    });
    await waitForPromises();

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

  it('does not render issuable author if author is not passed as a prop', async () => {
    createComponent();
    await waitForPromises();

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

  describe('when issuable author is passed as a prop', () => {
    it('moves issuable author on top of assigned list, if author is assigned', async () => {
      createComponent({
        props: {
          value: [assignee, mockUser2],
          issuableAuthor: mockUser2,
        },
      });
      await waitForPromises();

      expect(findSelectedParticipantByIndex(0).props('user')).toEqual(mockUser2);
    });

    it('moves issuable author on top of assigned list after current user, if author and current user are assigned', async () => {
      const currentUser = mockUser1;
      const issuableAuthor = mockUser2;

      createComponent({
        props: {
          value: [assignee, issuableAuthor, currentUser],
          issuableAuthor,
          currentUser,
        },
      });
      await waitForPromises();

      expect(findSelectedParticipantByIndex(0).props('user')).toEqual(currentUser);
      expect(findSelectedParticipantByIndex(1).props('user')).toEqual(issuableAuthor);
    });

    it('moves issuable author on top of unassigned list, if author is unassigned project member', async () => {
      createComponent({
        props: {
          issuableAuthor: mockUser2,
        },
      });
      await waitForPromises();

      expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(mockUser2);
    });

    it('moves issuable author on top of unassigned list after current user, if author and current user are unassigned project members', async () => {
      const currentUser = mockUser2;
      const issuableAuthor = mockUser1;

      createComponent({
        props: {
          issuableAuthor,
          currentUser,
        },
      });
      await waitForPromises();

      expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(currentUser);
      expect(findUnselectedParticipantByIndex(1).props('user')).toMatchObject(issuableAuthor);
    });

    it('displays author in a designated position if author is not assigned and not a project member', async () => {
      createComponent({
        props: {
          issuableAuthor: assignee,
        },
      });
      await waitForPromises();

      expect(findIssuableAuthor().exists()).toBe(true);
    });
  });

  it('displays correct amount of selected users', async () => {
    createComponent({
      props: {
        value: [assignee],
      },
    });
    await waitForPromises();

    expect(findSelectedParticipants()).toHaveLength(1);
  });

  it('does not render a `Cannot merge` tooltip', async () => {
    createComponent();
    await waitForPromises();

    expect(findUnselectedParticipants().at(0).attributes('title')).toBe('');
  });

  describe('when search is empty', () => {
    it('renders a merged list of participants and project members', async () => {
      createComponent();
      await waitForPromises();

      expect(findUnselectedParticipants()).toHaveLength(4);
    });

    it('renders `Unassigned` link with the checkmark when there are no selected users', async () => {
      createComponent();
      await waitForPromises();
      expect(findUnassignLink().props('isChecked')).toBe(true);
    });

    it('renders `Unassigned` link without the checkmark when there are selected users', async () => {
      createComponent({
        props: {
          value: [assignee],
        },
      });
      await waitForPromises();
      expect(findUnassignLink().props('isChecked')).toBe(false);
    });

    it('emits an input event with empty array after clicking on `Unassigned`', async () => {
      createComponent({
        props: {
          value: [assignee],
        },
      });
      await waitForPromises();
      findUnassignLink().trigger('click');

      expect(wrapper.emitted('input')).toEqual([[[]]]);
    });

    it('hides the dropdown after clicking on `Unassigned`', async () => {
      createComponent({
        props: {
          value: [assignee],
        },
      });
      wrapper.vm.$refs.dropdown.hide = jest.fn();
      await waitForPromises();

      findUnassignLink().trigger('click');

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

    it('emits an empty array after unselecting the only selected assignee', async () => {
      createComponent({
        props: {
          value: [assignee],
        },
      });
      await waitForPromises();

      findSelectedParticipants().at(0).trigger('click');
      expect(wrapper.emitted('input')).toEqual([[[]]]);
    });

    it('allows only one user to be selected if `allowMultipleAssignees` is false', async () => {
      createComponent({
        props: {
          value: [assignee],
        },
      });
      await waitForPromises();

      findUnselectedParticipants().at(0).trigger('click');

      expect(wrapper.emitted('input')).toMatchObject([
        [
          [
            {
              avatarUrl:
                'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
              id: 'gid://gitlab/User/1',
              name: 'Administrator',
              status: null,
              username: 'root',
              webUrl: '/root',
            },
          ],
        ],
      ]);
    });

    it('adds user to selected if `allowMultipleAssignees` is true', async () => {
      createComponent({
        props: {
          value: [assignee],
          allowMultipleAssignees: true,
        },
      });
      await waitForPromises();

      findUnselectedParticipants().at(0).trigger('click');
      expect(wrapper.emitted('input')[0][0]).toHaveLength(2);
    });
  });

  describe('when searching', () => {
    it('does not show loading spinner when debounce timer is still running', async () => {
      createComponent();
      await waitForPromises();
      findSearchField().vm.$emit('input', 'roo');

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

    it('shows loading spinner when searching for users', async () => {
      createComponent();
      await waitForPromises();
      findSearchField().vm.$emit('input', 'roo');
      jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
      await nextTick();

      expect(findParticipantsLoading().exists()).toBe(true);
    });

    it('renders a list of found users and external participants matching search term', async () => {
      createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
      await waitForPromises();

      findSearchField().vm.$emit('input', 'ro');
      await waitForSearch();

      expect(findUnselectedParticipants()).toHaveLength(3);
    });

    it('renders a list of found users only if no external participants match search term', async () => {
      createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
      await waitForPromises();

      findSearchField().vm.$emit('input', 'roo');
      await waitForSearch();

      expect(findUnselectedParticipants()).toHaveLength(2);
    });

    it('shows a message about no matches if search returned an empty list', async () => {
      const responseCopy = cloneDeep(searchResponse);
      responseCopy.data.workspace.users.nodes = [];

      createComponent({
        searchQueryHandler: jest.fn().mockResolvedValue(responseCopy),
      });
      await waitForPromises();
      findSearchField().vm.$emit('input', 'tango');
      await waitForSearch();

      expect(findUnselectedParticipants()).toHaveLength(0);
      expect(findEmptySearchResults().exists()).toBe(true);
    });
  });

  describe('when on merge request sidebar', () => {
    beforeEach(() => {
      createComponent({ props: { issuableType: TYPE_MERGE_REQUEST, issuableId: 1 } });
      return waitForPromises();
    });

    it('does not render a `Cannot merge` tooltip for a user that has merge permission', () => {
      expect(findUnselectedParticipants().at(0).attributes('title')).toBe('');
    });

    it('renders a `Cannot merge` tooltip for a user that does not have merge permission', () => {
      expect(findUnselectedParticipants().at(1).attributes('title')).toBe('Cannot merge');
    });
  });
});