import { 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_report/vulnerability_list.vue';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveBadge from 'ee/vulnerabilities/components/false_positive_badge.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { FIELDS } from 'ee/security_dashboard/components/shared/vulnerability_report/constants';
import { generateVulnerabilities, vulnerabilities } from '../../mock_data';

const { CHECKBOX, DETECTED, STATUS, SEVERITY, DESCRIPTION, IDENTIFIER, TOOL, ACTIVITY } = FIELDS;

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

  const createWrapper = ({ props = {}, listeners, provide = {}, stubs } = {}) => {
    wrapper = mountExtended(VulnerabilityList, {
      propsData: {
        vulnerabilities: [],
        fields: [CHECKBOX, DETECTED, STATUS, SEVERITY, DESCRIPTION, IDENTIFIER, TOOL, ACTIVITY],
        ...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 findCell = (label) => wrapper.find(`.js-${label}`);
  const findRows = () => wrapper.findAll('tbody tr');
  const findRow = (index = 0) => findRows().at(index);
  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 findTitleCell = (id) => wrapper.findByTestId(`title-${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();
      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(() => {
          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 displayed on instance or group level dashboard', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      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(
        expect.objectContaining({
          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(
        expect.objectContaining({
          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(
        expect.objectContaining({
          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(
        expect.objectContaining({
          text: location.path,
          position: 'middle',
        }),
      );
    });
  });

  describe('when displayed on a project level dashboard', () => {
    let newVulnerabilities;
    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      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(
        expect.objectContaining({
          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(
        expect.objectContaining({
          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(
        expect.objectContaining({
          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',
            },
          },
        ],
      };
      createWrapper({ props: { vulnerabilities: newVulnerabilities } });
    });

    it('should emit "vulnerability-clicked" with the vulnerability as a payload when a vulnerability-link is clicked', async () => {
      const clickedEventName = 'vulnerability-clicked';
      const vulnerability = newVulnerabilities[1];
      const link = findTitleCell(vulnerability.id).find('a');

      expect(wrapper.emitted(clickedEventName)).toBe(undefined);

      await link.trigger('click');
      const emittedEvents = wrapper.emitted(clickedEventName);

      expect(emittedEvents).toHaveLength(1);
      expect(emittedEvents[0][0]).toBe(vulnerability);
    });
  });

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

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].userNotesCount = 1;
      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' },
      }));
      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: '' } }));
      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' };
      createWrapper({
        props: {
          vulnerabilities: newVulnerabilities,
          shouldShowReportType: true,
        },
      });
    });

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

  describe('when a vulnerability has a false positive', () => {
    let newVulnerabilities;

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].falsePositive = true;
      createWrapper({
        props: { vulnerabilities: newVulnerabilities },
        provide: {
          falsePositiveDocUrl: '/docs',
          canViewFalsePositive: true,
        },
      });
    });

    it('should render the false positive info badge on the first vulnerability', () => {
      const row = findRow(0);
      const badge = row.findComponent(FalsePositiveBadge);

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

    it('should not render the false positive info badge on the second vulnerability', () => {
      const row = findRow(1);
      const badge = row.findComponent(FalsePositiveBadge);

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

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

    beforeEach(() => {
      newVulnerabilities = generateVulnerabilities();
      newVulnerabilities[0].resolvedOnDefaultBranch = true;
      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('loading prop', () => {
    it.each`
      phrase        | isLoading
      ${'show'}     | ${true}
      ${'not show'} | ${false}
    `('should $phrase the loading state', ({ isLoading }) => {
      createWrapper({ props: { isLoading, vulnerabilities } });

      expect(findCell('status').exists()).toEqual(!isLoading);
      expect(wrapper.find(GlSkeletonLoading).exists()).toEqual(isLoading);
    });
  });

  describe('with no vulnerabilities', () => {
    beforeEach(() => {
      createWrapper();
    });

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

  describe('sort-changed listener', () => {
    it('emits sort by data in expected format', () => {
      createWrapper();

      findTable().vm.$emit('sort-changed', { sortBy: 'state', sortDesc: true });

      expect(wrapper.emitted('sort-changed')[0][0]).toBe('state_desc');
    });
  });

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

    beforeEach(() => {
      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('fields prop', () => {
    it('shows the expected columns in the table', () => {
      const fields = [STATUS, SEVERITY];
      createWrapper({ props: { fields, vulnerabilities } });

      // Check that there are only 2 columns.
      expect(findRow().element.cells).toHaveLength(2);
      expect(findCell(STATUS.class).exists()).toBe(true);
      expect(findCell(SEVERITY.class).exists()).toBe(true);
    });
  });
});
