import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';

import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
  FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
  PACKAGE_TYPE_COMPOSER,
  DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
  DELETE_PACKAGE_FILE_ERROR_MESSAGE,
  PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';

import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import {
  packageDetailsQuery,
  packageData,
  packageVersions,
  dependencyLinks,
  emptyPackageDetailsQuery,
  packageFiles,
  packageDestroyFileMutation,
  packageDestroyFileMutationError,
} from '../../mock_data';

jest.mock('~/flash');
useMockLocationHelper();

const localVue = createLocalVue();

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

  const provide = {
    packageId: '111',
    titleComponent: 'PackageTitle',
    projectName: 'projectName',
    canDelete: 'canDelete',
    svgPath: 'svgPath',
    npmPath: 'npmPath',
    npmHelpPath: 'npmHelpPath',
    projectListUrl: 'projectListUrl',
    groupListUrl: 'groupListUrl',
  };

  function createComponent({
    resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
    fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
  } = {}) {
    localVue.use(VueApollo);

    const requestHandlers = [
      [getPackageDetails, resolver],
      [destroyPackageFileMutation, fileDeleteMutationResolver],
    ];
    apolloProvider = createMockApollo(requestHandlers);

    wrapper = shallowMountExtended(PackagesApp, {
      localVue,
      apolloProvider,
      provide,
      stubs: {
        PackageTitle,
        DeletePackage,
        GlModal: {
          template: '<div></div>',
          methods: {
            show: jest.fn(),
          },
        },
        GlTabs,
        GlTab,
      },
    });
  }

  const findEmptyState = () => wrapper.findComponent(GlEmptyState);
  const findPackageTitle = () => wrapper.findComponent(PackageTitle);
  const findPackageHistory = () => wrapper.findComponent(PackageHistory);
  const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata);
  const findInstallationCommands = () => wrapper.findComponent(InstallationCommands);
  const findDeleteModal = () => wrapper.findByTestId('delete-modal');
  const findDeleteButton = () => wrapper.findByTestId('delete-package');
  const findPackageFiles = () => wrapper.findComponent(PackageFiles);
  const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
  const findVersionRows = () => wrapper.findAllComponents(VersionRow);
  const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
  const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
  const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
  const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
  const findDeletePackage = () => wrapper.findComponent(DeletePackage);

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

  it('renders an empty state component', async () => {
    createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) });

    await waitForPromises();

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

  it('renders the app and displays the package title', async () => {
    createComponent();

    await waitForPromises();

    expect(findPackageTitle().exists()).toBe(true);
    expect(findPackageTitle().props()).toMatchObject({
      packageEntity: expect.objectContaining(packageData()),
    });
  });

  it('emits an error message if the load fails', async () => {
    createComponent({ resolver: jest.fn().mockRejectedValue() });

    await waitForPromises();

    expect(createFlash).toHaveBeenCalledWith(
      expect.objectContaining({
        message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
      }),
    );
  });

  it('renders history and has the right props', async () => {
    createComponent();

    await waitForPromises();

    expect(findPackageHistory().exists()).toBe(true);
    expect(findPackageHistory().props()).toMatchObject({
      packageEntity: expect.objectContaining(packageData()),
      projectName: provide.projectName,
    });
  });

  it('renders additional metadata and has the right props', async () => {
    createComponent();

    await waitForPromises();

    expect(findAdditionalMetadata().exists()).toBe(true);
    expect(findAdditionalMetadata().props()).toMatchObject({
      packageEntity: expect.objectContaining(packageData()),
    });
  });

  it('renders installation commands and has the right props', async () => {
    createComponent();

    await waitForPromises();

    expect(findInstallationCommands().exists()).toBe(true);
    expect(findInstallationCommands().props()).toMatchObject({
      packageEntity: expect.objectContaining(packageData()),
    });
  });

  describe('delete package', () => {
    const originalReferrer = document.referrer;
    const setReferrer = (value = provide.projectName) => {
      Object.defineProperty(document, 'referrer', {
        value,
        configurable: true,
      });
    };

    afterEach(() => {
      Object.defineProperty(document, 'referrer', {
        value: originalReferrer,
        configurable: true,
      });
    });

    it('shows the delete confirmation modal when delete is clicked', async () => {
      createComponent();

      await waitForPromises();

      await findDeleteButton().trigger('click');

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

    describe('successful request', () => {
      it('when referrer contains project name calls window.replace with project url', async () => {
        setReferrer();

        createComponent();

        await waitForPromises();

        findDeletePackage().vm.$emit('end');

        expect(window.location.replace).toHaveBeenCalledWith(
          'projectListUrl?showSuccessDeleteAlert=true',
        );
      });

      it('when referrer does not contain project name calls window.replace with group url', async () => {
        setReferrer('baz');

        createComponent();

        await waitForPromises();

        findDeletePackage().vm.$emit('end');

        expect(window.location.replace).toHaveBeenCalledWith(
          'groupListUrl?showSuccessDeleteAlert=true',
        );
      });
    });
  });

  describe('package files', () => {
    it('renders the package files component and has the right props', async () => {
      const expectedFile = { ...packageFiles()[0] };
      // eslint-disable-next-line no-underscore-dangle
      delete expectedFile.__typename;
      createComponent();

      await waitForPromises();

      expect(findPackageFiles().exists()).toBe(true);

      expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile);
    });

    it('does not render the package files table when the package is composer', async () => {
      createComponent({
        resolver: jest
          .fn()
          .mockResolvedValue(packageDetailsQuery({ packageType: PACKAGE_TYPE_COMPOSER })),
      });

      await waitForPromises();

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

    describe('deleting a file', () => {
      const [fileToDelete] = packageFiles();

      const doDeleteFile = () => {
        findPackageFiles().vm.$emit('delete-file', fileToDelete);

        findDeleteFileModal().vm.$emit('primary');

        return waitForPromises();
      };

      it('opens a confirmation modal', async () => {
        createComponent();

        await waitForPromises();

        findPackageFiles().vm.$emit('delete-file', fileToDelete);

        await nextTick();

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

      it('confirming on the modal deletes the file and shows a success message', async () => {
        const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
        createComponent({ resolver });

        await waitForPromises();

        await doDeleteFile();

        expect(createFlash).toHaveBeenCalledWith(
          expect.objectContaining({
            message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
          }),
        );
        // we are re-fetching the package details, so we expect the resolver to have been called twice
        expect(resolver).toHaveBeenCalledTimes(2);
      });

      describe('errors', () => {
        it('shows an error when the mutation request fails', async () => {
          createComponent({ fileDeleteMutationResolver: jest.fn().mockRejectedValue() });
          await waitForPromises();

          await doDeleteFile();

          expect(createFlash).toHaveBeenCalledWith(
            expect.objectContaining({
              message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
            }),
          );
        });

        it('shows an error when the mutation request returns an error payload', async () => {
          createComponent({
            fileDeleteMutationResolver: jest
              .fn()
              .mockResolvedValue(packageDestroyFileMutationError()),
          });
          await waitForPromises();

          await doDeleteFile();

          expect(createFlash).toHaveBeenCalledWith(
            expect.objectContaining({
              message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
            }),
          );
        });
      });
    });
  });

  describe('versions', () => {
    it('displays the correct version count when the package has versions', async () => {
      createComponent();

      await waitForPromises();

      expect(findVersionRows()).toHaveLength(packageVersions().length);
    });

    it('binds the correct props', async () => {
      const [versionPackage] = packageVersions();
      // eslint-disable-next-line no-underscore-dangle
      delete versionPackage.__typename;
      delete versionPackage.tags;

      createComponent();

      await waitForPromises();

      expect(findVersionRows().at(0).props()).toMatchObject({
        packageEntity: expect.objectContaining(versionPackage),
      });
    });

    it('displays the no versions message when there are none', async () => {
      createComponent({
        resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })),
      });

      await waitForPromises();

      expect(noVersionsMessage().exists()).toBe(true);
    });
  });
  describe('dependency links', () => {
    it('does not show the dependency links for a non nuget package', async () => {
      createComponent();

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

    it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => {
      createComponent({
        resolver: jest.fn().mockResolvedValue(
          packageDetailsQuery({
            packageType: PACKAGE_TYPE_NUGET,
            dependencyLinks: { nodes: [] },
          }),
        ),
      });

      await waitForPromises();

      expect(findDependenciesCountBadge().exists()).toBe(true);
      expect(findDependenciesCountBadge().text()).toBe('0');
      expect(findNoDependenciesMessage().exists()).toBe(true);
    });

    it('renders the correct number of dependency rows for a nuget package', async () => {
      createComponent({
        resolver: jest.fn().mockResolvedValue(
          packageDetailsQuery({
            packageType: PACKAGE_TYPE_NUGET,
          }),
        ),
      });
      await waitForPromises();

      expect(findDependenciesCountBadge().exists()).toBe(true);
      expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString());
      expect(findDependencyRows()).toHaveLength(dependencyLinks().length);
    });
  });
});
