<script>
import { difference } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { GlFormCheckbox, GlLink, GlSkeletonLoading, GlSprintf, GlTable } from '@gitlab/ui';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.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 SecurityScannerAlert from './security_scanner_alert.vue';

import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import IssueLink from 'ee/vulnerabilities/components/issue_link.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import SelectionSummary from './selection_summary.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';

export const SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY =
  'vulnerability_list_scanner_alert_dismissed';

export default {
  name: 'VulnerabilityList',
  components: {
    GlFormCheckbox,
    GlLink,
    GlSkeletonLoading,
    GlSprintf,
    GlTable,
    IssueLink,
    LocalStorageSync,
    RemediatedBadge,
    SecurityScannerAlert,
    SelectionSummary,
    SeverityBadge,
    VulnerabilityCommentIcon,
    FiltersProducedNoResults,
    DashboardHasNoVulnerabilities,
  },
  props: {
    filters: {
      type: Object,
      required: false,
      default: () => ({}),
    },
    shouldShowIdentifier: {
      type: Boolean,
      required: false,
      default: false,
    },
    shouldShowReportType: {
      type: Boolean,
      required: false,
      default: false,
    },
    securityScanners: {
      type: Object,
      required: false,
      default: () => ({}),
    },
    shouldShowSelection: {
      type: Boolean,
      required: false,
      default: true,
    },
    vulnerabilities: {
      type: Array,
      required: true,
    },
    isLoading: {
      type: Boolean,
      required: false,
      default: false,
    },
    shouldShowProjectNamespace: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
  data() {
    return {
      selectedVulnerabilities: {},
      scannerAlertDismissed: 'false',
    };
  },
  computed: {
    notEnabledSecurityScanners() {
      const { available = [], enabled = [] } = this.securityScanners;
      return difference(available, enabled);
    },
    noPipelineRunSecurityScanners() {
      const { enabled = [], pipelineRun = [] } = this.securityScanners;
      return difference(enabled, pipelineRun);
    },
    shouldShowScannersAlert() {
      return (
        this.scannerAlertDismissed !== 'true' &&
        (this.notEnabledSecurityScanners.length > 0 ||
          this.noPipelineRunSecurityScanners.length > 0)
      );
    },
    hasAnyFiltersSelected() {
      return Object.keys(this.filters).length > 0;
    },
    hasSelectedAllVulnerabilities() {
      if (!this.vulnerabilities.length) {
        return false;
      }
      return this.numOfSelectedVulnerabilities === this.vulnerabilities.length;
    },
    numOfSelectedVulnerabilities() {
      return Object.keys(this.selectedVulnerabilities).length;
    },
    shouldShowSelectionSummary() {
      return this.shouldShowSelection && Boolean(this.numOfSelectedVulnerabilities);
    },
    theadClass() {
      return this.shouldShowSelectionSummary ? 'below-selection-summary' : '';
    },
    fields() {
      const commonThClass = ['table-th-transparent', 'original-gl-th', 'gl-bg-white!'].join(' ');
      const baseFields = [
        {
          key: 'state',
          label: s__('Vulnerability|Status'),
          thClass: `vulnerability-td ${commonThClass}`,
        },
        {
          key: 'severity',
          label: s__('Vulnerability|Severity'),
          thClass: `vulnerability-td ${commonThClass}`,
        },
        {
          key: 'title',
          label: __('Description'),
          thClass: commonThClass,
          tdClass: 'gl-word-break-all',
        },
      ];

      if (this.shouldShowSelection) {
        baseFields.unshift({
          key: 'checkbox',
          thClass: `gl-w-9 ${commonThClass}`,
        });
      }

      if (this.shouldShowIdentifier) {
        baseFields.push({
          key: 'identifier',
          label: s__('Vulnerability|Identifier'),
          thClass: commonThClass,
          tdClass: 'gl-word-break-all',
        });
      }

      if (this.shouldShowReportType) {
        baseFields.push({
          key: 'reportType',
          label: s__('Reports|Scanner'),
          thClass: commonThClass,
        });
      }
      return baseFields;
    },
  },
  watch: {
    filters() {
      this.selectedVulnerabilities = {};
    },
    vulnerabilities(vulnerabilities) {
      const ids = new Set(vulnerabilities.map(v => v.id));

      Object.keys(this.selectedVulnerabilities).forEach(vulnerabilityId => {
        if (!ids.has(vulnerabilityId)) {
          this.$delete(this.selectedVulnerabilities, vulnerabilityId);
        }
      });
    },
  },
  methods: {
    createLocationString(location) {
      const { image, file, startLine } = location;

      if (image) {
        return image;
      }

      if (file && startLine) {
        return `${file} ${sprintf(__('(line: %{startLine})'), { startLine })}`;
      }

      return file;
    },
    deselectAllVulnerabilities() {
      this.selectedVulnerabilities = {};
    },
    extraIdentifierCount(identifiers) {
      return identifiers?.length - 1;
    },
    primaryIdentifier(identifiers) {
      return getPrimaryIdentifier(identifiers, 'externalType');
    },
    setScannerAlertDismissed(value) {
      this.scannerAlertDismissed = value;
    },
    isSelected(vulnerability = {}) {
      return Boolean(this.selectedVulnerabilities[vulnerability.id]);
    },
    selectAllVulnerabilities() {
      this.selectedVulnerabilities = this.vulnerabilities.reduce((acc, curr) => {
        acc[curr.id] = curr;
        return acc;
      }, {});
    },
    shouldShowExtraIdentifierCount(identifiers) {
      return identifiers?.length > 1;
    },
    shouldShowVulnerabilityPath(item) {
      return Boolean(item.location.image || item.location.file);
    },
    toggleAllVulnerabilities() {
      if (this.hasSelectedAllVulnerabilities) {
        this.deselectAllVulnerabilities();
      } else {
        this.selectAllVulnerabilities();
      }
    },
    toggleVulnerability(vulnerability) {
      if (!vulnerability) return;

      if (this.selectedVulnerabilities[vulnerability.id]) {
        this.$delete(this.selectedVulnerabilities, `${vulnerability.id}`);
      } else {
        this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability);
      }
    },
    issue(item) {
      return item.issueLinks?.nodes[0]?.issue;
    },
    hasComments(item) {
      return item.userNotesCount > 0;
    },
    useConvertReportType(reportType) {
      return convertReportType(reportType);
    },
  },
  VULNERABILITIES_PER_PAGE,
  SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
};
</script>

<template>
  <div class="vulnerability-list">
    <local-storage-sync
      :value="scannerAlertDismissed"
      :storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY"
      @input="setScannerAlertDismissed"
    />
    <selection-summary
      v-if="shouldShowSelectionSummary"
      :selected-vulnerabilities="Object.values(selectedVulnerabilities)"
      @deselect-all-vulnerabilities="deselectAllVulnerabilities"
      @refetch-vulnerabilities="$emit('refetch-vulnerabilities')"
    />
    <gl-table
      :busy="isLoading"
      :fields="fields"
      :items="vulnerabilities"
      :thead-class="theadClass"
      stacked="sm"
      show-empty
      responsive
    >
      <template #head(checkbox)>
        <gl-form-checkbox
          class="mr-0 mb-0"
          data-testid="vulnerability-checkbox-all"
          :checked="hasSelectedAllVulnerabilities"
          @change="toggleAllVulnerabilities"
        />
      </template>

      <template v-if="shouldShowScannersAlert" #top-row>
        <td :colspan="fields.length">
          <security-scanner-alert
            :not-enabled-scanners="notEnabledSecurityScanners"
            :no-pipeline-run-scanners="noPipelineRunSecurityScanners"
            @dismiss="setScannerAlertDismissed('true')"
          />
        </td>
      </template>

      <template #cell(checkbox)="{ item }">
        <gl-form-checkbox
          class="d-inline-block mr-0 mb-0"
          data-testid="vulnerability-checkbox"
          :checked="isSelected(item)"
          @change="toggleVulnerability(item)"
        />
      </template>

      <template #cell(state)="{ item }">
        <span class="text-capitalize js-status">{{ item.state.toLowerCase() }}</span>
      </template>

      <template #cell(severity)="{ item }">
        <severity-badge class="js-severity" :severity="item.severity" />
      </template>

      <template #cell(title)="{ item }">
        <div
          class="gl-display-flex gl-flex-direction-column flex-sm-row gl-align-items-end align-items-sm-center"
        >
          <gl-link
            class="text-body js-description"
            :href="item.vulnerabilityPath"
            data-qa-selector="vulnerability"
            :data-qa-vulnerability-description="`${item.title}`"
          >
            {{ item.title }}
          </gl-link>
          <issue-link v-if="issue(item)" :issue="issue(item)" />
          <vulnerability-comment-icon v-if="hasComments(item)" :vulnerability="item" />
        </div>
        <div
          v-if="item.location"
          :data-testid="`location-${item.id}`"
          class="gl-text-color-secondary gl-font-sm"
        >
          <div v-if="shouldShowProjectNamespace">
            {{ item.project.nameWithNamespace }}
          </div>
          <div v-if="shouldShowVulnerabilityPath(item)" class="monospace">
            {{ createLocationString(item.location) }}
          </div>
        </div>
        <remediated-badge v-if="item.resolvedOnDefaultBranch" class="ml-2" />
      </template>

      <template #cell(identifier)="{ item }">
        <div data-testid="vulnerability-identifier">
          {{ primaryIdentifier(item.identifiers) }}
        </div>
        <div
          v-if="shouldShowExtraIdentifierCount(item.identifiers)"
          data-testid="vulnerability-more-identifiers"
          class="gl-text-gray-300"
        >
          <gl-sprintf :message="__('+ %{count} more')">
            <template #count>
              {{ extraIdentifierCount(item.identifiers) }}
            </template>
          </gl-sprintf>
        </div>
      </template>

      <template #cell(reportType)="{ item }">
        <div data-testid="vulnerability-report-type" class="text-capitalize">
          {{ useConvertReportType(item.reportType) }}
        </div>
        <div data-testid="vulnerability-vendor" class="gl-text-gray-300">
          {{ item.scanner.vendor }}
        </div>
      </template>

      <template #table-busy>
        <gl-skeleton-loading
          v-for="n in $options.VULNERABILITIES_PER_PAGE"
          :key="n"
          class="m-2 js-skeleton-loader"
          :lines="2"
        />
      </template>

      <template #empty>
        <filters-produced-no-results v-if="hasAnyFiltersSelected && !isLoading" />
        <dashboard-has-no-vulnerabilities v-else-if="!isLoading" />
      </template>
    </gl-table>
  </div>
</template>
<style>
.vulnerability-td {
  width: 8rem;
}
</style>
