<script>
import {
  GlFormCheckbox,
  GlLink,
  GlSprintf,
  GlTruncate,
  GlSkeletonLoading,
  GlTooltipDirective,
  GlTable,
} from '@gitlab/ui';
import { difference } from 'lodash';
import AutoFixHelpText from 'ee/security_dashboard/components/auto_fix_help_text.vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/empty_states/filters_produced_no_results.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.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 RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';
import IssuesBadge from './issues_badge.vue';
import SecurityScannerAlert from './security_scanner_alert.vue';
import SelectionSummary from './selection_summary.vue';

export const SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY =
  'vulnerability_list_scanner_alert_dismissed';

export default {
  components: {
    GlFormCheckbox,
    GlLink,
    GlSkeletonLoading,
    GlSprintf,
    GlTable,
    GlTruncate,
    IssuesBadge,
    LocalStorageSync,
    AutoFixHelpText,
    RemediatedBadge,
    SecurityScannerAlert,
    SelectionSummary,
    SeverityBadge,
    VulnerabilityCommentIcon,
    FiltersProducedNoResults,
    DashboardHasNoVulnerabilities,
  },
  directives: {
    GlTooltip: GlTooltipDirective,
  },
  inject: {
    hasVulnerabilities: {
      default: false,
    },
    hasJiraVulnerabilitiesIntegrationEnabled: {
      default: false,
    },
  },

  props: {
    filters: {
      type: Object,
      required: false,
      default: () => ({}),
    },
    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',
      sortBy: 'severity',
      sortDesc: true,
    };
  },
  computed: {
    // This is a workaround to remove vulnerabilities from the list when their state has changed
    // through the bulk update feature, but no longer matches the filters. For more details:
    // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43468#note_420050017
    filteredVulnerabilities() {
      return this.vulnerabilities.filter((x) =>
        this.filters.state?.length ? this.filters.state.includes(x.state) : true,
      );
    },
    isSortable() {
      return Boolean(this.$listeners['sort-changed']);
    },
    hasAnyScannersOtherThanGitLab() {
      return this.filteredVulnerabilities.some(
        (v) => v.scanner?.vendor !== 'GitLab' && v.scanner?.vendor !== '',
      );
    },
    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)
      );
    },
    hasSelectedAllVulnerabilities() {
      if (!this.filteredVulnerabilities.length) {
        return false;
      }
      return this.numOfSelectedVulnerabilities === this.filteredVulnerabilities.length;
    },
    numOfSelectedVulnerabilities() {
      return Object.keys(this.selectedVulnerabilities).length;
    },
    shouldShowSelectionSummary() {
      return this.shouldShowSelection && this.numOfSelectedVulnerabilities > 0;
    },
    theadClass() {
      return this.shouldShowSelectionSummary ? 'below-selection-summary' : '';
    },
    fields() {
      const baseFields = [
        {
          key: 'detected',
          label: s__('Vulnerability|Detected'),
          class: 'detected',
          sortable: this.isSortable,
        },
        {
          key: 'state',
          label: s__('Vulnerability|Status'),
          class: 'status',
          sortable: this.isSortable,
        },
        {
          key: 'severity',
          label: s__('Vulnerability|Severity'),
          class: 'severity',
          sortable: this.isSortable,
        },
        {
          key: 'title',
          label: __('Description'),
          class: 'description gl-word-break-all',
          sortable: this.isSortable,
        },
        {
          key: 'identifier',
          label: s__('Vulnerability|Identifier'),
          class: 'identifier gl-word-break-all',
        },
        {
          key: 'reportType',
          label: s__('Reports|Scanner'),
          class: 'scanner',
          sortable: this.isSortable,
        },
        {
          key: 'activity',
          label: s__('Vulnerability|Activity'),
          thClass: 'gl-text-right',
          class: 'activity',
        },
      ];

      if (this.shouldShowSelection) {
        baseFields.unshift({
          key: 'checkbox',
          class: 'checkbox',
        });
      }

      // Apply gl-bg-white! to every header.
      baseFields.forEach((field) => {
        field.thClass = [field.thClass, 'gl-bg-white!']; // eslint-disable-line no-param-reassign
      });

      return baseFields;
    },
  },
  watch: {
    filters() {
      this.selectedVulnerabilities = {};
    },
    filteredVulnerabilities() {
      const ids = new Set(this.filteredVulnerabilities.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, path } = location;

      if (image) {
        return image;
      }

      if (file && startLine) {
        return `${file}:${startLine}`;
      }

      if (path) {
        return path;
      }

      return file;
    },
    deselectVulnerability(vulnerabilityId) {
      this.$delete(this.selectedVulnerabilities, vulnerabilityId);
    },
    deselectAllVulnerabilities() {
      this.selectedVulnerabilities = {};
    },
    extraIdentifierCount(identifiers) {
      return identifiers?.length - 1;
    },
    fileUrl(vulnerability) {
      const { startLine: start, endLine: end, blobPath } = vulnerability.location;
      const lineNumber = end > start ? `${start}-${end}` : start;

      if (!blobPath) {
        return '';
      }

      return `${blobPath}${lineNumber ? `#L${lineNumber}` : ''}`;
    },
    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 || item.location.path);
    },
    toggleAllVulnerabilities() {
      if (this.hasSelectedAllVulnerabilities) {
        this.deselectAllVulnerabilities();
      } else {
        this.selectAllVulnerabilities();
      }
    },
    toggleVulnerability(vulnerability) {
      if (this.selectedVulnerabilities[vulnerability.id]) {
        this.$delete(this.selectedVulnerabilities, `${vulnerability.id}`);
      } else {
        this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability);
      }
    },
    gitlabIssues(item) {
      return item.issueLinks?.nodes || [];
    },
    externalIssues(item) {
      return item.externalIssueLinks?.nodes || [];
    },
    jiraIssues(item) {
      return this.externalIssues(item).filter(({ issue }) => issue?.externalTracker === 'jira');
    },
    badgeIssues(item) {
      return this.hasJiraVulnerabilitiesIntegrationEnabled
        ? this.jiraIssues(item)
        : this.gitlabIssues(item);
    },
    formatDate(item) {
      return formatDate(item.detectedAt, 'yyyy-mm-dd');
    },
    formatDateTooltip(item) {
      return formatDate(item.detectedAt);
    },
    hasComments(item) {
      return item.userNotesCount > 0;
    },
    useConvertReportType(reportType) {
      return convertReportType(reportType);
    },
    handleSortChange(args) {
      if (args.sortBy) {
        this.$emit('sort-changed', { ...args, sortBy: convertToSnakeCase(args.sortBy) });
      }
    },
    getVulnerabilityState(state = '') {
      const stateName = state.toLowerCase();
      // Use the raw state name if we don't have a localization for it.
      return VULNERABILITY_STATES[stateName] || stateName;
    },
  },
  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)"
      @cancel-selection="deselectAllVulnerabilities"
      @vulnerability-updated="deselectVulnerability"
    />
    <gl-table
      :busy="isLoading"
      :fields="fields"
      :items="filteredVulnerabilities"
      :thead-class="theadClass"
      :sort-desc="sortDesc"
      :sort-by="sortBy"
      sort-icon-left
      no-local-sorting
      stacked="sm"
      class="vulnerability-list"
      show-empty
      responsive
      hover
      primary-key="id"
      :tbody-tr-class="{ 'gl-cursor-pointer': filteredVulnerabilities.length }"
      @sort-changed="handleSortChange"
      @row-clicked="toggleVulnerability"
    >
      <template #head(checkbox)>
        <gl-form-checkbox
          class="gl-m-0"
          data-testid="vulnerability-checkbox-all"
          :checked="hasSelectedAllVulnerabilities"
          @change="toggleAllVulnerabilities"
        />
      </template>

      <template v-if="shouldShowScannersAlert" #top-row>
        <td :colspan="fields.length" class="gl-px-0!">
          <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="gl-display-inline-block! gl-m-0 gl-pointer-events-none"
          data-testid="vulnerability-checkbox"
          :checked="isSelected(item)"
          @change="toggleVulnerability(item)"
        />
      </template>

      <template #cell(detected)="{ item }">
        <time v-gl-tooltip :data-testid="`detected-${item.id}`" :title="formatDateTooltip(item)">
          {{ formatDate(item) }}
        </time>
      </template>

      <template #cell(state)="{ item }">
        <span class="text-capitalize js-status">{{ getVulnerabilityState(item.state) }}</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="gl-text-body vulnerability-title js-description"
            :href="item.vulnerabilityPath"
            :data-qa-vulnerability-description="item.title"
            data-qa-selector="vulnerability"
          >
            {{ item.title }}
          </gl-link>
          <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)">
            <gl-link v-if="item.location.blobPath" :href="fileUrl(item)">
              <gl-truncate :text="createLocationString(item.location)" position="middle" />
            </gl-link>
            <gl-truncate v-else :text="createLocationString(item.location)" position="middle" />
          </div>
        </div>
      </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
          v-if="hasAnyScannersOtherThanGitLab"
          data-testid="vulnerability-vendor"
          class="gl-text-gray-300"
        >
          {{ item.scanner.vendor }}
        </div>
      </template>

      <template #cell(activity)="{ item }">
        <div class="gl-display-flex gl-justify-content-end">
          <auto-fix-help-text v-if="item.mergeRequest" :merge-request="item.mergeRequest" />
          <issues-badge
            v-if="badgeIssues(item).length > 0"
            :issues="badgeIssues(item)"
            :is-jira="hasJiraVulnerabilitiesIntegrationEnabled"
          />
          <remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" />
        </div>
      </template>

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

      <template #empty>
        <filters-produced-no-results v-if="hasVulnerabilities && !isLoading" />
        <dashboard-has-no-vulnerabilities v-else-if="!isLoading" />
      </template>
    </gl-table>
  </div>
</template>
