# frozen_string_literal: true

require 'spec_helper'

describe ReleaseTools::ProductVersion do
  include MetadataHelper

  def version(version_string)
    described_class.new(version_string)
  end

  describe "to_s" do
    it 'is a string representation of the given version' do
      expect(version('1.2.3').to_s).to eq('1.2.3')
    end
  end

  describe '.new' do
    it { expect(version('14.2.3')).to eq(version('14.2.3')) } # rubocop:disable RSpec/IdenticalEqualityAssertion
    it { expect(version('14.2')).to eq(version('14.2.0')) }
    it { expect(version('14.2.0-rc42')).to eq(version('14.2.0-rc42')) } # rubocop:disable RSpec/IdenticalEqualityAssertion
    it { expect(version('14.4.202110071021')).to eq(version('14.4.202110071021')) } # rubocop:disable RSpec/IdenticalEqualityAssertion
  end

  describe '.from_auto_deploy' do
    it 'returns nil when not an auto_deploy package' do
      expect(described_class.from_auto_deploy('14.3.0')).to be_nil
    end

    it 'returns the normalized version for an auto_deploy tag' do
      version = described_class.from_auto_deploy('14.4.202110080320+c92f14192f5.e2ad57eb8ba')

      expect(version).to eq(version('14.4.202110080320'))
    end

    it 'returns the normalized version for an auto_deploy omnibus version' do
      version = described_class.from_auto_deploy('14.4.202110080320-c92f14192f5.e2ad57eb8ba')

      expect(version).to eq(version('14.4.202110080320'))
    end
  end

  describe '.from_auto_deploy_tag' do
    it 'returns nil when not an auto_deploy tag' do
      expect(described_class.from_auto_deploy_tag('14.3.0')).to be_nil
    end

    it 'returns the normalized version for an auto_deploy Omnibus tag' do
      version = described_class.from_auto_deploy_tag('14.4.202110080320+c92f14192f5.e2ad57eb8ba')

      expect(version).to eq(version('14.4.202110080320'))
    end

    it 'returns the normalized version for an auto_deploy CNG tag' do
      version = described_class.from_auto_deploy_tag('14.4.202110080320+c92f14192f5')

      expect(version).to eq(version('14.4.202110080320'))
    end
  end

  describe '.from_package_version' do
    it 'returns nil when not a valid version' do
      expect(described_class.from_package_version('foo.bar+baz')).to be_nil
    end

    it 'returns the normalized version for a monthly release' do
      version = described_class.from_package_version('14.4.0-ee')

      expect(version).to eq(version('14.4.0'))
    end

    it 'returns the normalized version for a patch release' do
      version = described_class.from_package_version('14.4.1')

      expect(version).to eq(version('14.4.1'))
    end

    it 'returns the normalized version for an auto_deploy omnibus version' do
      version = described_class.from_package_version('14.4.202110080320-c92f14192f5.e2ad57eb8ba')

      expect(version).to eq(version('14.4.202110080320'))
    end
  end

  describe '.from_metadata_sha' do
    context 'when the metadata exists' do
      it 'returns the correct product version' do
        release = ReleaseTools::Version.new('14.0.1')
        metadata_commit_id = 'abc123'
        metadata_path = 'releases/14/14.0.1.json'
        diff = create(:diff, new_path: metadata_path, new_file: true)

        expect(ReleaseTools::GitlabOpsClient)
          .to receive(:commit_diff)
          .with(
            ReleaseTools::ReleaseMetadataUploader::PROJECT,
            metadata_commit_id
          )
          .and_return([diff])

        expect(described_class.from_metadata_sha(metadata_commit_id)).to eq(release)
      end
    end

    context 'when the metadata commit does not exist' do
      it 'returns nil' do
        metadata_commit_id = 'abc123'
        request_double = double(base_uri: 'https://gitlab.com/api/v4', path: '/something', options: {})
        response_double = double('response', parsed_response: { message: 'Not found' }, code: 404, request: request_double)

        expect(ReleaseTools::GitlabOpsClient)
          .to receive(:commit_diff)
          .with(
            ReleaseTools::ReleaseMetadataUploader::PROJECT,
            metadata_commit_id
          )
          .and_raise(Gitlab::Error::NotFound, response_double)

        expect(described_class.from_metadata_sha(metadata_commit_id)).to be_nil
      end
    end
  end

  context 'Enumerable implementation' do
    let(:versions) { build_list(:product_version, 5) }

    before do
      allow(described_class).to receive(:lazy_enumerator).and_return(versions.each.lazy)
    end

    describe '.each' do
      it 'yields product versions' do
        expect { |b| described_class.each(&b) }.to yield_successive_args(*versions)
      end
    end

    describe '.find' do
      let(:security_version) { build(:product_version, security: true) }
      let(:versions) { build_list(:product_version, 2) << security_version }

      it 'finds product version based on a given block' do
        expect(
          described_class.find do |version|
            version.metadata['security'] == true
          end
        ).to eq(security_version)
      end
    end
  end

  describe '.lazy_enumerator' do
    subject(:lazy_enumerator) { described_class.each }

    it 'fetches commits page by page' do
      first_commits = [build(:release_metadata_commit), build(:release_metadata_commit)]
      first_product_version1 = build(:product_version, metadata_commit_id: first_commits.first.id)
      first_product_version2 = build(:product_version, metadata_commit_id: first_commits.last.id)

      page_two = Gitlab::PaginatedResponse.new(first_commits)

      last_commits = [build(:release_metadata_commit), build(:release_metadata_commit)]
      last_product_version1 = build(:product_version, metadata_commit_id: last_commits.first.id)
      last_product_version2 = build(:product_version, metadata_commit_id: last_commits.last.id)
      page_one = Gitlab::PaginatedResponse.new(last_commits)

      allow(page_one).to receive(:next_page).and_return(page_two)

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:commits)
        .with(ReleaseTools::ReleaseMetadataUploader::PROJECT, trailers: true)
        .once
        .and_return(page_one)

      expect(described_class)
        .to receive(:new)
        .with(last_commits.first.trailers['Product-Version'])
        .and_return(last_product_version1)
      expect(described_class)
        .to receive(:new)
        .with(last_commits.last.trailers['Product-Version'])
        .and_return(last_product_version2)

      expect(lazy_enumerator.next).to eq(last_product_version1)
      expect(lazy_enumerator.next).to eq(last_product_version2)

      expect(described_class)
        .to receive(:new)
        .with(first_commits.first.trailers['Product-Version'])
        .and_return(first_product_version1)
      expect(described_class)
        .to receive(:new)
        .with(first_commits.last.trailers['Product-Version'])
        .and_return(first_product_version2)
      expect(page_one).to receive(:has_next_page?).and_return(true)

      expect(lazy_enumerator.next).to eq(first_product_version1)
      expect(lazy_enumerator.next).to eq(first_product_version2)

      expect(page_two).to receive(:has_next_page?).and_return(false)

      expect { lazy_enumerator.next }.to raise_exception(StopIteration)
    end

    it 'processes commits lazily' do
      last_commits = [build(:release_metadata_commit), build(:release_metadata_commit)]
      last_product_version = build(:product_version, metadata_commit_id: last_commits.first.id)
      page_one = Gitlab::PaginatedResponse.new(last_commits)

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:commits)
        .with(ReleaseTools::ReleaseMetadataUploader::PROJECT, trailers: true)
        .once
        .and_return(page_one)

      expect(page_one).not_to receive(:has_next_page?)
      expect(page_one).not_to receive(:next_page)
      expect(described_class)
        .to receive(:new)
        .with(last_commits.first.trailers['Product-Version'])
        .and_return(last_product_version)

      expect(described_class).not_to receive(:new).with(last_commits.last.trailers['Product-Version'])

      expect(lazy_enumerator.next).to eq(last_product_version)
    end

    it 'ignores commits without Product-Version trailer' do
      last_commits = [build(:commit), build(:release_metadata_commit)]
      last_product_version = build(:product_version, metadata_commit_id: last_commits.last.id)
      page_one = Gitlab::PaginatedResponse.new(last_commits)

      expect(ReleaseTools::GitlabOpsClient)
        .to receive(:commits)
        .with(ReleaseTools::ReleaseMetadataUploader::PROJECT, trailers: true)
        .once
        .and_return(page_one)

      expect(described_class)
        .to receive(:new)
        .with(last_commits.last.trailers['Product-Version'])
        .and_return(last_product_version)

      expect(lazy_enumerator.next).to eq(last_product_version)
    end
  end

  describe '.last_auto_deploy' do
    let(:auto_deploy_version) { build(:product_version) }
    let(:versions) { build_list(:product_version, 2, version: "15.5.5") << auto_deploy_version }

    it 'finds the most recent auto_deploy version' do
      allow(described_class).to receive(:lazy_enumerator).and_return(versions.each.lazy)

      expect(
        described_class.last_auto_deploy
      ).to eq(auto_deploy_version)
    end
  end

  describe '#monthly?' do
    it 'is true for monthly releases' do
      expect(version('14.2.0')).to be_monthly
    end

    it 'is false for patch releases' do
      expect(version('14.2.3')).not_to be_monthly
    end

    it 'is false for pre-releases' do
      expect(version('14.2.0-rc42')).not_to be_monthly
    end
  end

  describe '#patch?' do
    it 'is true for patch releases' do
      expect(version('14.2.3')).to be_patch
    end

    it 'is false for pre-releases' do
      expect(version('14.2.0-rc1')).not_to be_patch
    end

    it 'is false for minor releases' do
      expect(version('14.2.0')).not_to be_patch
    end

    it 'is false for invalid releases' do
      expect(version('wow.1')).not_to be_patch
    end
  end

  describe '#major' do
    it { expect(version('14.2.3').major).to eq(14) }
    it { expect(version('14.2.0-rc1').major).to eq(14) }
    it { expect(version('wow.1').major).to eq(0) }
  end

  describe '#minor' do
    it { expect(version('14.2.3').minor).to eq(2) }
    it { expect(version('14.2.0-rc1').minor).to eq(2) }
    it { expect(version('wow.1').minor).to eq(0) }
  end

  describe '#rc' do
    it { expect(version('14.2.3-rc').rc).to eq(0) }
    it { expect(version('14.2.3-rc42').rc).to eq(42) }
    it { expect(version('14.2.3').rc).to be_nil }
    it { expect(version('wow-rc1').rc).to be_nil }
  end

  describe '#rc?' do
    it { expect(version('14.2.3-rc')).to be_rc }
    it { expect(version('14.2.3-rc1')).to be_rc }
    it { expect(version('14.2.3')).not_to be_rc }
    it { expect(version('wow-rc1')).not_to be_rc }
  end

  describe '#auto_deploy?' do
    it { expect(version('14.2.3-rc')).not_to be_auto_deploy }
    it { expect(version('14.2.3-rc1')).not_to be_auto_deploy }
    it { expect(version('14.2.3')).not_to be_auto_deploy }
    it { expect(version('wow-rc1')).not_to be_auto_deploy }
    it { expect(version('14.6.202112160920')).to be_auto_deploy }
    it { expect(version('14.6.202112161020')).to be_auto_deploy }
  end

  describe '#<=>' do
    it { expect(version('14.6.202112160920') < version('14.6.202112161020')).to be_truthy }
    it { expect(version('14.10.202112160620') > version('14.9.202112160920')).to be_truthy }
  end

  describe '#metadata' do
    context 'when the metadata exists' do
      it 'returns the metadata as a Hash' do
        release = '14.0.1'
        metadata = { 'some' => 'data' }

        allow(ReleaseTools::GitlabOpsClient)
          .to receive(:get_file)
          .with(
            ReleaseTools::ReleaseMetadataUploader::PROJECT,
            'releases/14/14.0.1.json',
            ReleaseTools::ReleaseMetadataUploader::PROJECT.default_branch
          )
          .and_return(Gitlab::ObjectifiedHash.new(
                        last_commit_id: 'abc',
                        content: Base64.strict_encode64(JSON.dump(metadata))
                      ))

        expect(version(release).metadata).to eq(metadata)
      end
    end

    context 'when the metadata does not exist' do
      it 'returns an empty Hash' do
        release = '14.0.1'

        allow(ReleaseTools::GitlabOpsClient)
          .to receive(:get_file)
          .with(
            ReleaseTools::ReleaseMetadataUploader::PROJECT,
            'releases/14/14.0.1.json',
            ReleaseTools::ReleaseMetadataUploader::PROJECT.default_branch
          )
          .and_raise(gitlab_error(:NotFound))

        expect(version(release).metadata).to eq({})
      end
    end
  end

  describe '#[]' do
    subject(:product_version) { described_class.new('13.0.202005121540') }

    let(:metadata) do
      {
        'releases' => {
          'omnibus-gitlab-ee' => {
            'version' => 'cb5ec4eab2cfc0a2c02c66ffb47ff782c226e5b0',
            'sha' => 'cb5ec4eab2cfc0a2c02c66ffb47ff782c226e5b0',
            'ref' => '13-0-auto-deploy-20200512',
            'tag' => false
          }
        }
      }
    end

    before do
      allow(product_version).to receive(:metadata).and_return(metadata)
    end

    it 'returns metadata for the given component' do
      omnibus = product_version['omnibus-gitlab-ee']
      expect(omnibus).not_to be_nil

      expected_meta = metadata.dig('releases', 'omnibus-gitlab-ee')
      expect(omnibus.name).to eq('omnibus-gitlab-ee')
      expect(omnibus.version).to eq(expected_meta['version'])
      expect(omnibus.sha).to eq(expected_meta['sha'])
      expect(omnibus.ref).to eq(expected_meta['ref'])
      expect(omnibus.tag).to eq(expected_meta['tag'])
    end

    it 'returns metadata for the given component class' do
      omnibus = product_version[ReleaseTools::Project::OmnibusGitlab]
      expect(omnibus).not_to be_nil

      expected_meta = metadata.dig('releases', 'omnibus-gitlab-ee')
      expect(omnibus.name).to eq('omnibus-gitlab-ee')
      expect(omnibus.version).to eq(expected_meta['version'])
      expect(omnibus.sha).to eq(expected_meta['sha'])
      expect(omnibus.ref).to eq(expected_meta['ref'])
      expect(omnibus.tag).to eq(expected_meta['tag'])
    end

    it 'returns nil when the component does not exists' do
      expect(product_version['foo-bar']).to be_nil
    end
  end

  describe '#auto_deploy_package' do
    subject(:product_version) do
      described_class.new('14.8.202202091820')
    end

    before do
      metadata = build_metadata(
        auto_deploy_branch: '14.8.202202091820',
        gitlab_sha: '74f805e6aa4',
        omnibus_sha: '15e30c3a2fe',
        tag: true
      )

      allow(product_version)
        .to receive(:metadata)
        .and_return(metadata)
    end

    it 'returns auto-deploy package' do
      expect(product_version.auto_deploy_package)
        .to eq('14.8.202202091820-74f805e6aa4.15e30c3a2fe')
    end
  end
end
