import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
import deleteReleaseAssetLinkMutation from '~/releases/graphql/mutations/delete_release_link.mutation.graphql';
import updateReleaseMutation from '~/releases/graphql/mutations/update_release.mutation.graphql';
import * as actions from '~/releases/stores/modules/edit_new/actions';
import * as types from '~/releases/stores/modules/edit_new/mutation_types';
import createState from '~/releases/stores/modules/edit_new/state';
import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';

jest.mock('~/flash');

jest.mock('~/lib/utils/url_utility', () => ({
  redirectTo: jest.fn(),
  joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));

jest.mock('~/releases/util', () => ({
  ...jest.requireActual('~/releases/util'),
  gqClient: {
    query: jest.fn(),
    mutate: jest.fn(),
  },
}));

describe('Release edit/new actions', () => {
  let state;
  let releaseResponse;
  let error;

  const setupState = (updates = {}) => {
    const getters = {
      isExistingRelease: true,
    };

    state = {
      ...createState({
        projectId: '18',
        tagName: releaseResponse.tag_name,
        releasesPagePath: 'path/to/releases/page',
        markdownDocsPath: 'path/to/markdown/docs',
        markdownPreviewPath: 'path/to/markdown/preview',
      }),
      ...getters,
      ...updates,
    };
  };

  beforeEach(() => {
    releaseResponse = cloneDeep(originalOneReleaseForEditingQueryResponse);
    gon.api_version = 'v4';
    error = new Error('Yikes!');
    createFlash.mockClear();
  });

  describe('when creating a new release', () => {
    beforeEach(() => {
      setupState({ isExistingRelease: false });
    });

    describe('initializeRelease', () => {
      it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
        testAction(actions.initializeRelease, undefined, state, [
          { type: types.INITIALIZE_EMPTY_RELEASE },
        ]);
      });
    });

    describe('saveRelease', () => {
      it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => {
        testAction(
          actions.saveRelease,
          undefined,
          state,
          [{ type: types.REQUEST_SAVE_RELEASE }],
          [{ type: 'createRelease' }],
        );
      });
    });
  });

  describe('when editing an existing release', () => {
    beforeEach(setupState);

    describe('initializeRelease', () => {
      it('dispatches "fetchRelease"', () => {
        testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]);
      });
    });

    describe('saveRelease', () => {
      it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => {
        testAction(
          actions.saveRelease,
          undefined,
          state,
          [{ type: types.REQUEST_SAVE_RELEASE }],
          [{ type: 'updateRelease' }],
        );
      });
    });
  });

  describe('actions that behave the same whether creating a new release or editing an existing release', () => {
    beforeEach(setupState);

    describe('fetchRelease', () => {
      describe('when the network request to the Release API is successful', () => {
        beforeEach(() => {
          gqClient.query.mockResolvedValue(releaseResponse);
        });

        it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => {
          return testAction(actions.fetchRelease, undefined, state, [
            {
              type: types.REQUEST_RELEASE,
            },
            {
              type: types.RECEIVE_RELEASE_SUCCESS,
              payload: convertOneReleaseGraphQLResponse(releaseResponse).data,
            },
          ]);
        });
      });

      describe('when the GraphQL network request fails', () => {
        beforeEach(() => {
          gqClient.query.mockRejectedValue(error);
        });

        it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => {
          return testAction(actions.fetchRelease, undefined, state, [
            {
              type: types.REQUEST_RELEASE,
            },
            {
              type: types.RECEIVE_RELEASE_ERROR,
              payload: expect.any(Error),
            },
          ]);
        });

        it(`shows a flash message`, () => {
          return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
            expect(createFlash).toHaveBeenCalledTimes(1);
            expect(createFlash).toHaveBeenCalledWith({
              message: 'Something went wrong while getting the release details.',
            });
          });
        });
      });
    });

    describe('updateReleaseTagName', () => {
      it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
        const newTag = 'updated-tag-name';
        return testAction(actions.updateReleaseTagName, newTag, state, [
          { type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
        ]);
      });
    });

    describe('updateCreateFrom', () => {
      it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
        const newRef = 'my-feature-branch';
        return testAction(actions.updateCreateFrom, newRef, state, [
          { type: types.UPDATE_CREATE_FROM, payload: newRef },
        ]);
      });
    });

    describe('updateReleaseTitle', () => {
      it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
        const newTitle = 'The new release title';
        return testAction(actions.updateReleaseTitle, newTitle, state, [
          { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
        ]);
      });
    });

    describe('updateReleaseNotes', () => {
      it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
        const newReleaseNotes = 'The new release notes';
        return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
          { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
        ]);
      });
    });

    describe('updateReleaseMilestones', () => {
      it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
        const newReleaseMilestones = ['v0.0', 'v0.1'];
        return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
          { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
        ]);
      });
    });

    describe('updateReleaseGroupMilestones', () => {
      it(`commits ${types.UPDATE_RELEASE_GROUP_MILESTONES} with the updated release group milestones`, () => {
        const newReleaseGroupMilestones = ['v0.0', 'v0.1'];
        return testAction(actions.updateReleaseGroupMilestones, newReleaseGroupMilestones, state, [
          { type: types.UPDATE_RELEASE_GROUP_MILESTONES, payload: newReleaseGroupMilestones },
        ]);
      });
    });

    describe('addEmptyAssetLink', () => {
      it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => {
        return testAction(actions.addEmptyAssetLink, undefined, state, [
          { type: types.ADD_EMPTY_ASSET_LINK },
        ]);
      });
    });

    describe('updateAssetLinkUrl', () => {
      it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
        const params = {
          linkIdToUpdate: 2,
          newUrl: 'https://example.com/updated',
        };

        return testAction(actions.updateAssetLinkUrl, params, state, [
          { type: types.UPDATE_ASSET_LINK_URL, payload: params },
        ]);
      });
    });

    describe('updateAssetLinkName', () => {
      it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
        const params = {
          linkIdToUpdate: 2,
          newName: 'Updated link name',
        };

        return testAction(actions.updateAssetLinkName, params, state, [
          { type: types.UPDATE_ASSET_LINK_NAME, payload: params },
        ]);
      });
    });

    describe('updateAssetLinkType', () => {
      it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
        const params = {
          linkIdToUpdate: 2,
          newType: ASSET_LINK_TYPE.RUNBOOK,
        };

        return testAction(actions.updateAssetLinkType, params, state, [
          { type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
        ]);
      });
    });

    describe('removeAssetLink', () => {
      it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
        const idToRemove = 2;
        return testAction(actions.removeAssetLink, idToRemove, state, [
          { type: types.REMOVE_ASSET_LINK, payload: idToRemove },
        ]);
      });
    });

    describe('receiveSaveReleaseSuccess', () => {
      it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () =>
        testAction(actions.receiveSaveReleaseSuccess, releaseResponse, state, [
          { type: types.RECEIVE_SAVE_RELEASE_SUCCESS },
        ]));

      it("redirects to the release's dedicated page", () => {
        const { selfUrl } = releaseResponse.data.project.release.links;
        actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state }, selfUrl);
        expect(redirectTo).toHaveBeenCalledTimes(1);
        expect(redirectTo).toHaveBeenCalledWith(selfUrl);
      });
    });

    describe('createRelease', () => {
      let releaseLinksToCreate;

      beforeEach(() => {
        const { data: release } = convertOneReleaseGraphQLResponse(
          originalOneReleaseForEditingQueryResponse,
        );

        releaseLinksToCreate = release.assets.links.slice(0, 1);

        setupState({
          release,
          releaseLinksToCreate,
        });
      });

      describe('when the GraphQL request is successful', () => {
        const selfUrl = 'url/to/self';

        beforeEach(() => {
          gqClient.mutate.mockResolvedValue({
            data: {
              releaseCreate: {
                release: {
                  links: {
                    selfUrl,
                  },
                },
                errors: [],
              },
            },
          });
        });

        it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => {
          return testAction(
            actions.createRelease,
            undefined,
            state,
            [],
            [
              {
                type: 'receiveSaveReleaseSuccess',
                payload: selfUrl,
              },
            ],
          );
        });
      });

      describe('when the GraphQL network request fails', () => {
        beforeEach(() => {
          gqClient.mutate.mockRejectedValue(error);
        });

        it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
          return testAction(actions.createRelease, undefined, state, [
            {
              type: types.RECEIVE_SAVE_RELEASE_ERROR,
              payload: expect.any(Error),
            },
          ]);
        });

        it(`shows a flash message`, () => {
          return actions
            .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
            .then(() => {
              expect(createFlash).toHaveBeenCalledTimes(1);
              expect(createFlash).toHaveBeenCalledWith({
                message: 'Something went wrong while creating a new release.',
              });
            });
        });
      });
    });

    describe('updateRelease', () => {
      let getters;
      let dispatch;
      let commit;
      let release;

      beforeEach(() => {
        getters = {
          releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
          releaseLinksToCreate: [
            { id: 'new-link-1', name: 'Link 1', url: 'https://example.com/1', linkType: 'Other' },
            { id: 'new-link-2', name: 'Link 2', url: 'https://example.com/2', linkType: 'Package' },
          ],
          releaseUpdateMutatationVariables: {},
        };

        release = convertOneReleaseGraphQLResponse(releaseResponse).data;

        setupState({
          release,
          ...getters,
        });

        dispatch = jest.fn();
        commit = jest.fn();

        gqClient.mutate.mockResolvedValue({
          data: {
            releaseUpdate: {
              errors: [],
            },
            releaseAssetLinkDelete: {
              errors: [],
            },
            releaseAssetLinkCreate: {
              errors: [],
            },
          },
        });
      });

      describe('when the network request to the Release API is successful', () => {
        it('dispatches receiveSaveReleaseSuccess', async () => {
          await actions.updateRelease({ commit, dispatch, state, getters });
          expect(dispatch.mock.calls).toEqual([['receiveSaveReleaseSuccess', release._links.self]]);
        });

        it('updates the Release, then deletes all existing links, and then recreates new links', async () => {
          await actions.updateRelease({ commit, dispatch, state, getters });

          // First, update the release
          expect(gqClient.mutate.mock.calls[0]).toEqual([
            {
              mutation: updateReleaseMutation,
              variables: getters.releaseUpdateMutatationVariables,
            },
          ]);

          // Then, delete the first asset link
          expect(gqClient.mutate.mock.calls[1]).toEqual([
            {
              mutation: deleteReleaseAssetLinkMutation,
              variables: { input: { id: getters.releaseLinksToDelete[0].id } },
            },
          ]);

          // And the second
          expect(gqClient.mutate.mock.calls[2]).toEqual([
            {
              mutation: deleteReleaseAssetLinkMutation,
              variables: { input: { id: getters.releaseLinksToDelete[1].id } },
            },
          ]);

          // Recreate the first asset link
          expect(gqClient.mutate.mock.calls[3]).toEqual([
            {
              mutation: createReleaseAssetLinkMutation,
              variables: {
                input: {
                  projectPath: state.projectPath,
                  tagName: state.tagName,
                  name: getters.releaseLinksToCreate[0].name,
                  url: getters.releaseLinksToCreate[0].url,
                  linkType: getters.releaseLinksToCreate[0].linkType.toUpperCase(),
                },
              },
            },
          ]);

          // And finally, recreate the second
          expect(gqClient.mutate.mock.calls[4]).toEqual([
            {
              mutation: createReleaseAssetLinkMutation,
              variables: {
                input: {
                  projectPath: state.projectPath,
                  tagName: state.tagName,
                  name: getters.releaseLinksToCreate[1].name,
                  url: getters.releaseLinksToCreate[1].url,
                  linkType: getters.releaseLinksToCreate[1].linkType.toUpperCase(),
                },
              },
            },
          ]);
        });
      });

      describe('when the GraphQL network request fails', () => {
        beforeEach(() => {
          gqClient.mutate.mockRejectedValue(error);
        });

        it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', async () => {
          await actions.updateRelease({ commit, dispatch, state, getters });

          expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
        });

        it('shows a flash message', async () => {
          await actions.updateRelease({ commit, dispatch, state, getters });

          expect(createFlash).toHaveBeenCalledTimes(1);
          expect(createFlash).toHaveBeenCalledWith({
            message: 'Something went wrong while saving the release details.',
          });
        });
      });

      describe('when the GraphQL mutation returns errors-as-data', () => {
        const expectCorrectErrorHandling = () => {
          it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', async () => {
            await actions.updateRelease({ commit, dispatch, state, getters });

            expect(commit.mock.calls).toEqual([
              [types.RECEIVE_SAVE_RELEASE_ERROR, expect.any(Error)],
            ]);
          });

          it('shows a flash message', async () => {
            await actions.updateRelease({ commit, dispatch, state, getters });

            expect(createFlash).toHaveBeenCalledTimes(1);
            expect(createFlash).toHaveBeenCalledWith({
              message: 'Something went wrong while saving the release details.',
            });
          });
        };

        describe('when the releaseUpdate mutation returns errors-as-data', () => {
          beforeEach(() => {
            gqClient.mutate.mockResolvedValue({
              data: {
                releaseUpdate: {
                  errors: ['Something went wrong!'],
                },
                releaseAssetLinkDelete: {
                  errors: [],
                },
                releaseAssetLinkCreate: {
                  errors: [],
                },
              },
            });
          });

          expectCorrectErrorHandling();
        });

        describe('when the releaseAssetLinkDelete mutation returns errors-as-data', () => {
          beforeEach(() => {
            gqClient.mutate.mockResolvedValue({
              data: {
                releaseUpdate: {
                  errors: [],
                },
                releaseAssetLinkDelete: {
                  errors: ['Something went wrong!'],
                },
                releaseAssetLinkCreate: {
                  errors: [],
                },
              },
            });
          });

          expectCorrectErrorHandling();
        });

        describe('when the releaseAssetLinkCreate mutation returns errors-as-data', () => {
          beforeEach(() => {
            gqClient.mutate.mockResolvedValue({
              data: {
                releaseUpdate: {
                  errors: [],
                },
                releaseAssetLinkDelete: {
                  errors: [],
                },
                releaseAssetLinkCreate: {
                  errors: ['Something went wrong!'],
                },
              },
            });
          });

          expectCorrectErrorHandling();
        });
      });
    });
  });
});
