import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';

import { GlLoadingIcon } from '@gitlab/ui';
import { joinPaths, redirectTo } from '~/lib/utils/url_utility';

import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';

import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';

import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { ApolloMutation } from 'vue-apollo';

jest.mock('~/lib/utils/url_utility', () => ({
  getBaseURL: jest.fn().mockReturnValue('foo/'),
  redirectTo: jest.fn().mockName('redirectTo'),
  joinPaths: jest
    .fn()
    .mockName('joinPaths')
    .mockReturnValue('contentApiURL'),
}));

jest.mock('~/flash');

let flashSpy;

const contentMock = 'Foo Bar';
const rawPathMock = '/foo/bar';
const rawProjectPathMock = '/project/path';
const newlyEditedSnippetUrl = 'http://foo.bar';
const apiError = { message: 'Ufff' };
const mutationError = 'Bummer';

const attachedFilePath1 = 'foo/bar';
const attachedFilePath2 = 'alpha/beta';

const defaultProps = {
  snippetGid: 'gid://gitlab/PersonalSnippet/42',
  markdownPreviewPath: 'http://preview.foo.bar',
  markdownDocsPath: 'http://docs.foo.bar',
};

describe('Snippet Edit app', () => {
  let wrapper;
  let axiosMock;

  const resolveMutate = jest.fn().mockResolvedValue({
    data: {
      updateSnippet: {
        errors: [],
        snippet: {
          webUrl: newlyEditedSnippetUrl,
        },
      },
    },
  });

  const resolveMutateWithErrors = jest.fn().mockResolvedValue({
    data: {
      updateSnippet: {
        errors: [mutationError],
        snippet: {
          webUrl: newlyEditedSnippetUrl,
        },
      },
      createSnippet: {
        errors: [mutationError],
        snippet: null,
      },
    },
  });

  const rejectMutation = jest.fn().mockRejectedValue(apiError);

  const mutationTypes = {
    RESOLVE: resolveMutate,
    RESOLVE_WITH_ERRORS: resolveMutateWithErrors,
    REJECT: rejectMutation,
  };

  function createComponent({
    props = defaultProps,
    data = {},
    loading = false,
    mutationRes = mutationTypes.RESOLVE,
  } = {}) {
    const $apollo = {
      queries: {
        snippet: {
          loading,
        },
      },
      mutate: mutationRes,
    };

    wrapper = shallowMount(SnippetEditApp, {
      mocks: { $apollo },
      stubs: {
        FormFooterActions,
        ApolloMutation,
      },
      propsData: {
        ...props,
      },
      data() {
        return data;
      },
    });

    flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
  }

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

  const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
  const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
  const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');

  describe('rendering', () => {
    it('renders loader while the query is in flight', () => {
      createComponent({ loading: true });
      expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
    });

    it('renders all required components', () => {
      createComponent();

      expect(wrapper.contains(TitleField)).toBe(true);
      expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
      expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
      expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
      expect(wrapper.contains(FormFooterActions)).toBe(true);
    });

    it('does not fail if there is no snippet yet (new snippet creation)', () => {
      const snippetGid = '';
      createComponent({
        props: {
          ...defaultProps,
          snippetGid,
        },
      });

      expect(wrapper.props('snippetGid')).toBe(snippetGid);
    });

    it.each`
      title    | content  | expectation
      ${''}    | ${''}    | ${true}
      ${'foo'} | ${''}    | ${true}
      ${''}    | ${'foo'} | ${true}
      ${'foo'} | ${'bar'} | ${false}
    `(
      'disables submit button unless both title and content are present',
      ({ title, content, expectation }) => {
        createComponent({
          data: {
            snippet: { title },
            content,
          },
        });
        const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
        expect(isBtnDisabled).toBe(expectation);
      },
    );

    it.each`
      isNew    | status        | expectation
      ${true}  | ${`new`}      | ${`/snippets`}
      ${false} | ${`existing`} | ${newlyEditedSnippetUrl}
    `('sets correct href for the cancel button on a $status snippet', ({ isNew, expectation }) => {
      createComponent({
        data: {
          snippet: { webUrl: newlyEditedSnippetUrl },
          newSnippet: isNew,
        },
      });

      expect(findCancellButton().attributes('href')).toBe(expectation);
    });
  });

  describe('functionality', () => {
    describe('handling of the data from GraphQL response', () => {
      const snippet = {
        blob: {
          rawPath: rawPathMock,
        },
      };
      const getResSchema = newSnippet => {
        return {
          data: {
            snippets: {
              edges: newSnippet ? [] : [snippet],
            },
          },
        };
      };

      const bootstrapForExistingSnippet = resp => {
        createComponent({
          data: {
            snippet,
          },
        });

        if (resp === 500) {
          axiosMock.onGet('contentApiURL').reply(500);
        } else {
          axiosMock.onGet('contentApiURL').reply(200, contentMock);
        }
        wrapper.vm.onSnippetFetch(getResSchema());
      };

      const bootstrapForNewSnippet = () => {
        createComponent();
        wrapper.vm.onSnippetFetch(getResSchema(true));
      };

      beforeEach(() => {
        axiosMock = new AxiosMockAdapter(axios);
      });

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

      it('fetches blob content with the additional query', () => {
        bootstrapForExistingSnippet();

        return waitForPromises().then(() => {
          expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock);
          expect(wrapper.vm.newSnippet).toBe(false);
          expect(wrapper.vm.content).toBe(contentMock);
        });
      });

      it('flashes the error message if fetching content fails', () => {
        bootstrapForExistingSnippet(500);

        return waitForPromises().then(() => {
          expect(flashSpy).toHaveBeenCalled();
          expect(wrapper.vm.content).toBe('');
        });
      });

      it('does not fetch content for new snippet', () => {
        bootstrapForNewSnippet();

        return waitForPromises().then(() => {
          // we keep using waitForPromises to make sure we do not run failed test
          expect(wrapper.vm.newSnippet).toBe(true);
          expect(wrapper.vm.content).toBe('');
          expect(joinPaths).not.toHaveBeenCalled();
          expect(wrapper.vm.snippet).toEqual(wrapper.vm.$options.newSnippetSchema);
        });
      });
    });

    describe('form submission handling', () => {
      it.each`
        newSnippet | projectPath           | mutation                 | mutationName
        ${true}    | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'}
        ${true}    | ${''}                 | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'}
        ${false}   | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'}
        ${false}   | ${''}                 | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'}
      `('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => {
        createComponent({
          data: {
            newSnippet,
          },
          props: {
            ...defaultProps,
            projectPath,
          },
        });

        const mutationPayload = {
          mutation,
          variables: {
            input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
          },
        };

        clickSubmitBtn();

        expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
      });

      it('redirects to snippet view on successful mutation', () => {
        createComponent();
        clickSubmitBtn();

        return waitForPromises().then(() => {
          expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
        });
      });

      it.each`
        newSnippet | projectPath           | mutationName
        ${true}    | ${rawProjectPathMock} | ${'CreateSnippetMutation with projectPath'}
        ${true}    | ${''}                 | ${'CreateSnippetMutation without projectPath'}
        ${false}   | ${rawProjectPathMock} | ${'UpdateSnippetMutation with projectPath'}
        ${false}   | ${''}                 | ${'UpdateSnippetMutation without projectPath'}
      `(
        'does not redirect to snippet view if the seemingly successful' +
          ' $mutationName response contains errors',
        ({ newSnippet, projectPath }) => {
          createComponent({
            data: {
              newSnippet,
            },
            props: {
              ...defaultProps,
              projectPath,
            },
            mutationRes: mutationTypes.RESOLVE_WITH_ERRORS,
          });

          clickSubmitBtn();

          return waitForPromises().then(() => {
            expect(redirectTo).not.toHaveBeenCalled();
            expect(flashSpy).toHaveBeenCalledWith(mutationError);
          });
        },
      );

      it('flashes an error if mutation failed', () => {
        createComponent({
          mutationRes: mutationTypes.REJECT,
        });

        clickSubmitBtn();

        return waitForPromises().then(() => {
          expect(redirectTo).not.toHaveBeenCalled();
          expect(flashSpy).toHaveBeenCalledWith(apiError);
        });
      });

      it.each`
        isNew    | status        | expectation
        ${true}  | ${`new`}      | ${SNIPPET_CREATE_MUTATION_ERROR.replace('%{err}', '')}
        ${false} | ${`existing`} | ${SNIPPET_UPDATE_MUTATION_ERROR.replace('%{err}', '')}
      `(
        `renders the correct error message if mutation fails for $status snippet`,
        ({ isNew, expectation }) => {
          createComponent({
            data: {
              newSnippet: isNew,
            },
            mutationRes: mutationTypes.REJECT,
          });

          clickSubmitBtn();

          return waitForPromises().then(() => {
            expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation));
          });
        },
      );
    });

    describe('correctly includes attached files into the mutation', () => {
      const createMutationPayload = expectation => {
        return expect.objectContaining({
          variables: {
            input: expect.objectContaining({ uploadedFiles: expectation }),
          },
        });
      };

      const updateMutationPayload = () => {
        return expect.objectContaining({
          variables: {
            input: expect.not.objectContaining({ uploadedFiles: expect.anything() }),
          },
        });
      };

      it.each`
        paths                                     | expectation
        ${[attachedFilePath1]}                    | ${[attachedFilePath1]}
        ${[attachedFilePath1, attachedFilePath2]} | ${[attachedFilePath1, attachedFilePath2]}
        ${[]}                                     | ${[]}
      `(`correctly sends paths for $paths.length files`, ({ paths, expectation }) => {
        createComponent({
          data: {
            newSnippet: true,
          },
        });

        const fixtures = paths.map(path => {
          return path ? `<input name="files[]" value="${path}">` : undefined;
        });
        wrapper.vm.$el.innerHTML += fixtures.join('');

        clickSubmitBtn();

        expect(resolveMutate).toHaveBeenCalledWith(createMutationPayload(expectation));
      });

      it(`neither fails nor sends 'uploadedFiles' to update mutation`, () => {
        createComponent();

        clickSubmitBtn();
        expect(resolveMutate).toHaveBeenCalledWith(updateMutationPayload());
      });
    });
  });
});