#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet_pal'

describe 'Puppet Pal' do
  include PuppetSpec::Files

  let(:testing_env) do
    {
      'pal_env' => {
        'functions' => functions,
        'lib' => { 'puppet' => lib_puppet },
        'manifests' => manifests,
        'modules' => modules,
        'plans' => plans,
        'tasks' => tasks,
        'types' => types,
      },
      'other_env1' => { 'modules' => {} },
      'other_env2' => { 'modules' => {} },
    }
  end

  let(:functions) { {} }
  let(:manifests) { {} }
  let(:modules) { {} }
  let(:plans) { {} }
  let(:lib_puppet) { {} }
  let(:tasks) { {} }
  let(:types) { {} }

  let(:environments_dir) { Puppet[:environmentpath] }

  let(:testing_env_dir) do
    dir_contained_in(environments_dir, testing_env)
    env_dir = File.join(environments_dir, 'pal_env')
    PuppetSpec::Files.record_tmp(env_dir)
    PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env1'))
    PuppetSpec::Files.record_tmp(File.join(environments_dir, 'other_env2'))
    env_dir
  end

  let(:modules_dir) { File.join(testing_env_dir, 'modules') }

  # Without any facts - this speeds up the tests that do not require $facts to have any values
  let(:node_facts) { Hash.new }

  # TODO: to be used in examples for running in an existing env
  #  let(:env) { Puppet::Node::Environment.create(:testing, [modules_dir]) }

  context 'in general - without code in modules or env' do
    let(:modulepath) { [] }

    context "with a catalog compiler" do
      it 'errors if given both configured_by_env and manifest_file' do
        expect {
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler(configured_by_env: true, manifest_file: 'undef.pp') {|c|  }
          end
        }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/)
      end

      it 'errors if given both configured_by_env and code_string' do
        expect {
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler(configured_by_env: true, code_string: 'undef') {|c|  }
          end
        }.to raise_error(/manifest_file or code_string cannot be given when configured_by_env is true/)
      end

      it 'shadows target variables that collide with plan variables' do
        facts       = { 'var' => 'fact' }
        target_vars = { 'var' => 'target' }

        expect(Puppet).to receive(:warning).with(/Target variable \$var will be overridden by fact of the same name/)

        result = Puppet::Pal.in_tmp_environment('pal_env', facts: {}) do |ctx|
          ctx.with_catalog_compiler(facts: facts, target_variables: target_vars ) do |c|
            c.evaluate_string('$var')
          end
        end

        expect(result).to eq('fact')
      end

      it 'shadows target variables that collide with facts' do
        plan_vars   = { 'var' => 'plan' }
        target_vars = { 'var' => 'target' }

        expect(Puppet).to receive(:warning).with(/Target variable \$var will be overridden by plan variable of the same name/)
        
        result = Puppet::Pal.in_tmp_environment('pal_env', facts: {}) do |ctx|
          ctx.with_catalog_compiler(variables: plan_vars, target_variables: target_vars ) do |c|
            c.evaluate_string('$var')
          end
        end

        expect(result).to eq('plan')
      end

      it 'shadows plan variables that collide with facts' do
        facts     = { 'var' => 'fact' }
        plan_vars = { 'var' => 'plan' }

        expect(Puppet).to receive(:warning).with(/Plan variable \$var will be overridden by fact of the same name/)
        
        result = Puppet::Pal.in_tmp_environment('pal_env', facts: {}) do |ctx|
          ctx.with_catalog_compiler(facts: facts, variables: plan_vars ) do |c|
            c.evaluate_string('$var')
          end
        end

        expect(result).to eq('fact')
      end

      context "evaluate_string method" do
        it 'evaluates code string in a given tmp environment' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string('1+2+3') }
          end
          expect(result).to eq(6)
        end

        it 'can be evaluated more than once in a given tmp environment - each in fresh compiler' do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            expect(  ctx.with_catalog_compiler {|c| c.evaluate_string('$a = 1+2+3')}).to eq(6)
            expect { ctx.with_catalog_compiler {|c| c.evaluate_string('$a') }}.to raise_error(/Unknown variable: 'a'/)
          end
        end

        it 'instantiates a function definition in the given code string' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
            pal.with_catalog_compiler do |compiler|
              compiler.evaluate_string(<<-CODE)
                function run_me() { "worked1" }
                run_me()
                CODE
            end
          end
          expect(result).to eq('worked1')
        end

        it 'instantiates a user defined resource definition in the given code string' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
            pal.with_catalog_compiler do |compiler|
              compiler.evaluate_string(<<-CODE)
                define run_me() { }
                run_me { test: }
                CODE
            end
          end
          resource = result[0]
          expect(resource).to be_a(Puppet::Pops::Types::PResourceType)
          expect(resource.type_name).to eq("Run_me")
          expect(resource.title).to eq('test')
        end

        context 'catalog_data_hash' do
          it 'produces a data_hash encoding of a catalog' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => /a regexp/}")
                c.catalog_data_hash
              }
            end
            expect(result['resources']).to include(include('type' => 'Notify'))
          end
        end

        context 'the with_json_encoding()' do

          it 'produces json for a catalog' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => /a regexp/}")
                c.with_json_encoding() {|encoder| encoder.encode }
              }
            end
            parsed = JSON.parse(result)
            expect(parsed['resources']).to include(include('type' => 'Notify'))
          end

          it 'produces pretty json by default' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => /a regexp/}")
                c.with_json_encoding() {|encoder| encoder.encode }
              }
            end
            expect(result.count("\n")).to be > 10
          end

          it 'produces compact (non pretty) json when pretty is false' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => /a regexp/}")
                c.with_json_encoding(pretty: false) {|encoder| encoder.encode }
              }
            end
            expect(result.count("\n")).to be < 10
          end

          it 'produces json for an individual resource by giving type and title to encode_resource()' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => 'yay'}")
                c.with_json_encoding() {|encoder| encoder.encode_resource('notify', 'test') }
              }
            end
            expect(result).to match(/"message":"yay"/)
          end

          it 'encodes values as rich data when needed' do
            result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
              pal.with_catalog_compiler {|c|
                c.evaluate_string("notify {'test': message => /a regexp/}")
                c.with_json_encoding(pretty: true) {|encoder| encoder.encode_resource('notify', 'test') }
              }
            end
            expect(result).to match(/"__ptype":"Regexp"/)
          end
        end
      end

      context "evaluate_file method" do
        it 'evaluates a manifest file in a given tmp environment' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('testing.pp', "1+2+3+4")
            ctx.with_catalog_compiler {|c| c.evaluate_file(manifest) }
          end
          expect(result).to eq(10)
        end

        it 'instantiates definitions in the given code string' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |pal|
            pal.with_catalog_compiler do |compiler|
              manifest = file_containing('testing.pp', (<<-CODE))
                function run_me() { "worked1" }
                run_me()
                CODE
              pal.with_catalog_compiler {|c| c.evaluate_file(manifest) }
            end
          end
          expect(result).to eq('worked1')
        end
      end

      context "variables are supported such that" do
        it 'they can be set in any scope' do
          vars = {'a'=> 10, 'x::y' => 20}
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string("1+2+3+4+$a+$x::y")}
          end
          expect(result).to eq(40)
        end

        it 'an error is raised if a variable name is illegal' do
          vars = {'_a::b'=> 10}
          expect do
            Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx|
              manifest = file_containing('testing.pp', "ok")
              ctx.with_catalog_compiler {|c| c.evaluate_file(manifest) }
            end
          end.to raise_error(/has illegal name/)
        end

        it 'an error is raised if variable value is not RichData compliant' do
          vars = {'a'=> ArgumentError.new("not rich data")}
          expect do
            Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx|
              ctx.with_catalog_compiler {|c|  }
            end
          end.to raise_error(/has illegal type - got: ArgumentError/)
        end

        it 'variable given to script_compiler overrides those given for environment' do
          vars = {'a'=> 10, 'x::y' => 20}
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts, variables: vars) do |ctx|
            ctx.with_catalog_compiler(variables: {'x::y' => 40}) {|c| c.evaluate_string("1+2+3+4+$a+$x::y")}
          end
          expect(result).to eq(60)
        end
      end

      context "functions are supported such that" do
        it '"call_function" calls a function' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc($a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) {|c| c.call_function('myfunc', 6) }
          end
          expect(result).to eq(12)
        end

        it '"call_function" accepts a call with a ruby block' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.call_function('with', 6) {|x| x * 2} }
          end
          expect(result).to eq(12)
        end

        it '"function_signature" returns a signature of a function' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) do |c|
              c.function_signature('myfunc')
            end
          end
          expect(result.class).to eq(Puppet::Pal::FunctionSignature)
        end

        it '"FunctionSignature#callable_with?" returns boolean if function is callable with given argument values' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) do |c|
              signature = c.function_signature('myfunc')
              [ signature.callable_with?([10]),
                signature.callable_with?(['nope'])
              ]
            end
          end
          expect(result).to eq([true, false])
        end

        it '"FunctionSignature#callable_with?" calls a given lambda if there is an error' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) do |c|
              signature = c.function_signature('myfunc')
              local_result = 'not yay'
              signature.callable_with?(['nope']) {|error| local_result = error }
              local_result
            end
          end
          expect(result).to match(/'myfunc' parameter 'a' expects an Integer value, got String/)
        end

        it '"FunctionSignature#callable_with?" does not call a given lambda when there is no error' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) do |c|
              signature = c.function_signature('myfunc')
              local_result = 'yay'
              signature.callable_with?([10]) {|error| local_result = 'not yay' }
              local_result
            end
          end
          expect(result).to eq('yay')
        end

        it '"function_signature" gets the signatures from a ruby function with multiple dispatch' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.function_signature('lookup') }
          end
          # check two different signatures of the lookup function
          expect(result.callable_with?(['key'])).to eq(true)
          expect(result.callable_with?(['key'], lambda() {|k| })).to eq(true)
        end

        it '"function_signature" returns nil if function is not found' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.function_signature('no_where_to_be_found') }
          end
          expect(result).to eq(nil)
        end

        it '"FunctionSignature#callables" returns an array of callables' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
            ctx.with_catalog_compiler(manifest_file: manifest) do |c|
              c.function_signature('myfunc').callables
            end
          end
          expect(result.class).to eq(Array)
          expect(result.all? {|c| c.is_a?(Puppet::Pops::Types::PCallableType)}).to eq(true)
        end

        it '"list_functions" returns an array with all function names that can be loaded' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.list_functions() }
          end
          expect(result.is_a?(Array)).to eq(true)
          expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true)
          # there are certainly more than 30 functions in puppet - (56 when writing this, but some refactoring
          # may take place, so don't want an exact number here - jsut make sure it found "all of them"
          expect(result.count).to be > 30
        end

        it '"list_functions" filters on name based on a given regexp' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.list_functions(/epp/) }
          end
          expect(result.is_a?(Array)).to eq(true)
          expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true)
          # there are two functions currently that have 'epp' in their name
          expect(result.count).to eq(2)
        end

      end

      context 'supports puppet data types such that' do
        it '"type" parses and returns a Type from a string specification' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            manifest = file_containing('main.pp', "type MyType = Float")
            ctx.with_catalog_compiler(manifest_file: manifest) {|c| c.type('Variant[Integer, Boolean, MyType]') }
          end
          expect(result.is_a?(Puppet::Pops::Types::PVariantType)).to eq(true)
          expect(result.types.size).to eq(3)
          expect(result.instance?(3.14)).to eq(true)
        end

        it '"create" creates a new object from a puppet data type and args' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler { |c| c.create(Puppet::Pops::Types::PIntegerType::DEFAULT, '0x10') }
          end
          expect(result).to eq(16)
        end

        it '"create" creates a new object from puppet data type in string form and args' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler { |c| c.create('Integer', '010') }
          end
          expect(result).to eq(8)
        end
      end
    end

    context 'supports parsing such that' do
      it '"parse_string" parses a puppet language string' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler { |c| c.parse_string('$a = 10') }
        end
        expect(result.class).to eq(Puppet::Pops::Model::Program)
      end

      {  nil      => Puppet::Error,
        '0xWAT'   => Puppet::ParseErrorWithIssue,
        '$0 = 1'  => Puppet::ParseErrorWithIssue,
        'else 32' => Puppet::ParseErrorWithIssue,
      }.each_pair do |input, error_class|
        it "'parse_string' raises an error for invalid input: '#{input}'" do
          expect {
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler { |c| c.parse_string(input) }
          end
          }.to raise_error(error_class)
        end
      end

      it '"parse_file" parses a puppet language string' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          manifest = file_containing('main.pp', "$a = 10")
          ctx.with_catalog_compiler { |c| c.parse_file(manifest) }
        end
        expect(result.class).to eq(Puppet::Pops::Model::Program)
      end

      it "'parse_file' raises an error for invalid input: 'else 32'" do
        expect {
        Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          manifest = file_containing('main.pp', "else 32")
          ctx.with_catalog_compiler { |c| c.parse_file(manifest) }
        end
        }.to raise_error(Puppet::ParseErrorWithIssue)
      end

      it "'parse_file' raises an error for invalid input, file is not a string" do
        expect {
        Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler { |c| c.parse_file(42) }
        end
        }.to raise_error(Puppet::Error)
      end

      it 'the "evaluate" method evaluates the parsed AST' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler { |c| c.evaluate(c.parse_string('10 + 20')) }
        end
        expect(result).to eq(30)
      end

      it 'the "evaluate" method instantiates definitions when given a Program' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler { |c| c.evaluate(c.parse_string('function foo() { "yay"}; foo()')) }
        end
        expect(result).to eq('yay')
      end

      it 'the "evaluate" method does not instantiates definitions when given ast other than Program' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler do |c|
              program= c.parse_string('function foo() { "yay"}; foo()')
              c.evaluate(program.body)
            end
          end
        end.to raise_error(/Unknown function: 'foo'/)
      end

      it 'the "evaluate_literal" method evaluates AST being a representation of a literal value' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler { |c| c.evaluate_literal(c.parse_string('{10 => "hello"}')) }
        end
        expect(result).to eq({10 => 'hello'})
      end

      it 'the "evaluate_literal" method errors if ast is not representing a literal value' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler { |c| c.evaluate_literal(c.parse_string('{10+1 => "hello"}')) }
          end
        end.to raise_error(/does not represent a literal value/)
      end

      it 'the "evaluate_literal" method errors if ast contains definitions' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler { |c| c.evaluate_literal(c.parse_string('function foo() { }; 42')) }
          end
        end.to raise_error(/does not represent a literal value/)
      end

      it 'the "evaluate" method evaluates but does not evaluate lazy constructs' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler do |c|
            c.evaluate(c.parse_string('define foo() { notify {nope: }} foo { test: }'))
            c.with_json_encoding() {|encoder| encoder.encode }
          end
        end
        parsed = JSON.parse(result)
        expect(parsed['resources']).to_not include(include('type' => 'Notify'))
      end

      it 'an "evaluate" followed by "compile_additions" evaluates lazy constructs' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler do |c|
            c.evaluate(c.parse_string('define foo() { notify {nope: }} foo { test: }'))
            c.compile_additions
            c.with_json_encoding() {|encoder| encoder.encode }
          end
        end
        parsed = JSON.parse(result)
        expect(parsed['resources']).to include(include('type' => 'Notify'))
      end

      it 'an "evaluate" followed by "compile_additions" validates the result' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler do |c|
              c.evaluate(c.parse_string('define foo() { notify {nope: }} foo { test: before =>"Bar[nope]"}'))
              c.compile_additions
            end
          end
        end.to raise_error(Puppet::Error, /Could not find resource 'Bar\[nope\]'/)
      end

      it 'an "evaluate" followed by "evaluate_additions" does not validate the result' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler do |c|
            c.evaluate(c.parse_string('define foo() { notify {nope: }} foo { test: before =>"Bar[nope]"}'))
            c.evaluate_additions
            c.with_json_encoding() {|encoder| encoder.encode }
          end
        end
        parsed = JSON.parse(result)
        expect(parsed['resources']).to include(include('type' => 'Notify'))
      end

      it 'an "evaluate" followed by "evaluate_additions" and "validate" validates the result' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
            ctx.with_catalog_compiler do |c|
              c.evaluate(c.parse_string('define foo() { notify {nope: }} foo { test: before =>"Bar[nope]"}'))
              c.compile_additions
              c.validate
            end
          end
        end.to raise_error(Puppet::Error, /Could not find resource 'Bar\[nope\]'/)
      end

      it 'an "evaluate" followed by "evaluate_ast_node" will correctly parse a node definition' do
        Puppet[:node_name_value] = 'testing_node'
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do | ctx|
          ctx.with_catalog_compiler do |c|
            c.evaluate(c.parse_string("node 'testing_node' { notify {'PASSED': } }"))
            c.evaluate_ast_node
            c.compile_additions
            c.with_json_encoding() {|encoder| encoder.encode }
          end
        end
        parsed = JSON.parse(result)
        expect(parsed['resources']).to include(include('type' => 'Notify'))
      end
    end
  end

  context 'with code in modules and env' do
    let(:modulepath) { [modules_dir] }

    let(:metadata_json_a) {
      {
        'name' => 'example/a',
        'version' => '0.1.0',
        'source' => 'git@github.com/example/example-a.git',
        'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}],
        'author' => 'Bob the Builder',
        'license' => 'Apache-2.0'
      }
    }

    let(:metadata_json_b) {
      {
        'name' => 'example/b',
        'version' => '0.1.0',
        'source' => 'git@github.com/example/example-b.git',
        'dependencies' => [{'name' => 'c', 'version_range' => '>=0.1.0'}],
        'author' => 'Bob the Builder',
        'license' => 'Apache-2.0'
      }
    }

    let(:metadata_json_c) {
      {
        'name' => 'example/c',
        'version' => '0.1.0',
        'source' => 'git@github.com/example/example-c.git',
        'dependencies' => [],
        'author' => 'Bob the Builder',
        'license' => 'Apache-2.0'
      }
    }

    # TODO: there is something amiss with the metadata wrt dependencies - when metadata is present there is an error
    #       that dependencies could not be resolved. Metadata is therefore commented out.
    #       Dependency based visibility is probably something that we should remove...
    let(:modules) {
      {
        'a' => {
        'functions' => a_functions,
        'lib' => { 'puppet' => a_lib_puppet },
        'types' => a_types,
        },
        'b' => {
        'functions' => b_functions,
        'lib' => b_lib,
        'types' => b_types,
        },
        'c' => {
        'types' => c_types,
        },
      }
    }

    let(:a_types) {
      {
        'atype.pp' => <<-PUPPET.unindent,
        type A::Atype = Integer
        PUPPET
      }
    }

    let(:a_functions) {
      {
        'afunc.pp' => 'function a::afunc() { "a::afunc value" }',
      }
    }

    let(:a_lib_puppet) {
      {
        'functions' => {
          'a' => {
            'arubyfunc.rb' => <<-RUBY.unindent,
              require 'stuff/something'
              Puppet::Functions.create_function(:'a::arubyfunc') do
                def arubyfunc
                  Stuff::SOMETHING
                end
              end
              RUBY
            'mycatalogcompilerfunc.rb' => <<-RUBY.unindent,
              Puppet::Functions.create_function(:'a::mycatalogcompilerfunc', Puppet::Functions::InternalFunction) do
                dispatch :mycatalogcompilerfunc do
                  compiler_param
                  param 'String',:name
                end

                def mycatalogcompilerfunc(the_compiler, name)
                  the_compiler.is_a?(Puppet::Pal::CatalogCompiler) ? name : 'no go'
                end
              end
              RUBY
          }
        },
        'datatypes' => {
          'mytype.rb' => <<-RUBY.unindent,
            Puppet::DataTypes.create_type('Mytype') do
              interface <<-PUPPET
                attributes => {
                  name => { type => String },
                  year_of_birth => { type => Integer },
                  age => { type => Integer, kind => derived },
                }
                PUPPET

              implementation do
                def age
                  DateTime.now.year - @year_of_birth
                end
              end
            end
            RUBY
        }
      }
    }

    let(:b_types) {
      {
        'atype.pp' => <<-PUPPET.unindent,
        type B::Atype = Integer
        PUPPET
      }
    }

    let(:b_functions) {
      {
        'afunc.pp' => 'function b::afunc() {}',
      }
    }

    let(:b_lib) {
      {
        'puppet' => b_lib_puppet,
        'stuff' => {
          'something.rb' => "module Stuff; SOMETHING = 'something'; end"
        }
      }
    }

    let(:b_lib_puppet) {
      {
        'functions' => {
        'b' => {
        'arubyfunc.rb' => "Puppet::Functions.create_function(:'b::arubyfunc') { def arubyfunc; 'arubyfunc_value'; end }",
        }
        }
      }
    }

    let(:c_types) {
      {
        'atype.pp' => <<-PUPPET.unindent,
        type C::Atype = Integer
        PUPPET
      }
    }

    context 'configured as temporary environment such that' do
      it 'modules are available' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
          ctx.with_catalog_compiler {|c| c.evaluate_string('a::afunc()') }
        end
        expect(result).to eq("a::afunc value")
      end

      it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
          ctx.with_catalog_compiler {|c| c.evaluate_string('a::arubyfunc()') }
        end
        expect(result).to eql('something')
      end

      it 'errors if a block is not given to in_tmp_environment' do
        expect do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts)
        end.to raise_error(/A block must be given to 'in_tmp_environment/)
      end

      it 'errors if an env_name is given and is not a String[1]' do
        expect do
          Puppet::Pal.in_tmp_environment('', modulepath: modulepath, facts: node_facts) { |ctx| }
        end.to raise_error(/temporary environment name has wrong type/)

        expect do
          Puppet::Pal.in_tmp_environment(32, modulepath: modulepath, facts: node_facts) { |ctx| }
        end.to raise_error(/temporary environment name has wrong type/)
      end

      { 'a hash'                => {'a' => 'hm'},
        'an integer'            => 32,
        'separated strings'     => 'dir1;dir2',
        'empty string in array' => ['']
      }.each_pair do |what, value|
        it "errors if modulepath is #{what}" do
          expect do
            Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| }
          end.to raise_error(/modulepath has wrong type/)
        end
      end

      context 'facts are supported such that' do
        it 'they are obtained if they are not given' do
          facts = Puppet::Node::Facts.new(Puppet[:certname], 'puppetversion' => Puppet.version)
          Puppet::Node::Facts.indirection.save(facts)

          testing_env_dir # creates the structure
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath ) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[puppetversion] == '#{Puppet.version}'") }
          end
          expect(result).to eq(true)
        end

        it 'can be given as a hash when creating the environment' do
          testing_env_dir # creates the structure
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 42") }
          end
          expect(result).to eq(true)
        end

        it 'can be overridden with a hash when creating a script compiler' do
          testing_env_dir # creates the structure
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42 }) do |ctx|
            ctx.with_catalog_compiler(facts: { 'myfact' => 43 }) {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 43") }
          end
          expect(result).to eq(true)
        end
      end

      context 'datatypes are supported such that' do
        it 'datatypes defined as pcore in a module are deserialized' do
          testing_env_dir
          vars = {"bobs_age"=>{"__ptype"=>"Mytype", "name"=>"Bob", "year_of_birth"=>1984}}
          result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts, variables: vars) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string("$bobs_age.age") }
          end
          expect(result).to eq(DateTime.now.year - 1984)
        end
      end

      context 'configured as an existing given environment directory such that' do
        it 'modules in it are available from its "modules" directory' do
          result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string('a::afunc()') }
          end
          expect(result).to eq("a::afunc value")
        end

        it 'libs in a given "modulepath" are added to the Ruby $LOAD_PATH' do
          result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler {|c| c.evaluate_string('a::arubyfunc()') }
          end
          expect(result).to eql('something')
        end

        it 'a given "modulepath" overrides the default' do
          expect do
            Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, modulepath: [], facts: node_facts) do |ctx|
              ctx.with_catalog_compiler {|c| c.evaluate_string('a::afunc()') }
            end
          end.to raise_error(/Unknown function: 'a::afunc'/)
        end

        it 'a "pre_modulepath" is prepended and a "post_modulepath" is appended to the effective modulepath' do
          other_modules1 = File.join(environments_dir, 'other_env1/modules')
          other_modules2 = File.join(environments_dir, 'other_env2/modules')
          result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir,
            pre_modulepath: [other_modules1],
            post_modulepath: [other_modules2],
            facts: node_facts
          ) do |ctx|
            the_modulepath = Puppet.lookup(:environments).get('pal_env').modulepath
            the_modulepath[0] == other_modules1 && the_modulepath[-1] == other_modules2
          end
          expect(result).to be(true)
        end

        it 'can set variables in any scope' do
          vars = {'a'=> 10, 'x::y' => 20}
          result = Puppet::Pal.in_environment('pal_env', env_dir: testing_env_dir, facts: node_facts, variables: vars) do |ctx|
            ctx.with_catalog_compiler { |c| c.evaluate_string("1+2+3+4+$a+$x::y") }
          end
          expect(result).to eq(40)
        end

        it 'errors in a meaningful way when a non existing env name is given' do
          testing_env_dir # creates the structure
          expect do
            Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir.chop, facts: node_facts) { |ctx| }
          end.to raise_error(/The environment directory '.*' does not exist/)
        end

        it 'errors if an env_name is given and is not a String[1]' do
          expect do
            Puppet::Pal.in_environment('', env_dir: testing_env_dir, facts: node_facts)  { |ctx| }
          end.to raise_error(/env_name has wrong type/)

          expect do
            Puppet::Pal.in_environment(32, env_dir: testing_env_dir, facts: node_facts)  { |ctx| }
          end.to raise_error(/env_name has wrong type/)
        end
      end
    end

    context 'configured as existing given envpath such that' do
      it 'modules in it are available from its "modules" directory' do
        testing_env_dir # creates the structure
        result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx|
          ctx.with_catalog_compiler { |c| c.evaluate_string('a::afunc()') }
        end
        expect(result).to eq("a::afunc value")
      end

      it 'a given "modulepath" overrides the default' do
        testing_env_dir # creates the structure
        expect do
          Puppet::Pal.in_environment('pal_env', envpath: environments_dir, modulepath: [], facts: node_facts) do |ctx|
            ctx.with_catalog_compiler { |c| c.evaluate_string('a::afunc()') }
          end
        end.to raise_error(/Unknown function: 'a::afunc'/)
      end

      it 'the envpath can have multiple entries - that are searched for the given env' do
        testing_env_dir # creates the structure
        result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx|
          ctx.with_catalog_compiler {|c| c.evaluate_string('a::afunc()') }
        end
        expect(result).to eq("a::afunc value")
      end

      context 'with a catalog compiler' do
        it 'uses configured manifest_file if configured_by_env is true and Puppet[:code] is unset' do
          testing_env_dir # creates the structure
          Puppet[:manifest] = file_containing('afunc.pp', "function myfunc(Integer $a) { $a * 2 } ")
          result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler(configured_by_env: true) {|c|  c.call_function('myfunc', 4)}
          end
          expect(result).to eql(8)
        end

        it 'uses Puppet[:code] if configured_by_env is true and Puppet[:code] is set' do
          testing_env_dir # creates the structure
          Puppet[:manifest] = file_containing('amanifest.pp', "$a = 20")
          Puppet[:code] = '$a = 40'
          result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler(configured_by_env: true) {|c|  c.evaluate_string('$a')}
          end
          expect(result).to eql(40)
        end

        it 'makes the pal CatalogCompiler available as catalog_compiler_param to Function dispatcher' do
          testing_env_dir # creates the structure
          Puppet[:manifest] = file_containing('noop.pp', "undef")
          result = Puppet::Pal.in_environment('pal_env', envpath: environments_dir, facts: node_facts) do |ctx|
            ctx.with_catalog_compiler(configured_by_env: true) {|c|  c.call_function('a::mycatalogcompilerfunc', 'go')}
          end
          expect(result).to eql('go')
        end
      end
    end
  end
end

