import {
  GlAvatar,
  GlAvatarLabeled,
  GlIntersectionObserver,
  GlToken,
  GlTokenSelector,
  GlLoadingIcon,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';

import getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue';
import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';

describe('ProjectsTokenSelector', () => {
  const getProjectsQueryResponsePage2 = produce(
    getProjectsQueryResponse,
    (getProjectsQueryResponseDraft) => {
      /* eslint-disable no-param-reassign */
      getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false;
      getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null;
      getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1);
      getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100';
      /* eslint-enable no-param-reassign */
    },
  );

  const runDebounce = () => jest.runAllTimers();

  const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects;
  const project1 = projects[0];
  const project2 = projects[1];

  let wrapper;

  let resolveGetProjectsQuery;
  let resolveGetInitialProjectsQuery;
  const getProjectsQueryRequestHandler = jest.fn(
    ({ ids }) =>
      new Promise((resolve) => {
        if (ids) {
          resolveGetInitialProjectsQuery = resolve;
        } else {
          resolveGetProjectsQuery = resolve;
        }
      }),
  );

  const createComponent = ({
    propsData = {},
    apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]),
    resolveQueries = true,
  } = {}) => {
    Vue.use(VueApollo);

    wrapper = extendedWrapper(
      mount(ProjectsTokenSelector, {
        apolloProvider,
        propsData: {
          selectedProjects: [],
          initialProjectIds: [],
          ...propsData,
        },
        stubs: ['gl-intersection-observer'],
      }),
    );

    runDebounce();

    if (resolveQueries) {
      resolveGetProjectsQuery(getProjectsQueryResponse);

      return waitForPromises();
    }

    return Promise.resolve();
  };

  const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
  const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
  const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);

  it('renders dropdown items with project avatars', async () => {
    await createComponent();

    wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => {
      const project = projects[index];

      expect(avatarLabeledWrapper.attributes()).toEqual(
        expect.objectContaining({
          'entity-id': `${getIdFromGraphQLId(project.id)}`,
          'entity-name': project.name,
          ...(project.avatarUrl && { src: project.avatarUrl }),
        }),
      );

      expect(avatarLabeledWrapper.props()).toEqual(
        expect.objectContaining({
          label: project.name,
          subLabel: project.nameWithNamespace,
        }),
      );
    });
  });

  it('renders tokens with project avatars', () => {
    createComponent({
      propsData: {
        selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }],
      },
    });

    const token = wrapper.findComponent(GlToken);
    const avatar = token.findComponent(GlAvatar);

    expect(token.text()).toContain(project2.nameWithNamespace);
    expect(avatar.attributes('src')).toBe(project2.avatarUrl);
    expect(avatar.props()).toEqual(
      expect.objectContaining({
        entityId: getIdFromGraphQLId(project2.id),
        entityName: project2.name,
      }),
    );
  });

  describe('when `enter` key is pressed', () => {
    it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => {
      createComponent();

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

      findTokenSelectorInput().trigger('keydown.enter', event);

      expect(event.preventDefault).toHaveBeenCalled();
    });
  });

  describe('when text input is typed in', () => {
    const searchTerm = 'foo bar';

    beforeEach(async () => {
      await createComponent();

      await findTokenSelectorInput().setValue(searchTerm);
      runDebounce();
    });

    it('makes GraphQL request with `search` variable set', async () => {
      expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
        search: searchTerm,
        after: null,
        first: 20,
        ids: null,
      });
    });

    it('sets loading state while waiting for GraphQL request to resolve', async () => {
      expect(findTokenSelector().props('loading')).toBe(true);

      resolveGetProjectsQuery(getProjectsQueryResponse);
      await waitForPromises();

      expect(findTokenSelector().props('loading')).toBe(false);
    });
  });

  describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => {
    beforeEach(async () => {
      await createComponent();

      findIntersectionObserver().vm.$emit('appear');
    });

    it('makes GraphQL request with `after` variable set', async () => {
      expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
        after: pageInfo.endCursor,
        first: 20,
        search: '',
        ids: null,
      });
    });

    it('displays loading icon while waiting for GraphQL request to resolve', async () => {
      expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);

      resolveGetProjectsQuery(getProjectsQueryResponsePage2);
      await waitForPromises();

      expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
    });
  });

  describe('when there is not a next page of projects', () => {
    it('does not render `GlIntersectionObserver`', async () => {
      createComponent({ resolveQueries: false });

      resolveGetProjectsQuery(getProjectsQueryResponsePage2);
      await waitForPromises();

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

  describe('when `GlTokenSelector` emits `input` event', () => {
    it('emits `input` event used by `v-model`', () => {
      findTokenSelector().vm.$emit('input', project1);

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

  describe('when `GlTokenSelector` emits `focus` event', () => {
    it('emits `focus` event', () => {
      const event = { fakeEvent: 'foo' };
      findTokenSelector().vm.$emit('focus', event);

      expect(wrapper.emitted('focus')[0]).toEqual([event]);
    });
  });

  describe('when `initialProjectIds` is an empty array', () => {
    it('does not request initial projects', async () => {
      await createComponent();

      expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1);
      expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith(
        expect.objectContaining({
          ids: null,
        }),
      );
    });
  });

  describe('when `initialProjectIds` is an array of project IDs', () => {
    it('requests those projects and emits `input` event with result', async () => {
      await createComponent({
        propsData: {
          initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)],
        },
      });

      resolveGetInitialProjectsQuery(getProjectsQueryResponse);
      await waitForPromises();

      expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({
        after: '',
        first: null,
        search: '',
        ids: [project1.id, project2.id],
      });
      expect(wrapper.emitted('input')[0][0]).toEqual([
        { ...project1, id: getIdFromGraphQLId(project1.id) },
        { ...project2, id: getIdFromGraphQLId(project2.id) },
      ]);
    });
  });
});
