require 'spec_helper'
require 'semantic_puppet/version'

describe SemanticPuppet::Version do

  def subject(str)
    SemanticPuppet::Version.parse(str)
  end

  describe '.parse' do

    context 'Spec v2.0.0' do
      context 'Section 2' do
        # A normal version number MUST take the form X.Y.Z where X, Y, and Z are
        # non-negative integers, and MUST NOT contain leading zeroes. X is the
        # major version, Y is the minor version, and Z is the patch version.
        # Each element MUST increase numerically.
        # For instance: 1.9.0 -> 1.10.0 -> 1.11.0.

        let(:parse_failure) do
          /Unable to parse .* as a semantic version identifier/
        end

        it 'rejects versions that contain too few parts' do
          expect { subject('1.2') }.to raise_error(parse_failure)
        end

        it 'rejects versions that contain too many parts' do
          expect { subject('1.2.3.4') }.to raise_error(parse_failure)
        end

        it 'rejects versions that contain non-integers' do
          expect { subject('x.2.3') }.to raise_error(parse_failure)
          expect { subject('1.y.3') }.to raise_error(parse_failure)
          expect { subject('1.2.z') }.to raise_error(parse_failure)
        end

        it 'rejects versions that contain negative integers' do
          expect { subject('-1.2.3') }.to raise_error(parse_failure)
          expect { subject('1.-2.3') }.to raise_error(parse_failure)
          expect { subject('1.2.-3') }.to raise_error(parse_failure)
        end

        it 'rejects version numbers containing leading zeroes' do
          expect { subject('01.2.3') }.to raise_error(parse_failure)
          expect { subject('1.02.3') }.to raise_error(parse_failure)
          expect { subject('1.2.03') }.to raise_error(parse_failure)
        end

        it 'permits zeroes in version number parts' do
          expect { subject('0.2.3') }.to_not raise_error
          expect { subject('1.0.3') }.to_not raise_error
          expect { subject('1.2.0') }.to_not raise_error
        end

        context 'examples' do
          example '1.9.0' do
            version = subject('1.9.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 9
            expect(version.patch).to eql 0
          end

          example '1.10.0' do
            version = subject('1.10.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 10
            expect(version.patch).to eql 0
          end

          example '1.11.0' do
            version = subject('1.11.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 11
            expect(version.patch).to eql 0
          end
        end
      end

      context 'Section 9' do
        # A pre-release version MAY be denoted by appending a hyphen and a
        # series of dot separated identifiers immediately following the patch
        # version. Identifiers MUST comprise only ASCII alphanumerics and
        # hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric
        # identifiers MUST NOT include leading zeroes. Pre-release versions
        # have a lower precedence than the associated normal version. A
        # pre-release version indicates that the version is unstable and
        # might not satisfy the intended compatibility requirements as denoted
        # by its associated normal version.
        # Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

        let(:restricted_charset) do
          'Prerelease identifiers MUST use only ASCII alphanumerics and hyphens'
        end

        let(:must_not_be_empty) do
          'Prerelease identifiers MUST NOT be empty'
        end

        let(:no_leading_zeroes) do
          'Prerelease identifiers MUST NOT contain leading zeroes'
        end

        it 'rejects prerelease identifiers with non-alphanumerics' do
          expect { subject('1.2.3-$100') }.to raise_error(restricted_charset)
          expect { subject('1.2.3-rc.1@me') }.to raise_error(restricted_charset)
        end

        it 'rejects empty prerelease versions' do
          expect { subject('1.2.3-') }.to raise_error(must_not_be_empty)
        end

        it 'rejects empty prerelease version identifiers' do
          expect { subject('1.2.3-.rc1') }.to raise_error(must_not_be_empty)
          expect { subject('1.2.3-rc1.') }.to raise_error(must_not_be_empty)
          expect { subject('1.2.3-rc..1') }.to raise_error(must_not_be_empty)
        end

        it 'rejects numeric prerelease identifiers with leading zeroes' do
          expect { subject('1.2.3-01') }.to raise_error(no_leading_zeroes)
          expect { subject('1.2.3-rc.01') }.to raise_error(no_leading_zeroes)
        end

        it 'permits numeric prerelease identifiers of zero' do
          expect { subject('1.2.3-0') }.to_not raise_error
          expect { subject('1.2.3-rc.0') }.to_not raise_error
        end

        it 'permits non-numeric prerelease identifiers with leading zeroes' do
          expect { subject('1.2.3-0xDEADBEEF') }.to_not raise_error
          expect { subject('1.2.3-rc.0x10c') }.to_not raise_error
        end

        context 'examples' do
          example '1.0.0-alpha' do
            version = subject('1.0.0-alpha')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'alpha'
          end

          example '1.0.0-alpha.1' do
            version = subject('1.0.0-alpha.1')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'alpha.1'
          end

          example '1.0.0-0.3.7' do
            version = subject('1.0.0-0.3.7')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql '0.3.7'
          end

          example '1.0.0-x.7.z.92' do
            version = subject('1.0.0-x.7.z.92')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'x.7.z.92'
          end
        end
      end

      context 'Section 10' do
        # Build metadata is not yet in scope so
        it 'rejects build identifiers' do
          ver = '1.2.3+anything'
          expect { subject(ver) }.to raise_error("'#{ver}' MUST NOT include build identifiers")
        end
      end

      context 'Section 10', :pending => "build metadata is not yet in scope" do
        # Build metadata MAY be denoted by appending a plus sign and a series
        # of dot separated identifiers immediately following the patch or
        # pre-release version. Identifiers MUST comprise only ASCII
        # alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
        # Build metadata SHOULD be ignored when determining version precedence.
        # Thus two versions that differ only in the build metadata, have the
        # same precedence.
        # Examples: 1.0.0-alpha+001, 1.0.0+20130313144700,
        # 1.0.0-beta+exp.sha.5114f85.


        let(:restricted_charset) do
          'Build identifiers MUST use only ASCII alphanumerics and hyphens'
        end

        let(:must_not_be_empty) do
          'Build identifiers MUST NOT be empty'
        end

        it 'rejects build identifiers with non-alphanumerics' do
          expect { subject('1.2.3+$100') }.to raise_error(restricted_charset)
          expect { subject('1.2.3+rc.1@me') }.to raise_error(restricted_charset)
        end

        it 'rejects empty build metadata' do
          expect { subject('1.2.3+') }.to raise_error(must_not_be_empty)
        end

        it 'rejects empty build identifiers' do
          expect { subject('1.2.3+.rc1') }.to raise_error(must_not_be_empty)
          expect { subject('1.2.3+rc1.') }.to raise_error(must_not_be_empty)
          expect { subject('1.2.3+rc..1') }.to raise_error(must_not_be_empty)
        end

        it 'permits numeric build identifiers with leading zeroes' do
          expect { subject('1.2.3+01') }.to_not raise_error
          expect { subject('1.2.3+rc.01') }.to_not raise_error
        end

        it 'permits numeric build identifiers of zero' do
          expect { subject('1.2.3+0') }.to_not raise_error
          expect { subject('1.2.3+rc.0') }.to_not raise_error
        end

        it 'permits non-numeric build identifiers with leading zeroes' do
          expect { subject('1.2.3+0xDEADBEEF') }.to_not raise_error
          expect { subject('1.2.3+rc.0x10c') }.to_not raise_error
        end

        context 'examples' do
          example '1.0.0-alpha+001' do
            version = subject('1.0.0-alpha+001')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'alpha'
            expect(version.build).to eql '001'
          end

          example '1.0.0+20130313144700' do
            version = subject('1.0.0+20130313144700')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql nil
            expect(version.build).to eql '20130313144700'
          end

          example '1.0.0-beta+exp.sha.5114f85' do
            version = subject('1.0.0-beta+exp.sha.5114f85')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'beta'
            expect(version.build).to eql 'exp.sha.5114f85'
          end
        end
      end
    end

    context 'Spec v1.0.0' do
      context 'Section 2' do
        # A normal version number MUST take the form X.Y.Z where X, Y, and Z
        # are integers. X is the major version, Y is the minor version, and Z
        # is the patch version. Each element MUST increase numerically by
        # increments of one.
        # For instance: 1.9.0 -> 1.10.0 -> 1.11.0

        let(:parse_failure) do
          /Unable to parse .* as a semantic version identifier/
        end

        it 'rejects versions that contain too few parts' do
          expect { subject('1.2') }.to raise_error(parse_failure)
        end

        it 'rejects versions that contain too many parts' do
          expect { subject('1.2.3.4') }.to raise_error(parse_failure)
        end

        it 'rejects versions that contain non-integers' do
          expect { subject('x.2.3') }.to raise_error(parse_failure)
          expect { subject('1.y.3') }.to raise_error(parse_failure)
          expect { subject('1.2.z') }.to raise_error(parse_failure)
        end

        it 'permits zeroes in version number parts' do
          expect { subject('0.2.3') }.to_not raise_error
          expect { subject('1.0.3') }.to_not raise_error
          expect { subject('1.2.0') }.to_not raise_error
        end

        context 'examples' do
          example '1.9.0' do
            version = subject('1.9.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 9
            expect(version.patch).to eql 0
          end

          example '1.10.0' do
            version = subject('1.10.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 10
            expect(version.patch).to eql 0
          end

          example '1.11.0' do
            version = subject('1.11.0')
            expect(version.major).to eql 1
            expect(version.minor).to eql 11
            expect(version.patch).to eql 0
          end
        end
      end

      context 'Section 4' do
        # A pre-release version number MAY be denoted by appending an arbitrary
        # string immediately following the patch version and a dash. The string
        # MUST be comprised of only alphanumerics plus dash [0-9A-Za-z-].
        # Pre-release versions satisfy but have a lower precedence than the
        # associated normal version. Precedence SHOULD be determined by
        # lexicographic ASCII sort order.
        # For instance: 1.0.0-alpha1 < 1.0.0-beta1 < 1.0.0-beta2 < 1.0.0-rc1

        let(:restricted_charset) do
          'Prerelease identifiers MUST use only ASCII alphanumerics and hyphens'
        end

        let(:must_not_be_empty) do
          'Prerelease identifiers MUST NOT be empty'
        end

        let(:no_leading_zeroes) do
          'Prerelease identifiers MUST NOT contain leading zeroes'
        end

        it 'rejects prerelease identifiers with non-alphanumerics' do
          expect { subject('1.2.3-$100') }.to raise_error(restricted_charset)
          expect { subject('1.2.3-rc.1@me') }.to raise_error(restricted_charset)
        end

        it 'rejects empty prerelease versions' do
          expect { subject('1.2.3-') }.to raise_error(must_not_be_empty)
        end

        it 'rejects numeric prerelease identifiers with leading zeroes' do
          expect { subject('1.2.3-01') }.to raise_error(no_leading_zeroes)
        end

        it 'permits numeric prerelease identifiers of zero' do
          expect { subject('1.2.3-0') }.to_not raise_error
        end

        it 'permits non-numeric prerelease identifiers with leading zeroes' do
          expect { subject('1.2.3-0xDEADBEEF') }.to_not raise_error
        end

        context 'examples' do
          example '1.0.0-alpha1' do
            version = subject('1.0.0-alpha1')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'alpha1'
          end

          example '1.0.0-beta1' do
            version = subject('1.0.0-beta1')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'beta1'
          end

          example '1.0.0-beta2' do
            version = subject('1.0.0-beta2')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'beta2'
          end

          example '1.0.0-rc1' do
            version = subject('1.0.0-rc1')
            expect(version.major).to eql 1
            expect(version.minor).to eql 0
            expect(version.patch).to eql 0
            expect(version.prerelease).to eql 'rc1'
          end
        end
      end
    end

  end

  describe '.valid?' do
    # All the specific variations are tested in the .parse tests, so these are just basic
    # smoke tests.

    def subject(str)
      SemanticPuppet::Version.valid?(str)
    end

    it 'recognizes valid versions' do
      expect(subject('1.0.1')).to be true
      expect(subject('1.0.3-p324')).to be true
    end

    it 'does not recognize invalid versions' do
      expect(subject('1.0')).to be false      # too few segments
      expect(subject('1.0.3.6')).to be false  # too many segments
      expect(subject('1.03.14')).to be false  # leading zero in segment
    end
  end

  describe '#==' do
    def parse(vstring)
      SemanticPuppet::Version.parse(vstring)
    end

    it 'should yield true when comparing two equal instances' do
      x = parse('1.2.3-alpha')
      y = parse('1.2.3-alpha')
      expect(x == y).to eql(true)
    end

    it 'should yield false when the major differs' do
      x = parse('1.2.3-alpha')
      y = parse('2.2.3-alpha')
      expect(x == y).to eql(false)
    end

    it 'should yield false when the minor differs' do
      x = parse('1.2.3-alpha')
      y = parse('1.3.3-alpha')
      expect(x == y).to eql(false)
    end

    it 'should yield false when the patch differs' do
      x = parse('1.2.3-alpha')
      y = parse('1.2.4-alpha')
      expect(x == y).to eql(false)
    end

    it 'should yield false when the prerelease differs' do
      x = parse('1.2.3-alpha')
      y = parse('1.2.3-beta')
      expect(x == y).to eql(false)
    end

    it 'should yield false when compared to something that is not a Version' do
      x = parse('1.2.3-alpha')
      expect(x == :undef).to eql(false)
    end
  end

  describe '#<=>' do
    def parse(vstring)
      SemanticPuppet::Version.parse(vstring)
    end

    context 'Spec v2.0.0' do
      context 'Section 11' do
        # Precedence refers to how versions are compared to each other when
        # ordered. Precedence MUST be calculated by separating the version into
        # major, minor, patch and pre-release identifiers in that order (Build
        # metadata does not figure into precedence). Precedence is determined
        # by the first difference when comparing each of these identifiers from
        # left to right as follows: Major, minor, and patch versions are always
        # compared numerically.
        # Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
        # When major, minor, and patch are equal, a pre-release version has
        # lower precedence than a normal version.
        # Example: 1.0.0-alpha < 1.0.0.
        # Precedence for two pre-release versions with the same major, minor,
        # and patch version MUST be determined by comparing each dot separated
        # identifier from left to right until a difference is found as follows:
        # identifiers consisting of only digits are compared numerically and
        # identifiers with letters or hyphens are compared lexically in ASCII
        # sort order. Numeric identifiers always have lower precedence than
        # non-numeric identifiers. A larger set of pre-release fields has a
        # higher precedence than a smaller set, if all of the preceding
        # identifiers are equal.
        # Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta
        # < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

        context 'comparisons without prereleases' do
          subject do
            %w[ 1.0.0 2.0.0 2.1.0 2.1.1 ].map { |v| parse(v) }.shuffle
          end

          example 'sorted order' do
            sorted = subject.sort.map { |v| v.to_s }
            expect(sorted).to eql(%w[ 1.0.0 2.0.0 2.1.0 2.1.1 ])
          end
        end

        context 'comparisons against prereleases' do
          let(:stable) { parse('1.0.0') }
          let(:prerelease) { parse('1.0.0-alpha') }

          example 'prereleases have lower precedence' do
            expect(stable).to be > prerelease
            expect(prerelease).to be < stable
          end
        end

        context 'comparisions between prereleases' do
          example 'identical prereleases are equal' do
            expect(parse('1.0.0-rc1')).to eql parse('1.0.0-rc1')
          end

          example 'non-numeric identifiers sort ASCIIbetically' do
            alpha, beta = parse('1.0.0-alpha'), parse('1.0.0-beta')
            expect(alpha).to be < beta
            expect(beta).to be > alpha
          end

          example 'numeric identifiers sort numerically' do
            two, eleven = parse('1.0.0-2'), parse('1.0.0-11')
            expect(two).to be < eleven
            expect(eleven).to be > two
          end

          example 'non-numeric identifiers have a higher precendence' do
            number, word = parse('1.0.0-1'), parse('1.0.0-one')
            expect(number).to be < word
            expect(word).to be > number
          end

          example 'identifiers are parsed left-to-right' do
            a = parse('1.0.0-these.parts.are.the-same.but.not.waffles.123')
            b = parse('1.0.0-these.parts.are.the-same.but.not.123.waffles')
            expect(b).to be < a
            expect(a).to be > b
          end

          example 'larger identifier sets have precendence' do
            a = parse('1.0.0-alpha')
            b = parse('1.0.0-alpha.1')
            expect(a).to be < b
            expect(b).to be > a
          end

          example 'build metadata does not figure into precendence' do
            pending 'build metadata is not yet in scope'
            a = parse('1.0.0-alpha+SHA1')
            b = parse('1.0.0-alpha+MD5')
            expect(a).to eql b
            expect(a.to_s).to_not eql b.to_s
          end

          example 'sorted order' do
            list = %w[
              1.0.0-alpha
              1.0.0-alpha.1
              1.0.0-alpha.beta
              1.0.0-beta
              1.0.0-beta.2
              1.0.0-beta.11
              1.0.0-rc.1
              1.0.0
            ].map { |v| parse(v) }.shuffle

            sorted = list.sort.map { |v| v.to_s }
            expect(sorted).to eql %w[
              1.0.0-alpha
              1.0.0-alpha.1
              1.0.0-alpha.beta
              1.0.0-beta
              1.0.0-beta.2
              1.0.0-beta.11
              1.0.0-rc.1
              1.0.0
            ]
          end
        end
      end
    end

    context 'Spec v1.0.0' do
      context 'Section 4' do
        # A pre-release version number MAY be denoted by appending an arbitrary
        # string immediately following the patch version and a dash. The string
        # MUST be comprised of only alphanumerics plus dash [0-9A-Za-z-].
        # Pre-release versions satisfy but have a lower precedence than the
        # associated normal version. Precedence SHOULD be determined by
        # lexicographic ASCII sort order.
        # For instance: 1.0.0-alpha1 < 1.0.0-beta1 < 1.0.0-beta2 < 1.0.0-rc1 <
        # 1.0.0

        example 'sorted order' do
          list = %w[
            1.0.0-alpha1
            1.0.0-beta1
            1.0.0-beta2
            1.0.0-rc1
            1.0.0
          ].map { |v| parse(v) }.shuffle

          sorted = list.sort.map { |v| v.to_s }
          expect(sorted).to eql %w[
            1.0.0-alpha1
            1.0.0-beta1
            1.0.0-beta2
            1.0.0-rc1
            1.0.0
          ]
        end
      end
    end

  end

  describe '#next' do
    context 'with :major' do
      it 'returns the next major version' do
        expect(subject('1.0.0').next(:major)).to eql(subject('2.0.0'))
      end

      it 'does not modify the original version' do
        v1 = subject('1.0.0')
        v2 = v1.next(:major)
        expect(v1).to_not eql(v2)
      end

      it 'resets the minor and patch versions to 0' do
        expect(subject('1.1.1').next(:major)).to eql(subject('2.0.0'))
      end

      it 'removes any prerelease information' do
        expect(subject('1.0.0-alpha').next(:major)).to eql(subject('2.0.0'))
      end

      it 'removes any build information' do
        pending 'build metadata is not yet in scope'
        expect(subject('1.0.0+abc').next(:major)).to eql(subject('2.0.0'))
      end
    end

    context 'with :minor' do
      it 'returns the next minor version' do
        expect(subject('1.0.0').next(:minor)).to eql(subject('1.1.0'))
      end

      it 'does not modify the original version' do
        v1 = subject('1.0.0')
        v2 = v1.next(:minor)
        expect(v1).to_not eql(v2)
      end

      it 'resets the patch version to 0' do
        expect(subject('1.1.1').next(:minor)).to eql(subject('1.2.0'))
      end

      it 'removes any prerelease information' do
        expect(subject('1.1.0-alpha').next(:minor)).to eql(subject('1.2.0'))
      end

      it 'removes any build information' do
        pending 'build metadata is not yet in scope'
        expect(subject('1.1.0+abc').next(:minor)).to eql(subject('1.2.0'))
      end
    end

    context 'with :patch' do
      it 'returns the next patch version' do
        expect(subject('1.1.1').next(:patch)).to eql(subject('1.1.2'))
      end

      it 'does not modify the original version' do
        v1 = subject('1.0.0')
        v2 = v1.next(:patch)
        expect(v1).to_not eql(v2)
      end

      it 'removes any prerelease information' do
        expect(subject('1.0.0-alpha').next(:patch)).to eql(subject('1.0.1'))
      end

      it 'removes any build information' do
        pending 'build metadata is not yet in scope'
        expect(subject('1.0.0+abc').next(:patch)).to eql(subject('1.0.1'))
      end
    end
  end
end
