# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::PublicRelease::Release do
  include MetadataHelper

  let(:client) { class_spy(ReleaseTools::GitlabClient) }
  let(:version) { ReleaseTools::Version.new('1.2.3') }

  let(:release) do
    Klass.new(version, client: client)
  end

  before do
    stub_const(
      'Klass',
      Class.new do
        include ReleaseTools::PublicRelease::Release

        attr_reader :version, :client, :release_metadata

        def initialize(
          version,
          client: ReleaseTools::GitlabClient,
          release_metadata: ReleaseTools::ReleaseMetadata.new
        )
          @version = version
          @client = client
          @release_metadata = release_metadata
        end

        def project
          ReleaseTools::Project::GitlabCe
        end

        def source_for_target_branch
          'master'
        end
      end
    )
  end

  describe '#project_path' do
    context 'for a regular release' do
      it 'returns the regular project path' do
        expect(release.project_path).to eq(release.project.path)
      end
    end

    context 'for a patch release' do
      it 'returns the security path' do
        ReleaseTools::SharedStatus.as_security_release do
          expect(release.project_path).to eq(release.project.security_path)
        end
      end
    end
  end

  describe '#tag_name' do
    it 'returns the name of the tag to create' do
      expect(release.tag_name).to eq('v1.2.3')
    end
  end

  describe '#target_branch' do
    it 'returns the name of the target branch' do
      expect(release.target_branch).to eq('1-2-stable')
    end
  end

  describe '#create_target_branch' do
    it 'creates the target branch' do
      expect(client)
        .to receive(:find_or_create_branch)
        .with('1-2-stable', 'master', release.project_path)

      without_dry_run { release.create_target_branch }
    end

    context 'with-dry-run' do
      it 'skips API call' do
        expect(client).not_to receive(:find_or_create_branch)

        release.create_target_branch
      end
    end
  end

  describe '#notify_slack' do
    it 'sends a notification to Slack' do
      expect(ReleaseTools::Slack::TagNotification)
        .to receive(:release)
        .with(release.project, "v#{release.version}")

      release.notify_slack(release.project, release.version)
    end
  end

  describe '#commit_version_files' do
    context 'when a version file exists' do
      it 'updates it' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("1.2.3\n")

        allow(client)
          .to receive(:create_commit)
          .with(
            release.project_path,
            branch,
            'Update VERSION files',
            [{ action: 'update', file_path: 'VERSION', content: '4.5.6' }]
          )

        release.commit_version_files(branch, { 'VERSION' => '4.5.6' })

        expect(client).to have_received(:create_commit)
      end

      it 'supports skipping of CI builds' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("1.2.3\n")

        allow(client)
          .to receive(:create_commit)
          .with(
            release.project_path,
            branch,
            "Update VERSION files\n\n[ci skip]",
            [{ action: 'update', file_path: 'VERSION', content: '4.5.6' }]
          )

        release
          .commit_version_files(branch, { 'VERSION' => '4.5.6' }, skip_ci: true)

        expect(client).to have_received(:create_commit)
      end

      it 'supports skipping of Merge Train syncs' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("1.2.3\n")

        allow(client)
          .to receive(:create_commit)
          .with(
            release.project_path,
            branch,
            "Update VERSION files\n\n[merge-train skip]",
            [{ action: 'update', file_path: 'VERSION', content: '4.5.6' }]
          )

        release.commit_version_files(
          branch,
          { 'VERSION' => '4.5.6' },
          skip_merge_train: true
        )

        expect(client).to have_received(:create_commit)
      end

      it 'supports skipping of Merge Train syncs and CI builds' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("1.2.3\n")

        allow(client)
          .to receive(:create_commit)
          .with(
            release.project_path,
            branch,
            "Update VERSION files\n\n[ci skip]\n[merge-train skip]",
            [{ action: 'update', file_path: 'VERSION', content: '4.5.6' }]
          )

        release.commit_version_files(
          branch,
          { 'VERSION' => '4.5.6' },
          skip_ci: true,
          skip_merge_train: true
        )

        expect(client).to have_received(:create_commit)
      end
    end

    context 'when a version file does not exist' do
      it 'creates it' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .and_raise(gitlab_error(:NotFound))

        allow(client)
          .to receive(:create_commit)
          .with(
            release.project_path,
            branch,
            'Update VERSION files',
            [{ action: 'create', file_path: 'VERSION', content: '4.5.6' }]
          )

        release.commit_version_files(branch, { 'VERSION' => '4.5.6' })

        expect(client).to have_received(:create_commit)
      end
    end

    context 'when there are no changes' do
      it 'does not commit anything' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("4.5.6\n")

        allow(client).to receive(:create_commit)

        release.commit_version_files(branch, { 'VERSION' => '4.5.6' })

        expect(client).not_to have_received(:create_commit)
      end
    end

    context 'when there are no changes but the new version contains a trailing newline' do
      it 'does not commit anything' do
        branch = 'master'

        allow(client)
          .to receive(:file_contents)
          .with(release.project_path, 'VERSION', branch)
          .and_return("4.5.6\n")

        allow(client).to receive(:create_commit)

        release.commit_version_files(branch, { 'VERSION' => "4.5.6\n" })

        expect(client).not_to have_received(:create_commit)
      end
    end
  end

  describe '#last_production_commit' do
    context 'when release_metadata_as_source is disabled' do
      it 'returns the sha from the deployments' do
        allow(release)
          .to receive(:last_production_commit_deployments)
          .and_return('1234abcd')

        expect(release)
          .not_to receive(:last_production_commit_metadata)

        expect(release.last_production_commit).to eq('1234abcd')
      end
    end

    context 'when release_metadata_as_source is enabled' do
      before do
        enable_feature('release_metadata_as_source')
      end

      it 'compares the two commits' do
        allow(release)
          .to receive_messages(last_production_commit_deployments: '1234abcd', last_production_commit_metadata: '1234abcd')

        expect(release.last_production_commit).to eq('1234abcd')
      end

      context 'when the commits are different' do
        it 'returns the commit from deployments' do
          allow(release)
            .to receive_messages(last_production_commit_deployments: 'foo', last_production_commit_metadata: 'baz')

          expect(release.last_production_commit).to eq('foo')
        end
      end
    end
  end

  describe '#last_production_commit_deployments' do
    context 'when there are production deployments' do
      it 'returns the SHA of the last deployment' do
        expect(client)
          .to receive(:deployments)
          .with(release.project.path, 'gprd', { status: 'success' })
          .and_return([double(:deploy, sha: '123')])

        expect(release.last_production_commit_deployments).to eq('123')
      end
    end

    context 'when there are no production deployments' do
      it 'raises RuntimeError' do
        expect(client)
          .to receive(:deployments)
          .with(release.project.path, 'gprd', { status: 'success' })
          .and_return([])

        expect do
          release.last_production_commit_deployments
        end.to raise_error(RuntimeError)
      end
    end
  end

  describe '#last_production_commit_metadata' do
    let(:client) { class_spy(ReleaseTools::GitlabOpsClient) }

    before do
      stub_const('ReleaseTools::GitlabOpsClient', client)

      product_version = ReleaseTools::ProductVersion.new('42.1.2021110116')

      allow(client)
        .to receive(:deployments)
        .and_return([create(:deployment, :success)])

      allow(ReleaseTools::ProductVersion)
        .to receive(:from_metadata_sha)
        .and_return(product_version)

      allow(product_version)
        .to receive(:metadata)
        .and_return(build_metadata(gitlab_sha: '1234abcd'))
    end

    it 'fetches sha from metadata' do
      allow(release)
        .to receive(:project)
        .and_return(ReleaseTools::Project::GitlabEe)

      expect(release.last_production_commit_metadata).to eq('1234abcd')
    end
  end

  describe '#notify_stable_branch_creation' do
    let(:version) { ReleaseTools::Version.new('1.2.0-rc42') }

    let(:release_managers_schedule) do
      instance_spy(
        ReleaseTools::ReleaseManagers::Schedule,
        active_version: ReleaseTools::Version.new('1.2.0')
      )
    end

    let(:release) { KlassGitaly.new(version, client: client) }

    before do
      stub_const('KlassGitaly',
                 Class.new(Klass) do
                   def project
                     ReleaseTools::Project::Gitaly
                   end
                 end)

      allow(ReleaseTools::ReleaseManagers::Schedule)
        .to receive(:new)
        .and_return(release_managers_schedule)

      allow(ReleaseTools::GitlabClient)
        .to receive(:find_branch)
        .and_return(build(:branch))
    end

    context 'with valid version, project and branch' do
      it 'calls Security::NotifyStableBranchCreation' do
        expect(ReleaseTools::Security::NotifyStableBranchCreation)
          .to receive(:new)
          .with(
            instance_of(ReleaseTools::Security::IssueCrawler),
            ReleaseTools::Project::Gitaly,
            '1-2-stable'
          )
          .and_return(instance_spy(ReleaseTools::Security::NotifyStableBranchCreation, execute: nil))

        release.notify_stable_branch_creation
      end
    end

    context 'with non managed project' do
      before do
        stub_const('KlassGitaly',
                   Class.new(Klass) do
                     def project
                       ReleaseTools::Project::GitlabCe
                     end
                   end)
      end

      it 'does not call NotifyStableBranchCreation' do
        expect(ReleaseTools::Security::NotifyStableBranchCreation)
          .not_to receive(:new)

        release.notify_stable_branch_creation
      end
    end

    context 'with non RC version' do
      let(:version) { ReleaseTools::Version.new('1.2.0') }

      it 'does not call NotifyStableBranchCreation' do
        expect(ReleaseTools::Security::NotifyStableBranchCreation)
          .not_to receive(:new)

        release.notify_stable_branch_creation
      end
    end

    context 'when branch does not exist' do
      before do
        allow(ReleaseTools::GitlabClient)
          .to receive(:find_branch)
          .and_return(nil)
      end

      it 'does not call NotifyStableBranchCreation' do
        expect(ReleaseTools::Security::NotifyStableBranchCreation)
          .not_to receive(:new)

        release.notify_stable_branch_creation
      end
    end
  end
end
