import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { __ } from '~/locale';
import createFlash from '~/flash';
import {
  GlDeprecatedButton,
  GlLoadingIcon,
  GlLink,
  GlBadge,
  GlFormInput,
  GlAlert,
  GlSprintf,
} from '@gitlab/ui';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
import {
  severityLevel,
  severityLevelVariant,
  errorStatus,
} from '~/error_tracking/components/constants';
import Tracking from '~/tracking';
import {
  trackClickErrorLinkToSentryOptions,
  trackErrorDetailsViewsOptions,
  trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';

jest.mock('~/flash');

const localVue = createLocalVue();
localVue.use(Vuex);

describe('ErrorDetails', () => {
  let store;
  let wrapper;
  let actions;
  let getters;
  let mocks;
  const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';

  const findInput = name => {
    const inputs = wrapper.findAll(GlFormInput).filter(c => c.attributes('name') === name);
    return inputs.length ? inputs.at(0) : inputs;
  };

  const findUpdateIgnoreStatusButton = () =>
    wrapper.find('[data-testid="update-ignore-status-btn"]');
  const findUpdateResolveStatusButton = () =>
    wrapper.find('[data-testid="update-resolve-status-btn"]');
  const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]');

  function mountComponent() {
    wrapper = shallowMount(ErrorDetails, {
      stubs: { GlDeprecatedButton, GlSprintf },
      localVue,
      store,
      mocks,
      propsData: {
        issueId: '123',
        projectPath: '/root/gitlab-test',
        listPath: '/error_tracking',
        issueUpdatePath: '/123',
        issueStackTracePath: '/stacktrace',
        projectIssuesPath: '/test-project/issues/',
        csrfToken: 'fakeToken',
      },
    });
  }

  beforeEach(() => {
    actions = {
      startPollingStacktrace: () => {},
      updateIgnoreStatus: jest.fn().mockResolvedValue({}),
      updateResolveStatus: jest.fn().mockResolvedValue({ closed_issue_iid: 1 }),
    };

    getters = {
      sentryUrl: () => 'sentry.io',
      stacktrace: () => [{ context: [1, 2], lineNo: 53, filename: 'index.js' }],
    };

    const state = {
      stacktraceData: {},
      loadingStacktrace: true,
      errorStatus: '',
    };

    store = new Vuex.Store({
      modules: {
        details: {
          namespaced: true,
          actions,
          state,
          getters,
        },
      },
    });

    const query = jest.fn();
    mocks = {
      $apollo: {
        query,
        queries: {
          error: {
            loading: true,
            stopPolling: jest.fn(),
            setOptions: jest.fn(),
          },
        },
      },
    };
  });

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

  describe('loading', () => {
    beforeEach(() => {
      mountComponent();
    });

    it('should show spinner while loading', () => {
      expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
      expect(wrapper.find(GlLink).exists()).toBe(false);
      expect(wrapper.find(Stacktrace).exists()).toBe(false);
    });
  });

  describe('sentry response timeout', () => {
    const initTime = 300000;
    const endTime = initTime + 10000;

    beforeEach(() => {
      mocks.$apollo.queries.error.loading = false;
      jest.spyOn(Date, 'now').mockReturnValue(initTime);
      mountComponent();
    });

    it('when before timeout, still shows loading', () => {
      Date.now.mockReturnValue(endTime - 1);

      wrapper.vm.onNoApolloResult();

      return wrapper.vm.$nextTick().then(() => {
        expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
        expect(createFlash).not.toHaveBeenCalled();
        expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
      });
    });

    it('when timeout is hit and no apollo result, stops loading and shows flash', () => {
      Date.now.mockReturnValue(endTime + 1);

      wrapper.vm.onNoApolloResult();

      return wrapper.vm.$nextTick().then(() => {
        expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
        expect(wrapper.find(GlLink).exists()).toBe(false);
        expect(createFlash).toHaveBeenCalledWith(
          'Could not connect to Sentry. Refresh the page to try again.',
          'warning',
        );
        expect(mocks.$apollo.queries.error.stopPolling).toHaveBeenCalled();
      });
    });
  });

  describe('Error details', () => {
    beforeEach(() => {
      mocks.$apollo.queries.error.loading = false;
      mountComponent();
      wrapper.setData({
        error: {
          id: 'gid://gitlab/Gitlab::ErrorTracking::DetailedError/129381',
          sentryId: 129381,
          title: 'Issue title',
          externalUrl: 'http://sentry.gitlab.net/gitlab',
          firstSeen: '2017-05-26T13:32:48Z',
          lastSeen: '2018-05-26T13:32:48Z',
          count: 12,
          userCount: 2,
        },
      });
    });

    it('should show Sentry error details without stacktrace', () => {
      expect(wrapper.find(GlLink).exists()).toBe(true);
      expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
      expect(wrapper.find(Stacktrace).exists()).toBe(false);
      expect(wrapper.find(GlBadge).exists()).toBe(false);
      expect(wrapper.findAll(GlDeprecatedButton).length).toBe(3);
    });

    describe('unsafe chars for culprit field', () => {
      const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
      const culprit = '<script>console.log("surprise!")</script>';
      beforeEach(() => {
        store.state.details.loadingStacktrace = false;
        wrapper.setData({
          error: {
            culprit,
          },
        });
      });

      it('should not convert interpolated text to html entities', () => {
        expect(findReportedText().findAll('script').length).toEqual(0);
        expect(findReportedText().findAll('strong').length).toEqual(1);
      });

      it('should render text instead of converting to html entities', () => {
        expect(findReportedText().text()).toContain(culprit);
      });
    });

    describe('Badges', () => {
      it('should show language and error level badges', () => {
        wrapper.setData({
          error: {
            tags: { level: 'error', logger: 'ruby' },
          },
        });
        return wrapper.vm.$nextTick().then(() => {
          expect(wrapper.findAll(GlBadge).length).toBe(2);
        });
      });

      it('should NOT show the badge if the tag is not present', () => {
        wrapper.setData({
          error: {
            tags: { level: 'error' },
          },
        });
        return wrapper.vm.$nextTick().then(() => {
          expect(wrapper.findAll(GlBadge).length).toBe(1);
        });
      });

      it.each(Object.keys(severityLevel))(
        'should set correct severity level variant for %s badge',
        level => {
          wrapper.setData({
            error: {
              tags: { level: severityLevel[level] },
            },
          });
          return wrapper.vm.$nextTick().then(() => {
            expect(wrapper.find(GlBadge).props('variant')).toEqual(
              severityLevelVariant[severityLevel[level]],
            );
          });
        },
      );

      it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', () => {
        wrapper.setData({
          error: {
            tags: { level: 'someNewErrorLevel' },
          },
        });
        return wrapper.vm.$nextTick().then(() => {
          expect(wrapper.find(GlBadge).props('variant')).toEqual(
            severityLevelVariant[severityLevel.ERROR],
          );
        });
      });
    });

    describe('Stacktrace', () => {
      it('should show stacktrace', () => {
        store.state.details.loadingStacktrace = false;
        return wrapper.vm.$nextTick().then(() => {
          expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
          expect(wrapper.find(Stacktrace).exists()).toBe(true);
        });
      });

      it('should NOT show stacktrace if no entries', () => {
        store.state.details.loadingStacktrace = false;
        store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
        return wrapper.vm.$nextTick().then(() => {
          expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
          expect(wrapper.find(Stacktrace).exists()).toBe(false);
        });
      });
    });

    describe('When a user clicks the create issue button', () => {
      it('should send sentry_issue_identifier', () => {
        const sentryErrorIdInput = findInput(
          'issue[sentry_issue_attributes][sentry_issue_identifier]',
        );
        expect(sentryErrorIdInput.attributes('value')).toBe('129381');
      });

      it('should set the form values with title and description', () => {
        const csrfTokenInput = findInput('authenticity_token');
        const issueTitleInput = findInput('issue[title]');
        const issueDescriptionInput = wrapper.find('input[name="issue[description]"]');
        expect(csrfTokenInput.attributes('value')).toBe('fakeToken');
        expect(issueTitleInput.attributes('value')).toContain(wrapper.vm.issueTitle);
        expect(issueDescriptionInput.attributes('value')).toContain(wrapper.vm.issueDescription);
      });

      it('should submit the form', () => {
        window.HTMLFormElement.prototype.submit = () => {};
        const submitSpy = jest.spyOn(wrapper.vm.$refs.sentryIssueForm, 'submit');
        wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
        expect(submitSpy).toHaveBeenCalled();
        submitSpy.mockRestore();
      });
    });

    describe('Status update', () => {
      afterEach(() => {
        actions.updateIgnoreStatus.mockClear();
        actions.updateResolveStatus.mockClear();
      });

      describe('when error is unresolved', () => {
        beforeEach(() => {
          store.state.details.errorStatus = errorStatus.UNRESOLVED;

          return wrapper.vm.$nextTick();
        });

        it('displays Ignore and Resolve buttons', () => {
          expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore'));
          expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve'));
        });

        it('marks error as ignored when ignore button is clicked', () => {
          findUpdateIgnoreStatusButton().vm.$emit('click');
          expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.IGNORED }),
          );
        });

        it('marks error as resolved when resolve button is clicked', () => {
          findUpdateResolveStatusButton().vm.$emit('click');
          expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.RESOLVED }),
          );
        });
      });

      describe('when error is ignored', () => {
        beforeEach(() => {
          store.state.details.errorStatus = errorStatus.IGNORED;

          return wrapper.vm.$nextTick();
        });

        it('displays Undo Ignore and Resolve buttons', () => {
          expect(findUpdateIgnoreStatusButton().text()).toBe(__('Undo ignore'));
          expect(findUpdateResolveStatusButton().text()).toBe(__('Resolve'));
        });

        it('marks error as unresolved when ignore button is clicked', () => {
          findUpdateIgnoreStatusButton().vm.$emit('click');
          expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.UNRESOLVED }),
          );
        });

        it('marks error as resolved when resolve button is clicked', () => {
          findUpdateResolveStatusButton().vm.$emit('click');
          expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.RESOLVED }),
          );
        });
      });

      describe('when error is resolved', () => {
        beforeEach(() => {
          store.state.details.errorStatus = errorStatus.RESOLVED;

          return wrapper.vm.$nextTick();
        });

        it('displays Ignore and Unresolve buttons', () => {
          expect(findUpdateIgnoreStatusButton().text()).toBe(__('Ignore'));
          expect(findUpdateResolveStatusButton().text()).toBe(__('Unresolve'));
        });

        it('marks error as ignored when ignore button is clicked', () => {
          findUpdateIgnoreStatusButton().vm.$emit('click');
          expect(actions.updateIgnoreStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.IGNORED }),
          );
        });

        it('marks error as unresolved when unresolve button is clicked', () => {
          findUpdateResolveStatusButton().vm.$emit('click');
          expect(actions.updateResolveStatus.mock.calls[0][1]).toEqual(
            expect.objectContaining({ status: errorStatus.UNRESOLVED }),
          );
        });

        it('should show alert with closed issueId', () => {
          const findAlert = () => wrapper.find(GlAlert);
          const closedIssueId = 123;
          wrapper.setData({
            isAlertVisible: true,
            closedIssueId,
          });

          return wrapper.vm.$nextTick().then(() => {
            expect(findAlert().exists()).toBe(true);
            expect(findAlert().text()).toContain(`#${closedIssueId}`);
          });
        });
      });
    });

    describe('GitLab issue link', () => {
      const gitlabIssuePath = 'https://gitlab.example.com/issues/1';
      const findGitLabLink = () => wrapper.find(`[href="${gitlabIssuePath}"]`);
      const findCreateIssueButton = () => wrapper.find('[data-qa-selector="create_issue_button"]');
      const findViewIssueButton = () => wrapper.find('[data-qa-selector="view_issue_button"]');

      describe('is present', () => {
        beforeEach(() => {
          wrapper.setData({
            error: {
              gitlabIssuePath,
            },
          });
        });

        it('should display the View issue button', () => {
          expect(findViewIssueButton().exists()).toBe(true);
        });

        it('should display the issue link', () => {
          expect(findGitLabLink().exists()).toBe(true);
        });

        it('should not display a create issue button', () => {
          expect(findCreateIssueButton().exists()).toBe(false);
        });
      });

      describe('is not present', () => {
        beforeEach(() => {
          wrapper.setData({
            error: {
              gitlabIssuePath: null,
            },
          });
        });

        it('should not display the View issue button', () => {
          expect(findViewIssueButton().exists()).toBe(false);
        });

        it('should not display an issue link', () => {
          expect(findGitLabLink().exists()).toBe(false);
        });

        it('should display the create issue button', () => {
          expect(findCreateIssueButton().exists()).toBe(true);
        });
      });
    });

    describe('GitLab commit link', () => {
      const gitlabCommit = '7975be0116940bf2ad4321f79d02a55c5f7779aa';
      const gitlabCommitPath =
        '/gitlab-org/gitlab-test/commit/7975be0116940bf2ad4321f79d02a55c5f7779aa';
      const findGitLabCommitLink = () => wrapper.find(`[href$="${gitlabCommitPath}"]`);

      it('should display a link', () => {
        mocks.$apollo.queries.error.loading = false;
        wrapper.setData({
          error: {
            gitlabCommit,
            gitlabCommitPath,
          },
        });
        return wrapper.vm.$nextTick().then(() => {
          expect(findGitLabCommitLink().exists()).toBe(true);
        });
      });

      it('should not display a link', () => {
        mocks.$apollo.queries.error.loading = false;
        wrapper.setData({
          error: {
            gitlabCommit: null,
          },
        });
        return wrapper.vm.$nextTick().then(() => {
          expect(findGitLabCommitLink().exists()).toBe(false);
        });
      });
    });
  });

  describe('Snowplow tracking', () => {
    beforeEach(() => {
      jest.spyOn(Tracking, 'event');
      mocks.$apollo.queries.error.loading = false;
      mountComponent();
      wrapper.setData({
        error: { externalUrl },
      });
    });

    it('should track detail page views', () => {
      const { category, action } = trackErrorDetailsViewsOptions;
      expect(Tracking.event).toHaveBeenCalledWith(category, action);
    });

    it('should track IGNORE status update', () => {
      Tracking.event.mockClear();
      findUpdateIgnoreStatusButton().vm.$emit('click');
      setImmediate(() => {
        const { category, action, label } = trackErrorStatusUpdateOptions;
        expect(Tracking.event).toHaveBeenCalledWith(category, action, {
          label,
          property: 'ignored',
        });
      });
    });

    it('should track RESOLVE status update', () => {
      Tracking.event.mockClear();
      findUpdateResolveStatusButton().vm.$emit('click');
      setImmediate(() => {
        const { category, action, label } = trackErrorStatusUpdateOptions;
        expect(Tracking.event).toHaveBeenCalledWith(category, action, {
          label,
          property: 'resolved',
        });
      });
    });

    it('should track external Sentry link views', () => {
      Tracking.event.mockClear();
      findExternalUrl().trigger('click');
      setImmediate(() => {
        const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
          externalUrl,
        );
        expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
      });
    });
  });
});
