require 'spec_helper'
require 'puppet/pops'

describe 'The type calculator' do
  let(:calculator) {  Puppet::Pops::Types::TypeCalculator.new() }

  context 'when inferring ruby' do

    it 'fixnum translates to PIntegerType' do
      calculator.infer(1).class.should == Puppet::Pops::Types::PIntegerType
    end

    it 'large fixnum (or bignum depending on architecture) translates to PIntegerType' do
      calculator.infer(2**33).class.should == Puppet::Pops::Types::PIntegerType
    end

    it 'float translates to PFloatType' do
      calculator.infer(1.3).class.should == Puppet::Pops::Types::PFloatType
    end

    it 'string translates to PStringType' do
      calculator.infer('foo').class.should == Puppet::Pops::Types::PStringType
    end

    it 'boolean true translates to PBooleanType' do
      calculator.infer(true).class.should == Puppet::Pops::Types::PBooleanType
    end

    it 'boolean false translates to PBooleanType' do
      calculator.infer(false).class.should == Puppet::Pops::Types::PBooleanType
    end

    it 'regexp translates to PPatternType' do
      calculator.infer(/^a regular exception$/).class.should == Puppet::Pops::Types::PPatternType
    end

    it 'nil translates to PNilType' do
      calculator.infer(nil).class.should == Puppet::Pops::Types::PNilType
    end

    it 'an instance of class Foo translates to PRubyType[Foo]' do
      class Foo
      end

      t = calculator.infer(Foo.new)
      t.class.should == Puppet::Pops::Types::PRubyType
      t.ruby_class.should == 'Foo'
    end

    context 'array' do
      it 'translates to PArrayType' do
        calculator.infer([1,2]).class.should == Puppet::Pops::Types::PArrayType
      end

      it 'with fixnum values translates to PArrayType[PIntegerType]' do
        calculator.infer([1,2]).element_type.class.should == Puppet::Pops::Types::PIntegerType
      end

      it 'with 32 and 64 bit integer values translates to PArrayType[PIntegerType]' do
        calculator.infer([1,2**33]).element_type.class.should == Puppet::Pops::Types::PIntegerType
      end

      it 'with fixnum and float values translates to PArrayType[PNumericType]' do
        calculator.infer([1,2.0]).element_type.class.should == Puppet::Pops::Types::PNumericType
      end

      it 'with fixnum and string values translates to PArrayType[PLiteralType]' do
        calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with float and string values translates to PArrayType[PLiteralType]' do
        calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with fixnum, float, and string values translates to PArrayType[PLiteralType]' do
        calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with fixnum and regexp values translates to PArrayType[PLiteralType]' do
        calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with string and regexp values translates to PArrayType[PLiteralType]' do
        calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with string and symbol values translates to PArrayType[PObjectType]' do
        calculator.infer(['one', :two]).element_type.class.should == Puppet::Pops::Types::PObjectType
      end

      it 'with fixnum and nil values translates to PArrayType[PIntegerType]' do
        calculator.infer([1, nil]).element_type.class.should == Puppet::Pops::Types::PIntegerType
      end

      it 'with arrays of string values translates to PArrayType[PArrayType[PStringType]]' do
        et = calculator.infer([['first' 'array'], ['second','array']])
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PStringType
      end

      it 'with array of string values and array of fixnums translates to PArrayType[PArrayType[PLiteralType]]' do
        et = calculator.infer([['first' 'array'], [1,2]])
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PLiteralType
      end

      it 'with hashes of string values translates to PArrayType[PHashType[PStringType]]' do
        et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 'first', :second => 'second' }])
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PHashType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PStringType
      end

      it 'with hash of string values and hash of fixnums translates to PArrayType[PHashType[PLiteralType]]' do
        et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 1, :second => 2 }])
        et.class.should == Puppet::Pops::Types::PArrayType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PHashType
        et = et.element_type
        et.class.should == Puppet::Pops::Types::PLiteralType
      end
    end

    context 'hash' do
      it 'translates to PHashType' do
        calculator.infer({:first => 1, :second => 2}).class.should == Puppet::Pops::Types::PHashType
      end

      it 'with symbolic keys translates to PHashType[PRubyType[Symbol],value]' do
        k = calculator.infer({:first => 1, :second => 2}).key_type
        k.class.should == Puppet::Pops::Types::PRubyType
        k.ruby_class.should == 'Symbol'
      end

      it 'with string keys translates to PHashType[PStringType,value]' do
        calculator.infer({'first' => 1, 'second' => 2}).key_type.class.should == Puppet::Pops::Types::PStringType
      end

      it 'with fixnum values translates to PHashType[key,PIntegerType]' do
        calculator.infer({:first => 1, :second => 2}).element_type.class.should == Puppet::Pops::Types::PIntegerType
      end
    end
  end

  context 'when testing if x is assignable to y' do
    it 'should allow all object types to PObjectType' do
      t = Puppet::Pops::Types::PObjectType.new()
      calculator.assignable?(t, t).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PNilType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PDataType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PCollectionType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PRubyType.new()).should() == true
    end

    it 'should reject PObjectType to less generic types' do
      t = Puppet::Pops::Types::PObjectType.new()
      calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
    end

    it 'should allow all data types, array, and hash to PDataType' do
      t = Puppet::Pops::Types::PDataType.new()
      calculator.assignable?(t, t).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true
    end

    it 'should reject PDataType to less generic data types' do
      t = Puppet::Pops::Types::PDataType.new()
      calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
    end

    it 'should reject PDataType to non data types' do
      t = Puppet::Pops::Types::PDataType.new()
      calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(),t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(),t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(),t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
    end

    it 'should allow all literal types to PLiteralType' do
      t = Puppet::Pops::Types::PLiteralType.new()
      calculator.assignable?(t, t).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
      calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
    end

    it 'should reject PLiteralType to less generic literal types' do
      t = Puppet::Pops::Types::PLiteralType.new()
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
    end

    it 'should reject PLiteralType to non literal types' do
      t = Puppet::Pops::Types::PLiteralType.new()
      calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
    end

    it 'should allow all numeric types to PNumericType' do
      t = Puppet::Pops::Types::PNumericType.new()
      calculator.assignable?(t, t).should() == true
      calculator.assignable?(t, Puppet::Pops::Types::PIntegerType.new()).should() == true
      calculator.assignable?(t, Puppet::Pops::Types::PFloatType.new()).should() == true
    end

    it 'should reject PNumericType to less generic numeric types' do
      t = Puppet::Pops::Types::PNumericType.new()
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
    end

    it 'should reject PNumericType to non numeric types' do
      t = Puppet::Pops::Types::PNumericType.new()
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
    end

    it 'should allow all collection types to PCollectionType' do
      t = Puppet::Pops::Types::PCollectionType.new()
      calculator.assignable?(t, t).should() == true
      calculator.assignable?(t, Puppet::Pops::Types::PArrayType.new()).should() == true
      calculator.assignable?(t, Puppet::Pops::Types::PHashType.new()).should() == true
    end

    it 'should reject PCollectionType to less generic collection types' do
      t = Puppet::Pops::Types::PCollectionType.new()
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
    end

    it 'should reject PCollectionType to non collection types' do
      t = Puppet::Pops::Types::PCollectionType.new()
      calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
      calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
    end

    it 'should reject PArrayType to non array type collections' do
      t = Puppet::Pops::Types::PArrayType.new()
      calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
    end

    it 'should reject PHashType to non hash type collections' do
      t = Puppet::Pops::Types::PHashType.new()
      calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
    end

    it 'should recognize mapped ruby types' do 
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Integer).should == true
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Fixnum).should == true
      calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Bignum).should == true
      calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), Float).should == true
      calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), Numeric).should == true
      calculator.assignable?(Puppet::Pops::Types::PNilType.new(), NilClass).should == true
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), FalseClass).should == true
      calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), TrueClass).should == true
      calculator.assignable?(Puppet::Pops::Types::PStringType.new(), String).should == true
      calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), Regexp).should == true
      calculator.assignable?(Puppet::Pops::Types::TypeFactory.array_of_data(), Array).should == true
      calculator.assignable?(Puppet::Pops::Types::TypeFactory.hash_of_data(), Hash).should == true
    end

    it 'should recognize ruby type inheritance' do
      class Foo
      end

      class Bar < Foo
      end

      fooType = calculator.infer(Foo.new)
      barType = calculator.infer(Bar.new)

      calculator.assignable?(fooType, fooType).should == true
      calculator.assignable?(Foo, fooType).should == true

      calculator.assignable?(fooType, barType).should == true
      calculator.assignable?(Foo, barType).should == true

      calculator.assignable?(barType, fooType).should == false
      calculator.assignable?(Bar, fooType).should == false
    end
  end

  context 'when testing if x is instance of type t' do
    it 'should consider fixnum instanceof PIntegerType' do
      calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1)
    end

    it 'should consider fixnum instanceof Fixnum' do
      calculator.instance?(Fixnum, 1)
    end
  end

  context 'when converting a ruby class' do
    it 'should yield \'PIntegerType\' for Integer, Fixnum, and Bignum' do
      [Integer,Fixnum,Bignum].each do |c|
        calculator.type(c).class.should == Puppet::Pops::Types::PIntegerType
      end
    end

    it 'should yield \'PFloatType\' for Float' do
      calculator.type(Float).class.should == Puppet::Pops::Types::PFloatType
    end

    it 'should yield \'PBooleanType\' for FalseClass and TrueClass' do
      [FalseClass,TrueClass].each do |c|
        calculator.type(c).class.should == Puppet::Pops::Types::PBooleanType
      end
    end

    it 'should yield \'PNilType\' for NilClass' do
      calculator.type(NilClass).class.should == Puppet::Pops::Types::PNilType
    end

    it 'should yield \'PStringType\' for String' do
      calculator.type(String).class.should == Puppet::Pops::Types::PStringType
    end

    it 'should yield \'PPatternType\' for Regexp' do
      calculator.type(Regexp).class.should == Puppet::Pops::Types::PPatternType
    end

    it 'should yield \'PArrayType[PDataType]\' for Array' do
      t = calculator.type(Array)
      t.class.should == Puppet::Pops::Types::PArrayType
      t.element_type.class.should == Puppet::Pops::Types::PDataType
    end

    it 'should yield \'PHashType[PLiteralType,PDataType]\' for Hash' do
      t = calculator.type(Hash)
      t.class.should == Puppet::Pops::Types::PHashType
      t.key_type.class.should == Puppet::Pops::Types::PLiteralType
      t.element_type.class.should == Puppet::Pops::Types::PDataType
    end
  end

  context 'when representing the type as string' do
    it 'should yield \'Type\' for PType' do
      calculator.string(Puppet::Pops::Types::PType.new()).should == 'Type'
    end

    it 'should yield \'Object\' for PObjectType' do
      calculator.string(Puppet::Pops::Types::PObjectType.new()).should == 'Object'
    end

    it 'should yield \'Literal\' for PLiteralType' do
      calculator.string(Puppet::Pops::Types::PLiteralType.new()).should == 'Literal'
    end

    it 'should yield \'Boolean\' for PBooleanType' do
      calculator.string(Puppet::Pops::Types::PBooleanType.new()).should == 'Boolean'
    end

    it 'should yield \'Data\' for PDataType' do
      calculator.string(Puppet::Pops::Types::PDataType.new()).should == 'Data'
    end

    it 'should yield \'Numeric\' for PNumericType' do
      calculator.string(Puppet::Pops::Types::PNumericType.new()).should == 'Numeric'
    end

    it 'should yield \'Integer\' for PIntegerType' do
      calculator.string(Puppet::Pops::Types::PIntegerType.new()).should == 'Integer'
    end

    it 'should yield \'Float\' for PFloatType' do
      calculator.string(Puppet::Pops::Types::PFloatType.new()).should == 'Float'
    end

    it 'should yield \'Pattern\' for PPatternType' do
      calculator.string(Puppet::Pops::Types::PPatternType.new()).should == 'Pattern'
    end

    it 'should yield \'String\' for PStringType' do
      calculator.string(Puppet::Pops::Types::PStringType.new()).should == 'String'
    end

    it 'should yield \'Array[Integer]\' for PArrayType[PIntegerType]' do
      t = Puppet::Pops::Types::PArrayType.new()
      t.element_type = Puppet::Pops::Types::PIntegerType.new()
      calculator.string(t).should == 'Array[Integer]'
    end

    it 'should yield \'Hash[String, Integer]\' for PHashType[PStringType, PIntegerType]' do
      t = Puppet::Pops::Types::PHashType.new()
      t.key_type = Puppet::Pops::Types::PStringType.new()
      t.element_type = Puppet::Pops::Types::PIntegerType.new()
      calculator.string(t).should == 'Hash[String, Integer]'
    end
  end

  context 'when processing meta type' do
    it 'should infer PType as the type of all other types' do
      ptype = Puppet::Pops::Types::PType
      calculator.infer(Puppet::Pops::Types::PNilType.new()       ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PDataType.new()      ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PLiteralType.new()   ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PStringType.new()    ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PNumericType.new()   ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PIntegerType.new()   ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PFloatType.new()     ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PPatternType.new()   ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PBooleanType.new()   ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PCollectionType.new()).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PArrayType.new()     ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PHashType.new()      ).is_a?(ptype).should() == true
      calculator.infer(Puppet::Pops::Types::PRubyType.new()      ).is_a?(ptype).should() == true
    end

    it 'should infer PType as the type of ruby classes' do
      class Foo
      end
      [Object, Numeric, Integer, Fixnum, Bignum, Float, String, Regexp, Array, Hash, Foo].each do |c|
        calculator.infer(c).is_a?(Puppet::Pops::Types::PType).should() == true
      end
    end

    it 'should infer PType as the type of PType (meta regression short-circuit)' do
      calculator.infer(Puppet::Pops::Types::PType.new()).is_a?(Puppet::Pops::Types::PType).should() == true
    end
  end
end
