require 'spec_helper'
require 'puppet/pops'

module Puppet::Pops
module Serialization
  describe 'Passing values through ToDataConverter/FromDataConverter' do
  let(:dumper) { Model::ModelTreeDumper.new }
  let(:io) { StringIO.new }
  let(:parser) { Parser::EvaluatingParser.new }
  let(:env) { Puppet::Node::Environment.create(:testing, []) }
  let(:loaders) { Puppet::Pops::Loaders.new(env) }
  let(:loader) { loaders.find_loader(nil) }
  let(:to_converter) { ToDataConverter.new(:rich_data => true) }
  let(:from_converter) { FromDataConverter.new(:loader => loader) }

  before(:each) do
    Puppet.lookup(:environments).clear_all
    Puppet.push_context(:loaders => loaders, :current_environment => env)
  end

  after(:each) do
    Puppet.pop_context
  end

  def write(value)
    io.reopen
    value = to_converter.convert(value)
    expect(Types::TypeFactory.data).to be_instance(value)
    io << [value].to_json
    io.rewind
  end

  def write_raw(value)
    io.reopen
    expect(Types::TypeFactory.data).to be_instance(value)
    io << [value].to_json
    io.rewind
  end

  def read
    from_converter.convert(::JSON.parse(io.read)[0])
  end

  def parse(string)
    parser.parse_string(string, '/home/tester/experiments/manifests/init.pp')
  end

  context 'can write and read a' do
    it 'String' do
      val = 'the value'
      write(val)
      val2 = read
      expect(val2).to be_a(String)
      expect(val2).to eql(val)
    end

    it 'Integer' do
      val = 32
      write(val)
      val2 = read
      expect(val2).to be_a(Integer)
      expect(val2).to eql(val)
    end

    it 'Float' do
      val = 32.45
      write(val)
      val2 = read
      expect(val2).to be_a(Float)
      expect(val2).to eql(val)
    end

    it 'true' do
      val = true
      write(val)
      val2 = read
      expect(val2).to be_a(TrueClass)
      expect(val2).to eql(val)
    end

    it 'false' do
      val = false
      write(val)
      val2 = read
      expect(val2).to be_a(FalseClass)
      expect(val2).to eql(val)
    end

    it 'nil' do
      val = nil
      write(val)
      val2 = read
      expect(val2).to be_a(NilClass)
      expect(val2).to eql(val)
    end

    it 'Regexp' do
      val = /match me/
      write(val)
      val2 = read
      expect(val2).to be_a(Regexp)
      expect(val2).to eql(val)
    end

    it 'Sensitive' do
      sval = 'the sensitive value'
      val = Types::PSensitiveType::Sensitive.new(sval)
      write(val)
      val2 = read
      expect(val2).to be_a(Types::PSensitiveType::Sensitive)
      expect(val2.unwrap).to eql(sval)
    end

    it 'Timespan' do
      val = Time::Timespan.from_fields(false, 3, 12, 40, 31, 123)
      write(val)
      val2 = read
      expect(val2).to be_a(Time::Timespan)
      expect(val2).to eql(val)
    end

    it 'Timestamp' do
      val = Time::Timestamp.now
      write(val)
      val2 = read
      expect(val2).to be_a(Time::Timestamp)
      expect(val2).to eql(val)
    end

    it 'Version' do
      # It does succeed on rare occasions, so we need to repeat
      val = SemanticPuppet::Version.parse('1.2.3-alpha2')
      write(val)
      val2 = read
      expect(val2).to be_a(SemanticPuppet::Version)
      expect(val2).to eql(val)
    end

    it 'VersionRange' do
      # It does succeed on rare occasions, so we need to repeat
      val = SemanticPuppet::VersionRange.parse('>=1.2.3-alpha2 <1.2.4')
      write(val)
      val2 = read
      expect(val2).to be_a(SemanticPuppet::VersionRange)
      expect(val2).to eql(val)
    end

    it 'Binary' do
      val = Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
      write(val)
      val2 = read
      expect(val2).to be_a(Types::PBinaryType::Binary)
      expect(val2).to eql(val)
    end

    it 'ACII_8BIT String as Binary' do
      val = Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
      strval = val.binary_buffer
      write(strval)
      val2 = read
      expect(val2).to be_a(Types::PBinaryType::Binary)
      expect(val2).to eql(val)
    end

    it 'URI' do
      val = URI('http://bob:ewing@dallas.example.com:8080/oil/baron?crude=cash#leftovers')
      write(val)
      val2 = read
      expect(val2).to be_a(URI)
      expect(val2).to eql(val)
    end

    it 'Sensitive with rich data' do
      sval = Time::Timestamp.now
      val = Types::PSensitiveType::Sensitive.new(sval)
      write(val)
      val2 = read
      expect(val2).to be_a(Types::PSensitiveType::Sensitive)
      expect(val2.unwrap).to be_a(Time::Timestamp)
      expect(val2.unwrap).to eql(sval)
    end

    it 'Hash with Symbol keys' do
      val = { :one => 'one', :two => 'two' }
      write(val)
      val2 = read
      expect(val2).to be_a(Hash)
      expect(val2).to eql(val)
    end

    it 'Hash with Integer keys' do
      val = { 1 => 'one', 2 => 'two' }
      write(val)
      val2 = read
      expect(val2).to be_a(Hash)
      expect(val2).to eql(val)
    end

    it 'A Hash that references itself' do
      val = {}
      val['myself'] = val
      write(val)
      val2 = read
      expect(val2).to be_a(Hash)
      expect(val2['myself']).to equal(val2)
    end
  end

  context 'can write and read' do
    include_context 'types_setup'

    all_types.each do |t|
      it "the default for type #{t.name}" do
        val = t::DEFAULT
        write(val)
        val2 = read
        expect(val2).to be_a(t)
        expect(val2).to eql(val)
      end
    end

    context 'a parameterized' do
      it 'String' do
        val = Types::TypeFactory.string(Types::TypeFactory.range(1, :default))
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PStringType)
        expect(val2).to eql(val)
      end

      it 'Regex' do
        val = Types::TypeFactory.regexp(/foo/)
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PRegexpType)
        expect(val2).to eql(val)
      end

      it 'Collection' do
        val = Types::TypeFactory.collection(Types::TypeFactory.range(0, 20))
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PCollectionType)
        expect(val2).to eql(val)
      end

      it 'Array' do
        val = Types::TypeFactory.array_of(Types::TypeFactory.integer, Types::TypeFactory.range(0, 20))
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PArrayType)
        expect(val2).to eql(val)
      end

      it 'Hash' do
        val = Types::TypeFactory.hash_kv(Types::TypeFactory.string, Types::TypeFactory.integer, Types::TypeFactory.range(0, 20))
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PHashType)
        expect(val2).to eql(val)
      end

      it 'Variant' do
        val = Types::TypeFactory.variant(Types::TypeFactory.string, Types::TypeFactory.range(1, :default))
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PVariantType)
        expect(val2).to eql(val)
      end

      it 'Object' do
        val = Types::TypeParser.singleton.parse('Pcore::StringType', loader)
        write(val)
        val2 = read
        expect(val2).to be_a(Types::PObjectType)
        expect(val2).to eql(val)
      end

      context 'ObjectType' do
        let(:type) do
          Types::PObjectType.new({
            'name' => 'MyType',
            'type_parameters' => {
              'x' => Types::PIntegerType::DEFAULT
            },
            'attributes' => {
              'x' => Types::PIntegerType::DEFAULT
            }
          })
        end

        it 'with preserved parameters' do
          val = type.create(34)._pcore_type
          write(val)
          val2 = read
          expect(val2).to be_a(Types::PObjectTypeExtension)
          expect(val2).to eql(val)
        end

        it 'with POjbectTypeExtension type being converted' do
          val = { 'ObjectExtension' => type.create(34) }
          expect(to_converter.convert(val))
            .to eq({"ObjectExtension"=>{"__ptype"=>"MyType", "x"=>34}})
        end
      end
    end

    it 'Array of rich data' do
      # Sensitive omitted because it doesn't respond to ==
      val = [
        Time::Timespan.from_fields(false, 3, 12, 40, 31, 123),
        Time::Timestamp.now,
        SemanticPuppet::Version.parse('1.2.3-alpha2'),
        SemanticPuppet::VersionRange.parse('>=1.2.3-alpha2 <1.2.4'),
        Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
      ]
      write(val)
      val2 = read
      expect(val2).to eql(val)
    end

    it 'Hash of rich data' do
      # Sensitive omitted because it doesn't respond to ==
      val = {
        'duration' => Time::Timespan.from_fields(false, 3, 12, 40, 31, 123),
        'time' => Time::Timestamp.now,
        'version' => SemanticPuppet::Version.parse('1.2.3-alpha2'),
        'range' => SemanticPuppet::VersionRange.parse('>=1.2.3-alpha2 <1.2.4'),
        'binary' => Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
      }
      write(val)
      val2 = read
      expect(val2).to eql(val)
    end

    context 'an AST model' do
      it "Locator" do
        val = Parser::Locator::Locator19.new('here is some text', '/tmp/foo', [5])
        write(val)
        val2 = read
        expect(val2).to be_a(Parser::Locator::Locator19)
        expect(val2).to eql(val)
      end

      it 'nested Expression' do
        expr = parse(<<-CODE)
          $rootgroup = $osfamily ? {
              'Solaris'          => 'wheel',
              /(Darwin|FreeBSD)/ => 'wheel',
              default            => 'root',
          }

          file { '/etc/passwd':
            ensure => file,
            owner  => 'root',
            group  => $rootgroup,
          }
        CODE
        write(expr)
        expr2 = read
        expect(dumper.dump(expr)).to eq(dumper.dump(expr2))
      end
    end

    context 'PuppetObject' do
      before(:each) do
        class DerivedArray < Array
          include Types::PuppetObject

          def self._pcore_type
            @type
          end

          def self.register_ptype(loader, ir)
            @type = Pcore.create_object_type(loader, ir, DerivedArray, 'DerivedArray', nil, 'values' => Types::PArrayType::DEFAULT)
              .resolve(loader)
          end

          def initialize(values)
            concat(values)
          end

          def values
            Array.new(self)
          end
        end

        class DerivedHash < Hash
          include Types::PuppetObject

          def self._pcore_type
            @type
          end

          def self.register_ptype(loader, ir)
            @type = Pcore.create_object_type(loader, ir, DerivedHash, 'DerivedHash', nil, '_pcore_init_hash' => Types::PHashType::DEFAULT)
              .resolve(loader)
          end

          def initialize(_pcore_init_hash)
            merge!(_pcore_init_hash)
          end

          def _pcore_init_hash
            result = {}
            result.merge!(self)
            result
          end
        end
      end

      after(:each) do
        x = Puppet::Pops::Serialization
        x.send(:remove_const, :DerivedArray) if x.const_defined?(:DerivedArray)
        x.send(:remove_const, :DerivedHash) if x.const_defined?(:DerivedHash)
      end

      it 'derived from Array' do
        DerivedArray.register_ptype(loader, loaders.implementation_registry)

        # Sensitive omitted because it doesn't respond to ==
        val = DerivedArray.new([
          Time::Timespan.from_fields(false, 3, 12, 40, 31, 123),
          Time::Timestamp.now,
          SemanticPuppet::Version.parse('1.2.3-alpha2'),
          SemanticPuppet::VersionRange.parse('>=1.2.3-alpha2 <1.2.4'),
          Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
        ])
        write(val)
        val2 = read
        expect(val2).to eql(val)
      end

      it 'derived from Hash' do
        DerivedHash.register_ptype(loader, loaders.implementation_registry)

        # Sensitive omitted because it doesn't respond to ==
        val = DerivedHash.new({
          'duration' => Time::Timespan.from_fields(false, 3, 12, 40, 31, 123),
          'time' => Time::Timestamp.now,
          'version' => SemanticPuppet::Version.parse('1.2.3-alpha2'),
          'range' => SemanticPuppet::VersionRange.parse('>=1.2.3-alpha2 <1.2.4'),
          'binary' => Types::PBinaryType::Binary.from_base64('w5ZzdGVuIG1lZCByw7ZzdGVuCg==')
        })
        write(val)
        val2 = read
        expect(val2).to eql(val)
      end
    end
  end

  context 'deserializing an instance whose Object type was serialized by reference' do
    let(:to_converter) { ToDataConverter.new(:type_by_reference => true, :rich_data => true) }
    let(:type) do
      Types::PObjectType.new({
        'name' => 'MyType',
        'attributes' => {
          'x' => Types::PIntegerType::DEFAULT
        }
      })
    end

    context 'fails when deserializer is unaware of the referenced type' do
      it 'fails by default' do
        write(type.create(32))

        # Should fail since no loader knows about 'MyType'
        expect{ read }.to raise_error(Puppet::Error, 'No implementation mapping found for Puppet Type MyType')
      end

      it 'succeeds and produces a hash when a read __ptype key is something other than a string or a hash' do
        # i.e. this is for content that is not produced by ToDataHash - writing on-the-wire-format directly
        # to test that it does not crash.
        write_raw({'__ptype' => 10, 'x'=> 32})
        expect(read).to eql({'__ptype'=>10, 'x'=>32})
      end

      context "succeeds but produces an rich_type hash when deserializer has 'allow_unresolved' set to true" do
        let(:from_converter) { FromDataConverter.new(:allow_unresolved => true) }
        it do
          write(type.create(32))
          expect(read).to eql({'__ptype'=>'MyType', 'x'=>32})
        end
      end
    end

    it 'succeeds when deserializer is aware of the referenced type' do
      obj = type.create(32)
      write(obj)
      expect(loaders.find_loader(nil)).to receive(:load).with(:type, 'mytype').and_return(type)
      expect(read).to eql(obj)
    end
  end

  context 'with rich_data set to false' do
    let(:to_converter) { ToDataConverter.new(:message_prefix => 'Test Hash', :rich_data => false) }
    let(:logs) { [] }
    let(:warnings) { logs.select { |log| log.level == :warning }.map { |log| log.message } }

    it 'A Hash with Symbol keys is converted to hash with String keys with warning' do
      val = { :one => 'one', :two => 'two' }
      Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
        write(val)
        val2 = read
        expect(val2).to be_a(Hash)
        expect(val2).to eql({ 'one' => 'one', 'two' => 'two' })
      end
      expect(warnings).to eql([
        "Test Hash contains a hash with a Symbol key. It will be converted to the String 'one'",
        "Test Hash contains a hash with a Symbol key. It will be converted to the String 'two'"])
    end

    it 'A Hash with Version keys is converted to hash with String keys with warning' do
      val = { SemanticPuppet::Version.parse('1.0.0') => 'one', SemanticPuppet::Version.parse('2.0.0') => 'two' }
      Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
        write(val)
        val2 = read
        expect(val2).to be_a(Hash)
        expect(val2).to eql({ '1.0.0' => 'one', '2.0.0' => 'two' })
      end
      expect(warnings).to eql([
        "Test Hash contains a hash with a SemanticPuppet::Version key. It will be converted to the String '1.0.0'",
        "Test Hash contains a hash with a SemanticPuppet::Version key. It will be converted to the String '2.0.0'"])
    end

    context 'and symbol_as_string is set to true' do
      let(:to_converter) { ToDataConverter.new(:rich_data => false, :symbol_as_string => true) }

      it 'A Hash with Symbol keys is silently converted to hash with String keys' do
        val = { :one => 'one', :two => 'two' }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'one' => 'one', 'two' => 'two' })
        end
        expect(warnings).to be_empty
      end

      it 'A Hash with Symbol values is silently converted to hash with String values' do
        val = { 'one' => :one, 'two' => :two  }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'one' => 'one', 'two' => 'two' })
        end
        expect(warnings).to be_empty
      end

      it 'A Hash with default values will have the values converted to string with a warning' do
        val = { 'key' => :default  }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'key' => 'default' })
        end
        expect(warnings).to eql(["['key'] contains the special value default. It will be converted to the String 'default'"])
      end
    end
  end

  context 'with rich_data is set to true' do
    let(:to_converter) { ToDataConverter.new(:message_prefix => 'Test Hash', :rich_data => true) }
    let(:logs) { [] }
    let(:warnings) { logs.select { |log| log.level == :warning }.map { |log| log.message } }

    context 'and symbol_as_string is set to true' do
      let(:to_converter) { ToDataConverter.new(:rich_data => true, :symbol_as_string => true) }

      it 'A Hash with Symbol keys is silently converted to hash with String keys' do
        val = { :one => 'one', :two => 'two' }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'one' => 'one', 'two' => 'two' })
        end
        expect(warnings).to be_empty
      end

      it 'A Hash with Symbol values is silently converted to hash with String values' do
        val = { 'one' => :one, 'two' => :two  }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'one' => 'one', 'two' => 'two' })
        end
        expect(warnings).to be_empty
      end

      it 'A Hash with __ptype, __pvalue keys will not be taken as a pcore meta tag' do
        val = { '__ptype' => 42, '__pvalue' => 43  }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ '__ptype' => 42, '__pvalue' => 43 })
        end
        expect(warnings).to be_empty
      end

      it 'A Hash with default values will not loose type information' do
        val = { 'key' => :default  }
        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
          write(val)
          val2 = read
          expect(val2).to be_a(Hash)
          expect(val2).to eql({ 'key' => :default })
        end
        expect(warnings).to be_empty
      end
    end
  end

  context 'with local_reference set to false' do
    let(:to_converter) { ToDataConverter.new(:local_reference => false) }

    it 'A self referencing value will trigger an endless recursion error' do
      val = {}
      val['myself'] = val
      expect { write(val) }.to raise_error(/Endless recursion detected when attempting to serialize value of class Hash/)
    end
  end

  context 'will fail when' do
    it 'the value of a type description is something other than a String or a Hash' do
      expect do
        from_converter.convert({ '__ptype' => { '__ptype' => 'Pcore::TimestampType', '__pvalue' => 12345 }})
      end.to raise_error(/Cannot create a Pcore::TimestampType from a Integer/)
    end
  end
end
end
end
