import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
import errorsList from './list_mock.json';
import Tracking from '~/tracking';

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

describe('ErrorTrackingList', () => {
  let store;
  let wrapper;
  let actions;

  const findErrorListTable = () => wrapper.find('table');
  const findErrorListRows = () => wrapper.findAll('tbody tr');
  const dropdownsArray = () => wrapper.findAll(GlDropdown);
  const findRecentSearchesDropdown = () =>
    dropdownsArray()
      .at(0)
      .find(GlDropdown);
  const findStatusFilterDropdown = () =>
    dropdownsArray()
      .at(1)
      .find(GlDropdown);
  const findSortDropdown = () =>
    dropdownsArray()
      .at(2)
      .find(GlDropdown);
  const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
  const findPagination = () => wrapper.find(GlPagination);
  const findErrorActions = () => wrapper.find(ErrorTrackingActions);

  function mountComponent({
    errorTrackingEnabled = true,
    userCanEnableErrorTracking = true,
    stubs = {},
  } = {}) {
    wrapper = mount(ErrorTrackingList, {
      localVue,
      store,
      propsData: {
        indexPath: '/path',
        listPath: '/error_tracking',
        projectPath: 'project/test',
        enableErrorTrackingLink: '/link',
        userCanEnableErrorTracking,
        errorTrackingEnabled,
        illustrationPath: 'illustration/path',
      },
      stubs: {
        ...stubChildren(ErrorTrackingList),
        ...stubs,
      },
    });
  }

  beforeEach(() => {
    actions = {
      getErrorList: () => {},
      startPolling: jest.fn(),
      restartPolling: jest.fn().mockName('restartPolling'),
      addRecentSearch: jest.fn(),
      loadRecentSearches: jest.fn(),
      setIndexPath: jest.fn(),
      clearRecentSearches: jest.fn(),
      setEndpoint: jest.fn(),
      searchByQuery: jest.fn(),
      sortByField: jest.fn(),
      fetchPaginatedResults: jest.fn(),
      updateStatus: jest.fn(),
      removeIgnoredResolvedErrors: jest.fn(),
      filterByStatus: jest.fn(),
    };

    const state = {
      indexPath: '',
      recentSearches: [],
      errors: errorsList,
      loading: true,
      pagination: {
        previous: {
          cursor: 'previousCursor',
        },
        next: {
          cursor: 'nextCursor',
        },
      },
    };

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

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

  describe('loading', () => {
    beforeEach(() => {
      store.state.list.loading = true;
      mountComponent();
    });

    it('shows spinner', () => {
      expect(findLoadingIcon().exists()).toBe(true);
      expect(findErrorListTable().exists()).toBe(false);
    });
  });

  describe('results', () => {
    beforeEach(() => {
      store.state.list.loading = false;
      store.state.list.errors = errorsList;
      mountComponent({
        stubs: {
          GlTable: false,
          GlDropdown: false,
          GlDropdownItem: false,
          GlLink: false,
        },
      });
    });

    it('shows table', () => {
      expect(findLoadingIcon().exists()).toBe(false);
      expect(findErrorListTable().exists()).toBe(true);
      expect(findSortDropdown().exists()).toBe(true);
    });

    it('shows list of errors in a table', () => {
      expect(findErrorListRows().length).toEqual(store.state.list.errors.length);
    });

    it('each error in a list should have a link to the error page', () => {
      const errorTitle = wrapper.findAll('tbody tr a');

      errorTitle.wrappers.forEach((_, index) => {
        expect(errorTitle.at(index).attributes('href')).toEqual(
          expect.stringMatching(/error_tracking\/\d+\/details$/),
        );
      });
    });

    it('each error in the list should have an action button set', () => {
      findErrorListRows().wrappers.forEach(row => {
        expect(row.find(ErrorTrackingActions).exists()).toBe(true);
      });
    });

    describe('filtering', () => {
      const findSearchBox = () => wrapper.find(GlFormInput);

      it('shows search box & sort dropdown', () => {
        expect(findSearchBox().exists()).toBe(true);
        expect(findSortDropdown().exists()).toBe(true);
      });

      it('it searches by query', () => {
        findSearchBox().vm.$emit('input', 'search');
        findSearchBox().trigger('keyup.enter');
        expect(actions.searchByQuery.mock.calls[0][1]).toBe('search');
      });

      it('it sorts by fields', () => {
        const findSortItem = () => findSortDropdown().find('.dropdown-item');
        findSortItem().trigger('click');
        expect(actions.sortByField).toHaveBeenCalled();
      });

      it('it filters by status', () => {
        const findStatusFilter = () => findStatusFilterDropdown().find('.dropdown-item');
        findStatusFilter().trigger('click');
        expect(actions.filterByStatus).toHaveBeenCalled();
      });
    });
  });

  describe('no results', () => {
    const findRefreshLink = () => wrapper.find('.js-try-again');

    beforeEach(() => {
      store.state.list.loading = false;
      store.state.list.errors = [];

      mountComponent({
        stubs: {
          GlTable: false,
          GlDropdown: false,
          GlDropdownItem: false,
        },
      });
    });

    it('shows empty table', () => {
      expect(findLoadingIcon().exists()).toBe(false);
      expect(findErrorListRows().length).toEqual(1);
      expect(findSortDropdown().exists()).toBe(true);
    });

    it('shows a message prompting to refresh', () => {
      expect(findRefreshLink().text()).toContain('Check again');
    });

    it('restarts polling', () => {
      findRefreshLink().vm.$emit('click');
      expect(actions.restartPolling).toHaveBeenCalled();
    });
  });

  describe('error tracking feature disabled', () => {
    beforeEach(() => {
      mountComponent({ errorTrackingEnabled: false });
    });

    it('shows empty state', () => {
      expect(wrapper.find(GlEmptyState).exists()).toBe(true);
      expect(findLoadingIcon().exists()).toBe(false);
      expect(findErrorListTable().exists()).toBe(false);
      expect(dropdownsArray().length).toBe(0);
    });
  });

  describe('When the ignore button on an error is clicked', () => {
    beforeEach(() => {
      store.state.list.loading = false;
      store.state.list.errors = errorsList;

      mountComponent({
        stubs: {
          GlTable: false,
          GlLink: false,
          GlDeprecatedButton: false,
        },
      });
    });

    it('sends the "ignored" status and error ID', () => {
      findErrorActions().vm.$emit('update-issue-status', {
        errorId: errorsList[0].id,
        status: 'ignored',
      });
      expect(actions.updateStatus).toHaveBeenCalledWith(expect.anything(), {
        endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
        status: 'ignored',
      });
    });

    it('calls an action to remove the item from the list', () => {
      findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
      expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(expect.anything(), '1');
    });
  });

  describe('When the resolve button on an error is clicked', () => {
    beforeEach(() => {
      store.state.list.loading = false;
      store.state.list.errors = errorsList;

      mountComponent({
        stubs: {
          GlTable: false,
          GlLink: false,
          GlDeprecatedButton: false,
        },
      });
    });

    it('sends "resolved" status and error ID', () => {
      findErrorActions().vm.$emit('update-issue-status', {
        errorId: errorsList[0].id,
        status: 'resolved',
      });
      expect(actions.updateStatus).toHaveBeenCalledWith(expect.anything(), {
        endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
        status: 'resolved',
      });
    });

    it('calls an action to remove the item from the list', () => {
      findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
      expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(expect.anything(), '1');
    });
  });

  describe('When error tracking is disabled and user is not allowed to enable it', () => {
    beforeEach(() => {
      mountComponent({
        errorTrackingEnabled: false,
        userCanEnableErrorTracking: false,
        stubs: {
          GlLink: false,
          GlEmptyState: false,
        },
      });
    });

    it('shows empty state', () => {
      expect(wrapper.find('a').attributes('href')).toBe(
        '/help/user/project/operations/error_tracking.html',
      );
    });
  });

  describe('recent searches', () => {
    beforeEach(() => {
      mountComponent({
        stubs: {
          GlDropdown: false,
          GlDropdownItem: false,
        },
      });
    });

    it('shows empty message', () => {
      store.state.list.recentSearches = [];

      expect(findRecentSearchesDropdown().text()).toContain("You don't have any recent searches");
    });

    it('shows items', () => {
      store.state.list.recentSearches = ['great', 'search'];

      return wrapper.vm.$nextTick().then(() => {
        const dropdownItems = wrapper.findAll('.filtered-search-box li');
        expect(dropdownItems.length).toBe(3);
        expect(dropdownItems.at(0).text()).toBe('great');
        expect(dropdownItems.at(1).text()).toBe('search');
      });
    });

    describe('clear', () => {
      const clearRecentButton = () => wrapper.find({ ref: 'clearRecentSearches' });

      it('is hidden when list empty', () => {
        store.state.list.recentSearches = [];

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

      it('is visible when list has items', () => {
        store.state.list.recentSearches = ['some', 'searches'];

        return wrapper.vm.$nextTick().then(() => {
          expect(clearRecentButton().exists()).toBe(true);
          expect(clearRecentButton().text()).toBe('Clear recent searches');
        });
      });

      it('clears items on click', () => {
        store.state.list.recentSearches = ['some', 'searches'];

        return wrapper.vm.$nextTick().then(() => {
          clearRecentButton().vm.$emit('click');

          expect(actions.clearRecentSearches).toHaveBeenCalledTimes(1);
        });
      });
    });
  });

  describe('When pagination is not required', () => {
    beforeEach(() => {
      store.state.list.loading = false;
      store.state.list.pagination = {};
      mountComponent();
    });

    it('should not render the pagination component', () => {
      expect(findPagination().exists()).toBe(false);
    });
  });

  describe('When pagination is required', () => {
    describe('and the user is on the first page', () => {
      beforeEach(() => {
        store.state.list.loading = false;
        mountComponent({
          stubs: {
            GlPagination: false,
          },
        });
      });

      it('shows a disabled Prev button', () => {
        expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe('true');
      });
    });

    describe('and the user is not on the first page', () => {
      describe('and the previous button is clicked', () => {
        beforeEach(() => {
          store.state.list.loading = false;
          mountComponent({
            stubs: {
              GlTable: false,
              GlPagination: false,
            },
          });
          wrapper.setData({ pageValue: 2 });
          return wrapper.vm.$nextTick();
        });

        it('fetches the previous page of results', () => {
          expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe(undefined);
          wrapper.vm.goToPrevPage();
          expect(actions.fetchPaginatedResults).toHaveBeenCalled();
          expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
            expect.anything(),
            'previousCursor',
          );
        });
      });

      describe('and the next page button is clicked', () => {
        beforeEach(() => {
          store.state.list.loading = false;
          mountComponent();
        });

        it('fetches the next page of results', () => {
          window.scrollTo = jest.fn();
          findPagination().vm.$emit('input', 2);
          expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
          expect(actions.fetchPaginatedResults).toHaveBeenCalled();
          expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
            expect.anything(),
            'nextCursor',
          );
        });
      });
    });
  });

  describe('Snowplow tracking', () => {
    beforeEach(() => {
      jest.spyOn(Tracking, 'event');
      store.state.list.loading = false;
      store.state.list.errors = errorsList;
      mountComponent({
        stubs: {
          GlTable: false,
          GlLink: false,
          GlDeprecatedButton: false,
        },
      });
    });

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

    it('should track status updates', () => {
      Tracking.event.mockClear();
      const status = 'ignored';
      findErrorActions().vm.$emit('update-issue-status', {
        errorId: 1,
        status,
      });

      setImmediate(() => {
        const { category, action } = trackErrorStatusUpdateOptions(status);
        expect(Tracking.event).toHaveBeenCalledWith(category, action);
      });
    });
  });
});
