# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::RemoteRepository, :slow do
  include RuggedMatchers

  let(:fixture) { ReleaseFixture.new }
  let(:repo_path) { File.join(Dir.tmpdir, fixture.class.repository_name) }
  let(:rugged_repo) { Rugged::Repository.new(repo_path) }
  let(:repo_url) { "file://#{fixture.fixture_path}" }
  let(:repo_remotes) do
    { canonical: repo_url, foo: 'https://example.com/foo/bar/baz.git' }
  end

  def write_file(file, content)
    Dir.chdir(repo_path) do
      File.write(file, content)
    end
  end

  before do
    fixture.rebuild_fixture!
  end

  describe '.get' do
    let(:remotes) do
      {
        dev:    'https://example.com/foo/bar/dev.git',
        canonical: 'https://gitlab.com/gitlab-org/foo/gitlab.git'
      }
    end

    it 'generates a name from the first remote' do
      expect(described_class).to receive(:new).with("#{Dir.tmpdir}/dev", anything, anything)

      described_class.get(remotes)
    end

    it 'accepts a repository name' do
      expect(described_class).to receive(:new).with("#{Dir.tmpdir}/foo", anything, anything)

      described_class.get(remotes, 'foo')
    end

    it 'passes remotes to the initializer' do
      expect(described_class).to receive(:new).with(anything, remotes, anything)

      described_class.get(remotes)
    end

    it 'accepts a :global_depth option' do
      expect(described_class).to receive(:new)
        .with(anything, anything, a_hash_including(global_depth: 100))

      described_class.get(remotes, global_depth: 100)
    end
  end

  describe 'initialize' do
    it 'performs cleanup' do
      expect_any_instance_of(described_class).to receive(:cleanup)

      described_class.new(repo_path, {})
    end

    it 'performs a shallow clone of the repository' do
      described_class.new(repo_path, repo_remotes)

      # Note: Rugged has no clean way to do this, so we'll shell out
      expect(`git -C #{repo_path} log --oneline | wc -l`.to_i)
        .to eq(1)
    end

    it 'adds remotes to the repository' do
      expect_any_instance_of(described_class).to receive(:remotes=)
        .with(:remotes)

      described_class.new('foo', :remotes)
    end

    it 'assigns path' do
      repository = described_class.new('foo', {})

      expect(repository.path).to eq 'foo'
    end
  end

  describe '#remotes=' do
    it 'assigns the canonical remote' do
      remotes = { canonical: repo_url }

      repository = described_class.new(repo_path, remotes)

      expect(repository.canonical_remote.name).to eq(:canonical)
      expect(repository.canonical_remote.url).to eq(repo_url)
    end

    it 'assigns remotes' do
      remotes = { canonical: repo_url }

      repository = described_class.new(repo_path, remotes)

      expect(repository.remotes).to eq(remotes)
    end

    it 'adds remotes to the repository', :aggregate_failures do
      remotes = {
        canonical: repo_url,
        foo: '/foo/bar/baz.git'
      }

      repository = described_class.new(repo_path, remotes)
      rugged = Rugged::Repository.new(repository.path)

      expect(rugged.remotes.count).to eq(2)
      expect(rugged.remotes['canonical'].url).to eq(repo_url)
      expect(rugged.remotes['foo'].url).to eq('/foo/bar/baz.git')
    end
  end

  describe '#ensure_branch_exists' do
    subject { described_class.get(repo_remotes) }

    context 'with an existing branch' do
      it 'fetches and checks out the branch with the configured global depth', :aggregate_failures do
        expect(subject.logger).not_to receive(:fatal)

        subject.ensure_branch_exists('branch-1', base: 'master')

        expect(rugged_repo).to have_head('branch-1')
        expect(rugged_repo).to have_blob('README.md').with('Sample README.md')
        expect(`git -C #{repo_path} log --oneline | wc -l`.to_i).to eq(1)
      end
    end

    context 'with a non-existing branch' do
      it 'creates and checks out the branch with the configured global depth', :aggregate_failures do
        expect(subject.logger).to receive(:fatal).and_call_original

        subject.ensure_branch_exists('branch-2', base: 'master')

        expect(rugged_repo).to have_head('branch-2')
        expect(rugged_repo).to have_blob('README.md').with('Sample README.md')
        expect(`git -C #{repo_path} log --oneline | wc -l`.to_i).to eq(1)
      end
    end
  end

  describe '#fetch' do
    subject { described_class.get(repo_remotes) }

    it 'fetches the branch with the default configured global depth' do
      subject.fetch('branch-1')

      expect(`git -C #{repo_path} log --oneline refs/heads/branch-1 | wc -l`.to_i).to eq(1)
    end

    context 'with a depth option given' do
      it 'fetches the branch up to the given depth' do
        subject.fetch('branch-1', depth: 2)

        expect(`git -C #{repo_path} log --oneline refs/heads/branch-1 | wc -l`.to_i).to eq(2)
      end
    end
  end

  describe '#checkout_new_branch' do
    subject { described_class.get(repo_remotes) }

    it 'creates and checks out a new branch' do
      subject.checkout_new_branch('new-branch', base: 'master')

      expect(rugged_repo).to have_version('pages').at('4.5.0')
    end

    context 'with a given base branch' do
      it 'creates and checks out a new branch based on the given base branch' do
        subject.checkout_new_branch('new-branch', base: '9-1-stable')

        expect(rugged_repo).to have_version('pages').at('4.4.4')
      end
    end
  end

  describe '#merge' do
    subject { described_class.get(repo_remotes, global_depth: 10) }

    before do
      subject.fetch('master')
      subject.ensure_branch_exists('branch-1', base: 'master')
      subject.ensure_branch_exists('branch-2', base: 'master')
      write_file('README.md', 'Nice')
      subject.__send__(:commit, 'README.md', message: 'Update README.md')
      subject.ensure_branch_exists('branch-1', base: 'master')
    end

    it 'commits the given files with the given message in the current branch' do
      expect(subject.merge('branch-2', no_ff: true).status).to be_success
      log = subject.__send__(:log, format: :message)

      expect(log).to start_with("Merge branch 'branch-2' into branch-1\n")
      expect(File.read(File.join(repo_path, 'README.md'))).to eq 'Nice'
    end

    it 'performs an octopus merge with an array of commits' do
      write_file('foo', 'bar')
      subject.__send__(:commit, 'foo', message: 'Add foo')

      subject.ensure_branch_exists('branch-3', base: 'master')

      expect(subject.merge(%w[branch-1 branch-2], no_ff: true).status).to be_success
      log = subject.__send__(:log, format: :message)

      expect(File.read(File.join(repo_path, 'README.md'))).to eq 'Nice'
      expect(File.read(File.join(repo_path, 'foo'))).to eq 'bar'
      expect(log).to start_with("Merge branches 'branch-1' and 'branch-2' into branch-3\n")
    end

    context 'in case of conflicts' do
      before do
        subject.ensure_branch_exists('branch-1', base: 'master')
        write_file('README.md', 'branch-1')
        subject.__send__(:commit, 'README.md', message: 'Update README.md')

        subject.ensure_branch_exists('branch-2', base: 'master')
        write_file('foo', 'foo')
        subject.__send__(:commit, 'foo', message: 'Add foo')

        subject.ensure_branch_exists('branch-1', base: 'master')
      end

      it 'fails when no recursive strategy is specified' do
        expect(subject.merge('branch-2', no_ff: true).status).not_to be_success
      end
    end
  end

  describe '#pull' do
    subject { described_class.get(repo_remotes) }

    it 'pulls the branch with the configured depth' do
      expect { subject.pull('master') }
        .not_to(change { subject.__send__(:log, format: :message).lines.size })
    end

    context 'with a depth option given' do
      it 'pulls the branch with to the given depth' do
        expect { subject.pull('master', depth: 2) }
          .to change { subject.__send__(:log, format: :message).lines.size }.from(1).to(2)
      end
    end
  end

  describe '#pull_from_all_remotes' do
    subject { described_class.get(Hash[*repo_remotes.first]) }

    context 'when there are conflicts' do
      it 'raises a CannotPullError error' do
        allow(subject).to receive(:conflicts?).and_return(true)

        expect { subject.pull_from_all_remotes('1-9-stable') }
          .to raise_error(described_class::CannotPullError)
      end
    end

    it 'does not raise error' do
      expect { subject.pull_from_all_remotes('master') }
        .not_to raise_error
    end

    context 'with a depth option given' do
      it 'pulls the branch down to the given depth' do
        expect { subject.pull_from_all_remotes('master', depth: 2) }
          .to change { subject.__send__(:log, format: :message).lines.size }.from(1).to(2)
      end
    end
  end

  describe '#push' do
    subject { described_class.get(repo_remotes, global_depth: 10) }

    let(:remote) { 'canonical' }
    let(:branch) { 'master' }

    context 'when in dry run mode' do
      it 'always returns true' do
        expect(subject.push("non-existent-remote", "non-existent-branch")).to be true
      end
    end

    context 'when not in dry run mode' do
      context 'when push is successful' do
        it 'returns true' do
          without_dry_run do
            expect(subject.push(remote, branch)).to be true
          end
        end
      end

      context 'when feature flag is disabled' do
        before do
          disable_feature(:retry_git_push)
        end

        context 'when push is failed' do
          let(:remote) { "non-existent-remote" }

          it 'returns false' do
            without_dry_run do
              # 3 times - clone, remote, push
              expect(described_class).to receive(:run_git).and_call_original.at_most(3).times

              expect(subject.push(remote, branch)).to be false
            end
          end
        end
      end

      context 'when feature flag is enabled' do
        before do
          enable_feature(:retry_git_push)
        end

        context 'when push is failed' do
          let(:remote) { "non-existent-remote" }

          it 'returns false' do
            without_dry_run do
              # 5 times - clone, remote, push, push, push
              expect(described_class).to receive(:run_git).and_call_original.at_least(5).times

              expect(subject.push(remote, branch)).to be false
            end
          end
        end
      end
    end
  end

  describe '#push_to_all_remotes' do
    subject { described_class.get(Hash[*repo_remotes.first]) }

    it 'returns push status' do
      expect(subject.push_to_all_remotes('master')).to eq([true])
    end
  end

  describe '#cleanup' do
    it 'removes the repository path' do
      repository = described_class.new(repo_path, {})

      expect(FileUtils).to receive(:rm_rf).with(repo_path, { secure: true })

      repository.cleanup
    end
  end

  describe described_class::GitCommandError do
    it 'adds indented output to the error message' do
      error = described_class.new("Foo", "bar\nbaz")

      expect(error.message).to eq "Foo\n\n  bar\n  baz"
    end
  end
end
