#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: AJ Christensen (<aj@chef.io>)
# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "spec_helper"
require "chef/node/attribute"

describe Chef::Node::Attribute do
  let(:events) { instance_double(Chef::EventDispatch::Dispatcher) }
  let(:run_context) { instance_double(Chef::RunContext, events: events) }
  let(:node) { instance_double(Chef::Node, run_context: run_context) }
  before(:each) do
    allow(events).to receive(:attribute_changed)
    @attribute_hash =
      { "dmi" => {},
        "command" => { "ps" => "ps -ef" },
        "platform_version" => "10.5.7",
        "platform" => "mac_os_x",
        "ipaddress" => "192.168.0.117",
        "network" => { "default_interface" => "en1",
                       "interfaces" => { "vmnet1" => { "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                       "number" => "1",
                                                       "addresses" => { "00:50:56:c0:00:01" => { "family" => "lladdr" },
                                                                        "192.168.110.1" => { "broadcast" => "192.168.110.255",
                                                                                             "netmask" => "255.255.255.0",
                                                                                             "family" => "inet" } },
                                                       "mtu" => "1500",
                                                       "type" => "vmnet",
                                                       "arp" => { "192.168.110.255" => "ff:ff:ff:ff:ff:ff" },
                                                       "encapsulation" => "Ethernet" },
                                         "stf0" => { "flags" => [],
                                                     "number" => "0",
                                                     "addresses" => {},
                                                     "mtu" => "1280",
                                                     "type" => "stf",
                                                     "encapsulation" => "6to4" },
                                         "lo0" => { "flags" => %w{UP LOOPBACK RUNNING MULTICAST},
                                                    "number" => "0",
                                                    "addresses" => { "::1" => { "scope" => "Node", "prefixlen" => "128", "family" => "inet6" },
                                                                     "127.0.0.1" => { "netmask" => "255.0.0.0", "family" => "inet" },
                                                                     "fe80::1" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
                                                    "mtu" => "16384",
                                                    "type" => "lo",
                                                    "encapsulation" => "Loopback" },
                                         "gif0" => { "flags" => %w{POINTOPOINT MULTICAST},
                                                     "number" => "0",
                                                     "addresses" => {},
                                                     "mtu" => "1280",
                                                     "type" => "gif",
                                                     "encapsulation" => "IPIP" },
                                         "vmnet8" => { "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                       "number" => "8",
                                                       "addresses" => { "192.168.4.1" => { "broadcast" => "192.168.4.255",
                                                                                           "netmask" => "255.255.255.0",
                                                                                           "family" => "inet" },
                                                                        "00:50:56:c0:00:08" => { "family" => "lladdr" } },
                                                       "mtu" => "1500",
                                                       "type" => "vmnet",
                                                       "arp" => { "192.168.4.255" => "ff:ff:ff:ff:ff:ff" },
                                                       "encapsulation" => "Ethernet" },
                                         "en0" => { "status" => "inactive",
                                                    "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                    "number" => "0",
                                                    "addresses" => { "00:23:32:b0:32:f2" => { "family" => "lladdr" } },
                                                    "mtu" => "1500",
                                                    "media" => { "supported" => { "autoselect" => { "options" => [] },
                                                                                  "none" => { "options" => [] },
                                                                                  "1000baseT" => { "options" => %w{full-duplex flow-control hw-loopback} },
                                                                                  "10baseT/UTP" => { "options" => %w{half-duplex full-duplex flow-control hw-loopback} },
                                                                                  "100baseTX" => { "options" => %w{half-duplex full-duplex flow-control hw-loopback} } },
                                                                 "selected" => { "autoselect" => { "options" => [] } } },
                                                    "type" => "en",
                                                    "encapsulation" => "Ethernet" },
                                         "en1" => { "status" => "active",
                                                    "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                    "number" => "1",
                                                    "addresses" => { "fe80::223:6cff:fe7f:676c" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" },
                                                                     "00:23:6c:7f:67:6c" => { "family" => "lladdr" },
                                                                     "192.168.0.117" => { "broadcast" => "192.168.0.255",
                                                                                          "netmask" => "255.255.255.0",
                                                                                          "family" => "inet" } },
                                                    "mtu" => "1500",
                                                    "media" => { "supported" => { "autoselect" => { "options" => [] } },
                                                                 "selected" => { "autoselect" => { "options" => [] } } },
                                                    "type" => "en",
                                                    "arp" => { "192.168.0.72" => "0:f:ea:39:fa:d5",
                                                               "192.168.0.1" => "0:1c:fb:fc:6f:20",
                                                               "192.168.0.255" => "ff:ff:ff:ff:ff:ff",
                                                               "192.168.0.3" => "0:1f:33:ea:26:9b",
                                                               "192.168.0.77" => "0:23:12:70:f8:cf",
                                                               "192.168.0.152" => "0:26:8:7d:2:4c" },
                                                    "encapsulation" => "Ethernet" },
                                         "en2" => { "status" => "active",
                                                    "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                    "number" => "2",
                                                    "addresses" => { "169.254.206.152" => { "broadcast" => "169.254.255.255",
                                                                                            "netmask" => "255.255.0.0",
                                                                                            "family" => "inet" },
                                                                     "00:1c:42:00:00:01" => { "family" => "lladdr" },
                                                                     "fe80::21c:42ff:fe00:1" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
                                                    "mtu" => "1500",
                                                    "media" => { "supported" => { "autoselect" => { "options" => [] } },
                                                                 "selected" => { "autoselect" => { "options" => [] } } },
                                                    "type" => "en",
                                                    "encapsulation" => "Ethernet" },
                                         "fw0" => { "status" => "inactive",
                                                    "flags" => %w{BROADCAST SIMPLEX MULTICAST},
                                                    "number" => "0",
                                                    "addresses" => { "00:23:32:ff:fe:b0:32:f2" => { "family" => "lladdr" } },
                                                    "mtu" => "4078",
                                                    "media" => { "supported" => { "autoselect" => { "options" => ["full-duplex"] } },
                                                                 "selected" => { "autoselect" => { "options" => ["full-duplex"] } } },
                                                    "type" => "fw",
                                                    "encapsulation" => "1394" },
                                         "en3" => { "status" => "active",
                                                    "flags" => %w{UP BROADCAST SMART RUNNING SIMPLEX MULTICAST},
                                                    "number" => "3",
                                                    "addresses" => { "169.254.206.152" => { "broadcast" => "169.254.255.255",
                                                                                            "netmask" => "255.255.0.0",
                                                                                            "family" => "inet" },
                                                                     "00:1c:42:00:00:00" => { "family" => "lladdr" },
                                                                     "fe80::21c:42ff:fe00:0" => { "scope" => "Link", "prefixlen" => "64", "family" => "inet6" } },
                                                    "mtu" => "1500",
                                                    "media" => { "supported" => { "autoselect" => { "options" => [] } },
                                                                 "selected" => { "autoselect" => { "options" => [] } } },
                                                    "type" => "en",
                                                    "encapsulation" => "Ethernet" } } },
        "fqdn" => "latte.local",
        "ohai_time" => 1249065590.90391,
        "domain" => "local",
        "os" => "darwin",
        "platform_build" => "9J61",
        "os_version" => "9.7.0",
        "hostname" => "latte",
        "macaddress" => "00:23:6c:7f:67:6c",
        "music" => { "jimmy_eat_world" => "nice", "apophis" => false },
    }
    @default_hash = {
      "domain" => "opscode.com",
      "hot" => { "day" => "saturday" },
      "music" => {
        "jimmy_eat_world" => "is fun!",
        "mastodon" => "rocks",
        "mars_volta" => "is loud and nutty",
        "deeper" => { "gates_of_ishtar" => nil },
        "this" => { "apparatus" => { "must" => "be unearthed" } },
      },
    }
    @override_hash = {
      "macaddress" => "00:00:00:00:00:00",
      "hot" => { "day" => "sunday" },
      "fire" => "still burn",
      "music" => {
        "mars_volta" => "cicatriz",
      },
    }
    @automatic_hash = { "week" => "friday" }
    @attributes = Chef::Node::Attribute.new(@attribute_hash, @default_hash, @override_hash, @automatic_hash, node)
  end

  describe "initialize" do
    it "should return a Chef::Node::Attribute" do
      expect(@attributes).to be_a_kind_of(Chef::Node::Attribute)
    end

    it "should take an Automatioc, Normal, Default and Override hash" do
      expect { Chef::Node::Attribute.new({}, {}, {}, {}) }.not_to raise_error
    end

    %i{normal default override automatic}.each do |accessor|
      it "should set #{accessor}" do
        na = Chef::Node::Attribute.new({ normal: true }, { default: true }, { override: true }, { automatic: true })
        expect(na.send(accessor)).to eq({ accessor.to_s => true })
      end
    end

    it "should be enumerable" do
      expect(@attributes).to be_is_a(Enumerable)
    end
  end

  describe "when printing attribute components" do

    it "does not cause a type error" do
      # See CHEF-3799; IO#puts implicitly calls #to_ary on its argument. This
      # is expected to raise a NoMethodError or return an Array. `to_ary` is
      # the "strict" conversion method that should only be implemented by
      # things that are truly Array-like, so NoMethodError is the right choice.
      # (cf. there is no Hash#to_ary).
      expect { @attributes.default.to_ary }.to raise_error(NoMethodError)
    end

  end

  describe "when debugging attributes" do
    it "gives the value at each level of precedence for a path spec" do
      @attributes.default[:foo][:bar] = "default"
      @attributes.env_default[:foo][:bar] = "env_default"
      @attributes.role_default[:foo][:bar] = "role_default"
      @attributes.force_default[:foo][:bar] = "force_default"
      @attributes.normal[:foo][:bar] = "normal"
      @attributes.override[:foo][:bar] = "override"
      @attributes.role_override[:foo][:bar] = "role_override"
      @attributes.env_override[:foo][:bar] = "env_override"
      @attributes.force_override[:foo][:bar] = "force_override"
      @attributes.automatic[:foo][:bar] = "automatic"

      expected = [
        %w{default default},
        %w{env_default env_default},
        %w{role_default role_default},
        %w{force_default force_default},
        %w{normal normal},
        %w{override override},
        %w{role_override role_override},
        %w{env_override env_override},
        %w{force_override force_override},
        %w{automatic automatic},
      ]
      expect(@attributes.debug_value(:foo, :bar)).to eq(expected)
    end

    it "works through arrays" do
      @attributes.default["foo"] = [ { "bar" => "baz" } ]

      expect(@attributes.debug_value(:foo, 0)).to eq(
        [
          ["default", { "bar" => "baz" }],
          ["env_default", :not_present],
          ["role_default", :not_present],
          ["force_default", :not_present],
          ["normal", :not_present],
          ["override", :not_present],
          ["role_override", :not_present],
          ["env_override", :not_present],
          ["force_override", :not_present],
          ["automatic", :not_present],
        ]
      )
    end
  end

  describe "when fetching values based on precedence" do
    before do
      @attributes.default["default"] = "cookbook default"
      @attributes.override["override"] = "cookbook override"
    end

    it "prefers 'forced default' over any other default" do
      @attributes.force_default["default"] = "force default"
      @attributes.role_default["default"] = "role default"
      @attributes.env_default["default"] = "environment default"
      expect(@attributes["default"]).to eq("force default")
    end

    it "prefers role_default over environment or cookbook default" do
      @attributes.role_default["default"] = "role default"
      @attributes.env_default["default"] = "environment default"
      expect(@attributes["default"]).to eq("role default")
    end

    it "prefers environment default over cookbook default" do
      @attributes.env_default["default"] = "environment default"
      expect(@attributes["default"]).to eq("environment default")
    end

    it "returns the cookbook default when no other default values are present" do
      expect(@attributes["default"]).to eq("cookbook default")
    end

    it "prefers 'forced overrides' over role or cookbook overrides" do
      @attributes.force_override["override"] = "force override"
      @attributes.env_override["override"] = "environment override"
      @attributes.role_override["override"] = "role override"
      expect(@attributes["override"]).to eq("force override")
    end

    it "prefers environment overrides over role or cookbook overrides" do
      @attributes.env_override["override"] = "environment override"
      @attributes.role_override["override"] = "role override"
      expect(@attributes["override"]).to eq("environment override")
    end

    it "prefers role overrides over cookbook overrides" do
      @attributes.role_override["override"] = "role override"
      expect(@attributes["override"]).to eq("role override")
    end

    it "returns cookbook overrides when no other overrides are present" do
      expect(@attributes["override"]).to eq("cookbook override")
    end

    it "merges arrays within the default precedence" do
      @attributes.role_default["array"] = %w{role}
      @attributes.env_default["array"] = %w{env}
      expect(@attributes["array"]).to eq(%w{env role})
    end

    it "merges arrays within the override precedence" do
      @attributes.role_override["array"] = %w{role}
      @attributes.env_override["array"] = %w{env}
      expect(@attributes["array"]).to eq(%w{role env})
    end

    it "does not merge arrays between default and normal" do
      @attributes.role_default["array"] = %w{role}
      @attributes.normal["array"] = %w{normal}
      expect(@attributes["array"]).to eq(%w{normal})
    end

    it "does not merge arrays between normal and override" do
      @attributes.normal["array"] = %w{normal}
      @attributes.role_override["array"] = %w{role}
      expect(@attributes["array"]).to eq(%w{role})
    end

    it "merges nested hashes between precedence levels" do
      @attributes = Chef::Node::Attribute.new({}, {}, {}, {})
      @attributes.env_default = { "a" => { "b" => { "default" => "default" } } }
      @attributes.normal = { "a" => { "b" => { "normal" => "normal" } } }
      @attributes.override = { "a" => { "override" => "role" } }
      @attributes.automatic = { "a" => { "automatic" => "auto" } }
      expect(@attributes["a"]).to eq({ "b" => { "default" => "default", "normal" => "normal" },
                                       "override" => "role",
                                       "automatic" => "auto" })
    end
  end

  describe "when reading combined default or override values" do
    before do
      @attributes.default["cd"] = "cookbook default"
      @attributes.role_default["rd"] = "role default"
      @attributes.env_default["ed"] = "env default"
      @attributes.default!["fd"] = "force default"
      @attributes.override["co"] = "cookbook override"
      @attributes.role_override["ro"] = "role override"
      @attributes.env_override["eo"] = "env override"
      @attributes.override!["fo"] = "force override"
    end

    it "merges all types of overrides into a combined override" do
      expect(@attributes.combined_override["co"]).to eq("cookbook override")
      expect(@attributes.combined_override["ro"]).to eq("role override")
      expect(@attributes.combined_override["eo"]).to eq("env override")
      expect(@attributes.combined_override["fo"]).to eq("force override")
    end

    it "merges all types of defaults into a combined default" do
      expect(@attributes.combined_default["cd"]).to eq("cookbook default")
      expect(@attributes.combined_default["rd"]).to eq("role default")
      expect(@attributes.combined_default["ed"]).to eq("env default")
      expect(@attributes.combined_default["fd"]).to eq("force default")
    end

  end

  describe "[]" do
    it "should return override data if it exists" do
      expect(@attributes["macaddress"]).to eq("00:00:00:00:00:00")
    end

    it "should return attribute data if it is not overridden" do
      expect(@attributes["platform"]).to eq("mac_os_x")
    end

    it "should return data that doesn't have corresponding keys in every hash" do
      expect(@attributes["command"]["ps"]).to eq("ps -ef")
    end

    it "should return default data if it is not overriden or in attribute data" do
      expect(@attributes["music"]["mastodon"]).to eq("rocks")
    end

    it "should prefer the override data over an available default" do
      expect(@attributes["music"]["mars_volta"]).to eq("cicatriz")
    end

    it "should prefer the attribute data over an available default" do
      expect(@attributes["music"]["jimmy_eat_world"]).to eq("nice")
    end

    it "should prefer override data over default data if there is no attribute data" do
      expect(@attributes["hot"]["day"]).to eq("sunday")
    end

    it "should return the merged hash if all three have values" do
      result = @attributes["music"]
      expect(result["mars_volta"]).to eq("cicatriz")
      expect(result["jimmy_eat_world"]).to eq("nice")
      expect(result["mastodon"]).to eq("rocks")
    end
  end

  describe "[]=" do
    it "should error out when the type of attribute to set has not been specified" do
      @attributes.normal["the_ghost"] = {}
      expect { @attributes["the_ghost"]["exterminate"] = false }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
    end

    it "should let you set an attribute value when another hash has an intermediate value" do
      @attributes.normal["the_ghost"] = { "exterminate" => "the future" }
      expect { @attributes.normal["the_ghost"]["eviscerate"]["tomorrow"] = false }.not_to raise_error
    end

    it "should set the attribute value" do
      @attributes.normal["longboard"] = "surfing"
      expect(@attributes.normal["longboard"]).to eq("surfing")
      expect(@attributes.normal["longboard"]).to eq("surfing")
    end

    it "should set deeply nested attribute values when a precedence level is specified" do
      @attributes.normal["deftones"]["hunters"]["nap"] = "surfing"
      expect(@attributes.normal["deftones"]["hunters"]["nap"]).to eq("surfing")
    end

    it "should die if you try and do nested attributes that do not exist without read vivification" do
      expect { @attributes["foo"]["bar"] = :baz }.to raise_error(NoMethodError)
    end

    it "should let you set attributes manually without vivification" do
      @attributes.normal["foo"] = Mash.new
      @attributes.normal["foo"]["bar"] = :baz
      expect(@attributes.normal["foo"]["bar"]).to eq(:baz)
    end

    it "does not support ||= when setting" do
      # This is a limitation of auto-vivification.
      # Users who need this behavior can use set_unless and friends
      @attributes.normal["foo"] = Mash.new
      @attributes.normal["foo"]["bar"] ||= "stop the world"
      expect(@attributes.normal["foo"]["bar"]).to eq({})
    end
  end

  describe "to_hash" do
    it "should convert to a hash" do
      expect(@attributes.to_hash.class).to eq(Hash)
    end

    it "should convert to a hash based on current state" do
      hash = @attributes["hot"].to_hash
      expect(hash.class).to eq(Hash)
      expect(hash["day"]).to eq("sunday")
    end

    it "should create a deep copy of the node attribute" do
      @attributes.default["foo"]["bar"]["baz"] = "fizz"
      hash = @attributes["foo"].to_hash
      expect(hash).to eql({ "bar" => { "baz" => "fizz" } })
      hash["bar"]["baz"] = "buzz"
      expect(hash).to eql({ "bar" => { "baz" => "buzz" } })
      expect(@attributes.default["foo"]).to eql({ "bar" => { "baz" => "fizz" } })
    end

    it "should create a deep copy of arrays in the node attribute" do
      @attributes.default["foo"]["bar"] = ["fizz"]
      hash = @attributes["foo"].to_hash
      expect(hash).to eql({ "bar" => [ "fizz" ] })
      hash["bar"].push("buzz")
      expect(hash).to eql({ "bar" => %w{fizz buzz} })
      expect(@attributes.default["foo"]).to eql({ "bar" => [ "fizz" ] })
    end

    it "mutating strings should not mutate the attributes in a hash" do
      @attributes.default["foo"]["bar"]["baz"] = "fizz"
      hash = @attributes["foo"].to_hash
      expect(hash).to eql({ "bar" => { "baz" => "fizz" } })
      hash["bar"]["baz"] << "buzz"
      expect(hash).to eql({ "bar" => { "baz" => "fizzbuzz" } })
      expect(@attributes.default["foo"]).to eql({ "bar" => { "baz" => "fizz" } })
    end

    it "mutating array elements should not mutate the attributes" do
      @attributes.default["foo"]["bar"] = [ "fizz" ]
      hash = @attributes["foo"].to_hash
      expect(hash).to eql({ "bar" => [ "fizz" ] })
      hash["bar"][0] << "buzz"
      expect(hash).to eql({ "bar" => [ "fizzbuzz" ] })
      expect(@attributes.default["foo"]).to eql({ "bar" => [ "fizz" ] })
    end
  end

  describe "dup" do
    it "array can be duped even if some elements can't" do
      @attributes.default[:foo] = %w{foo bar baz} + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ]
      @attributes.default[:foo].dup
    end

    it "mutating strings should not mutate the attributes in a hash" do
      @attributes.default["foo"]["bar"]["baz"] = "fizz"
      hash = @attributes["foo"].dup
      expect(hash).to eql({ "bar" => { "baz" => "fizz" } })
      hash["bar"]["baz"] << "buzz"
      expect(hash).to eql({ "bar" => { "baz" => "fizzbuzz" } })
      expect(@attributes.default["foo"]).to eql({ "bar" => { "baz" => "fizz" } })
    end

    it "mutating array elements should not mutate the attributes" do
      @attributes.default["foo"]["bar"] = [ "fizz" ]
      hash = @attributes["foo"].dup
      expect(hash).to eql({ "bar" => [ "fizz" ] })
      hash["bar"][0] << "buzz"
      expect(hash).to eql({ "bar" => [ "fizzbuzz" ] })
      expect(@attributes.default["foo"]).to eql({ "bar" => [ "fizz" ] })
    end
  end

  describe "has_key?" do
    it "should return true if an attribute exists" do
      expect(@attributes.key?("music")).to eq(true)
    end

    it "should return false if an attribute does not exist" do
      expect(@attributes.key?("ninja")).to eq(false)
    end

    it "should return false if an attribute does not exist using dot notation" do
      expect(@attributes.key?("does_not_exist_at_all")).to eq(false)
    end

    it "should return true if an attribute exists but is set to false" do
      @attributes.key?("music")
      expect(@attributes["music"].key?("apophis")).to eq(true)
    end

    it "does not find keys above the current nesting level" do
      expect(@attributes["music"]["this"]["apparatus"]).not_to have_key("this")
    end

    it "does not find keys below the current nesting level" do
      expect(@attributes["music"]["this"]).not_to have_key("must")
    end

    %i{include? key? member?}.each do |method|
      it "should alias the method #{method} to itself" do
        expect(@attributes).to respond_to(method)
      end

      it "#{method} should behave like has_key?" do
        expect(@attributes.send(method, "music")).to eq(true)
      end
    end
  end

  describe "attribute?" do
    it "should return true if an attribute exists" do
      expect(@attributes.attribute?("music")).to eq(true)
    end

    it "should return false if an attribute does not exist" do
      expect(@attributes.attribute?("ninja")).to eq(false)
    end

  end

  describe "keys" do
    before(:each) do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => { "two" => "three" },
          "hut" => { "two" => "three" },
          "place" => {},
        },
        {
          "one" => { "four" => "five" },
          "snakes" => "on a plane",
        },
        {
          "one" => { "six" => "seven" },
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should yield each top level key" do
      collect = []
      @attributes.each_key do |k|
        collect << k
      end
      expect(collect.include?("one")).to eq(true)
      expect(collect.include?("hut")).to eq(true)
      expect(collect.include?("snakes")).to eq(true)
      expect(collect.include?("snack")).to eq(true)
      expect(collect.include?("place")).to eq(true)
      expect(collect.length).to eq(5)
    end

    it "should yield lower if we go deeper" do
      collect = []
      @attributes["one"].each_key do |k|
        collect << k
      end
      expect(collect.include?("two")).to eq(true)
      expect(collect.include?("four")).to eq(true)
      expect(collect.include?("six")).to eq(true)
      expect(collect.length).to eq(3)
    end

    it "should not raise an exception if one of the hashes has a nil value on a deep lookup" do
      expect { @attributes["place"].keys { |k| } }.not_to raise_error
    end
  end

  describe "each" do
    before(:each) do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should yield each top level key and value, post merge rules" do
      collect = {}
      @attributes.each do |k, v|
        collect[k] = v
      end

      expect(collect["one"]).to eq("six")
      expect(collect["hut"]).to eq("three")
      expect(collect["snakes"]).to eq("on a plane")
      expect(collect["snack"]).to eq("cookies")
    end

    it "should yield as a two-element array" do
      @attributes.each do |a|
        expect(a).to be_an_instance_of(Array)
      end
    end
  end

  describe "each_key" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to each_key" do
      expect(@attributes).to respond_to(:each_key)
    end

    it "should yield each top level key, post merge rules" do
      collect = []
      @attributes.each_key do |k|
        collect << k
      end

      expect(collect).to include("one")
      expect(collect).to include("snack")
      expect(collect).to include("hut")
      expect(collect).to include("snakes")
    end
  end

  describe "each_pair" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to each_pair" do
      expect(@attributes).to respond_to(:each_pair)
    end

    it "should yield each top level key and value pair, post merge rules" do
      collect = {}
      @attributes.each_pair do |k, v|
        collect[k] = v
      end

      expect(collect["one"]).to eq("six")
      expect(collect["hut"]).to eq("three")
      expect(collect["snakes"]).to eq("on a plane")
      expect(collect["snack"]).to eq("cookies")
    end
  end

  describe "each_value" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to each_value" do
      expect(@attributes).to respond_to(:each_value)
    end

    it "should yield each value, post merge rules" do
      collect = []
      @attributes.each_value do |v|
        collect << v
      end

      expect(collect).to include("cookies")
      expect(collect).to include("three")
      expect(collect).to include("on a plane")
    end

    it "should yield four elements" do
      collect = []
      @attributes.each_value do |v|
        collect << v
      end

      expect(collect.length).to eq(4)
    end
  end

  describe "empty?" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
      @empty = Chef::Node::Attribute.new({}, {}, {}, {})
    end

    it "should respond to empty?" do
      expect(@attributes).to respond_to(:empty?)
    end

    it "should return true when there are no keys" do
      expect(@empty.empty?).to eq(true)
    end

    it "should return false when there are keys" do
      expect(@attributes.empty?).to eq(false)
    end

  end

  describe "fetch" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to fetch" do
      expect(@attributes).to respond_to(:fetch)
    end

    describe "when the key exists" do
      it "should return the value of the key, post merge (same result as each)" do
        {
          "one" => "six",
          "hut" => "three",
          "snakes" => "on a plane",
          "snack" => "cookies",
        }.each do |k, v|
          expect(@attributes.fetch(k)).to eq(v)
        end
      end
    end

    describe "when the key does not exist" do
      describe "and no args are passed" do
        it "should raise an indexerror" do
          expect { @attributes.fetch("lololol") }.to raise_error(IndexError)
        end
      end

      describe "and a default arg is passed" do
        it "should return the value of the default arg" do
          expect(@attributes.fetch("lol", "blah")).to eq("blah")
        end
      end

      describe "and a block is passed" do
        it "should run the block and return its value" do
          expect(@attributes.fetch("lol") { |x| "#{x}, blah" }).to eq("lol, blah")
        end
      end
    end
  end

  describe "has_value?" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to has_value?" do
      expect(@attributes).to respond_to(:has_value?)
    end

    it "should return true if any key has the value supplied" do
      expect(@attributes.value?("cookies")).to eq(true)
    end

    it "should return false no key has the value supplied" do
      expect(@attributes.value?("lololol")).to eq(false)
    end

    it "should alias value?" do
      expect(@attributes).to respond_to(:value?)
    end
  end

  describe "index" do
    # Hash#index is deprecated and triggers warnings.
    def silence
      old_verbose = $VERBOSE
      $VERBOSE = nil
      yield
    ensure
      $VERBOSE = old_verbose
    end

    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to index" do
      expect(@attributes).to respond_to(:index)
    end

    describe "when the value is indexed" do
      it "should return the index" do
        silence do
          expect(@attributes.index("six")).to eq("one")
        end
      end
    end

    describe "when the value is not indexed" do
      it "should return nil" do
        silence do
          expect(@attributes.index("lolol")).to eq(nil)
        end
      end
    end

  end

  describe "values" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to values" do
      expect(@attributes).to respond_to(:values)
    end

    it "should return an array of values" do
      expect(@attributes.values.length).to eq(4)
    end

    it "should match the values output from each" do
      expect(@attributes.values).to include("six")
      expect(@attributes.values).to include("cookies")
      expect(@attributes.values).to include("three")
      expect(@attributes.values).to include("on a plane")
    end

  end

  describe "select" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )
    end

    it "should respond to select" do
      expect(@attributes).to respond_to(:select)
    end

    it "should not raise a LocalJumpError if no block is given" do
      expect { @attributes.select }.not_to raise_error
    end

    it "should return an empty hash/array (ruby-version-dependent) for a block containing nil" do
      expect(@attributes.select { nil }).to eq({}.select { nil })
    end

    # sorted for spec clarity
    it "should return a new array of k,v pairs for which the block returns true" do
      expect(@attributes.select { true }.sort).to eq(
        [
          %w{hut three},
          %w{one six},
          %w{snack cookies},
          ["snakes", "on a plane"],
        ]
      )
    end
  end

  describe "size" do
    before do
      @attributes = Chef::Node::Attribute.new(
        {
          "one" => "two",
          "hut" => "three",
        },
        {
          "one" => "four",
          "snakes" => "on a plane",
        },
        {
          "one" => "six",
          "snack" => "cookies",
        },
        {}
      )

      @empty = Chef::Node::Attribute.new({}, {}, {}, {})
    end

    it "should respond to size" do
      expect(@attributes).to respond_to(:size)
    end

    it "should alias length to size" do
      expect(@attributes).to respond_to(:length)
    end

    it "should return 0 for an empty attribute" do
      expect(@empty.size).to eq(0)
    end

    it "should return the number of pairs" do
      expect(@attributes.size).to eq(4)
    end
  end

  describe "kind_of?" do
    it "should falsely inform you that it is a Hash" do
      expect(@attributes).to be_a_kind_of(Hash)
    end

    it "should falsely inform you that it is a Mash" do
      expect(@attributes).to be_a_kind_of(Mash)
    end

    it "should inform you that it is a Chef::Node::Attribute" do
      expect(@attributes).to be_a_kind_of(Chef::Node::Attribute)
    end

    it "should inform you that it is anything else" do
      expect(@attributes).not_to be_a_kind_of(Chef::Node)
    end
  end

  describe "to_s" do
    it "should output simple attributes" do
      attributes = Chef::Node::Attribute.new(nil, nil, nil, nil)
      expect(attributes.to_s).to eq("{}")
    end

    it "should output merged attributes" do
      default_hash = {
          "a" => 1,
          "b" => 2,
      }
      override_hash = {
          "b" => 3,
          "c" => 4,
      }
      attributes = Chef::Node::Attribute.new(nil, default_hash, override_hash, nil)
      expect(attributes.to_s).to eq('{"a"=>1, "b"=>3, "c"=>4}')
    end
  end

  describe "inspect" do
    it "should be readable" do
      # NOTE: previous implementation hid the values, showing @automatic={...}
      # That is nice and compact, but hides a lot of info, which seems counter
      # to the point of calling #inspect...
      expect(@attributes.inspect).to match(/@automatic=\{.*\}/)
      expect(@attributes.inspect).to match(/@normal=\{.*\}/)
    end
  end

  describe "when not mutated" do

    it "does not reset the cache when dup'd [CHEF-3680]" do
      @attributes.default[:foo][:bar] = "set on original"
      subtree = @attributes[:foo]
      @attributes.default[:foo].dup[:bar] = "set on dup"
      expect(subtree[:bar]).to eq("set on original")
    end

  end

  describe "when setting a component attribute to a new value" do
    it "converts the input in to a VividMash tree (default)" do
      @attributes.default = {}
      @attributes.default["foo"] = "bar"
      expect(@attributes.merged_attributes[:foo]).to eq("bar")
    end

    it "converts the input in to a VividMash tree (normal)" do
      @attributes.normal = {}
      @attributes.normal["foo"] = "bar"
      expect(@attributes.merged_attributes[:foo]).to eq("bar")
    end

    it "converts the input in to a VividMash tree (override)" do
      @attributes.override = {}
      @attributes.override["foo"] = "bar"
      expect(@attributes.merged_attributes[:foo]).to eq("bar")
    end

    it "converts the input in to a VividMash tree (automatic)" do
      @attributes.automatic = {}
      @attributes.automatic["foo"] = "bar"
      expect(@attributes.merged_attributes[:foo]).to eq("bar")
    end
  end

  describe "when deep-merging between precedence levels" do
    it "correctly deep merges hashes and preserves the original contents" do
      @attributes.default = { "arglebargle" => { "foo" => "bar" } }
      @attributes.override = { "arglebargle" => { "fizz" => "buzz" } }
      expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" })
      expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" })
      expect(@attributes.override[:arglebargle]).to eq({ "fizz" => "buzz" })
    end

    it "does not deep merge arrays, and preserves the original contents" do
      @attributes.default = { "arglebargle" => [ 1, 2, 3 ] }
      @attributes.override = { "arglebargle" => [ 4, 5, 6 ] }
      expect(@attributes.merged_attributes[:arglebargle]).to eq([ 4, 5, 6 ])
      expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ])
      expect(@attributes.override[:arglebargle]).to eq([ 4, 5, 6 ])
    end

    it "correctly deep merges hashes and preserves the original contents when merging default and role_default" do
      @attributes.default = { "arglebargle" => { "foo" => "bar" } }
      @attributes.role_default = { "arglebargle" => { "fizz" => "buzz" } }
      expect(@attributes.merged_attributes[:arglebargle]).to eq({ "foo" => "bar", "fizz" => "buzz" })
      expect(@attributes.default[:arglebargle]).to eq({ "foo" => "bar" })
      expect(@attributes.role_default[:arglebargle]).to eq({ "fizz" => "buzz" })
    end

    it "correctly deep merges arrays, and preserves the original contents when merging default and role_default" do
      @attributes.default = { "arglebargle" => [ 1, 2, 3 ] }
      @attributes.role_default = { "arglebargle" => [ 4, 5, 6 ] }
      expect(@attributes.merged_attributes[:arglebargle]).to eq([ 1, 2, 3, 4, 5, 6 ])
      expect(@attributes.default[:arglebargle]).to eq([ 1, 2, 3 ])
      expect(@attributes.role_default[:arglebargle]).to eq([ 4, 5, 6 ])
    end
  end

  describe "when attemping to write without specifying precedence" do
    it "raises an error when using []=" do
      expect { @attributes[:new_key] = "new value" }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
    end
  end

  describe "deeply converting values" do
    it "converts values through an array" do
      @attributes.default[:foo] = [ { bar: true } ]
      expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableArray)
      expect(@attributes["foo"][0].class).to eql(Chef::Node::ImmutableMash)
      expect(@attributes["foo"][0]["bar"]).to be true
    end

    it "converts values through nested arrays" do
      @attributes.default[:foo] = [ [ { bar: true } ] ]
      expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableArray)
      expect(@attributes["foo"][0].class).to eql(Chef::Node::ImmutableArray)
      expect(@attributes["foo"][0][0].class).to eql(Chef::Node::ImmutableMash)
      expect(@attributes["foo"][0][0]["bar"]).to be true
    end

    it "converts values through nested hashes" do
      @attributes.default[:foo] = { baz: { bar: true } }
      expect(@attributes["foo"].class).to eql(Chef::Node::ImmutableMash)
      expect(@attributes["foo"]["baz"].class).to eql(Chef::Node::ImmutableMash)
      expect(@attributes["foo"]["baz"]["bar"]).to be true
    end
  end

  describe "node state" do
    it "sets __root__ correctly" do
      @attributes.default["foo"]["bar"]["baz"] = "quux"
      expect(@attributes["foo"].__root__).to eql(@attributes)
      expect(@attributes["foo"]["bar"].__root__).to eql(@attributes)
      expect(@attributes.default["foo"].__root__).to eql(@attributes)
      expect(@attributes.default["foo"]["bar"].__root__).to eql(@attributes)
    end

    it "sets __node__ correctly" do
      @attributes.default["foo"]["bar"]["baz"] = "quux"
      expect(@attributes["foo"].__node__).to eql(node)
      expect(@attributes["foo"]["bar"].__node__).to eql(node)
      expect(@attributes.default["foo"].__node__).to eql(node)
      expect(@attributes.default["foo"]["bar"].__node__).to eql(node)
    end

    it "sets __path__ correctly" do
      @attributes.default["foo"]["bar"]["baz"] = "quux"
      expect(@attributes["foo"].__path__).to eql(["foo"])
      expect(@attributes["foo"]["bar"].__path__).to eql(%w{foo bar})
      expect(@attributes.default["foo"].__path__).to eql(["foo"])
      expect(@attributes.default["foo"]["bar"].__path__).to eql(%w{foo bar})
    end

    it "sets __precedence__ correctly" do
      @attributes.default["foo"]["bar"]["baz"] = "quux"
      expect(@attributes["foo"].__precedence__).to eql(:merged)
      expect(@attributes["foo"]["bar"].__precedence__).to eql(:merged)
      expect(@attributes.default["foo"].__precedence__).to eql(:default)
      expect(@attributes.default["foo"]["bar"].__precedence__).to eql(:default)
    end

    it "notifies on attribute changes" do
      expect(events).to receive(:attribute_changed).with(:default, ["foo"], {})
      expect(events).to receive(:attribute_changed).with(:default, %w{foo bar}, {})
      expect(events).to receive(:attribute_changed).with(:default, %w{foo bar baz}, "quux")
      @attributes.default["foo"]["bar"]["baz"] = "quux"
    end
  end

  describe "frozen immutable strings" do
    it "strings in hashes should be frozen" do
      @attributes.default["foo"]["bar"]["baz"] = "fizz"
      expect { @attributes["foo"]["bar"]["baz"] << "buzz" }.to raise_error(RuntimeError, "can't modify frozen String")
    end

    it "strings in arrays should be frozen" do
      @attributes.default["foo"]["bar"] = [ "fizz" ]
      expect { @attributes["foo"]["bar"][0] << "buzz" }.to raise_error(RuntimeError, "can't modify frozen String")
    end
  end

  describe "deep merging with nils" do
    it "nils when deep merging between default levels knocks out values" do
      @attributes.default["foo"] = "bar"
      expect(@attributes["foo"]).to eql("bar")
      @attributes.force_default["foo"] = nil
      expect(@attributes["foo"]).to be nil
    end

    it "nils when deep merging between override levels knocks out values" do
      @attributes.override["foo"] = "bar"
      expect(@attributes["foo"]).to eql("bar")
      @attributes.force_override["foo"] = nil
      expect(@attributes["foo"]).to be nil
    end

    it "nils when deep merging between default+override levels knocks out values" do
      @attributes.default["foo"] = "bar"
      expect(@attributes["foo"]).to eql("bar")
      @attributes.override["foo"] = nil
      expect(@attributes["foo"]).to be nil
    end

    it "nils when deep merging between normal+automatic levels knocks out values" do
      @attributes.normal["foo"] = "bar"
      expect(@attributes["foo"]).to eql("bar")
      @attributes.automatic["foo"] = nil
      expect(@attributes["foo"]).to be nil
    end
  end

  describe "to_json" do
    it "should convert to a valid json string" do
      json = @attributes["hot"].to_json
      expect { JSON.parse(json) }.not_to raise_error
    end

    it "should convert to a json based on current state" do
      expect(@attributes["hot"].to_json).to eq("{\"day\":\"sunday\"}")
    end
  end

  describe "to_yaml" do
    it "should convert to a valid yaml format" do
      json = @attributes["hot"].to_yaml
      expect { YAML.parse(json) }.not_to raise_error
    end

    it "should convert to a yaml based on current state" do
      expect(@attributes["hot"].to_yaml).to eq("---\nday: sunday\n")
    end
  end
end
