import { mount } from '@vue/test-utils';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import SecurityScannerAlert from 'ee/security_dashboard/components/security_scanner_alert.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import VulnerabilityList, {
  SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
} from 'ee/security_dashboard/components/vulnerability_list.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import { trimText } from 'helpers/text_helper';
import { generateVulnerabilities, vulnerabilities } from './mock_data';

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

  let wrapper;

  const createWrapper = ({ props = {} }) => {
    return mount(VulnerabilityList, {
      propsData: {
        vulnerabilities: [],
        ...props,
      },
      stubs: {
        GlPopover: true,
      },
      provide: () => ({
        noVulnerabilitiesSvgPath: '#',
        dashboardDocumentation: '#',
        emptyStateSvgPath: '#',
        notEnabledScannersHelpPath: '#',
        noPipelineRunScannersHelpPath: '#',
      }),
    });
  };

  const findCell = label => wrapper.find(`.js-${label}`);
  const findRow = (index = 0) => wrapper.findAll('tbody tr').at(index);
  const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert);
  const findDismissalButton = () => findSecurityScannerAlert().find('button[aria-label="Dismiss"]');
  const findSelectionSummary = () => wrapper.find(SelectionSummary);
  const findRowVulnerabilityCommentIcon = row => findRow(row).find(VulnerabilityCommentIcon);
  const findDataCell = label => wrapper.find(`[data-testid="${label}"]`);
  const findDataCells = label => wrapper.findAll(`[data-testid="${label}"]`);
  const findCellText = label => findDataCell(label).text();
  const findFiltersProducedNoResults = () => wrapper.find(FiltersProducedNoResults);
  const findDashboardHasNoVulnerabilities = () => wrapper.find(DashboardHasNoVulnerabilities);
  const findVendorNames = () => wrapper.find(`[data-testid="vulnerability-vendor"]`);

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

  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(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 not show the selection summary if no vulnerabilities are selected', () => {
      expect(findSelectionSummary().exists()).toBe(false);
    });

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

      return wrapper.vm.$nextTick().then(() => {
        expect(findSelectionSummary().exists()).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().exists()).toBe(false);
    });
  });

  describe('when vulnerability selection is disabled', () => {
    beforeEach(() => {
      wrapper = createWrapper({
        props: { vulnerabilities, shouldShowSelection: false },
      });
    });

    it('should not show the checkboxes if shouldShowSelection is passed in', () => {
      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 cellText = findCellText(`location-${id}`);
      expect(cellText).toContain(project.nameWithNamespace);
      expect(cellText).toContain(location.image);
      expect(cellText).not.toContain('(line: ');
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cellText = findCellText(`location-${id}`);
      expect(cellText).toContain(project.nameWithNamespace);
      expect(cellText).toContain(location.file);
      expect(cellText).toContain(`(line: ${location.startLine})`);
    });

    it('should display the vulnerability locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cellText = findCellText(`location-${id}`);
      expect(cellText).toContain(project.nameWithNamespace);
      expect(cellText).toContain(location.file);
      expect(cellText).not.toContain('(line: ');
    });

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

    it('should not display the vulnerability identifier cell', () => {
      const identifier = findDataCell('vulnerability-identifier');
      expect(identifier.exists()).toBe(false);

      const extraIdentifierCount = findDataCell('vulnerability-more-identifiers');
      expect(extraIdentifierCount.exists()).toBe(false);
    });

    it('should not display the vulnerability report type', () => {
      const cell = findDataCell('vulnerability-report-type');
      expect(cell.exists()).toBe(false);
    });
  });

  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 cellText = findCellText(`location-${id}`);
      expect(cellText).not.toContain(project.nameWithNamespace);
      expect(cellText).toEqual(location.image);
    });

    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 GMT+0000');
    });

    it('should display the vulnerability locations for code', () => {
      const { id, project, location } = newVulnerabilities[1];
      const cellText = findCellText(`location-${id}`);
      expect(cellText).not.toContain(project.nameWithNamespace);
      expect(cellText).toEqual(`${location.file} (line: ${location.startLine})`);
    });

    it('should not display the vulnerability group/project locations for code with no line data', () => {
      const { id, project, location } = newVulnerabilities[2];
      const cellText = findCellText(`location-${id}`);
      expect(cellText).not.toContain(project.nameWithNamespace);
      expect(cellText).toEqual(location.file);
    });

    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 display the vulnerability report type', () => {
      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 display the vulnerability vendor if the vulnerability vendor does exist', () => {
      const cells = findDataCells('vulnerability-vendor');
      expect(cells.at(0).text()).toBe('GitLab');
    });

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

  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 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 when there are no filters', () => {
    beforeEach(() => {
      wrapper = createWrapper({});
    });

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

    it('should not show the vulnerability check-all checkbox as checked with no vulnerabilities', () => {
      expect(findDataCell('vulnerability-checkbox-all').exists()).toBe(true);
      expect(findDataCell('vulnerability-checkbox-all').element.checked).toBe(false);
    });
  });

  describe('with no vulnerabilities when there are filters', () => {
    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('security scanner alerts', () => {
    describe.each`
      available   | enabled     | pipelineRun | expectAlertShown
      ${['DAST']} | ${[]}       | ${[]}       | ${true}
      ${['DAST']} | ${['DAST']} | ${[]}       | ${true}
      ${['DAST']} | ${[]}       | ${['DAST']} | ${true}
      ${['DAST']} | ${['DAST']} | ${['DAST']} | ${false}
      ${[]}       | ${[]}       | ${[]}       | ${false}
    `('visibility', ({ available, enabled, pipelineRun, expectAlertShown }) => {
      it(`should${expectAlertShown ? '' : ' not'} show the alert`, () => {
        wrapper = createWrapper({
          props: { securityScanners: { available, enabled, pipelineRun } },
        });

        expect(findSecurityScannerAlert().exists()).toBe(expectAlertShown);
      });

      it('should never show the alert once it has been dismissed', async () => {
        window.localStorage.setItem(SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY, 'true');

        wrapper = createWrapper({
          props: { securityScanners: { available, enabled, pipelineRun } },
        });

        await wrapper.vm.$nextTick();

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

    describe('dismissal', () => {
      beforeEach(() => {
        wrapper = createWrapper({
          props: { securityScanners: { available: ['DAST'], enabled: [] } },
        });
      });

      it('should hide the alert when it is dismissed', async () => {
        expect(findSecurityScannerAlert().exists()).toBe(true);

        findDismissalButton().trigger('click');

        await wrapper.vm.$nextTick();

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

      it('should remember the dismissal state', async () => {
        findDismissalButton().trigger('click');

        await wrapper.vm.$nextTick();

        expect(window.localStorage.setItem.mock.calls).toContainEqual([
          SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
          'true',
        ]);
      });
    });
  });
});
