# frozen_string_literal: true

require 'spec_helper'
require 'release_tools/tasks'

describe ReleaseTools::PublicRelease::GitlabRelease do
  describe '#initialize' do
    it 'converts CE versions to EE versions' do
      version = ReleaseTools::Version.new('42.0.0')
      release = described_class.new(version)

      expect(release.version).to eq('42.0.0-ee')
    end
  end

  describe '#execute' do
    let(:version) { ReleaseTools::Version.new('42.0.0-ee') }

    subject(:release) { described_class.new(version) }

    it 'runs the release' do
      ce_tag = double(:tag, name: 'v42.0.0')
      ee_tag = double(:tag, name: 'v42.0.0-ee')

      expect(release).to receive(:create_ce_target_branch)
      expect(release).to receive(:create_ee_target_branch)

      expect(release).to receive(:update_monthly_tag_metrics)

      expect(release).to receive(:update_managed_component_versions)
      expect(release).to receive(:compile_changelogs)
      expect(release).to receive(:update_versions)
      expect(release).to receive(:wait_for_ee_to_ce_sync)
      expect(release).to receive(:create_ce_commit_to_run_ci)

      expect(release).to receive(:create_ce_tag).and_return(ce_tag)
      expect(release).to receive(:create_ee_tag).and_return(ee_tag)

      expect(release).to receive(:start_new_minor_release)

      expect(release)
        .to receive(:add_release_data_for_tags)
        .with(ce_tag, ee_tag)

      expect(release)
        .to receive(:notify_slack)
        .with(ReleaseTools::Project::GitlabCe, version.to_ce)

      expect(release)
        .to receive(:notify_slack)
        .with(ReleaseTools::Project::GitlabEe, version)

      without_dry_run { release.execute }
    end

    context 'with dry-run mode' do
      it 'skips most of the tasks' do
        expect(release).to receive(:create_ce_target_branch)
        expect(release).to receive(:create_ee_target_branch)

        expect(release).not_to receive(:update_managed_component_versions)
        expect(release).not_to receive(:compile_changelogs)
        expect(release).not_to receive(:update_versions)
        expect(release).not_to receive(:wait_for_ee_to_ce_sync)
        expect(release).not_to receive(:create_ce_commit_to_run_ci)
        expect(release).not_to receive(:create_ce_tag)
        expect(release).not_to receive(:create_ee_tag)
        expect(release).not_to receive(:update_monthly_tag_metrics)
        expect(release).not_to receive(:start_new_minor_release)
        expect(release).not_to receive(:add_release_data_for_tags)
        expect(release).not_to receive(:notify_slack)
        expect(release).not_to receive(:notify_slack)

        release.execute
      end
    end
  end

  describe '#create_ce_target_branch' do
    let(:client) { class_spy(ReleaseTools::GitlabClient) }
    let(:version) { ReleaseTools::Version.new('42.1.0-ee') }

    subject(:release) { described_class.new(version, client: client) }

    it 'creates the CE target branch from the last stable branch' do
      expect(client)
        .to receive(:find_or_create_branch)
        .with('42-1-stable', '42-0-stable', release.ce_project_path)

      without_dry_run do
        release.create_ce_target_branch
      end
    end

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

        release.create_ce_target_branch
      end
    end
  end

  describe '#create_ee_target_branch' do
    let(:client) { class_spy(ReleaseTools::GitlabClient) }
    let(:version) { ReleaseTools::Version.new('42.1.0-ee') }

    it 'calls notify_stable_branch_creation' do
      release = described_class.new(version, client: client, commit: 'foo')

      expect(release).to receive(:notify_stable_branch_creation)

      without_dry_run do
        release.create_ee_target_branch
      end
    end

    context 'with dry run enabled' do
      it 'calls notify_stable_branch_creation' do
        release = described_class.new(version, client: client, commit: 'foo')

        expect(release).to receive(:notify_stable_branch_creation)

        release.create_ee_target_branch
      end
    end

    context 'when a custom commit is specified' do
      it 'creates the EE target branch from the custom commit' do
        release = described_class.new(version, client: client, commit: 'foo')

        expect(client)
          .to receive(:find_or_create_branch)
          .with('42-1-stable-ee', 'foo', release.project_path)

        without_dry_run do
          release.create_ee_target_branch
        end
      end
    end

    context "when a custom commit isn't specified" do
      it 'creates the EE target branch from the last production commit' do
        release = described_class.new(version, client: client)

        expect(release)
          .to receive(:last_production_commit)
          .and_return('123abc')

        expect(client)
          .to receive(:find_or_create_branch)
          .with('42-1-stable-ee', '123abc', release.project_path)

        without_dry_run do
          release.create_ee_target_branch
        end
      end
    end

    context 'with dry-run mode' do
      it 'skips api call' do
        release = described_class.new(version, client: client, commit: 'foo')

        expect(client).not_to receive(:find_or_create_branch)

        release.create_ee_target_branch
      end
    end
  end

  describe '#update_monthly_tag_metrics' do
    before do
      version = ReleaseTools::Version.new('16.10')

      allow(ReleaseTools::GitlabReleasesGemClient)
        .to receive(:version_for_date)
        .and_return(version)
    end

    context 'when tagging a patch release' do
      let(:client) { class_spy(ReleaseTools::GitlabClient) }
      let(:version) { ReleaseTools::Version.new('16.10.1-ee') }

      subject(:release) { described_class.new(version, client: client) }

      it 'does nothing' do
        expect(ReleaseTools::Metrics::MonthlyReleaseStatus).not_to receive(:new)

        without_dry_run do
          release.update_monthly_tag_metrics
        end
      end
    end

    context 'when tagging a minor release' do
      let(:client) { class_spy(ReleaseTools::GitlabClient) }
      let(:version) { ReleaseTools::Version.new('16.10.0') }

      subject(:release) { described_class.new(version, client: client) }

      it 'does nothing' do
        expect(ReleaseTools::Metrics::MonthlyReleaseStatus).not_to receive(:new)

        without_dry_run do
          release.update_monthly_tag_metrics
        end
      end
    end

    context 'when tagging a release candidate' do
      let(:client) { class_spy(ReleaseTools::GitlabClient) }
      let(:version) { ReleaseTools::Version.new('16.10.0-rc42') }

      subject(:release) { described_class.new(version, client: client) }

      it 'updates the monthly release status metric' do
        expect(ReleaseTools::Metrics::MonthlyReleaseStatus).to receive(:new).with(status: :tagged_rc)
          .and_return(instance_double(ReleaseTools::Metrics::MonthlyReleaseStatus, execute: true))

        without_dry_run do
          release.update_monthly_tag_metrics
        end
      end
    end

    context 'when tagging an older release candidate' do
      let(:client) { class_spy(ReleaseTools::GitlabClient) }
      let(:version) { ReleaseTools::Version.new('16.9.0-rc42') }

      subject(:release) { described_class.new(version, client: client) }

      it 'does nothing' do
        expect(ReleaseTools::Metrics::MonthlyReleaseStatus).not_to receive(:new)

        without_dry_run do
          release.update_monthly_tag_metrics
        end
      end
    end
  end

  describe '#update_managed_component_versions' do
    before do
      enable_feature(:fetch_managed_component_version)
    end

    context 'when releasing an RC' do
      it 'does nothing' do
        version = ReleaseTools::Version.new('42.1.0-rc42-ee')
        release = described_class.new(version)

        expect(release).not_to receive(:commit_version_files)

        release.update_managed_component_versions
      end
    end

    context 'when releasing a non-RC release' do
      let(:version) { ReleaseTools::Version.new('42.1.0-ee') }
      let(:release) { described_class.new(version) }

      context 'with fetch_managed_component_version feature flag enabled' do
        it "updates managed version file with the version from the component" do
          expect(release.client)
            .to receive(:file_contents)
            .with('gitlab-org/gitaly', 'VERSION', '42-1-stable')
            .and_return('9.0.0')

          expect(release.client)
            .to receive(:file_contents)
            .with('gitlab-org/gitlab-pages', 'VERSION', '42-1-stable')
            .and_return('42.1.0')

          expect(release.logger).to receive(:warn).with(
            'Managed component version does not match version being released',
            {
              project: 'gitlab-org/gitaly',
              component_version: '9.0.0',
              release_version: '42.1.0'
            }
          )

          expect(release)
            .to receive(:commit_version_files)
            .with(
              '42-1-stable-ee',
              {
                'GITALY_SERVER_VERSION' => '9.0.0',
                'GITLAB_PAGES_VERSION' => '42.1.0'
              },
              {
                message: "Update managed components version to 42.1.0",
                skip_ci: true
              }
            )

          release.update_managed_component_versions
        end
      end

      context 'with fetch_managed_component_version feature flag disabled' do
        before do
          disable_feature(:fetch_managed_component_version)
        end

        it "updates managed version file with the version from the release" do
          expect(release.client)
            .not_to receive(:file_contents)

          expect(release)
            .to receive(:commit_version_files)
            .with(
              '42-1-stable-ee',
              {
                'GITALY_SERVER_VERSION' => '42.1.0',
                'GITLAB_PAGES_VERSION' => '42.1.0'
              },
              {
                message: "Update managed components version to 42.1.0",
                skip_ci: true
              }
            )

          release.update_managed_component_versions
        end
      end

      context 'with release_the_kas feature flag enabled' do
        it 'updates KAS with the version from the release' do
          enable_feature(:fetch_managed_component_version, :release_the_kas)

          expect(release.client)
            .not_to receive(:file_contents)
            .with('gitlab-org/cluster-integration/gitlab-agent', 'VERSION', '42-1-stable')

          expect(release.client)
            .to receive(:file_contents)
            .with('gitlab-org/gitaly', 'VERSION', '42-1-stable')
            .and_return('9.0.0')

          expect(release.client)
            .to receive(:file_contents)
            .with('gitlab-org/gitlab-pages', 'VERSION', '42-1-stable')
            .and_return('42.1.0')

          expect(release)
            .to receive(:commit_version_files)
            .with(
              '42-1-stable-ee',
              {
                'GITALY_SERVER_VERSION' => '9.0.0',
                'GITLAB_PAGES_VERSION' => '42.1.0',
                'GITLAB_KAS_VERSION' => '42.1.0'
              },
              {
                message: "Update managed components version to 42.1.0",
                skip_ci: true
              }
            )

          release.update_managed_component_versions
        end
      end
    end
  end

  describe '#wait_for_ee_to_ce_sync' do
    before do
      pipeline = double(:pipeline, web_url: 'foo', id: 1)

      allow(ReleaseTools::GitlabOpsClient)
        .to receive(:create_pipeline)
        .with(
          ReleaseTools::Project::MergeTrain,
          {
            MERGE_FOSS: '1',
            SOURCE_PROJECT: 'gitlab-org/gitlab',
            SOURCE_BRANCH: '42-1-stable-ee',
            TARGET_PROJECT: 'gitlab-org/gitlab-foss',
            TARGET_BRANCH: '42-1-stable'
          }
        )
        .and_return(pipeline)
    end

    it 'returns when EE has been synced to CE' do
      pipeline = double(:pipeline, status: 'success')
      version = ReleaseTools::Version.new('42.1.0-ee')
      release = described_class.new(version)

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:pipeline)
        .with(ReleaseTools::Project::MergeTrain, 1)
        .and_return(pipeline)

      expect { release.wait_for_ee_to_ce_sync }.not_to raise_error
    end

    it 'raises when the pipeline fails' do
      pipeline = double(:pipeline, status: 'failed')
      version = ReleaseTools::Version.new('42.1.0-ee')
      release = described_class.new(version)

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:pipeline)
        .with(ReleaseTools::Project::MergeTrain, 1)
        .and_return(pipeline)

      expect { release.wait_for_ee_to_ce_sync }
        .to raise_error(described_class::PipelineFailed)
    end

    it 'raises when the pipeline does not succeed in a timely manner' do
      pipeline = double(:pipeline, status: 'running')
      version = ReleaseTools::Version.new('42.1.0-ee')
      release = described_class.new(version)

      stub_const(
        'ReleaseTools::PublicRelease::GitlabRelease::WAIT_SYNC_INTERVALS',
        Array.new(15, 0)
      )

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:pipeline)
        .with(ReleaseTools::Project::MergeTrain, 1)
        .exactly(16)
        .times
        .and_return(pipeline)

      expect { release.wait_for_ee_to_ce_sync }
        .to raise_error(described_class::PipelineTooSlow)
    end

    it 'raises when the pipeline is waiting for resources' do
      pipeline = double(:pipeline, status: 'waiting_for_resource')
      version = ReleaseTools::Version.new('42.1.0-ee')
      release = described_class.new(version)

      stub_const(
        'ReleaseTools::PublicRelease::GitlabRelease::WAIT_SYNC_INTERVALS',
        Array.new(15, 0)
      )

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:pipeline)
        .with(ReleaseTools::Project::MergeTrain, 1)
        .exactly(16)
        .times
        .and_return(pipeline)

      expect { release.wait_for_ee_to_ce_sync }
        .to raise_error(described_class::PipelineTooSlow)
    end

    it 'retries the operation when the pipeline is still running' do
      version = ReleaseTools::Version.new('42.1.0-ee')
      release = described_class.new(version)

      stub_const(
        'ReleaseTools::PublicRelease::GitlabRelease::WAIT_SYNC_INTERVALS',
        Array.new(15, 0)
      )

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:pipeline)
        .with(ReleaseTools::Project::MergeTrain, 1)
        .twice
        .and_return(
          double(:pipeline, status: 'running'),
          double(:pipeline, status: 'success')
        )

      expect { release.wait_for_ee_to_ce_sync }.not_to raise_error
    end
  end

  describe '#create_ce_commit_to_run_ci' do
    let(:release) { described_class.new(ReleaseTools::Version.new('42.1.0-ee')) }

    context 'when the last commit includes "ci skip"' do
      it 'creates a new empty commit' do
        commit = double(:commit, message: "Foo\n[ci skip]")

        expect(release.client)
          .to receive(:commit)
          .with('gitlab-org/gitlab-foss', { ref: '42-1-stable' })
          .and_return(commit)

        expect(release.client)
          .to receive(:create_commit)
          .with(
            'gitlab-org/gitlab-foss',
            '42-1-stable',
            an_instance_of(String),
            [
              {
                action: 'update',
                file_path: 'VERSION',
                content: '42.1.0'
              }
            ]
          )

        release.create_ce_commit_to_run_ci
      end
    end

    context "when the last commit doesn't include \"ci skip\"" do
      it "doesn't create a new commit" do
        commit = double(:commit, message: 'Bump version to X')

        expect(release.client)
          .to receive(:commit)
          .with('gitlab-org/gitlab-foss', { ref: '42-1-stable' })
          .and_return(commit)

        expect(release.client).not_to receive(:create_commit)

        release.create_ce_commit_to_run_ci
      end
    end
  end

  describe '#compile_changelogs' do
    it 'compiles the changelog for a stable release' do
      client = class_spy(ReleaseTools::GitlabClient)
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version, client: client)
      compiler = instance_spy(ReleaseTools::ChangelogCompiler)

      expect(ReleaseTools::ChangelogCompiler)
        .to receive(:new)
        .with(release.project.canonical_or_security_path, { client: client })
        .and_return(compiler)

      expect(compiler)
        .to receive(:compile)
        .with(version, { branch: '42-0-stable-ee' })

      release.compile_changelogs
    end

    it 'does not compile the changelog for an RC' do
      client = class_spy(ReleaseTools::GitlabClient)
      version = ReleaseTools::Version.new('42.0.0-rc42-ee')
      release = described_class.new(version, client: client)

      expect(ReleaseTools::ChangelogCompiler).not_to receive(:new)

      release.compile_changelogs
    end
  end

  describe '#update_versions' do
    it 'updates the VERSION files for CE and EE' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)

      expect(release)
        .to receive(:commit_version_files)
        .with(
          '42-0-stable-ee',
          { 'VERSION' => '42.0.0-ee' },
          {
            skip_ci: false,
            skip_merge_train: true
          }
        )

      expect(release)
        .to receive(:commit_version_files)
        .with(
          '42-0-stable',
          { 'VERSION' => '42.0.0' },
          {
            project: release.ce_project_path,
            skip_ci: true
          }
        )

      release.update_versions
    end
  end

  describe '#start_new_minor_release' do
    context 'when releasing a patch release' do
      it 'does nothing' do
        version = ReleaseTools::Version.new('42.1.1-ee')
        release = described_class.new(version)

        expect(release).not_to receive(:commit_version_files)

        release.start_new_minor_release
      end
    end

    context 'when releasing a new minor release' do
      it 'updates the VERSION files on the source branches' do
        version = ReleaseTools::Version.new('42.0.0-ee')
        release = described_class.new(version)

        allow(version).to receive_messages(to_upcoming_pre_release: '42.1.0-pre', to_ce: double(to_upcoming_pre_release: '42.1.0-pre'))

        expect(release)
          .to receive(:commit_version_files)
          .with('master', { 'VERSION' => '42.1.0-pre' }, { skip_ci: true })

        expect(release)
          .to receive(:commit_version_files)
          .with(
            'master',
            { 'VERSION' => '42.1.0-pre' },
            {
              project: release.ce_project_path,
              skip_ci: true
            }
          )

        release.start_new_minor_release
      end
    end
  end

  describe '#create_ce_tag' do
    let(:fake_metrics) { instance_spy(ReleaseTools::Metrics::Client) }
    let(:version) { ReleaseTools::Version.new('42.0.0-ee') }
    let(:release) { described_class.new(version) }

    before do
      allow(ReleaseTools::Metrics::Client).to receive(:new).and_return(fake_metrics)
    end

    context 'when the tag already exists' do
      it 'returns the tag' do
        tag = double(:tag)

        allow(release.client)
         .to receive(:tag)
         .with(
           release.ce_project_path,
           { tag: version.to_ce.tag }
         )
        .and_return(tag)

        expect(release.client)
         .not_to receive(:create_tag)

        expect(fake_metrics).not_to receive(:inc)
        expect(release.create_ce_tag).to eq(tag)

        release.create_ce_tag
      end
    end

    context 'when the tag does not exist' do
      before do
        tag = double(:tag)

        allow(release.client)
          .to receive(:tag)
          .with(
            release.ce_project_path,
            { tag: version.to_ce.tag }
          )
          .and_raise(gitlab_error(:NotFound))

        allow(release.client).to receive(:create_tag).and_return(tag)

        allow(ReleaseTools::SharedStatus)
          .to receive(:critical_patch_release?)
          .and_return(false)
      end

      it 'creates the tag for CE' do
        expect(release.client)
          .to receive(:create_tag)
          .with(
            release.ce_project_path,
            version.to_ce.tag,
            '42-0-stable',
            'Version v42.0.0'
          )

        expect(ReleaseTools::Metrics::Client.new).to receive(:inc)

        release.create_ce_tag
      end
    end
  end

  describe '#create_ee_tag' do
    it 'creates the tag for EE' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)

      expect(release.client)
        .to receive(:find_or_create_tag)
        .with(
          release.project_path,
          version.tag,
          '42-0-stable-ee',
          { message: 'Version v42.0.0-ee' }
        )

      release.create_ee_tag
    end
  end

  describe '#add_release_data_for_tags' do
    it 'adds the release data for CE and EE' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)
      ce_tag = double(:tag, name: 'foo', commit: double(:commit, id: 'a'))
      ee_tag = double(:tag, name: 'bar', commit: double(:commit, id: 'b'))

      expect(release.release_metadata).to receive(:add_release).with(
        {
          name: 'gitlab-ce',
          version: '42.0.0',
          sha: 'a',
          ref: 'foo',
          tag: true
        }
      )

      expect(release.release_metadata).to receive(:add_release).with(
        {
          name: 'gitlab-ee',
          version: '42.0.0',
          sha: 'b',
          ref: 'bar',
          tag: true
        }
      )

      release.add_release_data_for_tags(ce_tag, ee_tag)
    end
  end

  describe '#project' do
    it 'returns the project to release' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)

      expect(release.project).to eq(ReleaseTools::Project::GitlabEe)
    end
  end

  describe '#ce_target_branch' do
    it 'returns the target branch for CE' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)

      expect(release.ce_target_branch).to eq('42-0-stable')
    end
  end

  describe '#ce_project_path' do
    it 'returns the path to CE' do
      version = ReleaseTools::Version.new('42.0.0-ee')
      release = described_class.new(version)

      expect(release.ce_project_path)
        .to eq(ReleaseTools::Project::GitlabCe.canonical_or_security_path)
    end
  end

  describe '#source_for_target_branch' do
    let(:version) { ReleaseTools::Version.new('42.0.0-ee') }

    context 'when a custom commit is specified' do
      it 'returns the commit' do
        release = described_class.new(version, commit: 'foo')

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

    context 'when no custom commit is specified' do
      it 'returns the SHA of the last deployment' do
        release = described_class.new(version)

        allow(release).to receive(:last_production_commit).and_return('foo')

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