import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
import GroupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
import {
  DELETE_IMAGE_SUCCESS_MESSAGE,
  DELETE_IMAGE_ERROR_MESSAGE,
  SORT_FIELDS,
} from '~/registry/explorer/constants';
import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/registry/explorer/pages/list.vue';
import Tracking from '~/tracking';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';

import { $toast } from '../../shared/mocks';
import {
  graphQLImageListMock,
  graphQLImageDeleteMock,
  deletedContainerRepository,
  graphQLEmptyImageListMock,
  graphQLEmptyGroupImageListMock,
  pageInfo,
  graphQLProjectImageRepositoriesDetailsMock,
  dockerCommands,
} from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';

const localVue = createLocalVue();

describe('List Page', () => {
  let wrapper;
  let apolloProvider;

  const findDeleteModal = () => wrapper.findComponent(GlModal);
  const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);

  const findEmptyState = () => wrapper.findComponent(GlEmptyState);

  const findCliCommands = () => wrapper.findComponent(CliCommands);
  const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState);
  const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState);
  const findRegistryHeader = () => wrapper.findComponent(RegistryHeader);

  const findDeleteAlert = () => wrapper.findComponent(GlAlert);
  const findImageList = () => wrapper.findComponent(ImageList);
  const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
  const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
  const findDeleteImage = () => wrapper.findComponent(DeleteImage);
  const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);

  const waitForApolloRequestRender = async () => {
    jest.runOnlyPendingTimers();
    await waitForPromises();
    await nextTick();
  };

  const mountComponent = ({
    mocks,
    resolver = jest.fn().mockResolvedValue(graphQLImageListMock),
    detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
    mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
    config = { isGroupPage: false },
    query = {},
  } = {}) => {
    localVue.use(VueApollo);

    const requestHandlers = [
      [getContainerRepositoriesQuery, resolver],
      [getContainerRepositoriesDetails, detailsResolver],
      [deleteContainerRepositoryMutation, mutationResolver],
    ];

    apolloProvider = createMockApollo(requestHandlers);

    wrapper = shallowMount(component, {
      localVue,
      apolloProvider,
      stubs: {
        GlModal,
        GlEmptyState,
        GlSprintf,
        RegistryHeader,
        TitleArea,
        DeleteImage,
      },
      mocks: {
        $toast,
        $route: {
          name: 'foo',
          query,
        },
        ...mocks,
      },
      provide() {
        return {
          config,
          ...dockerCommands,
        };
      },
    });
  };

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

  it('contains registry header', async () => {
    mountComponent();

    await waitForApolloRequestRender();

    expect(findRegistryHeader().exists()).toBe(true);
    expect(findRegistryHeader().props()).toMatchObject({
      imagesCount: 2,
      metadataLoading: false,
    });
  });

  describe.each([
    { error: 'connectionError', errorName: 'connection error' },
    { error: 'invalidPathError', errorName: 'invalid path error' },
  ])('handling $errorName', ({ error }) => {
    const config = {
      containersErrorImage: 'foo',
      helpPagePath: 'bar',
      isGroupPage: false,
    };
    config[error] = true;

    it('should show an empty state', () => {
      mountComponent({ config });

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

    it('empty state should have an svg-path', () => {
      mountComponent({ config });

      expect(findEmptyState().props('svgPath')).toBe(config.containersErrorImage);
    });

    it('empty state should have a description', () => {
      mountComponent({ config });

      expect(findEmptyState().props('title')).toContain('connection error');
    });

    it('should not show the loading or default state', () => {
      mountComponent({ config });

      expect(findSkeletonLoader().exists()).toBe(false);
      expect(findImageList().exists()).toBe(false);
    });
  });

  describe('isLoading is true', () => {
    it('shows the skeleton loader', async () => {
      mountComponent();

      await nextTick();

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

    it('imagesList is not visible', () => {
      mountComponent();

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

    it('cli commands is not visible', () => {
      mountComponent();

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

    it('title has the metadataLoading props set to true', async () => {
      mountComponent();

      await nextTick();

      expect(findRegistryHeader().props('metadataLoading')).toBe(true);
    });
  });

  describe('list is empty', () => {
    describe('project page', () => {
      const resolver = jest.fn().mockResolvedValue(graphQLEmptyImageListMock);

      it('cli commands is not visible', async () => {
        mountComponent({ resolver });

        await waitForApolloRequestRender();

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

      it('project empty state is visible', async () => {
        mountComponent({ resolver });

        await waitForApolloRequestRender();

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

    describe('group page', () => {
      const resolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);

      const config = {
        isGroupPage: true,
      };

      it('group empty state is visible', async () => {
        mountComponent({ resolver, config });

        await waitForApolloRequestRender();

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

      it('cli commands is not visible', async () => {
        mountComponent({ resolver, config });

        await waitForApolloRequestRender();

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

  describe('list is not empty', () => {
    describe('unfiltered state', () => {
      it('quick start is visible', async () => {
        mountComponent();

        await waitForApolloRequestRender();

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

      it('list component is visible', async () => {
        mountComponent();

        await waitForApolloRequestRender();

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

      describe('additional metadata', () => {
        it('is called on component load', async () => {
          const detailsResolver = jest
            .fn()
            .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
          mountComponent({ detailsResolver });

          jest.runOnlyPendingTimers();
          await waitForPromises();

          expect(detailsResolver).toHaveBeenCalled();
        });

        it('does not block the list ui to show', async () => {
          const detailsResolver = jest.fn().mockRejectedValue();
          mountComponent({ detailsResolver });

          await waitForApolloRequestRender();

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

        it('loading state is passed to list component', async () => {
          // this is a promise that never resolves, to trick apollo to think that this request is still loading
          const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {}));

          mountComponent({ detailsResolver });
          await waitForApolloRequestRender();

          expect(findImageList().props('metadataLoading')).toBe(true);
        });
      });

      describe('delete image', () => {
        const selectImageForDeletion = async () => {
          await waitForApolloRequestRender();

          findImageList().vm.$emit('delete', deletedContainerRepository);
        };

        it('should call deleteItem when confirming deletion', async () => {
          const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
          mountComponent({ mutationResolver });

          await selectImageForDeletion();

          findDeleteModal().vm.$emit('primary');
          await waitForApolloRequestRender();

          expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);

          const updatedImage = findImageList()
            .props('images')
            .find((i) => i.id === deletedContainerRepository.id);

          expect(updatedImage.status).toBe(deletedContainerRepository.status);
        });

        it('should show a success alert when delete request is successful', async () => {
          mountComponent();

          await selectImageForDeletion();

          findDeleteImage().vm.$emit('success');
          await nextTick();

          const alert = findDeleteAlert();
          expect(alert.exists()).toBe(true);
          expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
            DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
          );
        });

        describe('when delete request fails it shows an alert', () => {
          it('user recoverable error', async () => {
            mountComponent();

            await selectImageForDeletion();

            findDeleteImage().vm.$emit('error');
            await nextTick();

            const alert = findDeleteAlert();
            expect(alert.exists()).toBe(true);
            expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
              DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
            );
          });
        });
      });
    });

    describe('search and sorting', () => {
      const doSearch = async () => {
        await waitForApolloRequestRender();
        findRegistrySearch().vm.$emit('filter:changed', [
          { type: FILTERED_SEARCH_TERM, value: { data: 'centos6' } },
        ]);

        findRegistrySearch().vm.$emit('filter:submit');

        await nextTick();
      };

      it('has a search box element', async () => {
        mountComponent();

        await waitForApolloRequestRender();

        const registrySearch = findRegistrySearch();
        expect(registrySearch.exists()).toBe(true);
        expect(registrySearch.props()).toMatchObject({
          filter: [],
          sorting: { orderBy: 'UPDATED', sort: 'desc' },
          sortableFields: SORT_FIELDS,
          tokens: [],
        });
      });

      it('performs sorting', async () => {
        const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
        mountComponent({ resolver });

        await waitForApolloRequestRender();

        findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
        await nextTick();

        expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
      });

      it('performs a search', async () => {
        const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
        mountComponent({ resolver });

        await doSearch();

        expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ name: 'centos6' }));
      });

      it('when search result is empty displays an empty search message', async () => {
        const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
        const detailsResolver = jest
          .fn()
          .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
        mountComponent({ resolver, detailsResolver });

        await waitForApolloRequestRender();

        resolver.mockResolvedValue(graphQLEmptyImageListMock);
        detailsResolver.mockResolvedValue(graphQLEmptyImageListMock);

        await doSearch();

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

    describe('pagination', () => {
      it('prev-page event triggers a fetchMore request', async () => {
        const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
        const detailsResolver = jest
          .fn()
          .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
        mountComponent({ resolver, detailsResolver });

        await waitForApolloRequestRender();

        findImageList().vm.$emit('prev-page');
        await nextTick();

        expect(resolver).toHaveBeenCalledWith(
          expect.objectContaining({ before: pageInfo.startCursor }),
        );
        expect(detailsResolver).toHaveBeenCalledWith(
          expect.objectContaining({ before: pageInfo.startCursor }),
        );
      });

      it('next-page event triggers a fetchMore request', async () => {
        const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
        const detailsResolver = jest
          .fn()
          .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
        mountComponent({ resolver, detailsResolver });

        await waitForApolloRequestRender();

        findImageList().vm.$emit('next-page');
        await nextTick();

        expect(resolver).toHaveBeenCalledWith(
          expect.objectContaining({ after: pageInfo.endCursor }),
        );
        expect(detailsResolver).toHaveBeenCalledWith(
          expect.objectContaining({ after: pageInfo.endCursor }),
        );
      });
    });
  });

  describe('modal', () => {
    beforeEach(() => {
      mountComponent();
    });

    it('exists', () => {
      expect(findDeleteModal().exists()).toBe(true);
    });

    it('contains a description with the path of the item to delete', async () => {
      findImageList().vm.$emit('delete', { path: 'foo' });
      await nextTick();
      expect(findDeleteModal().html()).toContain('foo');
    });
  });

  describe('tracking', () => {
    beforeEach(() => {
      mountComponent();
    });

    const testTrackingCall = (action) => {
      expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
        label: 'registry_repository_delete',
      });
    };

    beforeEach(() => {
      jest.spyOn(Tracking, 'event');
    });

    it('send an event when delete button is clicked', () => {
      findImageList().vm.$emit('delete', {});

      testTrackingCall('click_button');
    });

    it('send an event when cancel is pressed on modal', () => {
      const deleteModal = findDeleteModal();
      deleteModal.vm.$emit('cancel');
      testTrackingCall('cancel_delete');
    });

    it('send an event when the deletion starts', () => {
      findDeleteImage().vm.$emit('start');
      testTrackingCall('confirm_delete');
    });
  });

  describe('url query string handling', () => {
    const defaultQueryParams = {
      search: [1, 2],
      sort: 'asc',
      orderBy: 'CREATED',
    };
    const queryChangePayload = 'foo';

    it('query:updated event pushes the new query to the router', async () => {
      const push = jest.fn();
      mountComponent({ mocks: { $router: { push } } });

      await nextTick();

      findRegistrySearch().vm.$emit('query:changed', queryChangePayload);

      expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
    });

    it('graphql API call has the variables set from the URL', async () => {
      const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
      mountComponent({ query: defaultQueryParams, resolver });

      await nextTick();

      expect(resolver).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 1,
          sort: 'CREATED_ASC',
        }),
      );
    });

    it.each`
      sort         | orderBy      | search            | payload
      ${'ASC'}     | ${undefined} | ${undefined}      | ${{ sort: 'UPDATED_ASC' }}
      ${undefined} | ${'bar'}     | ${undefined}      | ${{ sort: 'BAR_DESC' }}
      ${'ASC'}     | ${'bar'}     | ${undefined}      | ${{ sort: 'BAR_ASC' }}
      ${undefined} | ${undefined} | ${undefined}      | ${{}}
      ${undefined} | ${undefined} | ${['one']}        | ${{ name: 'one' }}
      ${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
      ${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
      ${'ASC'}     | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
    `(
      'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
      async ({ sort, orderBy, search, payload }) => {
        const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
        mountComponent({ query: { sort, orderBy, search }, resolver });

        await nextTick();

        expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
      },
    );
  });

  describe('cleanup is on alert', () => {
    it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
      mountComponent({
        config: {
          showCleanupPolicyOnAlert: true,
          projectPath: 'foo',
          isGroupPage: false,
          cleanupPoliciesSettingsPath: 'bar',
        },
      });

      await waitForApolloRequestRender();

      expect(findCleanupAlert().exists()).toBe(true);
      expect(findCleanupAlert().props()).toMatchObject({
        projectPath: 'foo',
        cleanupPoliciesSettingsPath: 'bar',
      });
    });

    it('is hidden when showCleanupPolicyOnAlert is false', async () => {
      mountComponent();

      await waitForApolloRequestRender();

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