require 'spec_helper'
require 'puppet/pops'

module Puppet::Pops
module Serialization
[JSON].each do |packer_module|
  describe "the Puppet::Pops::Serialization over #{packer_module.name}" 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(:reader_class) { packer_module::Reader }
  let(:writer_class) { packer_module::Writer }
  let(:serializer) { Serializer.new(writer_class.new(io)) }
  let(:deserializer) { Deserializer.new(reader_class.new(io), loaders.find_loader(nil)) }

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

  after(:each) do
    Puppet.pop_context()
  end

  def write(*values)
    io.reopen
    values.each { |val| serializer.write(val) }
    serializer.finish
    io.rewind
  end

  def read
    deserializer.read
  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 '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
  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
      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(:serializer) { Serializer.new(writer_class.new(io), :type_by_reference => true) }
    let(:type) do
      Types::PObjectType.new({
        'name' => 'MyType',
        'attributes' => {
          'x' => Types::PIntegerType::DEFAULT
        }
      })
    end

    it 'fails when deserializer is unaware of the referenced type' 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 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 'When debugging' do
    let(:debug_io) { StringIO.new }
    let(:writer) { reader_class.new(io, { :debug_io => debug_io, :tabulate => false, :verbose => true }) }

    it 'can write and read an AST 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
end
end
end
end
