import { GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
import {
  simpleViewerMock,
  richViewerMock,
  projectMock,
  userPermissionsMock,
  propsMock,
  refMock,
} from '../mock_data';

jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/common_utils');

let wrapper;
let mockResolver;

const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);

const createComponent = async (mockData = {}, mountFn = shallowMount) => {
  localVue.use(VueApollo);

  const {
    blob = simpleViewerMock,
    empty = projectMock.repository.empty,
    pushCode = userPermissionsMock.pushCode,
    forkProject = userPermissionsMock.forkProject,
    downloadCode = userPermissionsMock.downloadCode,
    createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
    isBinary,
    inject = {},
  } = mockData;

  const project = {
    ...projectMock,
    userPermissions: {
      pushCode,
      forkProject,
      downloadCode,
      createMergeRequestIn,
    },
    repository: {
      empty,
      blobs: { nodes: [blob] },
    },
  };

  mockResolver = jest.fn().mockResolvedValue({
    data: { isBinary, project },
  });

  const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);

  wrapper = mountFn(BlobContentViewer, {
    localVue,
    apolloProvider: fakeApollo,
    propsData: propsMock,
    mixins: [{ data: () => ({ ref: refMock }) }],
    provide: { ...inject },
  });

  wrapper.setData({ project, isBinary });

  await waitForPromises();
};

describe('Blob content viewer component', () => {
  const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
  const findBlobHeader = () => wrapper.findComponent(BlobHeader);
  const findBlobEdit = () => wrapper.findComponent(BlobEdit);
  const findBlobContent = () => wrapper.findComponent(BlobContent);
  const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
  const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);

  beforeEach(() => {
    gon.features = { refactorTextViewer: true };
    isLoggedIn.mockReturnValue(true);
  });

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

  it('renders a GlLoadingIcon component', () => {
    createComponent();

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

  describe('simple viewer', () => {
    it('renders a BlobHeader component', async () => {
      await createComponent();

      expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
      expect(findBlobHeader().props('hasRenderError')).toEqual(false);
      expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
      expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
    });

    it('renders a BlobContent component', async () => {
      await createComponent();

      expect(findBlobContent().props('isRawContent')).toBe(true);
      expect(findBlobContent().props('activeViewer')).toEqual({
        fileType: 'text',
        tooLarge: false,
        type: 'simple',
        renderError: null,
      });
    });

    describe('legacy viewers', () => {
      it('loads a legacy viewer when a viewer component is not available', async () => {
        await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });

        expect(mockAxios.history.get).toHaveLength(1);
        expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
      });
    });
  });

  describe('rich viewer', () => {
    it('renders a BlobHeader component', async () => {
      await createComponent({ blob: richViewerMock });

      expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
      expect(findBlobHeader().props('hasRenderError')).toEqual(false);
      expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
      expect(findBlobHeader().props('blob')).toEqual(richViewerMock);
    });

    it('renders a BlobContent component', async () => {
      await createComponent({ blob: richViewerMock });

      expect(findBlobContent().props('isRawContent')).toBe(true);
      expect(findBlobContent().props('activeViewer')).toEqual({
        fileType: 'markup',
        tooLarge: false,
        type: 'rich',
        renderError: null,
      });
    });

    it('updates viewer type when viewer changed is clicked', async () => {
      await createComponent({ blob: richViewerMock });

      expect(findBlobContent().props('activeViewer')).toEqual(
        expect.objectContaining({
          type: 'rich',
        }),
      );
      expect(findBlobHeader().props('activeViewerType')).toEqual('rich');

      findBlobHeader().vm.$emit('viewer-changed', 'simple');
      await nextTick();

      expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
      expect(findBlobContent().props('activeViewer')).toEqual(
        expect.objectContaining({
          type: 'simple',
        }),
      );
    });
  });

  describe('legacy viewers', () => {
    it('loads a legacy viewer when a viewer component is not available', async () => {
      await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });

      expect(mockAxios.history.get).toHaveLength(1);
      expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
    });
  });

  describe('Blob viewer', () => {
    afterEach(() => {
      loadViewer.mockRestore();
      viewerProps.mockRestore();
    });

    it('does not render a BlobContent component if a Blob viewer is available', async () => {
      loadViewer.mockReturnValue(() => true);
      await createComponent({ blob: richViewerMock });

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

    it.each`
      viewer        | loadViewerReturnValue | viewerPropsReturnValue
      ${'empty'}    | ${EmptyViewer}        | ${{}}
      ${'download'} | ${DownloadViewer}     | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }}
      ${'text'}     | ${TextViewer}         | ${{ content: 'test', fileName: 'test.js', readOnly: true }}
    `(
      'renders viewer component for $viewer files',
      async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => {
        loadViewer.mockReturnValue(loadViewerReturnValue);
        viewerProps.mockReturnValue(viewerPropsReturnValue);

        createComponent({
          blob: {
            ...simpleViewerMock,
            fileType: 'null',
            simpleViewer: {
              ...simpleViewerMock.simpleViewer,
              fileType: viewer,
            },
          },
        });

        await nextTick();

        expect(loadViewer).toHaveBeenCalledWith(viewer);
        expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
      },
    );
  });

  describe('BlobHeader action slot', () => {
    const { ideEditPath, editBlobPath } = simpleViewerMock;

    it('renders BlobHeaderEdit buttons in simple viewer', async () => {
      await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);

      expect(findBlobEdit().props()).toMatchObject({
        editPath: editBlobPath,
        webIdePath: ideEditPath,
        showEditButton: true,
      });
    });

    it('renders BlobHeaderEdit button in rich viewer', async () => {
      await createComponent({ blob: richViewerMock }, mount);

      expect(findBlobEdit().props()).toMatchObject({
        editPath: editBlobPath,
        webIdePath: ideEditPath,
        showEditButton: true,
      });
    });

    it('renders BlobHeaderEdit button for binary files', async () => {
      await createComponent({ blob: richViewerMock, isBinary: true }, mount);

      expect(findBlobEdit().props()).toMatchObject({
        editPath: editBlobPath,
        webIdePath: ideEditPath,
        showEditButton: false,
      });
    });

    describe('blob header binary file', () => {
      it('passes the correct isBinary value when viewing a binary file', async () => {
        await createComponent({ blob: richViewerMock, isBinary: true });

        expect(findBlobHeader().props('isBinary')).toBe(true);
      });

      it('passes the correct header props when viewing a non-text file', async () => {
        await createComponent(
          {
            blob: {
              ...simpleViewerMock,
              simpleViewer: {
                ...simpleViewerMock.simpleViewer,
                fileType: 'image',
              },
            },
            isBinary: true,
          },
          mount,
        );

        expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
        expect(findBlobHeader().props('isBinary')).toBe(true);
        expect(findBlobEdit().props('showEditButton')).toBe(false);
      });
    });

    describe('BlobButtonGroup', () => {
      const { name, path, replacePath, webPath } = simpleViewerMock;
      const {
        userPermissions: { pushCode, downloadCode },
        repository: { empty },
      } = projectMock;

      it('renders component', async () => {
        window.gon.current_user_id = 1;

        await createComponent({ pushCode, downloadCode, empty }, mount);

        expect(findBlobButtonGroup().props()).toMatchObject({
          name,
          path,
          replacePath,
          deletePath: webPath,
          canPushCode: pushCode,
          canLock: true,
          isLocked: false,
          emptyRepo: empty,
        });
      });

      it.each`
        canPushCode | canDownloadCode | canLock
        ${true}     | ${true}         | ${true}
        ${false}    | ${true}         | ${false}
        ${true}     | ${false}        | ${false}
      `('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => {
        await createComponent(
          {
            pushCode: canPushCode,
            downloadCode: canDownloadCode,
            empty,
          },
          mount,
        );

        expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
      });

      it('does not render if not logged in', async () => {
        isLoggedIn.mockReturnValueOnce(false);

        await createComponent();

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

  describe('blob info query', () => {
    it('is called with originalBranch value if the prop has a value', async () => {
      await createComponent({ inject: { originalBranch: 'some-branch' } });

      expect(mockResolver).toHaveBeenCalledWith(
        expect.objectContaining({
          ref: 'some-branch',
        }),
      );
    });

    it('is called with ref value if the originalBranch prop has no value', async () => {
      await createComponent();

      expect(mockResolver).toHaveBeenCalledWith(
        expect.objectContaining({
          ref: 'default-ref',
        }),
      );
    });
  });

  describe('edit blob', () => {
    beforeEach(() => createComponent({}, mount));

    it('simple edit redirects to the simple editor', () => {
      findBlobEdit().vm.$emit('edit', 'simple');
      expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
    });

    it('IDE edit redirects to the IDE editor', () => {
      findBlobEdit().vm.$emit('edit', 'ide');
      expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
    });

    it.each`
      loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
      ${true}  | ${false}      | ${true}              | ${true}     | ${true}
      ${false} | ${false}      | ${true}              | ${true}     | ${false}
      ${true}  | ${true}       | ${false}             | ${true}     | ${false}
      ${true}  | ${true}       | ${true}              | ${false}    | ${false}
    `(
      'shows/hides a fork suggestion according to a set of conditions',
      async ({
        loggedIn,
        canModifyBlob,
        createMergeRequestIn,
        forkProject,
        showForkSuggestion,
      }) => {
        isLoggedIn.mockReturnValueOnce(loggedIn);
        await createComponent(
          {
            blob: { ...simpleViewerMock, canModifyBlob },
            createMergeRequestIn,
            forkProject,
          },
          mount,
        );

        findBlobEdit().vm.$emit('edit', 'simple');
        await nextTick();

        expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
      },
    );
  });
});
