import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlTable, GlTruncate } from '@gitlab/ui';
import { capitalize } from 'lodash';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/shared/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/shared/empty_states/filters_produced_no_results.vue';
import IssuesBadge from 'ee/security_dashboard/components/shared/issues_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/shared/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/shared/vulnerability_comment_icon.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { generateVulnerabilities, vulnerabilities } from '../mock_data';

describe('Vulnerability list component', () => {
  let wrapper;

  const createWrapper = ({ props = {}, listeners, provide = {}, stubs } = {}) => {
    return mountExtended(VulnerabilityList, {
      propsData: {
        vulnerabilities: [],
        ...props,
      },
      stubs: {
        GlPopover: true,
        ...stubs,
      },
      listeners,
      provide: () => ({
        dashboardType: DASHBOARD_TYPES.PROJECT,
        noVulnerabilitiesSvgPath: '#',
        dashboardDocumentation: '#',
        emptyStateSvgPath: '#',
        notEnabledScannersHelpPath: '#',
        noPipelineRunScannersHelpPath: '#',
        hasVulnerabilities: true,
        hasJiraVulnerabilitiesIntegrationEnabled: false,
        canAdminVulnerability: true,
        ...provide,
      }),
    });
  };

  const locationText = ({ file, startLine }) => `${file}:${startLine}`;
  const findTable = () => wrapper.findComponent(GlTable);
  const findSortableColumn = () => wrapper.find('[aria-sort="descending"]');
  const findCell = (label) => wrapper.find(`.js-${label}`);
  const findRows = () => wrapper.findAll('tbody tr');
  const findRow = (index = 0) => findRows().at(index);
  const findColumn = (className) => wrapper.find(`[role="columnheader"].${className}`);
  const findRowById = (id) => wrapper.find(`tbody tr[data-pk="${id}"`);
  const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]');
  const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index);
  const findRemediatedBadge = () => wrapper.findComponent(RemediatedBadge);
  const findSelectionSummary = () => wrapper.findComponent(SelectionSummary);
  const findRowVulnerabilityCommentIcon = (row) =>
    findRow(row).findComponent(VulnerabilityCommentIcon);
  const findDataCell = (label) => wrapper.findByTestId(label);
  const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
  const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`);
  const findLocationTextWrapper = (cell) => cell.find(GlTruncate);
  const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
  const findDashboardHasNoVulnerabilities = () =>
    wrapper.findComponent(DashboardHasNoVulnerabilities);
  const findVendorNames = () => wrapper.findByTestId('vulnerability-vendor');

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

  describe('with vulnerabilities', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render a list of vulnerabilities', () => {
      expect(wrapper.findAll('.js-status')).toHaveLength(newVulnerabilities.length);
    });

    it('should correctly render the status', () => {
      const cell = findCell('status');

      expect(cell.text()).toBe(capitalize(newVulnerabilities[0].state));
    });

    it('should correctly render the severity', () => {
      const cell = findCell('severity');
      expect(cell.text().toLowerCase()).toBe(newVulnerabilities[0].severity);
    });

    it('should correctly render the description', () => {
      const cell = findCell('description');
      expect(cell.text()).toBe(newVulnerabilities[0].title);
    });

    it('should display the remediated badge', () => {
      expect(findRemediatedBadge().exists()).toBe(true);
    });

    it('should display autoFixIcon for first Item', () => {
      expect(findAutoFixBulbInRow(findRow(0)).exists()).toBe(true);
    });

    it('should not display autoFixIcon for second Item', () => {
      expect(findAutoFixBulbInRow(findRow(1)).exists()).toBe(false);
    });

    it('should correctly render the identifier cell', () => {
      const identifiers = findDataCells('vulnerability-identifier');
      const extraIdentifierCounts = findDataCells('vulnerability-more-identifiers');

      const firstIdentifiers = newVulnerabilities[0].identifiers;
      expect(identifiers.at(0).text()).toBe(firstIdentifiers[0].name);
      expect(trimText(extraIdentifierCounts.at(0).text())).toContain(
        `${firstIdentifiers.length - 1} more`,
      );

      expect(identifiers.at(1).text()).toBe(newVulnerabilities[1].identifiers[0].name);
      expect(extraIdentifierCounts).toHaveLength(1);
    });

    it('should correctly render the report type cell', () => {
      const cells = findDataCells('vulnerability-report-type');
      expect(cells.at(0).text()).toBe('SAST');
      expect(cells.at(1).text()).toBe('Dependency Scanning');
      expect(cells.at(2).text()).toBe('Custom scanner without translation');
      expect(cells.at(3).text()).toBe('');
    });

    it('should correctly render the vulnerability vendor if the vulnerability vendor does exist', () => {
      const cells = findDataCells('vulnerability-vendor');
      expect(cells.at(0).text()).toBe('GitLab');
    });

    it('should correctly render an empty string if the vulnerability vendor does not exist', () => {
      const cells = findDataCells('vulnerability-vendor');
      expect(cells.at(3).text()).toBe('');
    });

    it('should not show the selection summary if no vulnerabilities are selected', () => {
      expect(findSelectionSummary().props('visible')).toBe(false);
    });

    it('should show the selection summary when a checkbox is selected', async () => {
      findDataCell('vulnerability-checkbox').setChecked(true);
      await wrapper.vm.$nextTick();

      expect(findSelectionSummary().props('visible')).toBe(true);
    });

    it('should sync selected vulnerabilities when the vulnerability list is updated', async () => {
      findDataCell('vulnerability-checkbox').setChecked(true);

      await wrapper.vm.$nextTick();

      expect(findSelectionSummary().props('selectedVulnerabilities')).toHaveLength(1);

      wrapper.setProps({ vulnerabilities: [] });

      await wrapper.vm.$nextTick();

      expect(findSelectionSummary().props('visible')).toBe(false);
    });

    it('should uncheck a selected vulnerability after the vulnerability is updated', async () => {
      const checkbox = () => findDataCell('vulnerability-checkbox');
      checkbox().setChecked(true);
      expect(checkbox().element.checked).toBe(true);

      await wrapper.vm.$nextTick();
      findSelectionSummary().vm.$emit('vulnerability-updated', newVulnerabilities[0].id);
      await wrapper.vm.$nextTick();

      expect(checkbox().element.checked).toBe(false);
    });

    describe.each([true, false])(
      'issues badge when "hasJiraVulnerabilitiesIntegrationEnabled" is set to "%s"',
      (hasJiraVulnerabilitiesIntegrationEnabled) => {
        beforeEach(() => {
          wrapper = createWrapper({
            props: { vulnerabilities: generateVulnerabilities() },
            provide: { hasJiraVulnerabilitiesIntegrationEnabled },
          });
        });

        it('should display the issues badge for the first item', () => {
          expect(findIssuesBadge(0).exists()).toBe(true);
        });

        it('should not display the issues badge for the second item', () => {
          expect(() => findIssuesBadge(1)).toThrow();
        });

        it('should render the badge as Jira issues', () => {
          expect(findIssuesBadge(0).props('isJira')).toBe(hasJiraVulnerabilitiesIntegrationEnabled);
        });
      },
    );
  });

  describe('when user has no permission to admin vulnerabilities', () => {
    beforeEach(() => {
      wrapper = createWrapper({
        props: { vulnerabilities },
        provide: {
          canAdminVulnerability: false,
        },
      });
    });

    it('should not show the checkboxes', () => {
      expect(findDataCell('vulnerability-checkbox-all').exists()).toBe(false);
      expect(findDataCell('vulnerability-checkbox').exists()).toBe(false);
    });
  });

  describe('when displayed on instance or group level dashboard', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      wrapper = createWrapper({
        props: { vulnerabilities: newVulnerabilities, shouldShowProjectNamespace: true },
      });
    });

    it('should display the vulnerability locations for images', () => {
      const { id, project, location } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: location.image,
        position: 'middle',
      });
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: locationText(location),
        position: 'middle',
      });
    });

    it('should display the vulnerability locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: location.file,
        position: 'middle',
      });
    });

    it('should not display the vulnerability locations for vulnerabilities without a location', () => {
      const { id, project } = newVulnerabilities[4];
      const cellText = findLocationCell(id).text();
      expect(cellText).toEqual(project.nameWithNamespace);
      expect(cellText).not.toContain(':');
    });

    it('should display the vulnerability locations for path', () => {
      const { id, project, location } = newVulnerabilities[5];
      const cell = findLocationCell(id);
      expect(cell.text()).toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: location.path,
        position: 'middle',
      });
    });
  });

  describe('when displayed on a project level dashboard', () => {
    let newVulnerabilities;
    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      wrapper = createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowIdentifier: true,
          shouldShowReportType: true,
        },
      });
    });

    it('should not display the vulnerability group/project locations for images', () => {
      const { id, project, location } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: location.image,
        position: 'middle',
      });
    });

    it('should display the detected time', () => {
      const { id } = newVulnerabilities[1];
      const cell = findDataCell(`detected-${id}`);
      expect(cell.text()).toEqual(`2020-07-22`);
      expect(cell.attributes('title')).toEqual('Jul 22, 2020 7:31pm UTC');
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: locationText(location),
        position: 'middle',
      });
    });

    it('should make the file path linkable', () => {
      const { id, location } = newVulnerabilities[1];
      const cell = findLocationCell(id);
      expect(cell.find('a').attributes('href')).toBe(`${location.blobPath}#L${location.startLine}`);
    });

    it('should not make the file path linkable if blobPath is missing', () => {
      const { id } = newVulnerabilities[0];
      const cell = findLocationCell(id);
      expect(cell.find('a').exists()).toBe(false);
    });

    it('should not display the vulnerability group/project locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cell = findLocationCell(id);
      expect(cell.text()).not.toContain(project.nameWithNamespace);
      expect(findLocationTextWrapper(cell).props()).toEqual({
        text: location.file,
        position: 'middle',
      });
    });
  });

  describe('when has an issue associated', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].issueLinks = {
        nodes: [
          {
            issue: {
              title: 'my-title',
              iid: 114,
              state: 'opened',
              webUrl: 'http://localhost/issues/~/114',
            },
          },
        ],
      };
      wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });
  });

  describe('when has comments', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].userNotesCount = 1;
      wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render the comments badge on the first vulnerability', () => {
      expect(findRowVulnerabilityCommentIcon(0).exists()).toBe(true);
    });

    it('should not render the comments badge on the second vulnerability', () => {
      expect(findRowVulnerabilityCommentIcon(1).exists()).toBe(false);
    });
  });

  describe('when GitLab is the only scanner in the reports', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities = newVulnerabilities.map((v) => ({
        ...v,
        scanner: { vendor: 'GitLab' },
      }));
      wrapper = createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(false);
    });
  });

  describe('when vendor name is not provided in the reports', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities = newVulnerabilities.map((v) => ({ ...v, scanner: { vendor: '' } }));
      wrapper = createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(false);
    });
  });

  describe('when there are other scanners in the report', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].scanner = { vendor: 'GitLab' };
      newVulnerabilities[1].scanner = { vendor: 'Third Party Scanner' };
      wrapper = createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

    it('should not render the vendor name', () => {
      expect(findVendorNames().exists()).toBe(true);
    });
  });

  describe('when a vulnerability is resolved on the default branch', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].resolvedOnDefaultBranch = true;
      wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should render the remediated info badge on the first vulnerability', () => {
      const row = findRow(0);
      const badge = row.find(RemediatedBadge);

      expect(badge.exists()).toEqual(true);
    });

    it('should not render the remediated info badge on the second vulnerability', () => {
      const row = findRow(1);
      const badge = row.find(RemediatedBadge);

      expect(badge.exists()).toEqual(false);
    });
  });

  describe('when loading', () => {
    beforeEach(() => {
      wrapper = createWrapper({ props: { isLoading: true } });
    });

    it('should show the loading state', () => {
      expect(findCell('status').exists()).toEqual(false);
      expect(wrapper.find(GlSkeletonLoading).exists()).toEqual(true);
    });
  });

  describe('with no vulnerabilities', () => {
    beforeEach(() => {
      wrapper = createWrapper({ props: { filters: { someFilter: 'true' } } });
    });

    it('should show the empty state', () => {
      expect(findCell('status').exists()).toEqual(false);
      expect(findFiltersProducedNoResults().exists()).toEqual(true);
      expect(findDashboardHasNoVulnerabilities().exists()).toEqual(false);
    });
  });

  describe('with vulnerabilities when there are filters', () => {
    it.each`
      state
      ${['DETECTED']}
      ${['DISMISSED']}
      ${[]}
      ${['DETECTED', 'DISMISSED']}
    `('should only show vulnerabilities that match filter $state', (state) => {
      wrapper = createWrapper({ props: { vulnerabilities, filters: { state } } });
      const filteredVulnerabilities = vulnerabilities.filter((x) =>
        state.length ? state.includes(x.state) : true,
      );
      expect(findRows().length).toBe(filteredVulnerabilities.length);

      filteredVulnerabilities.forEach((vulnerability) => {
        expect(findRowById(vulnerability.id).exists()).toBe(true);
      });
    });
  });

  describe('when has a sort-changed listener defined', () => {
    let spy;

    beforeEach(() => {
      spy = jest.fn();
      wrapper = createWrapper({
        listeners: { 'sort-changed': spy },
      });
    });

    it('is sortable', () => {
      expect(findSortableColumn().attributes('class')).toContain('severity');
    });

    it('triggers the listener when sortBy is not an empty value', () => {
      const args = { sortBy: 'severity', sortDesc: false };
      findTable().vm.$emit('sort-changed', args);
      expect(spy).toHaveBeenCalledWith(args);
    });

    it('triggers the listener when sortBy is camelCased and transforms it to snake_case', () => {
      const args = { sortBy: 'reportType', sortDesc: false };
      findTable().vm.$emit('sort-changed', args);
      expect(spy).toHaveBeenCalledWith({ ...args, sortBy: 'report_type' });
    });

    it('does not trigger the listener when sortBy is an empty value', () => {
      findTable().vm.$emit('sort-changed', {});
      expect(spy).not.toHaveBeenCalled();
    });
  });

  describe('when does not have a sort-changed listener defined', () => {
    beforeEach(() => {
      wrapper = createWrapper();
    });

    it('is not sortable', () => {
      expect(findSortableColumn().exists()).toBe(false);
    });
  });

  describe('row click', () => {
    const findRowCheckbox = (index) =>
      findRow(index).find('[data-testid="vulnerability-checkbox"]');

    beforeEach(() => {
      wrapper = createWrapper({ props: { vulnerabilities } });
    });

    it('will select and deselect vulnerabilities', async () => {
      const rowCount = vulnerabilities.length;
      const rowsToClick = [0, 1, 2];
      const clickRows = () => rowsToClick.forEach((row) => findRow(row).trigger('click'));
      const expectRowCheckboxesToBe = (condition) => {
        for (let i = 0; i < rowCount; i += 1)
          expect(findRowCheckbox(i).element.checked).toBe(condition(i));
      };

      clickRows();
      await wrapper.vm.$nextTick();
      expectRowCheckboxesToBe((i) => rowsToClick.includes(i));

      clickRows();
      await wrapper.vm.$nextTick();
      expectRowCheckboxesToBe(() => false);
    });
  });

  describe('when it is the pipeline dashboard', () => {
    beforeEach(() => {
      wrapper = createWrapper({
        props: { vulnerabilities },
        provide: { dashboardType: DASHBOARD_TYPES.PIPELINE },
        stubs: {
          GlTable,
        },
      });
    });

    it.each([['detected'], ['activity']])('does not render %s column', (className) => {
      expect(findColumn(className).exists()).toBe(false);
    });

    it.each([['status'], ['severity'], ['description'], ['identifier'], ['scanner']])(
      'renders %s column',
      (className) => {
        expect(findColumn(className).exists()).toBe(true);
      },
    );
  });
});
