import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { createStore } from '~/ide/stores';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
  getMergeRequestData,
  getMergeRequestChanges,
  getMergeRequestVersions,
  openMergeRequest,
} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';

const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;

jest.mock('~/flash');

describe('IDE store merge request actions', () => {
  let store;
  let mock;

  beforeEach(() => {
    store = createStore();

    mock = new MockAdapter(axios);

    store.state.projects[TEST_PROJECT] = {
      id: TEST_PROJECT_ID,
      mergeRequests: {},
      userPermissions: {
        [PERMISSION_READ_MR]: true,
      },
    };
  });

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

  describe('getMergeRequestsForBranch', () => {
    describe('success', () => {
      const mrData = { iid: 2, source_branch: 'bar' };
      const mockData = [mrData];

      describe('base case', () => {
        beforeEach(() => {
          jest.spyOn(service, 'getProjectMergeRequests');
          mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
        });

        it('calls getProjectMergeRequests service method', done => {
          store
            .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
            .then(() => {
              expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
                source_branch: 'bar',
                source_project_id: TEST_PROJECT_ID,
                state: 'opened',
                order_by: 'created_at',
                per_page: 1,
              });

              done();
            })
            .catch(done.fail);
        });

        it('sets the "Merge Request" Object', done => {
          store
            .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
            .then(() => {
              expect(store.state.projects.abcproject.mergeRequests).toEqual({
                '2': expect.objectContaining(mrData),
              });
              done();
            })
            .catch(done.fail);
        });

        it('sets "Current Merge Request" object to the most recent MR', done => {
          store
            .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
            .then(() => {
              expect(store.state.currentMergeRequestId).toEqual('2');
              done();
            })
            .catch(done.fail);
        });

        it('does nothing if user cannot read MRs', done => {
          store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false;

          store
            .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
            .then(() => {
              expect(service.getProjectMergeRequests).not.toHaveBeenCalled();
              expect(store.state.currentMergeRequestId).toBe('');
            })
            .then(done)
            .catch(done.fail);
        });
      });

      describe('no merge requests for branch available case', () => {
        beforeEach(() => {
          jest.spyOn(service, 'getProjectMergeRequests');
          mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
        });

        it('does not fail if there are no merge requests for current branch', done => {
          store
            .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' })
            .then(() => {
              expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
              expect(store.state.currentMergeRequestId).toEqual('');
              done();
            })
            .catch(done.fail);
        });
      });
    });

    describe('error', () => {
      beforeEach(() => {
        mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
      });

      it('flashes message, if error', done => {
        store
          .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
          .catch(() => {
            expect(createFlash).toHaveBeenCalled();
            expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar');
          })
          .then(done)
          .catch(done.fail);
      });
    });
  });

  describe('getMergeRequestData', () => {
    describe('success', () => {
      beforeEach(() => {
        jest.spyOn(service, 'getProjectMergeRequestData');

        mock
          .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
          .reply(200, { title: 'mergerequest' });
      });

      it('calls getProjectMergeRequestData service method', done => {
        store
          .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1);

            done();
          })
          .catch(done.fail);
      });

      it('sets the Merge Request Object', done => {
        store
          .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(store.state.currentMergeRequestId).toBe(1);
            expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe(
              'mergerequest',
            );

            done();
          })
          .catch(done.fail);
      });
    });

    describe('error', () => {
      beforeEach(() => {
        mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
      });

      it('dispatches error action', done => {
        const dispatch = jest.fn();

        getMergeRequestData(
          {
            commit() {},
            dispatch,
            state: store.state,
          },
          { projectId: TEST_PROJECT, mergeRequestId: 1 },
        )
          .then(done.fail)
          .catch(() => {
            expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
              text: 'An error occurred while loading the merge request.',
              action: expect.any(Function),
              actionText: 'Please try again',
              actionPayload: {
                projectId: TEST_PROJECT,
                mergeRequestId: 1,
                force: false,
              },
            });

            done();
          });
      });
    });
  });

  describe('getMergeRequestChanges', () => {
    beforeEach(() => {
      store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] };
    });

    describe('success', () => {
      beforeEach(() => {
        jest.spyOn(service, 'getProjectMergeRequestChanges');

        mock
          .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
          .reply(200, { title: 'mergerequest' });
      });

      it('calls getProjectMergeRequestChanges service method', done => {
        store
          .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);

            done();
          })
          .catch(done.fail);
      });

      it('sets the Merge Request Changes Object', done => {
        store
          .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
              'mergerequest',
            );
            done();
          })
          .catch(done.fail);
      });
    });

    describe('error', () => {
      beforeEach(() => {
        mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
      });

      it('dispatches error action', done => {
        const dispatch = jest.fn();

        getMergeRequestChanges(
          {
            commit() {},
            dispatch,
            state: store.state,
          },
          { projectId: TEST_PROJECT, mergeRequestId: 1 },
        )
          .then(done.fail)
          .catch(() => {
            expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
              text: 'An error occurred while loading the merge request changes.',
              action: expect.any(Function),
              actionText: 'Please try again',
              actionPayload: {
                projectId: TEST_PROJECT,
                mergeRequestId: 1,
                force: false,
              },
            });

            done();
          });
      });
    });
  });

  describe('getMergeRequestVersions', () => {
    beforeEach(() => {
      store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] };
    });

    describe('success', () => {
      beforeEach(() => {
        mock
          .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
          .reply(200, [{ id: 789 }]);
        jest.spyOn(service, 'getProjectMergeRequestVersions');
      });

      it('calls getProjectMergeRequestVersions service method', done => {
        store
          .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);

            done();
          })
          .catch(done.fail);
      });

      it('sets the Merge Request Versions Object', done => {
        store
          .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
          .then(() => {
            expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
            done();
          })
          .catch(done.fail);
      });
    });

    describe('error', () => {
      beforeEach(() => {
        mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
      });

      it('dispatches error action', done => {
        const dispatch = jest.fn();

        getMergeRequestVersions(
          {
            commit() {},
            dispatch,
            state: store.state,
          },
          { projectId: TEST_PROJECT, mergeRequestId: 1 },
        )
          .then(done.fail)
          .catch(() => {
            expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
              text: 'An error occurred while loading the merge request version data.',
              action: expect.any(Function),
              actionText: 'Please try again',
              actionPayload: {
                projectId: TEST_PROJECT,
                mergeRequestId: 1,
                force: false,
              },
            });

            done();
          });
      });
    });
  });

  describe('openMergeRequest', () => {
    const mr = {
      projectId: TEST_PROJECT,
      targetProjectId: 'defproject',
      mergeRequestId: 2,
    };
    let testMergeRequest;
    let testMergeRequestChanges;

    const mockGetters = { findBranch: () => ({ commit: { id: 'abcd2322' } }) };

    beforeEach(() => {
      testMergeRequest = {
        source_branch: 'abcbranch',
      };
      testMergeRequestChanges = {
        changes: [],
      };
      store.state.entries = {
        foo: {
          type: 'blob',
        },
        bar: {
          type: 'blob',
        },
      };

      store.state.currentProjectId = 'test/test';
      store.state.currentBranchId = 'master';

      store.state.projects['test/test'] = {
        branches: {
          master: {
            commit: {
              id: '7297abc',
            },
          },
          abcbranch: {
            commit: {
              id: '29020fc',
            },
          },
        },
      };

      const originalDispatch = store.dispatch;

      jest.spyOn(store, 'dispatch').mockImplementation((type, payload) => {
        switch (type) {
          case 'getMergeRequestData':
            return Promise.resolve(testMergeRequest);
          case 'getMergeRequestChanges':
            return Promise.resolve(testMergeRequestChanges);
          case 'getFiles':
          case 'getMergeRequestVersions':
          case 'getBranchData':
          case 'setFileMrChange':
            return Promise.resolve();
          default:
            return originalDispatch(type, payload);
        }
      });
      jest.spyOn(service, 'getFileData').mockImplementation(() =>
        Promise.resolve({
          headers: {},
        }),
      );
    });

    it('dispatches actions for merge request data', done => {
      openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
        .then(() => {
          expect(store.dispatch.mock.calls).toEqual([
            ['getMergeRequestData', mr],
            ['setCurrentBranchId', testMergeRequest.source_branch],
            [
              'getBranchData',
              {
                projectId: mr.projectId,
                branchId: testMergeRequest.source_branch,
              },
            ],
            [
              'getFiles',
              {
                projectId: mr.projectId,
                branchId: testMergeRequest.source_branch,
                ref: 'abcd2322',
              },
            ],
            ['getMergeRequestVersions', mr],
            ['getMergeRequestChanges', mr],
          ]);
        })
        .then(done)
        .catch(done.fail);
    });

    it('updates activity bar view and gets file data, if changes are found', done => {
      store.state.entries.foo = {
        type: 'blob',
      };
      store.state.entries.bar = {
        type: 'blob',
      };

      testMergeRequestChanges.changes = [
        { new_path: 'foo', path: 'foo' },
        { new_path: 'bar', path: 'bar' },
      ];

      openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
        .then(() => {
          expect(store.dispatch).toHaveBeenCalledWith(
            'updateActivityBarView',
            leftSidebarViews.review.name,
          );

          testMergeRequestChanges.changes.forEach((change, i) => {
            expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', {
              file: store.state.entries[change.new_path],
              mrChange: change,
            });

            expect(store.dispatch).toHaveBeenCalledWith('getFileData', {
              path: change.new_path,
              makeFileActive: i === 0,
              openFile: true,
            });
          });

          expect(store.state.openFiles.length).toBe(testMergeRequestChanges.changes.length);
        })
        .then(done)
        .catch(done.fail);
    });

    it('flashes message, if error', done => {
      store.dispatch.mockRejectedValue();

      openMergeRequest(store, mr)
        .catch(() => {
          expect(createFlash).toHaveBeenCalledWith(expect.any(String));
        })
        .then(done)
        .catch(done.fail);
    });
  });
});
