require 'spec_helper'
require 'puppet/pops'
require 'puppet_spec/compiler'

module Puppet::Pops
module Types
describe 'Puppet Type System' do
  include PuppetSpec::Compiler

  let(:tf) { TypeFactory }
  context 'Integer type' do
    let!(:a) { tf.range(10, 20) }
    let!(:b) { tf.range(18, 28) }
    let!(:c) { tf.range( 2, 12) }
    let!(:d) { tf.range(12, 18) }
    let!(:e) { tf.range( 8, 22) }
    let!(:f) { tf.range( 8,  9) }
    let!(:g) { tf.range(21, 22) }
    let!(:h) { tf.range(30, 31) }
    let!(:i) { tf.float_range(1.0, 30.0) }
    let!(:j) { tf.float_range(1.0, 9.0) }

    context 'when testing if ranges intersect' do
      it 'detects an intersection when self is before its argument' do
        expect(a.intersect?(b)).to be_truthy
      end

      it 'detects an intersection when self is after its argument' do
        expect(a.intersect?(c)).to be_truthy
      end

      it 'detects an intersection when self covers its argument' do
        expect(a.intersect?(d)).to be_truthy
      end

      it 'detects an intersection when self equals its argument' do
        expect(a.intersect?(a)).to be_truthy
      end

      it 'detects an intersection when self is covered by its argument' do
        expect(a.intersect?(e)).to be_truthy
      end

      it 'does not consider an adjacent range to be intersecting' do
        [f, g].each {|x| expect(a.intersect?(x)).to be_falsey }
      end

      it 'does not consider an range that is apart to be intersecting' do
        expect(a.intersect?(h)).to be_falsey
      end

      it 'does not consider an overlapping float range to be intersecting' do
        expect(a.intersect?(i)).to be_falsey
      end
    end

    context 'when testing if ranges are adjacent' do
      it 'detects an adjacent type when self is after its argument' do
        expect(a.adjacent?(f)).to be_truthy
      end

      it 'detects an adjacent type when self is before its argument' do
        expect(a.adjacent?(g)).to be_truthy
      end

      it 'does not consider overlapping types to be adjacent' do
        [a, b, c, d, e].each { |x| expect(a.adjacent?(x)).to be_falsey }
      end

      it 'does not consider an range that is apart to be adjacent' do
        expect(a.adjacent?(h)).to be_falsey
      end

      it 'does not consider an adjacent float range to be adjancent' do
        expect(a.adjacent?(j)).to be_falsey
      end
    end

    context 'when merging ranges' do
      it 'will merge intersecting ranges' do
        expect(a.merge(b)).to eq(tf.range(10, 28))
      end

      it 'will merge adjacent ranges' do
        expect(a.merge(g)).to eq(tf.range(10, 22))
      end

      it 'will not merge ranges that are apart' do
        expect(a.merge(h)).to be_nil
      end

      it 'will not merge overlapping float ranges' do
        expect(a.merge(i)).to be_nil
      end

      it 'will not merge adjacent float ranges' do
        expect(a.merge(j)).to be_nil
      end
    end
  end

  context 'Float type' do
    let!(:a) { tf.float_range(10.0, 20.0) }
    let!(:b) { tf.float_range(18.0, 28.0) }
    let!(:c) { tf.float_range( 2.0, 12.0) }
    let!(:d) { tf.float_range(12.0, 18.0) }
    let!(:e) { tf.float_range( 8.0, 22.0) }
    let!(:f) { tf.float_range(30.0, 31.0) }
    let!(:g) { tf.range(1, 30) }

    context 'when testing if ranges intersect' do
      it 'detects an intersection when self is before its argument' do
        expect(a.intersect?(b)).to be_truthy
      end

      it 'detects an intersection when self is after its argument' do
        expect(a.intersect?(c)).to be_truthy
      end

      it 'detects an intersection when self covers its argument' do
        expect(a.intersect?(d)).to be_truthy
      end

      it 'detects an intersection when self equals its argument' do
        expect(a.intersect?(a)).to be_truthy
      end

      it 'detects an intersection when self is covered by its argument' do
        expect(a.intersect?(e)).to be_truthy
      end

      it 'does not consider an range that is apart to be intersecting' do
        expect(a.intersect?(f)).to be_falsey
      end

      it 'does not consider an overlapping integer range to be intersecting' do
        expect(a.intersect?(g)).to be_falsey
      end
    end

    context 'when merging ranges' do
      it 'will merge intersecting ranges' do
        expect(a.merge(b)).to eq(tf.float_range(10.0, 28.0))
      end

      it 'will not merge ranges that are apart' do
        expect(a.merge(f)).to be_nil
      end

      it 'will not merge overlapping integer ranges' do
        expect(a.merge(g)).to be_nil
      end
    end
  end

  context 'Boolean type' do
    it 'parameterized type is assignable to base type' do
      code = <<-CODE
        notice(Boolean[true] < Boolean)
        notice(Boolean[false] < Boolean)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true'])
    end

    it 'boolean literals are instances of the base type' do
      code = <<-CODE
        notice(true =~ Boolean)
        notice(false =~ Boolean)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true'])
    end

    it 'boolean literals are instances of type parameterized with the same literal' do
      code = <<-CODE
        notice(true =~ Boolean[true])
        notice(false =~ Boolean[false])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true'])
    end

    it 'boolean literals are not instances of type parameterized with a different literal' do
      code = <<-CODE
        notice(true =~ Boolean[false])
        notice(false =~ Boolean[true])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false', 'false'])
    end
  end

  context 'Enum type' do
    it 'sorts its entries' do
      code = <<-CODE
        Enum[c,b,a].each |$e| { notice $e }
      CODE
      expect(eval_and_collect_notices(code)).to eq(['a', 'b', 'c'])
    end

    it 'makes entries unique' do
      code = <<-CODE
        Enum[a,b,c,b,a].each |$e| { notice $e }
      CODE
      expect(eval_and_collect_notices(code)).to eq(['a', 'b', 'c'])
    end

    it 'is case sensitive by default' do
      code = <<-CODE
        notice('A' =~ Enum[a,b])
        notice('a' =~ Enum[a,b])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false', 'true'])
    end

    it 'is case insensitive when last parameter argument is true' do
      code = <<-CODE
        notice('A' =~ Enum[a,b,true])
        notice('a' =~ Enum[a,b,true])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true'])
    end
  end

  context 'Iterable type' do
    it 'can be parameterized with element type' do
      code = <<-CODE
      function foo(Iterable[String] $x) {
        $x.each |$e| {
          notice $e
        }
      }
      foo([bar, baz, cake])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['bar', 'baz', 'cake'])
    end
  end

  context 'Iterator type' do
    let!(:iterint) { tf.iterator(tf.integer) }

    context 'when testing instance?' do
      it 'will consider an iterable on an integer is an instance of Iterator[Integer]' do
        expect(iterint.instance?(Iterable.on(3))).to be_truthy
      end

      it 'will consider an iterable on string to be an instance of Iterator[Integer]' do
        expect(iterint.instance?(Iterable.on('string'))).to be_falsey
      end
    end

    context 'when testing assignable?' do
      it 'will consider an iterator with an assignable type as assignable' do
        expect(tf.iterator(tf.numeric).assignable?(iterint)).to be_truthy
      end

      it 'will not consider an iterator with a non assignable type as assignable' do
        expect(tf.iterator(tf.string).assignable?(iterint)).to be_falsey
      end
    end

    context 'when asked for an iterable type' do
      it 'the default iterator type returns the default iterable type' do
        expect(PIteratorType::DEFAULT.iterable_type).to be(PIterableType::DEFAULT)
      end

      it 'a typed iterator type returns the an equally typed iterable type' do
        expect(iterint.iterable_type).to eq(tf.iterable(tf.integer))
      end
    end

    it 'can be parameterized with an element type' do
      code = <<-CODE
      function foo(Iterator[String] $x) {
        $x.each |$e| {
          notice $e
        }
      }
      foo([bar, baz, cake].reverse_each)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['cake', 'baz', 'bar'])
    end
  end

  context 'Collection type' do
    it 'can be parameterized with a range' do
      code = <<-CODE
      notice(Collection[5, default] == Collection[5])
      notice(Collection[5, 5] > Tuple[Integer, 0, 10])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'false'])
    end
  end

  context 'Struct type' do
    context 'can be used as key in hash' do
      it 'compacts optional in optional in optional to just optional' do
        key1 = tf.struct({'foo' => tf.string})
        key2 = tf.struct({'foo' => tf.string})
        expect({key1 => 'hi'}[key2]).to eq('hi')
      end
    end
  end

  context 'Optional type' do
    let!(:overlapping_ints) { tf.variant(tf.range(10, 20), tf.range(18, 28)) }
    let!(:optoptopt) { tf.optional(tf.optional(tf.optional(overlapping_ints))) }
    let!(:optnu) { tf.optional(tf.not_undef(overlapping_ints)) }

    context 'when normalizing' do
      it 'compacts optional in optional in optional to just optional' do
        expect(optoptopt.normalize).to eq(tf.optional(tf.range(10, 28)))
      end
    end

    it 'compacts NotUndef in Optional to just Optional' do
      expect(optnu.normalize).to eq(tf.optional(tf.range(10, 28)))
    end
  end

  context 'NotUndef type' do
    let!(:nununu) { tf.not_undef(tf.not_undef(tf.not_undef(tf.any))) }
    let!(:nuopt) { tf.not_undef(tf.optional(tf.any)) }
    let!(:nuoptint) { tf.not_undef(tf.optional(tf.integer)) }

    context 'when normalizing' do
      it 'compacts NotUndef in NotUndef in NotUndef to just NotUndef' do
        expect(nununu.normalize).to eq(tf.not_undef(tf.any))
      end

      it 'compacts Optional in NotUndef to just NotUndef' do
        expect(nuopt.normalize).to eq(tf.not_undef(tf.any))
      end

      it 'compacts NotUndef[Optional[Integer]] in NotUndef to just Integer' do
        expect(nuoptint.normalize).to eq(tf.integer)
      end
    end
  end

  context 'Variant type' do
    let!(:overlapping_ints) { tf.variant(tf.range(10, 20), tf.range(18, 28)) }
    let!(:adjacent_ints) { tf.variant(tf.range(10, 20), tf.range(8, 9)) }
    let!(:mix_ints) { tf.variant(overlapping_ints, adjacent_ints) }
    let!(:overlapping_floats) { tf.variant(tf.float_range(10.0, 20.0), tf.float_range(18.0, 28.0)) }
    let!(:enums) { tf.variant(tf.enum('a', 'b'), tf.enum('b', 'c')) }
    let!(:enums_s_is) { tf.variant(tf.enum('a', 'b'), tf.enum('b', 'c', true)) }
    let!(:enums_is_is) { tf.variant(tf.enum('A', 'b', true), tf.enum('B', 'c', true)) }
    let!(:patterns) { tf.variant(tf.pattern('a', 'b'), tf.pattern('b', 'c')) }
    let!(:with_undef) { tf.variant(tf.undef, tf.range(1,10)) }
    let!(:all_optional) { tf.variant(tf.optional(tf.range(1,10)), tf.optional(tf.range(11,20))) }
    let!(:groups) { tf.variant(mix_ints, overlapping_floats, enums, patterns, with_undef, all_optional) }

    context 'when normalizing contained types that' do
      it 'are overlapping ints, the result is a range' do
        expect(overlapping_ints.normalize).to eq(tf.range(10, 28))
      end

      it 'are adjacent ints, the result is a range' do
        expect(adjacent_ints.normalize).to eq(tf.range(8, 20))
      end

      it 'are mixed variants with adjacent and overlapping ints, the result is a range' do
        expect(mix_ints.normalize).to eq(tf.range(8, 28))
      end

      it 'are overlapping floats, the result is a float range' do
        expect(overlapping_floats.normalize).to eq(tf.float_range(10.0, 28.0))
      end

      it 'are enums, the result is an enum' do
        expect(enums.normalize).to eq(tf.enum('a', 'b', 'c'))
      end

      it 'are case sensitive versus case insensitive enums, does not merge the enums' do
        expect(enums_s_is.normalize).to eq(enums_s_is)
      end

      it 'are case insensitive enums, result is case insensitive and unique irrespective of case' do
        expect(enums_is_is.normalize).to eq(tf.enum('a', 'b', 'c', true))
      end

      it 'are patterns, the result is a pattern' do
        expect(patterns.normalize).to eq(tf.pattern('a', 'b', 'c'))
      end

      it 'contains an Undef, the result is Optional' do
        expect(with_undef.normalize).to eq(tf.optional(tf.range(1,10)))
      end

      it 'are all Optional, the result is an Optional with normalized type' do
        expect(all_optional.normalize).to eq(tf.optional(tf.range(1,20)))
      end

      it 'can be normalized in groups, the result is a Variant containing the resulting normalizations' do
        expect(groups.normalize).to eq(tf.optional(
          tf.variant(
            tf.range(1, 28),
            tf.float_range(10.0, 28.0),
            tf.enum('a', 'b', 'c'),
            tf.pattern('a', 'b', 'c'))))
      end
    end

    context 'when generalizing' do
      it 'will generalize and compact contained types' do
        expect(tf.variant(tf.string(tf.range(3,3)), tf.string(tf.range(5,5))).generalize).to eq(tf.variant(tf.string))
      end
    end
  end

  context 'Runtime type' do
    it 'can be created with a runtime and a runtime type name' do
      expect(tf.runtime('ruby', 'Hash').to_s).to eq("Runtime[ruby, 'Hash']")
    end

    it 'can be created with a runtime and, puppet name pattern, and runtime replacement' do
      expect(tf.runtime('ruby', [/^MyPackage::(.*)$/, 'MyModule::\1']).to_s).to eq("Runtime[ruby, [/^MyPackage::(.*)$/, 'MyModule::\\1']]")
    end

    it 'will map a Puppet name to a runtime type' do
      t = tf.runtime('ruby', [/^MyPackage::(.*)$/, 'MyModule::\1'])
      expect(t.from_puppet_name('MyPackage::MyType').to_s).to eq("Runtime[ruby, 'MyModule::MyType']")
    end

    it 'with parameters is assignable to the default Runtime type' do
      code = <<-CODE
      notice(Runtime[ruby, 'Symbol'] < Runtime)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true'])
    end

    it 'with parameters is not assignable from the default Runtime type' do
      code = <<-CODE
      notice(Runtime < Runtime[ruby, 'Symbol'])
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false'])
    end

    it 'default is assignable to itself' do
      code = <<-CODE
      notice(Runtime < Runtime)
      notice(Runtime <= Runtime)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false', 'true'])
    end
  end

  context 'Type aliases' do
    it 'will resolve nested objects using self recursion' do
      code = <<-CODE
      type Tree = Hash[String,Variant[String,Tree]]
      notice({a => {b => {c => d}}} =~ Tree)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true'])
    end

    it 'will find mismatches using self recursion' do
      code = <<-CODE
      type Tree = Hash[String,Variant[String,Tree]]
      notice({a => {b => {c => 1}}} =~ Tree)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false'])
    end

    it 'will not allow an alias chain to only contain aliases' do
      code = <<-CODE
      type Foo = Bar
      type Fee = Foo
      type Bar = Fee
      notice(0 =~ Bar)
      CODE
      expect { eval_and_collect_notices(code) }.to raise_error(Puppet::Error, /Type alias 'Bar' cannot be resolved to a real type/)
    end

    it 'will not allow an alias chain that contains nothing but aliases and variants' do
      code = <<-CODE
      type Foo = Bar
      type Fee = Foo
      type Bar = Variant[Fee,Foo]
      notice(0 =~ Bar)
      CODE
      expect { eval_and_collect_notices(code) }.to raise_error(Puppet::Error, /Type alias 'Bar' cannot be resolved to a real type/)
    end

    it 'will not allow an alias to directly reference itself' do
      code = <<-CODE
      type Foo = Foo
      notice(0 =~ Foo)
      CODE
      expect { eval_and_collect_notices(code) }.to raise_error(Puppet::Error, /Type alias 'Foo' cannot be resolved to a real type/)
    end

    it 'will allow an alias to directly reference itself in a variant with other types' do
      code = <<-CODE
      type Foo = Variant[Foo,String]
      notice(a =~ Foo)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true'])
    end

    it 'will allow an alias where a variant references an alias with a variant that references itself' do
      code = <<-CODE
      type X = Variant[Y, Integer]
      type Y = Variant[X, String]

      notice(X >= X)
      notice(X >= Y)
      notice(Y >= X)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true','true','true'])
    end

    it 'will detect a mismatch in an alias that directly references itself in a variant with other types' do
      code = <<-CODE
      type Foo = Variant[Foo,String]
      notice(3 =~ Foo)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['false'])
    end

    it 'will normalize a Variant containing a self reference so that the self reference is removed' do
      code = <<-CODE
      type Foo = Variant[Foo,String,Integer]
      assert_type(Foo, /x/)
      CODE
      expect { eval_and_collect_notices(code) }.to raise_error(/expects a Foo = Variant\[String, Integer\] value, got Regexp/)
    end

    it 'will handle a scalar correctly in combinations of nested aliased variants' do
      code = <<-CODE
      type Bar = Variant[Foo,Integer]
      type Foo = Variant[Bar,String]
      notice(a =~ Foo)
      notice(1 =~ Foo)
      notice(/x/ =~ Foo)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true', 'false'])
    end

    it 'will handle a non scalar correctly in combinations of nested aliased array with nested variants' do
      code = <<-CODE
      type Bar = Variant[Foo,Integer]
      type Foo = Array[Variant[Bar,String]]
      notice([a] =~ Foo)
      notice([1] =~ Foo)
      notice([/x/] =~ Foo)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true', 'false'])
    end

    it 'will handle a non scalar correctly in combinations of nested aliased variants with array' do
      code = <<-CODE
      type Bar = Variant[Foo,Array[Integer]]
      type Foo = Variant[Bar,Array[String]]
      notice([a] =~ Foo)
      notice([1] =~ Foo)
      notice([/x/] =~ Foo)
      CODE
      expect(eval_and_collect_notices(code)).to eq(['true', 'true', 'false'])
    end

    it 'will not allow dynamic constructs in type definition' do
      code = <<-CODE
      type Foo = Enum[$facts[os][family]]
      notice(Foo)
      CODE
      expect{ eval_and_collect_notices(code) }.to raise_error(Puppet::Error,
        /The expression <\$facts\[os\]\[family\]> is not a valid type specification/)
    end
  end

  context 'Type mappings' do
    it 'can register a singe type mapping' do
      source = <<-CODE
        type MyModule::ImplementationRegistry = Object[{}]
        type Runtime[ruby, 'Puppet::Pops::Types::ImplementationRegistry'] = MyModule::ImplementationRegistry
        notice(true)
      CODE
      collect_notices(source) do |compiler|
        compiler.compile do |catalog|
          type = Loaders.implementation_registry.type_for_module(ImplementationRegistry)
          expect(type).to be_a(PObjectType)
          expect(type.name).to eql('MyModule::ImplementationRegistry')
          catalog
        end
      end
    end

    it 'can register a regexp based mapping' do
      source = <<-CODE
        type MyModule::TypeMismatchDescriber = Object[{}]
        type Runtime[ruby, [/^Puppet::Pops::Types::(\\w+)$/, 'MyModule::\\1']] = [/^MyModule::(\\w+)$/, 'Puppet::Pops::Types::\\1']
        notice(true)
      CODE
      collect_notices(source) do |compiler|
        compiler.compile do |catalog|
          type = Loaders.implementation_registry.type_for_module(TypeMismatchDescriber)
          expect(type).to be_a(PObjectType)
          expect(type.name).to eql('MyModule::TypeMismatchDescriber')
          catalog
        end
      end
    end

    it 'a type mapping affects type inference' do
      source = <<-CODE
        type MyModule::ImplementationRegistry = Object[{}]
        type Runtime[ruby, 'Puppet::Pops::Types::ImplementationRegistry'] = MyModule::ImplementationRegistry
        notice(true)
      CODE
      collect_notices(source) do |compiler|
        compiler.compile do |catalog|
          type = TypeCalculator.singleton.infer(Loaders.implementation_registry)
          expect(type).to be_a(PObjectType)
          expect(type.name).to eql('MyModule::ImplementationRegistry')
          catalog
        end
      end
    end
  end

  context 'When attempting to redefine a built in type' do
    it 'such as Integer, an error is raised' do
      code = <<-CODE
        type Integer = String
        notice 'hello' =~ Integer
      CODE
      expect{ eval_and_collect_notices(code) }.to raise_error(/Attempt to redefine entity 'http:\/\/puppet\.com\/2016\.1\/runtime\/type\/integer'. Originally set by Puppet-Type-System\/Static-Loader/)
    end
  end

  context 'instantiation via new_function is supported by' do
    let(:loader) { Loader::BaseLoader.new(nil, "types_unit_test_loader") }
    it 'Integer' do
      func_class = tf.integer.new_function
      expect(func_class).to be_a(Class)
      expect(func_class.superclass).to be(Puppet::Functions::Function)
    end

    it 'Optional[Integer]' do
      func_class = tf.optional(tf.integer).new_function
      expect(func_class).to be_a(Class)
      expect(func_class.superclass).to be(Puppet::Functions::Function)
    end

    it 'Regexp' do
      func_class = tf.regexp.new_function
      expect(func_class).to be_a(Class)
      expect(func_class.superclass).to be(Puppet::Functions::Function)
    end
  end

  context 'instantiation via new_function is not supported by' do
    let(:loader) { Loader::BaseLoader.new(nil, "types_unit_test_loader") }

      it 'Any, Scalar, Collection' do
        [tf.any, tf.scalar, tf.collection ].each do |t|
        expect { t.new_function
        }.to raise_error(ArgumentError, /Creation of new instance of type '#{t.to_s}' is not supported/)
      end
    end
  end

  context 'instantiation via ruby create function' do
    around(:each) do |example|
      Puppet.override(:loaders => Loaders.new(Puppet::Node::Environment.create(:testing, []))) do
        example.run
      end
    end

    it 'is supported by Integer' do
      int = tf.integer.create('32')
      expect(int).to eq(32)
    end

    it 'is supported by Regexp' do
      rx = tf.regexp.create('[a-z]+')
      expect(rx).to eq(/[a-z]+/)
    end

    it 'is supported by Optional[Integer]' do
      int = tf.optional(tf.integer).create('32')
      expect(int).to eq(32)
    end

    it 'is not supported by Any, Scalar, Collection' do
      [tf.any, tf.scalar, tf.collection ].each do |t|
        expect { t.create }.to raise_error(ArgumentError, /Creation of new instance of type '#{t.to_s}' is not supported/)
      end
    end
  end

  context 'creation of parameterized type via ruby create function on class' do
    around(:each) do |example|
      Puppet.override(:loaders => Loaders.new(Puppet::Node::Environment.create(:testing, []))) do
        example.run
      end
    end

    it 'is supported by Integer' do
      int_type = tf.integer.class.create(0, 32)
      expect(int_type).to eq(tf.range(0, 32))
    end

    it 'is supported by Regexp' do
      rx_type = tf.regexp.class.create('[a-z]+')
      expect(rx_type).to eq(tf.regexp(/[a-z]+/))
    end
  end

  context 'backward compatibility' do
    it 'PTypeType can be accessed from PType' do
      # should appoint the exact same instance
      expect(PType).to equal(PTypeType)
    end

    it 'PClassType can be accessed from PHostClassType' do
      # should appoint the exact same instance
      expect(PHostClassType).to equal(PClassType)
    end
  end
end
end
end
