# frozen_string_literal: true

require 'spec_helper'

describe VulnerabilityFeedback::CreateService, '#execute' do
  let(:group)   { create(:group) }
  let(:project) { create(:project, :public, :repository, namespace: group) }
  let(:user)    { create(:user) }
  let(:pipeline) { create(:ci_pipeline, project: project) }

  before do
    group.add_developer(user)
  end

  context 'when params are valid' do
    let(:feedback_params) do
      {
        feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast',
        project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8',
        comment: 'a dismissal comment',
        vulnerability_data: {
          category: 'sast',
          priority: 'Low', line: '41',
          file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
          cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
          name: 'Predictable pseudorandom number generator',
          description: 'Description of Predictable pseudorandom number generator',
          tool: 'find_sec_bugs'
        }
      }
    end

    context 'when user is not authorized' do
      let(:unauthorized_user) { create(:user) }

      it 'raise error if permission is denied' do
        expect { described_class.new(project, unauthorized_user, feedback_params).execute }
          .to raise_error(Gitlab::Access::AccessDeniedError)
      end
    end

    context 'when feedback_type is dismissal' do
      let(:result) { described_class.new(project, user, feedback_params).execute }

      it 'creates the feedback with the given params' do
        expect(result[:status]).to eq(:success)
        feedback = result[:vulnerability_feedback]
        expect(feedback).to be_persisted
        expect(feedback.project).to eq(project)
        expect(feedback.author).to eq(user)
        expect(feedback.feedback_type).to eq('dismissal')
        expect(feedback.pipeline_id).to eq(pipeline.id)
        expect(feedback.category).to eq('sast')
        expect(feedback.project_fingerprint).to eq('418291a26024a1445b23fe64de9380cdcdfd1fa8')
        expect(feedback.for_dismissal?).to eq(true)
        expect(feedback.for_issue?).to eq(false)
        expect(feedback.issue).to be_nil
        expect(feedback.for_merge_request?).to eq(false)
        expect(feedback.merge_request).to be_nil
      end

      context 'when feedback params has a comment' do
        it 'sets the comment attributes' do
          feedback = result[:vulnerability_feedback]

          expect(feedback.comment).to eq('a dismissal comment')
          expect(feedback.comment_author).to eq(user)
          expect(feedback.comment_timestamp).not_to be_nil
        end
      end

      context 'when feedback params does not have a comment' do
        before do
          feedback_params[:comment] = nil
        end

        it 'does not set comment attributes' do
          feedback = result[:vulnerability_feedback]

          expect(feedback.comment).to be_nil
          expect(feedback.comment_author).to be_nil
          expect(feedback.comment_timestamp).to be_nil
        end
      end
    end

    context 'when feedback_type is issue' do
      let(:result) do
        described_class.new(
          project,
          user,
          feedback_params.merge(feedback_type: 'issue')
        ).execute
      end

      it 'creates the feedback with the given params' do
        expect(result[:status]).to eq(:success)
        feedback = result[:vulnerability_feedback]
        expect(feedback).to be_persisted
        expect(feedback.project).to eq(project)
        expect(feedback.author).to eq(user)
        expect(feedback.feedback_type).to eq('issue')
        expect(feedback.pipeline_id).to eq(pipeline.id)
        expect(feedback.category).to eq('sast')
        expect(feedback.project_fingerprint).to eq('418291a26024a1445b23fe64de9380cdcdfd1fa8')
        expect(feedback.for_dismissal?).to eq(false)
        expect(feedback.for_issue?).to eq(true)
        expect(feedback.issue).to be_an(Issue)
        expect(feedback.for_merge_request?).to eq(false)
        expect(feedback.merge_request).to be_nil
      end

      it 'updates the feedback when it already exists' do
        result

        expect { described_class.new(project, user, feedback_params.merge(feedback_type: 'issue')).execute }.not_to change(Vulnerabilities::Feedback, :count)
      end

      it 'creates a new issue when feedback already exists and issue has been deleted' do
        result

        expect { result[:vulnerability_feedback].issue.destroy}.to change(Issue, :count).by(-1)
        expect { described_class.new(project, user, feedback_params.merge(feedback_type: 'issue')).execute }.to change(Issue, :count).by(1)
      end

      it 'delegates the Issue creation to CreateFromVulnerabilityDataService' do
        expect_any_instance_of(Issues::CreateFromVulnerabilityDataService)
          .to receive(:execute).once.and_call_original

        expect(result[:status]).to eq(:success)
      end
    end

    context 'when feedback_type is merge_request' do
      let(:remediations_folder) { Rails.root.join('ee/spec/fixtures/security_reports/remediations') }
      let(:yarn_lock_content) do
        File.read(
          File.join(remediations_folder, "yarn.lock")
        )
      end

      let(:project) do
        create(:project, :custom_repo, namespace: group, files: { 'yarn.lock' => yarn_lock_content })
      end

      let(:remediation_diff) do
        Base64.encode64(
          File.read(
            File.join(remediations_folder, "remediation.patch")
          )
        )
      end

      let(:result) do
        params = feedback_params.merge(
          feedback_type: 'merge_request',
          category: 'dependency_scanning'
        )
        params[:vulnerability_data][:category] = 'dependency_scanning'
        params[:vulnerability_data][:remediations] = [
          { diff: remediation_diff }
        ]

        described_class.new(
          project,
          user,
          params
        ).execute
      end

      it 'creates the feedback with the given params' do
        expect(result[:status]).to eq(:success)
        feedback = result[:vulnerability_feedback]
        expect(feedback).to be_persisted
        expect(feedback.project).to eq(project)
        expect(feedback.author).to eq(user)
        expect(feedback.feedback_type).to eq('merge_request')
        expect(feedback.pipeline_id).to eq(pipeline.id)
        expect(feedback.category).to eq('dependency_scanning')
        expect(feedback.project_fingerprint).to eq('418291a26024a1445b23fe64de9380cdcdfd1fa8')
        expect(feedback.for_dismissal?).to eq(false)
        expect(feedback.for_issue?).to eq(false)
        expect(feedback.issue).to be_nil
        expect(feedback.for_merge_request?).to eq(true)
        expect(feedback.merge_request).to be_an(MergeRequest)
      end

      it 'delegates the MergeRequest creation to CreateFromVulnerabilityDataService' do
        expect_next_instance_of(MergeRequests::CreateFromVulnerabilityDataService) do |service|
          expect(service).to receive(:execute).once.and_call_original
        end

        expect(result[:status]).to eq(:success)
      end

      it 'destroys merge_request and branch if feedback fails to persist' do
        expect_next_instance_of(Vulnerabilities::Feedback) do |feedback|
          expect(feedback).to receive(:save).and_return(false)
        end

        expect(result[:status]).to eq(:error)
        expect(Vulnerabilities::Feedback.count).to eq 0
        expect(MergeRequest.count).to eq 0

        branches = BranchesFinder.new(project.repository, {}).execute
        expect(branches.length).to eq 1
      end
    end
  end

  context 'when params are invalid' do
    context 'when vulnerability_data params is missing and feedback_type is issue' do
      let(:feedback_params) do
        {
          feedback_type: 'issue', pipeline_id: pipeline.id, category: 'sast',
          project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8'
        }
      end

      let(:result) { described_class.new(project, user, feedback_params).execute }

      it 'returns error with correct message' do
        expect(result[:status]).to eq(:error)
        expect(result[:message][:vulnerability_data]).to eq(["can't be blank"])
      end
    end

    context 'when feedback_type is invalid' do
      let(:feedback_params) do
        {
          feedback_type: 'foo', pipeline_id: pipeline.id, category: 'sast',
          project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8'
        }
      end

      let(:result) { described_class.new(project, user, feedback_params).execute }

      it 'returns error with correct message' do
        expect(result[:status]).to eq(:error)
        expect(result[:message]).to eq("'foo' is not a valid feedback_type")
      end
    end

    context 'when category is invalid' do
      let(:feedback_params) do
        {
          feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'foo',
          project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8'
        }
      end

      let(:result) { described_class.new(project, user, feedback_params).execute }

      it 'returns error with correct message' do
        expect(result[:status]).to eq(:error)
        expect(result[:message]).to eq("'foo' is not a valid category")
      end
    end
  end
end
