<script>
import { difference } from 'lodash';
import {
  GlFormCheckbox,
  GlLink,
  GlSprintf,
  GlTable,
  GlSkeletonLoading,
  GlTooltipDirective,
} from '@gitlab/ui';
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 SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityCommentIcon from 'ee/security_dashboard/components/vulnerability_comment_icon.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 SecurityScannerAlert from './security_scanner_alert.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import { s__, __, sprintf } from '~/locale';
import SelectionSummary from './selection_summary.vue';
import IssuesBadge from './issues_badge.vue';
import { VULNERABILITIES_PER_PAGE } from '../store/constants';

export const SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY =
  'vulnerability_list_scanner_alert_dismissed';

export default {
  components: {
    GlFormCheckbox,
    GlLink,
    GlSkeletonLoading,
    GlSprintf,
    GlTable,
    IssuesBadge,
    LocalStorageSync,
    RemediatedBadge,
    SecurityScannerAlert,
    SelectionSummary,
    SeverityBadge,
    VulnerabilityCommentIcon,
    FiltersProducedNoResults,
    DashboardHasNoVulnerabilities,
  },
  directives: {
    GlTooltip: GlTooltipDirective,
  },
  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: {
    isSortable() {
      return Boolean(this.$listeners['sort-changed']);
    },
    hasAnyScannersOtherThanGitLab() {
      return this.vulnerabilities.some(v => v.scanner?.vendor !== 'GitLab');
    },
    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 baseFields = [
        {
          key: 'detected',
          label: s__('Vulnerability|Detected'),
          thClass: 'detected',
          tdClass: 'detected',
          sortable: this.isSortable,
        },
        {
          key: 'state',
          label: s__('Vulnerability|Status'),
          thClass: 'status',
          tdClass: 'status',
        },
        {
          key: 'severity',
          label: s__('Vulnerability|Severity'),
          thClass: 'severity',
          tdClass: 'severity',
          sortable: this.isSortable,
        },
        {
          key: 'title',
          label: __('Description'),
          thClass: 'description',
          tdClass: 'description gl-word-break-all',
          sortable: this.isSortable,
        },
        {
          key: 'identifier',
          label: s__('Vulnerability|Identifier'),
          thClass: 'identifier',
          tdClass: 'identifier gl-word-break-all',
        },
        {
          key: 'reportType',
          label: s__('Reports|Scanner'),
          thClass: 'scanner',
          tdClass: 'scanner',
        },
        {
          key: 'activity',
          label: s__('Vulnerability|Activity'),
          thClass: 'gl-text-right',
        },
      ];

      if (this.shouldShowSelection) {
        baseFields.unshift({
          key: 'checkbox',
          thClass: 'checkbox',
          tdClass: '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 = {};
    },
    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);
      }
    },
    issues(item) {
      return item.issueLinks?.nodes || [];
    },
    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);
      }
    },
  },
  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"
    />
    <gl-table
      :busy="isLoading"
      :fields="fields"
      :items="vulnerabilities"
      :thead-class="theadClass"
      :sort-desc="sortDesc"
      :sort-by="sortBy"
      sort-icon-left
      no-local-sorting
      stacked="sm"
      class="vulnerability-list"
      show-empty
      responsive
      @sort-changed="handleSortChange"
    >
      <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"
          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">{{ item.state.toLowerCase() }}</span>
      </template>

      <template #cell(severity)="{ item }">
        <severity-badge class="js-severity gl-white-space-normal!" :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>
          <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>
      </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">
          <issues-badge :issues="issues(item)" />
          <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="hasAnyFiltersSelected && !isLoading" />
        <dashboard-has-no-vulnerabilities v-else-if="!isLoading" />
      </template>
    </gl-table>
  </div>
</template>
