# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Discussions::UpdateDiffPositionService, feature_category: :code_review_workflow do
  let(:project) { create(:project, :repository) }
  let(:current_user) { project.first_owner }
  let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
  let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
  let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }

  let(:path) { "files/ruby/popen.rb" }

  let(:old_diff_refs) do
    Gitlab::Diff::DiffRefs.new(
      base_sha: create_commit.parent_id,
      head_sha: modify_commit.sha
    )
  end

  let(:new_diff_refs) do
    Gitlab::Diff::DiffRefs.new(
      base_sha: create_commit.parent_id,
      head_sha: edit_commit.sha
    )
  end

  subject do
    described_class.new(
      project,
      current_user,
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: [path]
    )
  end

  # old diff:
  #     1 + require 'fileutils'
  #     2 + require 'open3'
  #     3 +
  #     4 + module Popen
  #     5 +   extend self
  #     6 +
  #     7 +   def popen(cmd, path=nil)
  #     8 +     unless cmd.is_a?(Array)
  #     9 +       raise "System commands must be given as an array of strings"
  #    10 +     end
  #    11 +
  #    12 +     path ||= Dir.pwd
  #    13 +     vars = { "PWD" => path }
  #    14 +     options = { chdir: path }
  #    15 +
  #    16 +     unless File.directory?(path)
  #    17 +       FileUtils.mkdir_p(path)
  #    18 +     end
  #    19 +
  #    20 +     @cmd_output = ""
  #    21 +     @cmd_status = 0
  #    22 +     Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  #    23 +       @cmd_output << stdout.read
  #    24 +       @cmd_output << stderr.read
  #    25 +       @cmd_status = wait_thr.value.exitstatus
  #    26 +     end
  #    27 +
  #    28 +     return @cmd_output, @cmd_status
  #    29 +   end
  #    30 + end
  #
  # new diff:
  #     1 + require 'fileutils'
  #     2 + require 'open3'
  #     3 +
  #     4 + module Popen
  #     5 +   extend self
  #     6 +
  #     7 +   def popen(cmd, path=nil)
  #     8 +     unless cmd.is_a?(Array)
  #     9 +       raise RuntimeError, "System commands must be given as an array of strings"
  #    10 +     end
  #    11 +
  #    12 +     path ||= Dir.pwd
  #    13 +
  #    14 +     vars = {
  #    15 +       "PWD" => path
  #    16 +     }
  #    17 +
  #    18 +     options = {
  #    19 +       chdir: path
  #    20 +     }
  #    21 +
  #    22 +     unless File.directory?(path)
  #    23 +       FileUtils.mkdir_p(path)
  #    24 +     end
  #    25 +
  #    26 +     @cmd_output = ""
  #    27 +     @cmd_status = 0
  #    28 +
  #    29 +     Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  #    30 +       @cmd_output << stdout.read
  #    31 +       @cmd_output << stderr.read
  #    32 +       @cmd_status = wait_thr.value.exitstatus
  #    33 +     end
  #    34 +
  #    35 +     return @cmd_output, @cmd_status
  #    36 +   end
  #    37 + end
  #
  # old->new diff:
  # .. .. @@ -6,12 +6,18 @@ module Popen
  #  6  6
  #  7  7    def popen(cmd, path=nil)
  #  8  8      unless cmd.is_a?(Array)
  #  9    -      raise "System commands must be given as an array of strings"
  #     9 +      raise RuntimeError, "System commands must be given as an array of strings"
  # 10 10      end
  # 11 11
  # 12 12      path ||= Dir.pwd
  # 13    -    vars = { "PWD" => path }
  # 14    -    options = { chdir: path }
  #    13 +
  #    14 +    vars = {
  #    15 +      "PWD" => path
  #    16 +    }
  #    17 +
  #    18 +    options = {
  #    19 +      chdir: path
  #    20 +    }
  # 15 21
  # 16 22      unless File.directory?(path)
  # 17 23        FileUtils.mkdir_p(path)
  # 18 24      end
  # 19 25
  # 20 26      @cmd_output = ""
  # 21 27      @cmd_status = 0
  #    28 +
  # 22 29      Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  # 23 30        @cmd_output << stdout.read
  # 24 31        @cmd_output << stderr.read
  # .. ..

  describe "#execute" do
    let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion }

    let(:old_position) do
      Gitlab::Diff::Position.new(
        old_path: path,
        new_path: path,
        old_line: nil,
        new_line: line,
        diff_refs: old_diff_refs
      )
    end

    shared_examples 'outdated diff note' do
      it "doesn't update the position" do
        subject.execute(discussion)

        expect(discussion.original_position).to eq(old_position)
        expect(discussion.position).to eq(old_position)
      end

      it 'sets the change position' do
        subject.execute(discussion)

        change_position = discussion.change_position
        expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
        expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
        expect(change_position.formatter.old_line).to eq(9)
        expect(change_position.formatter.new_line).to be_nil
      end

      it 'creates a system discussion' do
        expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
          discussion, project, current_user, instance_of(Gitlab::Diff::Position))

        subject.execute(discussion)
      end
    end

    context "when the diff line is the same" do
      let(:line) { 16 }

      it "updates the position" do
        subject.execute(discussion)

        expect(discussion.original_position).to eq(old_position)
        expect(discussion.position).not_to eq(old_position)
        expect(discussion.position.formatter.new_line).to eq(22)
      end

      context 'when the resolve_outdated_diff_discussions setting is set' do
        before do
          project.update!(resolve_outdated_diff_discussions: true)
        end

        it 'does not resolve the discussion' do
          subject.execute(discussion)

          expect(discussion).not_to be_resolved
          expect(discussion).not_to be_resolved_by_push
        end
      end
    end

    context "when the diff line has changed" do
      let(:line) { 9 }

      include_examples 'outdated diff note'

      context 'when the resolve_outdated_diff_discussions setting is set' do
        before do
          project.update!(resolve_outdated_diff_discussions: true)
        end

        it 'sets resolves the discussion and sets resolved_by_push' do
          subject.execute(discussion)

          expect(discussion).to be_resolved
          expect(discussion).to be_resolved_by_push
        end

        include_examples 'outdated diff note'
      end
    end

    context 'when position tracer returns nil for outdated discussion' do
      let(:line) { 9 }
      let(:tracer_double) { instance_double(Gitlab::Diff::PositionTracer) }

      before do
        allow(Gitlab::Diff::PositionTracer).to receive(:new).and_return(tracer_double)
        allow(tracer_double).to receive(:trace).and_return({ position: nil, outdated: true })
      end

      it "doesn't update the position" do
        subject.execute(discussion)

        expect(discussion.original_position).to eq(old_position)
        expect(discussion.position).to eq(old_position)
      end

      it 'creates a system discussion' do
        expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
          discussion, project, current_user, nil)

        subject.execute(discussion)
      end
    end
  end
end
