require 'spec_helper'

describe Puppet::Parser::Resource do
  before do
    environment = Puppet::Node::Environment.create(:testing, [])
    @node = Puppet::Node.new("yaynode", :environment => environment)
    @known_resource_types = environment.known_resource_types
    @compiler = Puppet::Parser::Compiler.new(@node)
    @source = newclass ""
    @scope = @compiler.topscope
  end

  def mkresource(args = {})
    args[:source] ||= @source
    args[:scope] ||= @scope

    params = args[:parameters] || {:one => "yay", :three => "rah"}
    if args[:parameters] == :none
      args.delete(:parameters)
    elsif not args[:parameters].is_a? Array
      args[:parameters] = paramify(args[:source], params)
    end

    Puppet::Parser::Resource.new("resource", "testing", args)
  end

  def param(name, value, source)
    Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source)
  end

  def paramify(source, hash)
    hash.collect do |name, value|
      Puppet::Parser::Resource::Param.new(
        :name => name, :value => value, :source => source
      )
    end
  end

  def newclass(name)
    @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name)
  end

  def newdefine(name)
    @known_resource_types.add Puppet::Resource::Type.new(:definition, name)
  end

  def newnode(name)
    @known_resource_types.add Puppet::Resource::Type.new(:node, name)
  end

  it "should get its environment from its scope" do
    scope = double('scope', :source => double("source"))
    expect(scope).to receive(:environment).and_return("foo").at_least(:once)
    expect(scope).to receive(:lookupdefaults).and_return({})
    expect(Puppet::Parser::Resource.new("file", "whatever", :scope => scope).environment).to eq("foo")
  end

  it "should use the scope's environment as its environment" do
    expect(@scope).to receive(:environment).and_return("myenv").at_least(:once)
    expect(Puppet::Parser::Resource.new("file", "whatever", :scope => @scope).environment).to eq("myenv")
  end

  it "should be isomorphic if it is builtin and models an isomorphic type" do
    expect(Puppet::Type.type(:file)).to receive(:isomorphic?).and_return(true)
    @resource = expect(Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?).to be_truthy
  end

  it "should not be isomorphic if it is builtin and models a non-isomorphic type" do
    expect(Puppet::Type.type(:file)).to receive(:isomorphic?).and_return(false)
    @resource = expect(Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?).to be_falsey
  end

  it "should be isomorphic if it is not builtin" do
    newdefine "whatever"
    @resource = expect(Puppet::Parser::Resource.new("whatever", "whatever", :scope => @scope, :source => @source).isomorphic?).to be_truthy
  end

  it "should have an array-indexing method for retrieving parameter values" do
    @resource = mkresource
    expect(@resource[:one]).to eq("yay")
  end

  it "should use a Puppet::Resource for converting to a ral resource" do
    trans = double('resource', :to_ral => "yay")
    @resource = mkresource
    expect(@resource).to receive(:copy_as_resource).and_return(trans)
    expect(@resource.to_ral).to eq("yay")
  end

  it "should be able to use the indexing operator to access parameters" do
    resource = Puppet::Parser::Resource.new("resource", "testing", :source => "source", :scope => @scope)
    resource["foo"] = "bar"
    expect(resource["foo"]).to eq("bar")
  end

  it "should return the title when asked for a parameter named 'title'" do
    expect(Puppet::Parser::Resource.new("resource", "testing", :source => @source, :scope => @scope)[:title]).to eq("testing")
  end

  describe "when initializing" do
    before do
      @arguments = {:scope => @scope}
    end

    it "should fail unless hash is specified" do
      expect {
        Puppet::Parser::Resource.new('file', '/my/file', nil)
      }.to raise_error(ArgumentError, /Resources require a hash as last argument/)
    end

    it "should attempt to externalize filepaths via the environment" do
      environment = Puppet::Node::Environment.create(:testing, [])
      expect(environment).to receive(:externalize_path).at_least(:once).and_return("foo")
      Puppet[:code] = "notify { 'hello': }"
      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone', environment: environment)
      notify = catalog.resource('Notify[hello]')
      expect(notify.file).to eq("foo")
    end

    it "should set the reference correctly" do
      res = Puppet::Parser::Resource.new("resource", "testing", @arguments)
      expect(res.ref).to eq("Resource[testing]")
    end

    it "should be tagged with user tags" do
      tags = [ "tag1", "tag2" ]
      @arguments[:parameters] = [ param(:tag, tags , :source) ]
      res = Puppet::Parser::Resource.new("resource", "testing", @arguments)
      expect(res).to be_tagged("tag1")
      expect(res).to be_tagged("tag2")
    end
  end

  describe "when evaluating" do
    before do
      @catalog = Puppet::Resource::Catalog.new
      source = double('source')
      allow(source).to receive(:module_name)
      @scope = Puppet::Parser::Scope.new(@compiler, :source => source)
      @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @scope))
    end

    it "should evaluate the associated AST definition" do
      definition = newdefine "mydefine"
      res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source, :catalog => @catalog)
      expect(definition).to receive(:evaluate_code).with(res)

      res.evaluate
    end

    it "should evaluate the associated AST class" do
      @class = newclass "myclass"
      res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source, :catalog => @catalog)
      expect(@class).to receive(:evaluate_code).with(res)
      res.evaluate
    end

    it "should evaluate the associated AST node" do
      nodedef = newnode("mynode")
      res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source, :catalog => @catalog)
      expect(nodedef).to receive(:evaluate_code).with(res)
      res.evaluate
    end

    it "should add an edge to any specified stage for class resources" do
      @compiler.environment.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})

      other_stage = Puppet::Parser::Resource.new(:stage, "other", :scope => @scope, :catalog => @catalog)
      @compiler.add_resource(@scope, other_stage)
      resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
      resource[:stage] = 'other'
      @compiler.add_resource(@scope, resource)

      resource.evaluate

      expect(@compiler.catalog.edge?(other_stage, resource)).to be_truthy
    end

    it "should fail if an unknown stage is specified" do
      @compiler.environment.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})

      resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
      resource[:stage] = 'other'

      expect { resource.evaluate }.to raise_error(ArgumentError, /Could not find stage other specified by/)
    end

    it "should add edges from the class resources to the parent's stage if no stage is specified" do
      foo_stage = Puppet::Parser::Resource.new(:stage, :foo_stage, :scope => @scope, :catalog => @catalog)
      @compiler.add_resource(@scope, foo_stage)
      @compiler.environment.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
      resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
      resource[:stage] = 'foo_stage'
      @compiler.add_resource(@scope, resource)

      resource.evaluate

      expect(@compiler.catalog).to be_edge(foo_stage, resource)
    end

    it 'should allow a resource reference to be undef' do
      Puppet[:code] = "notify { 'hello': message=>'yo', notify => undef }"
      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
      edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}
      expect(edges).to include(['Class[main]', 'Notify[hello]'])
    end

    it 'should evaluate class in the same file without include' do
      Puppet[:code] = <<-MANIFEST
        class a($myvar = 'hello') {}
        class { 'a': myvar => 'goodbye' }
        notify { $a::myvar: }
      MANIFEST
      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
      expect(catalog.resource('Notify[goodbye]')).to be_a(Puppet::Resource)
    end

    it "should allow edges to propagate multiple levels down the scope hierarchy" do
      Puppet[:code] = <<-MANIFEST
        stage { before: before => Stage[main] }

        class alpha {
          include beta
        }
        class beta {
          include gamma
        }
        class gamma { }
        class { alpha: stage => before }
      MANIFEST

      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')

      # Stringify them to make for easier lookup
      edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}

      expect(edges).to include(["Stage[before]", "Class[Alpha]"])
      expect(edges).to include(["Stage[before]", "Class[Beta]"])
      expect(edges).to include(["Stage[before]", "Class[Gamma]"])
    end

    it "should use the specified stage even if the parent scope specifies one" do
      Puppet[:code] = <<-MANIFEST
        stage { before: before => Stage[main], }
        stage { after: require => Stage[main], }

        class alpha {
          class { beta: stage => after }
        }
        class beta { }
        class { alpha: stage => before }
      MANIFEST

      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')

      edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}

      expect(edges).to include(["Stage[before]", "Class[Alpha]"])
      expect(edges).to include(["Stage[after]", "Class[Beta]"])
    end

    it "should add edges from top-level class resources to the main stage if no stage is specified" do
      main = @compiler.catalog.resource(:stage, :main)
      @compiler.environment.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
      resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
      @compiler.add_resource(@scope, resource)

      resource.evaluate

      expect(@compiler.catalog).to be_edge(main, resource)
    end

    it 'should assign default value to generated resource' do
      Puppet[:code] = <<-PUPPET
        define one($var) {
          notify { "${var} says hello": }
        }
        
        define two($x = $title) {
          One {
            var => $x
          }
          one { a: }
          one { b: var => 'bill'}
        }
        two { 'bob': }
      PUPPET

      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
      edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}

      expect(edges).to include(['One[a]', 'Notify[bob says hello]'])
      expect(edges).to include(['One[b]', 'Notify[bill says hello]'])
    end

    it 'should override default value with new value' do
      Puppet[:code] = <<-PUPPET.unindent
        class foo {
          File {
            ensure => file,
            mode   => '644',
            owner  => 'root',
            group  => 'root',
          }
        
          file { '/tmp/foo':
            ensure  => directory
          }
        
          File['/tmp/foo'] { mode => '0755' }
        }
        include foo
        PUPPET

      catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
      file = catalog.resource('File[/tmp/foo]')
      expect(file).to be_a(Puppet::Resource)
      expect(file['mode']).to eql('0755')
    end
  end

  describe 'when evaluating resource defaults' do
    let(:resource) { Puppet::Parser::Resource.new('file', 'whatever', :scope => @scope, :source => @source) }

    it 'should add all defaults available from the scope' do
      expect(@scope).to receive(:lookupdefaults).with('File').and_return(:owner => param(:owner, 'default', @source))

      expect(resource[:owner]).to eq('default')
    end

    it 'should not replace existing parameters with defaults' do
      expect(@scope).to receive(:lookupdefaults).with('File').and_return(:owner => param(:owner, 'replaced', @source))
      r = Puppet::Parser::Resource.new('file', 'whatever', :scope => @scope, :source => @source, :parameters => [ param(:owner, 'oldvalue', @source) ])
      expect(r[:owner]).to eq('oldvalue')
    end

    it 'should override defaults with new parameters' do
      expect(@scope).to receive(:lookupdefaults).with('File').and_return(:owner => param(:owner, 'replaced', @source))

      resource.set_parameter(:owner, 'newvalue')
      expect(resource[:owner]).to eq('newvalue')
    end

    it 'should add a copy of each default, rather than the actual default parameter instance' do
      newparam = param(:owner, 'default', @source)
      other = newparam.dup
      other.value = "other"
      expect(newparam).to receive(:dup).and_return(other)
      expect(@scope).to receive(:lookupdefaults).with('File').and_return(:owner => newparam)

      expect(resource[:owner]).to eq('other')
    end

    it "should tag with value of default parameter named 'tag'" do
      expect(@scope).to receive(:lookupdefaults).with('File').and_return(:tag => param(:tag, 'the_tag', @source))

      expect(resource.tags).to include('the_tag')
    end
  end

  describe "when finishing" do
    before do
      @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source)
    end

    it "should do nothing if it has already been finished" do
      @resource.finish
      expect(@resource).not_to receive(:add_scope_tags)
      @resource.finish
    end

    it "converts parameters with Sensitive values to unwrapped values and metadata" do
      @resource[:content] = Puppet::Pops::Types::PSensitiveType::Sensitive.new("hunter2")
      @resource.finish
      expect(@resource[:content]).to eq "hunter2"
      expect(@resource.sensitive_parameters).to eq [:content]
    end
  end

  describe "when being tagged" do
    before do
      @scope_resource = double('scope_resource', :tags => %w{srone srtwo})
      allow(@scope).to receive(:resource).and_return(@scope_resource)
      @resource = Puppet::Parser::Resource.new("file", "yay", :scope => @scope, :source => double('source'))
    end

    it "should get tagged with the resource type" do
      expect(@resource.tags).to be_include("file")
    end

    it "should get tagged with the title" do
      expect(@resource.tags).to be_include("yay")
    end

    it "should get tagged with each name in the title if the title is a qualified class name" do
      resource = Puppet::Parser::Resource.new("file", "one::two", :scope => @scope, :source => double('source'))
      expect(resource.tags).to be_include("one")
      expect(resource.tags).to be_include("two")
    end

    it "should get tagged with each name in the type if the type is a qualified class name" do
      resource = Puppet::Parser::Resource.new("one::two", "whatever", :scope => @scope, :source => double('source'))
      expect(resource.tags).to be_include("one")
      expect(resource.tags).to be_include("two")
    end

    it "should not get tagged with non-alphanumeric titles" do
      resource = Puppet::Parser::Resource.new("file", "this is a test", :scope => @scope, :source => double('source'))
      expect(resource.tags).not_to be_include("this is a test")
    end

    it "should fail on tags containing '*' characters" do
      expect { @resource.tag("bad*tag") }.to raise_error(Puppet::ParseError)
    end

    it "should fail on tags starting with '-' characters" do
      expect { @resource.tag("-badtag") }.to raise_error(Puppet::ParseError)
    end

    it "should fail on tags containing ' ' characters" do
      expect { @resource.tag("bad tag") }.to raise_error(Puppet::ParseError)
    end

    it "should allow alpha tags" do
      expect { @resource.tag("good_tag") }.to_not raise_error
    end
  end

  describe "when merging overrides" do
    def resource_type(name)
      double(name, :child_of? => false)
    end

    before do
      @source = resource_type("source1")
      @resource = mkresource :source => @source
      @override = mkresource :source => @source
    end

    it "should fail when the override was not created by a parent class" do
      @override.source = resource_type("source2")
      expect(@override.source).to receive(:child_of?).with(@source).and_return(false)
      expect { @resource.merge(@override) }.to raise_error(Puppet::ParseError)
    end

    it "should succeed when the override was created in the current scope" do
      @source3 = resource_type("source3")
      @resource.source = @source3
      @override.source = @resource.source
      expect(@override.source).not_to receive(:child_of?).with(@source3)
      params = {:a => :b, :c => :d}
      expect(@override).to receive(:parameters).and_return(params)
      expect(@resource).to receive(:override_parameter).with(:b)
      expect(@resource).to receive(:override_parameter).with(:d)
      @resource.merge(@override)
    end

    it "should succeed when a parent class created the override" do
      @source3 = resource_type("source3")
      @resource.source = @source3
      @override.source = resource_type("source4")
      expect(@override.source).to receive(:child_of?).with(@source3).and_return(true)
      params = {:a => :b, :c => :d}
      expect(@override).to receive(:parameters).and_return(params)
      expect(@resource).to receive(:override_parameter).with(:b)
      expect(@resource).to receive(:override_parameter).with(:d)
      @resource.merge(@override)
    end

    it "should add new parameters when the parameter is not set" do
      allow(@source).to receive(:child_of?).and_return(true)
      @override.set_parameter(:testing, "value")
      @resource.merge(@override)

      expect(@resource[:testing]).to eq("value")
    end

    it "should replace existing parameter values" do
      allow(@source).to receive(:child_of?).and_return(true)
      @resource.set_parameter(:testing, "old")
      @override.set_parameter(:testing, "value")

      @resource.merge(@override)

      expect(@resource[:testing]).to eq("value")
    end

    it "should add values to the parameter when the override was created with the '+>' syntax" do
      allow(@source).to receive(:child_of?).and_return(true)
      param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source)
      param.add = true

      @override.set_parameter(param)

      @resource.set_parameter(:testing, "other")

      @resource.merge(@override)

      expect(@resource[:testing]).to eq(%w{other testing})
    end

    it "should not merge parameter values when multiple resources are overriden with '+>' at once " do
      @resource_2 = mkresource :source => @source

      @resource.  set_parameter(:testing, "old_val_1")
      @resource_2.set_parameter(:testing, "old_val_2")

      allow(@source).to receive(:child_of?).and_return(true)
      param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "new_val", :source => @resource.source)
      param.add = true
      @override.set_parameter(param)

      @resource.  merge(@override)
      @resource_2.merge(@override)

      expect(@resource  [:testing]).to eq(%w{old_val_1 new_val})
      expect(@resource_2[:testing]).to eq(%w{old_val_2 new_val})
    end

    it "should promote tag overrides to real tags" do
      allow(@source).to receive(:child_of?).and_return(true)
      param = Puppet::Parser::Resource::Param.new(:name => :tag, :value => "testing", :source => @resource.source)

      @override.set_parameter(param)

      @resource.merge(@override)

      expect(@resource.tagged?("testing")).to be_truthy
    end

  end

  it "should be able to be converted to a normal resource" do
    @source = double('scope', :name => "myscope")
    @resource = mkresource :source => @source
    expect(@resource).to respond_to(:copy_as_resource)
  end

  describe "when being converted to a resource" do
    before do
      @parser_resource = mkresource :scope => @scope, :parameters => {:foo => "bar", :fee => "fum"}
    end

    it "should create an instance of Puppet::Resource" do
      expect(@parser_resource.copy_as_resource).to be_instance_of(Puppet::Resource)
    end

    it "should set the type correctly on the Puppet::Resource" do
      expect(@parser_resource.copy_as_resource.type).to eq(@parser_resource.type)
    end

    it "should set the title correctly on the Puppet::Resource" do
      expect(@parser_resource.copy_as_resource.title).to eq(@parser_resource.title)
    end

    it "should copy over all of the parameters" do
      result = @parser_resource.copy_as_resource.to_hash

      # The name will be in here, also.
      expect(result[:foo]).to eq("bar")
      expect(result[:fee]).to eq("fum")
    end

    it "should copy over the tags" do
      @parser_resource.tag "foo"
      @parser_resource.tag "bar"

      expect(@parser_resource.copy_as_resource.tags).to eq(@parser_resource.tags)
    end

    it "should copy over the line" do
      @parser_resource.line = 40
      expect(@parser_resource.copy_as_resource.line).to eq(40)
    end

    it "should copy over the file" do
      @parser_resource.file = "/my/file"
      expect(@parser_resource.copy_as_resource.file).to eq("/my/file")
    end

    it "should copy over the 'exported' value" do
      @parser_resource.exported = true
      expect(@parser_resource.copy_as_resource.exported).to be_truthy
    end

    it "should copy over the 'virtual' value" do
      @parser_resource.virtual = true
      expect(@parser_resource.copy_as_resource.virtual).to be_truthy
    end

    it "should convert any parser resource references to Puppet::Resource instances" do
      ref = Puppet::Resource.new("file", "/my/file")
      @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ref}
      result = @parser_resource.copy_as_resource
      expect(result[:fee]).to eq(Puppet::Resource.new(:file, "/my/file"))
    end

    it "should convert any parser resource references to Puppet::Resource instances even if they are in an array" do
      ref = Puppet::Resource.new("file", "/my/file")
      @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", ref]}
      result = @parser_resource.copy_as_resource
      expect(result[:fee]).to eq(["a", Puppet::Resource.new(:file, "/my/file")])
    end

    it "should convert any parser resource references to Puppet::Resource instances even if they are in an array of array, and even deeper" do
      ref1 = Puppet::Resource.new("file", "/my/file1")
      ref2 = Puppet::Resource.new("file", "/my/file2")
      @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", [ref1,ref2]]}
      result = @parser_resource.copy_as_resource
      expect(result[:fee]).to eq(["a", Puppet::Resource.new(:file, "/my/file1"), Puppet::Resource.new(:file, "/my/file2")])
    end

    it "should fail if the same param is declared twice" do
      expect do
        @parser_resource = mkresource :source => @source, :parameters => [
          Puppet::Parser::Resource::Param.new(
            :name => :foo, :value => "bar", :source => @source
          ),
          Puppet::Parser::Resource::Param.new(
            :name => :foo, :value => "baz", :source => @source
          )
        ]
      end.to raise_error(Puppet::ParseError)
    end
  end

  describe "when setting parameters" do
    before do
      @source = newclass "foobar"
      @resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => @source
    end

    it "should accept Param instances and add them to the parameter list" do
      param = Puppet::Parser::Resource::Param.new :name => "foo", :value => "bar", :source => @source
      @resource.set_parameter(param)
      expect(@resource["foo"]).to eq("bar")
    end

    it "should allow parameters to be set to 'false'" do
      @resource.set_parameter("myparam", false)
      expect(@resource["myparam"]).to be_falsey
    end

    it "should use its source when provided a parameter name and value" do
      @resource.set_parameter("myparam", "myvalue")
      expect(@resource["myparam"]).to eq("myvalue")
    end
  end

  # part of #629 -- the undef keyword.  Make sure 'undef' params get skipped.
  it "should not include 'undef' parameters when converting itself to a hash" do
    resource = Puppet::Parser::Resource.new "file", "/tmp/testing", :source => double("source"), :scope => @scope
    resource[:owner] = :undef
    resource[:mode] = "755"
    expect(resource.to_hash[:owner]).to be_nil
  end
end
