require 'spec_helper'
require 'semantic_puppet/version'

describe SemanticPuppet::VersionRange do

  describe '.parse' do
    def self.test_range(range_list, str, includes, excludes)
      Array(range_list).each do |expr|
        example "#{expr.inspect} stringifies as #{str}" do
          range = SemanticPuppet::VersionRange.parse(expr)
          expect(range.to_s).to eql str
        end

        includes.each do |vstring|
          example "#{expr.inspect} includes #{vstring}" do
            range = SemanticPuppet::VersionRange.parse(expr)
            expect(range).to include(SemanticPuppet::Version.parse(vstring))
          end

          example "parse(#{expr.inspect}).to_s includes #{vstring}" do
            range = SemanticPuppet::VersionRange.parse(expr)
            range = SemanticPuppet::VersionRange.parse(range.to_s)
            expect(range).to include(SemanticPuppet::Version.parse(vstring))
          end
        end

        excludes.each do |vstring|
          example "#{expr.inspect} excludes #{vstring}" do
            range = SemanticPuppet::VersionRange.parse(expr)
            expect(range).to_not include(SemanticPuppet::Version.parse(vstring))
          end

          example "parse(#{expr.inspect}).to_s excludes #{vstring}" do
            range = SemanticPuppet::VersionRange.parse(expr)
            range = SemanticPuppet::VersionRange.parse(range.to_s)
            expect(range).to_not include(SemanticPuppet::Version.parse(vstring))
          end
        end
      end
    end

    context 'loose version expressions' do
      expressions = {
        [ '1.2.3-alpha' ] => {
          :to_str   => '1.2.3-alpha',
          :includes => [ '1.2.3-alpha'  ],
          :excludes => [ '1.2.3-999', '1.2.3-beta' ],
        },
        [ '1.2.3' ] => {
          :to_str   => '1.2.3',
          :includes => [ '1.2.3-alpha', '1.2.3' ],
          :excludes => [ '1.2.2', '1.2.4-alpha' ],
        },
        [ '1.2', '1.2.x', '1.2.X' ] => {
          :to_str   => '1.2.x',
          :includes => [ '1.2.0-alpha', '1.2.0', '1.2.999' ],
          :excludes => [ '1.1.999', '1.3.0-0' ],
        },
        [ '1', '1.x', '1.X' ] => {
          :to_str   => '1.x',
          :includes => [ '1.0.0-alpha', '1.999.0' ],
          :excludes => [ '0.999.999', '2.0.0-0' ],
        },
      }

      expressions.each do |range, vs|
        test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
      end
    end

    context 'open-ended expressions' do
      expressions = {
        [ '>1.2.3', '> 1.2.3' ] => {
          :to_str   => '>=1.2.4',
          :includes => [ '1.2.4-0', '999.0.0' ],
          :excludes => [ '1.2.3' ],
        },
        [ '>1.2.3-alpha', '> 1.2.3-alpha' ] => {
          :to_str   => '>1.2.3-alpha',
          :includes => [ '1.2.3-alpha.0', '1.2.3-alpha0', '999.0.0' ],
          :excludes => [ '1.2.3-alpha' ],
        },

        [ '>=1.2.3', '>= 1.2.3' ] => {
          :to_str   => '>=1.2.3',
          :includes => [ '1.2.3-0', '999.0.0' ],
          :excludes => [ '1.2.2' ],
        },
        [ '>=1.2.3-alpha', '>= 1.2.3-alpha' ] => {
          :to_str   => '>=1.2.3-alpha',
          :includes => [ '1.2.3-alpha', '1.2.3-alpha0', '999.0.0' ],
          :excludes => [ '1.2.3-alph' ],
        },

        [ '<1.2.3', '< 1.2.3' ] => {
          :to_str   => '<1.2.3',
          :includes => [ '0.0.0-0', '1.2.2' ],
          :excludes => [ '1.2.3-0', '2.0.0' ],
        },
        [ '<1.2.3-alpha', '< 1.2.3-alpha' ] => {
          :to_str   => '<1.2.3-alpha',
          :includes => [ '0.0.0-0', '1.2.3-alph' ],
          :excludes => [ '1.2.3-alpha', '2.0.0' ],
        },

        [ '<=1.2.3', '<= 1.2.3' ] => {
          :to_str   => '<1.2.4',
          :includes => [ '0.0.0-0', '1.2.3' ],
          :excludes => [ '1.2.4-0' ],
        },
        [ '<=1.2.3-alpha', '<= 1.2.3-alpha' ] => {
          :to_str   => '<=1.2.3-alpha',
          :includes => [ '0.0.0-0', '1.2.3-alpha' ],
          :excludes => [ '1.2.3-alpha0', '1.2.3-alpha.0', '1.2.3-alpha'.next ],
        },
      }

      expressions.each do |range, vs|
        test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
      end
    end

    context '"reasonably close" expressions' do
      expressions = {
        [ '~ 1', '~1' ] => {
          :to_str   => '1.x',
          :includes => [ '1.0.0-0', '1.999.999' ],
          :excludes => [ '0.999.999', '2.0.0-0' ],
        },
        [ '~ 1.2', '~1.2' ] => {
          :to_str   => '1.2.x',
          :includes => [ '1.2.0-0', '1.2.999' ],
          :excludes => [ '1.1.999', '1.3.0-0' ],
        },
        [ '~ 1.2.3', '~1.2.3' ] => {
          :to_str   => '>=1.2.3 <1.3.0',
          :includes => [ '1.2.3-0', '1.2.5' ],
          :excludes => [ '1.2.2', '1.3.0-0' ],
        },
        [ '~ 1.2.3-alpha', '~1.2.3-alpha' ] => {
          :to_str   => '>=1.2.3-alpha <1.2.4',
          :includes => [ '1.2.3-alpha', '1.2.3' ],
          :excludes => [ '1.2.3-alph', '1.2.4-0' ],
        },
      }

      expressions.each do |range, vs|
        test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
      end
    end

    context 'inclusive range expressions' do
      expressions = {
        '1.2.3 - 1.3.4' => {
          :to_str   => '>=1.2.3 <1.3.5',
          :includes => [ '1.2.3-0', '1.3.4' ],
          :excludes => [ '1.2.2', '1.3.5-0' ],
        },
        '1.2.3 - 1.3.4-alpha' => {
          :to_str   => '>=1.2.3 <=1.3.4-alpha',
          :includes => [ '1.2.3-0', '1.3.4-alpha' ],
          :excludes => [ '1.2.2', '1.3.4-alpha0', '1.3.5' ],
        },

        '1.2.3-alpha - 1.3.4' => {
          :to_str   => '>=1.2.3-alpha <1.3.5',
          :includes => [ '1.2.3-alpha', '1.3.4' ],
          :excludes => [ '1.2.3-alph', '1.3.5-0' ],
        },
        '1.2.3-alpha - 1.3.4-alpha' => {
          :to_str   => '>=1.2.3-alpha <=1.3.4-alpha',
          :includes => [ '1.2.3-alpha', '1.3.4-alpha' ],
          :excludes => [ '1.2.3-alph', '1.3.4-alpha0', '1.3.5' ],
        },
      }

      expressions.each do |range, vs|
        test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
      end
    end

    context 'unioned expressions' do
      expressions = {
        [ '1.2 <1.2.5' ] => {
          :to_str   => '>=1.2.0 <1.2.5',
          :includes => [ '1.2.0-0', '1.2.4' ],
          :excludes => [ '1.1.999', '1.2.5-0', '1.9.0' ],
        },
        [ '1 <=1.2.5' ] => {
          :to_str   => '>=1.0.0 <1.2.6',
          :includes => [ '1.0.0-0', '1.2.5' ],
          :excludes => [ '0.999.999', '1.2.6-0', '1.9.0' ],
        },
        [ '>1.0.0 >2.0.0 >=3.0.0 <5.0.0' ] => {
          :to_str   => '>=3.0.0 <5.0.0',
          :includes => [ '3.0.0-0', '4.999.999' ],
          :excludes => [ '2.999.999', '5.0.0-0' ],
        },
        [ '<1.0.0 >2.0.0' ] => {
          :to_str   => '<0.0.0',
          :includes => [  ],
          :excludes => [ '0.0.0-0' ],
        },
      }

      expressions.each do |range, vs|
        test_range(range, vs[:to_str], vs[:includes], vs[:excludes])
      end
    end

    context 'invalid expressions' do
      example 'raise an appropriate exception' do
        ex = [ ArgumentError, 'Unparsable version range: "invalid"' ]
        expect { SemanticPuppet::VersionRange.parse('invalid') }.to raise_error(*ex)
      end
    end
  end

  describe '#intersection' do
    def self.v(num)
      SemanticPuppet::Version.parse("#{num}.0.0")
    end

    def self.range(x, y, ex = false)
      SemanticPuppet::VersionRange.new(v(x), v(y), ex)
    end

    EMPTY_RANGE = SemanticPuppet::VersionRange::EMPTY_RANGE

    tests = {
      # This falls entirely before the target range
      range(1, 4) => [ EMPTY_RANGE ],

      # This falls entirely after the target range
      range(11, 15) => [ EMPTY_RANGE ],

      # This overlaps the beginning of the target range
      range(1, 6) => [ range(5, 6) ],

      # This overlaps the end of the target range
      range(9, 15) => [ range(9, 10), range(9, 10, true) ],

      # This shares the first value of the target range
      range(1, 5) => [ range(5, 5) ],

      # This shares the last value of the target range
      range(10, 15)  => [ range(10, 10), EMPTY_RANGE ],

      # This shares both values with the target range
      range(5, 10) => [ range(5, 10), range(5, 10, true) ],

      # This is a superset of the target range
      range(4, 11) => [ range(5, 10), range(5, 10, true) ],

      # This is a subset of the target range
      range(6, 9) => [ range(6, 9) ],

      # This shares the first value of the target range, but excludes it
      range(1, 5, true)   => [ EMPTY_RANGE ],

      # This overlaps the beginning of the target range, with an excluded end
      range(1, 7, true)   => [ range(5, 7, true) ],

      # This shares both values with the target range, and excludes the end
      range(5, 10, true)  => [ range(5, 10, true) ],
    }

    inclusive = range(5, 10)
    context "between #{inclusive} &" do
      tests.each do |subject, result|
        result = result.first

        example subject do
          expect(inclusive & subject).to eql(result)
        end
      end
    end

    exclusive = range(5, 10, true)
    context "between #{exclusive} &" do
      tests.each do |subject, result|
        result = result.last

        example subject do
          expect(exclusive & subject).to eql(result)
        end
      end
    end

    context 'is commutative' do
      tests.each do |subject, _|
        example "between #{inclusive} & #{subject}" do
          expect(inclusive & subject).to eql(subject & inclusive)
        end
        example "between #{exclusive} & #{subject}" do
          expect(exclusive & subject).to eql(subject & exclusive)
        end
      end
    end

    it 'cannot intersect with non-VersionRanges' do
      msg = "value must be a SemanticPuppet::VersionRange"
      expect { inclusive.intersection(1..2) }.to raise_error(msg)
    end
  end

end
