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 'deprecated PAL API methods work and' do
      it '"evaluate_script_string" evaluates a code string in a given tmp environment' do
        result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
          ctx.evaluate_script_string('1+2+3')
        end
        expect(result).to eq(6)
      end

      it '"evaluate_script_manifest" 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.evaluate_script_manifest(manifest)
        end
        expect(result).to eq(10)
      end
    end

    context "with a script 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_script_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_script_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

      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_script_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_script_compiler {|c| c.evaluate_string('$a = 1+2+3')}).to eq(6)
            expect { ctx.with_script_compiler {|c| c.evaluate_string('$a') }}.to raise_error(/Unknown variable: 'a'/)
          end
        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_script_compiler do |compiler|
              compiler.evaluate_string(<<-CODE)
                function run_me() { "worked1" }
                run_me()
                CODE
            end
          end
          expect(result).to eq('worked1')
        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_script_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_script_compiler do |compiler|
              manifest = file_containing('testing.pp', (<<-CODE))
                function run_me() { "worked1" }
                run_me()
                CODE
              pal.with_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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 plans such that' do
        it '"plan_signature" returns the signatures of a plan' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "plan myplan(Integer $a) {  } ")
            ctx.with_script_compiler(manifest_file: manifest) do |c|
              signature = c.plan_signature('myplan')
              [ signature.callable_with?({'a' => 10}),
                signature.callable_with?({'a' => 'nope'})
              ]
            end
          end
          expect(result).to eq([true, false])
        end

        it 'a PlanSignature.callable_with? calls a given lambda with any errors as a formatted string' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "plan myplan(Integer $a, Integer $b) {  } ")
            ctx.with_script_compiler(manifest_file: manifest) do |c|
              signature = c.plan_signature('myplan')
              local_result = nil
              signature.callable_with?({'a' => 'nope'}) {|errors| local_result = errors }
              local_result
            end
          end
          # Note that errors are indented one space and on separate lines
          #
          expect(result).to eq(" parameter 'a' expects an Integer value, got String\n expects a value for parameter 'b'")
        end

        it 'a PlanSignature.callable_with? does not call a given lambda if there are no errors' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "plan myplan(Integer $a) {  } ")
            ctx.with_script_compiler(manifest_file: manifest) do |c|
              signature = c.plan_signature('myplan')
              local_result = 'yay'
              signature.callable_with?({'a' => 1}) {|errors| local_result = 'not yay' }
              local_result
            end
          end
          expect(result).to eq('yay')
        end

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

        it '"PlanSignature#params_type" returns a map of all parameters and their types' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            manifest = file_containing('afunc.pp', "plan myplan(Integer $a, String $b) {  } ")
            ctx.with_script_compiler(manifest_file: manifest) do |c|
              c.plan_signature('myplan').params_type
            end
          end
          expect(result.class).to eq(Puppet::Pops::Types::PStructType)
          expect(result.to_s).to eq("Struct[{'a' => Integer, 'b' => String}]")
        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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_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_script_compiler { |c| c.evaluate_literal(c.parse_string('function foo() { }; 42')) }
          end
        end.to raise_error(/does not represent a literal value/)
      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 },
        'plans' => a_plans,
        'tasks' => a_tasks,
        'types' => a_types,
#        'metadata.json' => metadata_json_a.to_json
        },
        'b' => {
        'functions' => b_functions,
        'lib' => b_lib,
        'plans' => b_plans,
        'tasks' => b_tasks,
        'types' => b_types,
#        'metadata.json' => metadata_json_b.to_json
        },
        'c' => {
        'types' => c_types,
#        'metadata.json' => metadata_json_c.to_json
        },
      }
    }

    let(:a_plans) {
      {
        'aplan.pp' => <<-PUPPET.unindent,
        plan a::aplan() { 'a::aplan value' }
        PUPPET
      }
    }

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

    let(:a_tasks) {
      {
        'atask' => '',
      }
    }

    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
            'myscriptcompilerfunc.rb' => <<-RUBY.unindent,
              Puppet::Functions.create_function(:'a::myscriptcompilerfunc', Puppet::Functions::InternalFunction) do
                dispatch :myscriptcompilerfunc do
                  script_compiler_param
                  param 'String',:name
                end

                def myscriptcompilerfunc(script_compiler, name)
                  script_compiler.is_a?(Puppet::Pal::ScriptCompiler) ? name : 'no go'
                end
              end
              RUBY
          }
        }
      }
    }

    let(:b_plans) {
      {
        'aplan.pp' => <<-PUPPET.unindent,
        plan b::aplan() {}
        PUPPET
      }
    }

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

    let(:b_tasks) {
      {
        'atask' => "# doing exactly nothing\n",
        'atask.json' => <<-JSONTEXT.unindent
          {
            "description": "test task b::atask",
            "input_method": "stdin",
            "parameters": {
              "string_param": {
                "description": "A string parameter",
                "type": "String[1]"
              },
              "int_param": {
                "description": "An integer parameter",
                "type": "Integer"
              }
            }
          }
        JSONTEXT
      }
    }

    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_script_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_script_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_script_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_script_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_script_compiler(facts: { 'myfact' => 43 }) {|c| c.evaluate_string("$facts =~ Hash and $facts[myfact] == 43") }
          end
          expect(result).to eq(true)
        end

        it 'can be disabled with the :set_local_facts option' do
          Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: { 'myfact' => 42}) do |ctx|
            ctx.with_script_compiler(facts: { 'myfact' => 42 }, set_local_facts: false) do |compiler|
              expect { compiler.evaluate_string('$facts') }.to raise_error(
                Puppet::PreformattedError,
                /Unknown variable: 'facts'/
              )
            end
          end
        end
      end

      context 'supports tasks such that' do
        it '"task_signature" returns the signatures of a generic task' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('a::atask')
              [ signature.runnable_with?('whatever' => 10),
                signature.runnable_with?('anything_goes' => 'foo')
              ]
            end
          end
          expect(result).to eq([true, true])
        end

        it '"TaskSignature#runnable_with?" calls a given lambda if there is an error in a generic task' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('a::atask')
              local_result = 'not yay'
              signature.runnable_with?('string_param' => /not data/) {|error| local_result = error }
              local_result
            end
          end
          expect(result).to match(/Task a::atask:\s+entry 'string_param' expects a Data value, got Regexp/m)
        end

        it '"task_signature" returns the signatures of a task defined with metadata' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('b::atask')
              [ signature.runnable_with?('string_param' => 'foo', 'int_param' => 10),
                signature.runnable_with?('anything_goes' => 'foo'),
                signature.task_hash['name'],
                signature.task_hash['metadata']['parameters']['string_param']['description'],
                signature.task_hash['metadata']['description'],
                signature.task_hash['metadata']['parameters']['int_param']['type'],
              ]
            end
          end
          expect(result).to eq([true, false, 'b::atask', 'A string parameter', 'test task b::atask', 'Integer'])
        end

        it '"TaskSignature#runnable_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|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('b::atask')
              local_result = 'not yay'
              signature.runnable_with?('string_param' => 10) {|error| local_result = error }
              local_result
            end
          end
          expect(result).to match(/Task b::atask:\s+parameter 'string_param' expects a String value, got Integer/m)
        end

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

        it 'default task input_method is nil' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('a::atask')
              signature.task_hash
            end
          end
          expect(result['metadata']['input_method']).to be_nil
        end

        it 'task input_method is parsed from task metadata' do
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler do |c|
              signature = c.task_signature('b::atask')
              signature.task_hash
            end
          end
          expect(result['metadata']['input_method']).to eq('stdin')
        end

        it '"list_tasks" returns an array with all tasks that can be loaded' do
          testing_env_dir # creates the structure
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler {|c| c.list_tasks() }
          end
          expect(result.is_a?(Array)).to eq(true)
          expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true)
          expect(result.map {|tn| tn.name}).to contain_exactly('a::atask', 'b::atask')
        end

        it '"list_tasks" filters on name based on a given regexp' do
          testing_env_dir # creates the structure
          result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: modulepath, facts: node_facts) do |ctx|
            ctx.with_script_compiler {|c| c.list_tasks(/^a::/) }
          end
          expect(result.is_a?(Array)).to eq(true)
          expect(result.all? {|s| s.is_a?(Puppet::Pops::Loader::TypedName) }).to eq(true)
          expect(result.map {|tn| tn.name}).to eq(['a::atask'])
        end
      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_script_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_script_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_script_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_script_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

      { '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_environment('pal_env', env_dir: testing_env_dir, modulepath: {'a' => 'hm'}, facts: node_facts) { |ctx| }
            Puppet::Pal.in_tmp_environment('pal_env', modulepath: value, facts: node_facts) { |ctx| }
          end.to raise_error(/modulepath has wrong type/)
        end
      end

      it 'errors if env_dir and envpath are both given' do
        testing_env_dir # creates the structure
        expect do
          Puppet::Pal.in_environment('blah_env', env_dir: testing_env_dir, envpath: environments_dir, facts: node_facts) { |ctx| }
        end.to raise_error(/Cannot use 'env_dir' and 'envpath' at the same time/)
      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_script_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_script_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
        testing_env_dir # creates the structure
        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', envpath: environments_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 '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_script_compiler {|c| c.evaluate_string('a::afunc()') }
        end
        expect(result).to eq("a::afunc value")
      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', envpath: environments_dir, facts: node_facts) { |ctx| }
        end.to raise_error(/No directory found for the environment 'blah_env' on the path '.*'/)
      end

      it 'errors if a block is not given to in_environment' do
        expect do
          Puppet::Pal.in_environment('blah_env', envpath: environments_dir, facts: node_facts)
        end.to raise_error(/A block must be given to 'in_environment/)
      end

      it 'errors if envpath is something other than a string' do
        testing_env_dir # creates the structure
        expect do
          Puppet::Pal.in_environment('blah_env', envpath: '', facts: node_facts)  { |ctx| }
        end.to raise_error(/envpath has wrong type/)

        expect do
          Puppet::Pal.in_environment('blah_env', envpath: [environments_dir], facts: node_facts) { |ctx| }
        end.to raise_error(/envpath has wrong type/)
      end

      context 'with a script 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_script_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_script_compiler(configured_by_env: true) {|c|  c.evaluate_string('$a')}
          end
          expect(result).to eql(40)
        end

        it 'makes the pal ScriptCompiler available as script_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_script_compiler(configured_by_env: true) {|c|  c.call_function('a::myscriptcompilerfunc', 'go')}
          end
          expect(result).to eql('go')
        end
      end
    end
  end
end
