import { InMemoryCache } from 'apollo-cache-inmemory';
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import {
  clientTypenames,
  createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/group_and_project.query.graphql';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';

import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';

jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
  StatusPoller: jest.fn().mockImplementation(function mock() {
    this.startPolling = jest.fn();
  }),
}));

const FAKE_ENDPOINTS = {
  status: '/fake_status_url',
  availableNamespaces: '/fake_available_namespaces',
  createBulkImport: '/fake_create_bulk_import',
  jobs: '/fake_jobs',
};

const FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER = jest.fn().mockResolvedValue({
  data: {
    existingGroup: null,
    existingProject: null,
  },
});

describe('Bulk import resolvers', () => {
  let axiosMockAdapter;
  let client;

  const createClient = (extraResolverArgs) => {
    const mockedClient = createMockClient({
      cache: new InMemoryCache({
        fragmentMatcher: { match: () => true },
        addTypename: false,
      }),
      resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
    });

    mockedClient.setRequestHandler(groupAndProjectQuery, FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER);

    return mockedClient;
  };

  beforeEach(() => {
    axiosMockAdapter = new MockAdapter(axios);
    client = createClient();
  });

  afterEach(() => {
    axiosMockAdapter.restore();
  });

  describe('queries', () => {
    describe('availableNamespaces', () => {
      let results;

      beforeEach(async () => {
        axiosMockAdapter
          .onGet(FAKE_ENDPOINTS.availableNamespaces)
          .reply(httpStatus.OK, availableNamespacesFixture);

        const response = await client.query({ query: availableNamespacesQuery });
        results = response.data.availableNamespaces;
      });

      it('mirrors REST endpoint response fields', () => {
        const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path });

        expect(results.map(extractRelevantFields)).toStrictEqual(
          availableNamespacesFixture.map(extractRelevantFields),
        );
      });
    });

    describe('bulkImportSourceGroup', () => {
      beforeEach(async () => {
        axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
        axiosMockAdapter
          .onGet(FAKE_ENDPOINTS.availableNamespaces)
          .reply(httpStatus.OK, availableNamespacesFixture);

        return client.query({
          query: bulkImportSourceGroupsQuery,
        });
      });

      it('returns group', async () => {
        const { id } = statusEndpointFixture.importable_data[0];
        const {
          data: { bulkImportSourceGroup: group },
        } = await client.query({
          query: bulkImportSourceGroupQuery,
          variables: { id: id.toString() },
        });

        expect(group).toMatchObject(statusEndpointFixture.importable_data[0]);
      });
    });

    describe('bulkImportSourceGroups', () => {
      let results;

      beforeEach(async () => {
        axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
        axiosMockAdapter
          .onGet(FAKE_ENDPOINTS.availableNamespaces)
          .reply(httpStatus.OK, availableNamespacesFixture);
      });

      it('respects cached import state when provided by group manager', async () => {
        const FAKE_JOB_ID = '1';
        const FAKE_STATUS = 'DEMO_STATUS';
        const FAKE_IMPORT_TARGET = {
          new_name: 'test-name',
          target_namespace: 'test-namespace',
        };
        const TARGET_INDEX = 0;

        const clientWithMockedManager = createClient({
          GroupsManager: jest.fn().mockImplementation(() => ({
            getImportStateFromStorageByGroupId(groupId) {
              if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
                return {
                  jobId: FAKE_JOB_ID,
                  importState: {
                    status: FAKE_STATUS,
                    importTarget: FAKE_IMPORT_TARGET,
                  },
                };
              }

              return null;
            },
          })),
        });

        const clientResponse = await clientWithMockedManager.query({
          query: bulkImportSourceGroupsQuery,
        });
        const clientResults = clientResponse.data.bulkImportSourceGroups.nodes;

        expect(clientResults[TARGET_INDEX].import_target).toStrictEqual(FAKE_IMPORT_TARGET);
        expect(clientResults[TARGET_INDEX].progress.status).toBe(FAKE_STATUS);
      });

      it('populates each result instance with empty import_target when there are no available namespaces', async () => {
        axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(httpStatus.OK, []);

        const response = await client.query({ query: bulkImportSourceGroupsQuery });
        results = response.data.bulkImportSourceGroups.nodes;

        expect(results.every((r) => r.import_target.target_namespace === '')).toBe(true);
      });

      describe('when called', () => {
        beforeEach(async () => {
          const response = await client.query({ query: bulkImportSourceGroupsQuery });
          results = response.data.bulkImportSourceGroups.nodes;
        });

        it('mirrors REST endpoint response fields', () => {
          const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
          expect(
            results.every((r, idx) =>
              MIRRORED_FIELDS.every(
                (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
              ),
            ),
          ).toBe(true);
        });

        it('populates each result instance with status default to none', () => {
          expect(results.every((r) => r.progress.status === STATUSES.NONE)).toBe(true);
        });

        it('populates each result instance with import_target defaulted to first available namespace', () => {
          expect(
            results.every(
              (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
            ),
          ).toBe(true);
        });

        it('starts polling when request completes', async () => {
          const [statusPoller] = StatusPoller.mock.instances;
          expect(statusPoller.startPolling).toHaveBeenCalled();
        });

        it('requests validation status when request completes', async () => {
          expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).not.toHaveBeenCalled();
          jest.runOnlyPendingTimers();
          expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalled();
        });
      });

      it.each`
        variable     | queryParam    | value
        ${'filter'}  | ${'filter'}   | ${'demo'}
        ${'perPage'} | ${'per_page'} | ${30}
        ${'page'}    | ${'page'}     | ${3}
      `(
        'properly passes GraphQL variable $variable as REST $queryParam query parameter',
        async ({ variable, queryParam, value }) => {
          await client.query({
            query: bulkImportSourceGroupsQuery,
            variables: { [variable]: value },
          });
          const restCall = axiosMockAdapter.history.get.find(
            (q) => q.url === FAKE_ENDPOINTS.status,
          );
          expect(restCall.params[queryParam]).toBe(value);
        },
      );
    });
  });

  describe('mutations', () => {
    const GROUP_ID = 1;

    beforeEach(() => {
      client.writeQuery({
        query: bulkImportSourceGroupsQuery,
        data: {
          bulkImportSourceGroups: {
            nodes: [
              {
                __typename: clientTypenames.BulkImportSourceGroup,
                id: GROUP_ID,
                progress: {
                  id: `test-${GROUP_ID}`,
                  status: STATUSES.NONE,
                },
                web_url: 'https://fake.host/1',
                full_path: 'fake_group_1',
                full_name: 'fake_name_1',
                import_target: {
                  target_namespace: 'root',
                  new_name: 'group1',
                },
                last_import_target: {
                  target_namespace: 'root',
                  new_name: 'group1',
                },
                validation_errors: [],
              },
            ],
            pageInfo: {
              page: 1,
              perPage: 20,
              total: 37,
              totalPages: 2,
            },
          },
        },
      });
    });

    describe('setImportTarget', () => {
      it('updates group target namespace and name', async () => {
        const NEW_TARGET_NAMESPACE = 'target';
        const NEW_NAME = 'new';

        const {
          data: {
            setImportTarget: {
              id: idInResponse,
              import_target: { target_namespace: namespaceInResponse, new_name: newNameInResponse },
            },
          },
        } = await client.mutate({
          mutation: setImportTargetMutation,
          variables: {
            sourceGroupId: GROUP_ID,
            targetNamespace: NEW_TARGET_NAMESPACE,
            newName: NEW_NAME,
          },
        });

        expect(idInResponse).toBe(GROUP_ID);
        expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
        expect(newNameInResponse).toBe(NEW_NAME);
      });

      it('invokes validation', async () => {
        const NEW_TARGET_NAMESPACE = 'target';
        const NEW_NAME = 'new';

        await client.mutate({
          mutation: setImportTargetMutation,
          variables: {
            sourceGroupId: GROUP_ID,
            targetNamespace: NEW_TARGET_NAMESPACE,
            newName: NEW_NAME,
          },
        });

        expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalledWith({
          fullPath: `${NEW_TARGET_NAMESPACE}/${NEW_NAME}`,
        });
      });
    });

    describe('importGroup', () => {
      it('sets status to SCHEDULING when request initiates', async () => {
        axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));

        client.mutate({
          mutation: importGroupsMutation,
          variables: { sourceGroupIds: [GROUP_ID] },
        });
        await waitForPromises();

        const {
          bulkImportSourceGroups: { nodes: intermediateResults },
        } = client.readQuery({
          query: bulkImportSourceGroupsQuery,
        });

        expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING);
      });

      describe('when request completes', () => {
        let results;

        beforeEach(() => {
          client
            .watchQuery({
              query: bulkImportSourceGroupsQuery,
              fetchPolicy: 'cache-only',
            })
            .subscribe(({ data }) => {
              results = data.bulkImportSourceGroups.nodes;
            });
        });

        it('sets import status to CREATED when request completes', async () => {
          axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
          await client.mutate({
            mutation: importGroupsMutation,
            variables: { sourceGroupIds: [GROUP_ID] },
          });
          await waitForPromises();

          expect(results[0].progress.status).toBe(STATUSES.CREATED);
        });

        it('resets status to NONE if request fails', async () => {
          axiosMockAdapter
            .onPost(FAKE_ENDPOINTS.createBulkImport)
            .reply(httpStatus.INTERNAL_SERVER_ERROR);

          client
            .mutate({
              mutation: [importGroupsMutation],
              variables: { sourceGroupIds: [GROUP_ID] },
            })
            .catch(() => {});
          await waitForPromises();

          expect(results[0].progress.status).toBe(STATUSES.NONE);
        });
      });

      it('shows default error message when server error is not provided', async () => {
        axiosMockAdapter
          .onPost(FAKE_ENDPOINTS.createBulkImport)
          .reply(httpStatus.INTERNAL_SERVER_ERROR);

        client
          .mutate({
            mutation: importGroupsMutation,
            variables: { sourceGroupIds: [GROUP_ID] },
          })
          .catch(() => {});
        await waitForPromises();

        expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
      });

      it('shows provided error message when error is included in backend response', async () => {
        const CUSTOM_MESSAGE = 'custom message';

        axiosMockAdapter
          .onPost(FAKE_ENDPOINTS.createBulkImport)
          .reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });

        client
          .mutate({
            mutation: importGroupsMutation,
            variables: { sourceGroupIds: [GROUP_ID] },
          })
          .catch(() => {});
        await waitForPromises();

        expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
      });
    });

    it('setImportProgress updates group progress and sets import target', async () => {
      const NEW_STATUS = 'dummy';
      const FAKE_JOB_ID = 5;
      const IMPORT_TARGET = {
        __typename: 'ClientBulkImportTarget',
        new_name: 'fake_name',
        target_namespace: 'fake_target',
      };
      const {
        data: {
          setImportProgress: { progress, last_import_target: lastImportTarget },
        },
      } = await client.mutate({
        mutation: setImportProgressMutation,
        variables: {
          sourceGroupId: GROUP_ID,
          status: NEW_STATUS,
          jobId: FAKE_JOB_ID,
          importTarget: IMPORT_TARGET,
        },
      });

      expect(lastImportTarget).toStrictEqual(IMPORT_TARGET);

      expect(progress).toStrictEqual({
        __typename: clientTypenames.BulkImportProgress,
        id: FAKE_JOB_ID,
        status: NEW_STATUS,
      });
    });

    it('updateImportStatus returns new status', async () => {
      const NEW_STATUS = 'dummy';
      const FAKE_JOB_ID = 5;
      const {
        data: { updateImportStatus: statusInResponse },
      } = await client.mutate({
        mutation: updateImportStatusMutation,
        variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
      });

      expect(statusInResponse).toStrictEqual({
        __typename: clientTypenames.BulkImportProgress,
        id: FAKE_JOB_ID,
        status: NEW_STATUS,
      });
    });

    it('addValidationError adds error to group', async () => {
      const FAKE_FIELD = 'some-field';
      const FAKE_MESSAGE = 'some-message';
      const {
        data: {
          addValidationError: { validation_errors: validationErrors },
        },
      } = await client.mutate({
        mutation: addValidationErrorMutation,
        variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
      });

      expect(validationErrors).toStrictEqual([
        {
          __typename: clientTypenames.BulkImportValidationError,
          field: FAKE_FIELD,
          message: FAKE_MESSAGE,
        },
      ]);
    });

    it('removeValidationError removes error from group', async () => {
      const FAKE_FIELD = 'some-field';
      const FAKE_MESSAGE = 'some-message';

      await client.mutate({
        mutation: addValidationErrorMutation,
        variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
      });

      const {
        data: {
          removeValidationError: { validation_errors: validationErrors },
        },
      } = await client.mutate({
        mutation: removeValidationErrorMutation,
        variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
      });

      expect(validationErrors).toStrictEqual([]);
    });
  });
});
