#
# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright (c) 2009-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
# 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 File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))

describe Mixlib::Config do
  before(:each) do
    ConfigIt.configure do |c|
      c[:alpha] = "omega"
      c[:foo] = nil
    end
  end

  it "loads a config file" do
    allow(File).to receive(:exists?).and_return(true)
    allow(File).to receive(:readable?).and_return(true)
    allow(IO).to receive(:read).with("config.rb").and_return("alpha = 'omega'\nfoo = 'bar'")
    expect(lambda do
      ConfigIt.from_file("config.rb")
    end).to_not raise_error
  end

  it "doesn't raise an ArgumentError with an explanation if you try and set a non-existent variable" do
    expect(lambda do
      ConfigIt[:foobar] = "blah"
    end).to_not raise_error
  end

  it "raises an Errno::ENOENT if it can't find the file" do
    expect(lambda do
      ConfigIt.from_file("/tmp/timmytimmytimmy")
    end).to raise_error(Errno::ENOENT)
  end

  it "allows the error to bubble up when it's anything other than IOError" do
    allow(IO).to receive(:read).with("config.rb").and_return("@#asdf")
    expect(lambda do
      ConfigIt.from_file("config.rb")
    end).to raise_error(SyntaxError)
  end

  it "allows you to reference a value by index" do
    expect(ConfigIt[:alpha]).to eql("omega")
  end

  it "allows you to reference a value by string index" do
    expect(ConfigIt["alpha"]).to eql("omega")
  end

  it "allows you to set a value by index" do
    ConfigIt[:alpha] = "one"
    expect(ConfigIt[:alpha]).to eql("one")
  end

  it "allows you to set a value by string index" do
    ConfigIt["alpha"] = "one"
    expect(ConfigIt[:alpha]).to eql("one")
  end

  it "allows setting a value with attribute form" do
    ConfigIt.arbitrary_value = 50
    expect(ConfigIt.arbitrary_value).to eql(50)
    expect(ConfigIt[:arbitrary_value]).to eql(50)
  end

  it "allows setting a value with method form" do
    ConfigIt.arbitrary_value 50
    expect(ConfigIt.arbitrary_value).to eql(50)
    expect(ConfigIt[:arbitrary_value]).to eql(50)
  end

  describe "when strict mode is on" do
    class StrictClass
      extend ::Mixlib::Config
      config_strict_mode true
      default :x, 1
    end

    it "allows you to get and set configured values" do
      StrictClass.x = StrictClass.x * 2
      StrictClass[:x] = StrictClass[:x] * 2
    end

    it "raises an error when you get an arbitrary config option with .y" do
      expect(lambda { StrictClass.y }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Reading unsupported config value y.")
    end

    it "raises an error when you get an arbitrary config option with [:y]" do
      expect(lambda { StrictClass[:y] }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Reading unsupported config value y.")
    end

    it "raises an error when you set an arbitrary config option with .y = 10" do
      expect(lambda { StrictClass.y = 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end

    it "raises an error when you set an arbitrary config option with .y 10" do
      expect(lambda { StrictClass.y 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end

    it "raises an error when you set an arbitrary config option with [:y] = 10" do
      expect(lambda { StrictClass[:y] = 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end

    it "does not break config_context_list" do
      expect(lambda do
        StrictClass.class_eval do
          config_context_list(:lists, :list) do
            default :y, 20
          end
        end
      end).not_to raise_error
    end

    it "does not break config_context_hash" do
      expect(lambda do
        StrictClass.class_eval do
          config_context_hash(:hashes, :hash) do
            default :z, 20
          end
        end
      end).not_to raise_error
    end
  end

  describe "when a block has been used to set config values" do
    before do
      ConfigIt.configure { |c| c[:cookbook_path] = "monkey_rabbit"; c[:otherthing] = "boo" }
    end

    { cookbook_path: "monkey_rabbit", otherthing: "boo" }.each do |k, v|
      it "allows you to retrieve the config value for #{k} via []" do
        expect(ConfigIt[k]).to eql(v)
      end
      it "allows you to retrieve the config value for #{k} via method_missing" do
        expect(ConfigIt.send(k)).to eql(v)
      end
    end
  end

  it "doesn't raise an ArgumentError if you access a config option that does not exist" do
    expect(lambda { ConfigIt[:snob_hobbery] }).to_not raise_error
  end

  it "returns true or false with has_key?" do
    expect(ConfigIt.key?(:monkey)).to be false
    ConfigIt[:monkey] = "gotcha"
    expect(ConfigIt.key?(:monkey)).to be true
  end

  it "returns true or false with key?" do
    expect(ConfigIt.key?(:monkey2)).to be false
    ConfigIt[:monkey2] = "gotcha"
    expect(ConfigIt.key?(:monkey2)).to be true
  end

  describe "when a class method override writer exists" do
    before do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        config_attr_writer :test_method do |blah|
          blah.is_a?(Integer) ? blah * 1000 : blah
        end
      end
    end

    it "multiplies an integer by 1000" do
      @klass[:test_method] = 53
      expect(@klass[:test_method]).to eql(53000)
    end

    it "multiplies an integer by 1000 with the method_missing form" do
      @klass.test_method = 63
      expect(@klass.test_method).to eql(63000)
    end

    it "multiplies an integer by 1000 with the instance_eval DSL form" do
      @klass.instance_eval("test_method 73")
      expect(@klass.test_method).to eql(73000)
    end

    it "multiplies an integer by 1000 via from-file, too" do
      allow(IO).to receive(:read).with("config.rb").and_return("test_method 99")
      @klass.from_file("config.rb")
      expect(@klass.test_method).to eql(99000)
    end

    it "receives internal_set with the method name and config value" do
      expect(@klass).to receive(:internal_set).with(:test_method, 53).and_return(true)
      @klass[:test_method] = 53
    end

  end

  describe "When a configurable exists" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        configurable :daemonizeme
        default :a, 1
        config_attr_writer(:b) { |v| v }
        config_context(:c)
      end
    end

    it "Getter methods are created for the configurable" do
      expect(@klass.respond_to?(:daemonizeme)).to be true
      expect(@klass.respond_to?(:a)).to be true
      expect(@klass.respond_to?(:b)).to be true
      expect(@klass.respond_to?(:c)).to be true
      expect(@klass.respond_to?(:z)).to be false
    end

    it "Setter methods are created for the configurable" do
      expect(@klass.respond_to?("daemonizeme=".to_sym)).to be true
      expect(@klass.respond_to?("a=".to_sym)).to be true
      expect(@klass.respond_to?("b=".to_sym)).to be true
      expect(@klass.respond_to?("c=".to_sym)).to be true
      expect(@klass.respond_to?("z=".to_sym)).to be false
    end

    it "returns true for is_default? for a default value" do
      expect(@klass[:a]).to eql(1)
      expect(@klass.is_default?(:a)).to be true
    end

    it "returns true for is_default? for an overwritten default value" do
      @klass[:a] = 1
      expect(@klass[:a]).to eql(1)
      expect(@klass.is_default?(:a)).to be true
    end

    it "returns false for is_default? for a value that is not the default" do
      @klass[:a] = 2
      expect(@klass[:a]).to eql(2)
      expect(@klass.is_default?(:a)).to be false
    end

    describe "and extra methods have been dumped into Object" do
      class NopeError < StandardError
      end
      before :each do
        Object.send :define_method, :daemonizeme do
          raise NopeError, "NOPE"
        end
        Object.send :define_method, "daemonizeme=".to_sym do
          raise NopeError, "NOPE"
        end
      end

      after do
        Object.send :remove_method, :daemonizeme
        Object.send :remove_method, :'daemonizeme='
      end

      it "Normal classes call the extra method" do
        normal_class = Class.new
        normal_class.extend(::Mixlib::Config)
        expect(lambda { normal_class.daemonizeme }).to raise_error(NopeError)
      end

      it "Configurables with the same name as the extra method can be set" do
        @klass.daemonizeme = 10
        expect(@klass[:daemonizeme]).to eql(10)
      end

      it "Configurables with the same name as the extra method can be retrieved" do
        @klass[:daemonizeme] = 10
        expect(@klass.daemonizeme).to eql(10)
      end
    end
  end

  describe "When config has a default value" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval { default :attr, 4 }
    end

    it "defaults to that value" do
      expect(@klass.attr).to eql(4)
    end

    it "defaults to that value when retrieved as a hash" do
      expect(@klass[:attr]).to eql(4)
    end

    it "is settable to another value" do
      @klass.attr 5
      expect(@klass.attr).to eql(5)
    end

    it "still defaults to that value after delete" do
      @klass.attr 5
      @klass.delete(:attr)
      expect(@klass.attr).to eql(4)
    end

    it "still defaults to that value after reset" do
      @klass.attr 5
      @klass.reset
      expect(@klass.attr).to eql(4)
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: 4 })
    end

    it "saves the new value if it gets set" do
      @klass.attr 5
      expect((saved = @klass.save)).to eql({ attr: 5 })
      @klass.reset
      expect(@klass.attr).to eql(4)
      @klass.restore(saved)
      expect(@klass.attr).to eql(5)
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr 4
      expect((saved = @klass.save)).to eql({ attr: 4 })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: 4 })
    end
  end

  describe "When config has a default value block" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        default :x, 4
        default(:attr) { x * 2 }
      end
    end

    it "defaults to that value" do
      expect(@klass.attr).to eql(8)
    end

    it "is recalculated each time it is retrieved" do
      expect(@klass.attr).to eql(8)
      @klass.x = 2
      expect(@klass.attr).to eql(4)
    end

    it "defaults to that value when retrieved as a hash" do
      expect(@klass[:attr]).to eql(8)
    end

    it "is settable to another value" do
      @klass.attr 5
      expect(@klass.attr).to eql(5)
    end

    it "still defaults to that value after delete" do
      @klass.attr 5
      @klass.delete(:attr)
      expect(@klass.attr).to eql(8)
    end

    it "still defaults to that value after reset" do
      @klass.attr 5
      @klass.reset
      expect(@klass.attr).to eql(8)
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: 8, x: 4 })
    end

    it "saves the new value if it gets set" do
      @klass.attr 5
      expect((saved = @klass.save)).to eql({ attr: 5 })
      @klass.reset
      expect(@klass.attr).to eql(8)
      @klass.restore(saved)
      expect(@klass.attr).to eql(5)
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr 8
      expect((saved = @klass.save)).to eql({ attr: 8 })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: 8 })
    end
  end

  describe "When config has an array default value" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval { default :attr, [] }
    end

    it "reset clears it to its default" do
      @klass.attr << "x"
      expect(@klass.attr).to eql([ "x" ])
      @klass.reset
      expect(@klass.attr).to eql([])
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: [] })
    end

    it "saves the new value if it gets set" do
      @klass.attr << "x"
      expect((saved = @klass.save)).to eql({ attr: [ "x" ] })
      @klass.reset
      expect(@klass.attr).to eql([])
      @klass.restore(saved)
      expect(@klass.attr).to eql([ "x" ])
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr = []
      expect((saved = @klass.save)).to eql({ attr: [] })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: [] })
    end
  end

  describe "When config has a hash default value" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval { default :attr, {} }
    end

    it "reset clears it to its default" do
      @klass.attr[:x] = 10
      expect(@klass.attr[:x]).to eql(10)
      @klass.reset
      expect(@klass.attr[:x]).to be_nil
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: {} })
    end

    it "saves the new value if it gets set" do
      @klass.attr[:hi] = "lo"
      expect((saved = @klass.save)).to eql({ attr: { hi: "lo" } })
      @klass.reset
      expect(@klass.attr).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: { hi: "lo" } })
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr = {}
      expect((saved = @klass.save)).to eql({ attr: {} })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: {} })
    end
  end

  describe "When config has a string default value" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval { default :attr, "hello" }
    end

    it "reset clears it to its default" do
      @klass.attr << " world"
      expect(@klass.attr).to eql("hello world")
      @klass.reset
      expect(@klass.attr).to eql("hello")
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: "hello" })
    end

    it "saves the new value if it gets set" do
      @klass.attr << " world"
      expect((saved = @klass.save)).to eql({ attr: "hello world" })
      @klass.reset
      expect(@klass.attr).to eql("hello")
      @klass.restore(saved)
      expect(@klass.attr).to eql("hello world")
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr "hello world"
      expect((saved = @klass.save)).to eql({ attr: "hello world" })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: "hello world" })
    end
  end

  describe "When config has a a default value block" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        default(:attr) { 4 }
      end
    end

    it "defaults to that value" do
      expect(@klass.attr).to eql(4)
    end

    it "defaults to that value when retrieved as a hash" do
      expect(@klass[:attr]).to eql(4)
    end

    it "is settable to another value" do
      @klass.attr 5
      expect(@klass.attr).to eql(5)
      expect(@klass[:attr]).to eql(5)
    end

    it "still defaults to that value after delete" do
      @klass.attr 5
      @klass.delete(:attr)
      expect(@klass.attr).to eql(4)
    end

    it "still defaults to that value after reset" do
      @klass.attr 5
      @klass.reset
      expect(@klass.attr).to eql(4)
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: 4 })
    end

    it "saves the new value if it gets set" do
      @klass.attr 5
      expect((saved = @klass.save)).to eql({ attr: 5 })
      @klass.reset
      expect(@klass.attr).to eql(4)
      @klass.restore(saved)
      expect(@klass.attr).to eql(5)
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr 4
      expect((saved = @klass.save)).to eql({ attr: 4 })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: 4 })
    end
  end

  describe "When a configurable exists with writer and default value" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        configurable(:attr) do |c|
          c.defaults_to(4)
          c.writes_value { |value| value * 2 }
        end
      end
    end

    it "defaults to that value" do
      expect(@klass.attr).to eql(4)
    end

    it "defaults to that value when retrieved as a hash" do
      expect(@klass[:attr]).to eql(4)
    end

    it "is settable to another value" do
      @klass.attr 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "is settable to another value with attr=" do
      @klass.attr = 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "is settable to another value with [:attr]=" do
      @klass[:attr] = 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "still defaults to that value after delete" do
      @klass.attr 5
      @klass.delete(:attr)
      expect(@klass.attr).to eql(4)
    end

    it "still defaults to that value after reset" do
      @klass.attr 5
      @klass.reset
      expect(@klass.attr).to eql(4)
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: 4 })
    end

    it "saves the new value if it gets set" do
      @klass.attr 5
      expect((saved = @klass.save)).to eql({ attr: 10 })
      @klass.reset
      expect(@klass.attr).to eql(4)
      @klass.restore(saved)
      expect(@klass.attr).to eql(10)
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr 4
      expect((saved = @klass.save)).to eql({ attr: 8 })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: 8 })
    end
  end

  describe "When a configurable exists with writer and default value set in chained form" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        configurable(:attr).defaults_to(4).writes_value { |value| value * 2 }
      end
    end

    it "defaults to that value" do
      expect(@klass.attr).to eql(4)
    end

    it "defaults to that value when retrieved as a hash" do
      expect(@klass[:attr]).to eql(4)
    end

    it "is settable to another value" do
      @klass.attr 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "is settable to another value with attr=" do
      @klass.attr = 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "is settable to another value with [:attr]=" do
      @klass[:attr] = 5
      expect(@klass.attr).to eql(10)
      expect(@klass[:attr]).to eql(10)
    end

    it "still defaults to that value after delete" do
      @klass.attr 5
      @klass.delete(:attr)
      expect(@klass.attr).to eql(4)
    end

    it "still defaults to that value after reset" do
      @klass.attr 5
      @klass.reset
      expect(@klass.attr).to eql(4)
    end

    it "save should not save anything for it" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ attr: 4 })
    end

    it "saves the new value if it gets set" do
      @klass.attr 5
      expect((saved = @klass.save)).to eql({ attr: 10 })
      @klass.reset
      expect(@klass.attr).to eql(4)
      @klass.restore(saved)
      expect(@klass.attr).to eql(10)
    end

    it "saves the new value even if it is set to its default value" do
      @klass.attr 2
      expect((saved = @klass.save)).to eql({ attr: 4 })
      @klass.reset
      expect(@klass.save).to eql({})
      @klass.restore(saved)
      expect(@klass.save).to eql({ attr: 4 })
    end
  end

  describe "When a configurable exists with a context" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        configurable :x
        config_context(:blah) do
          default :x, 5
        end
      end
    end

    it "configurable defaults in that context work" do
      expect(@klass.blah.x).to eql(5)
    end

    it "after setting values in the context, the values remain set" do
      @klass.blah.x = 10
      expect(@klass.blah.x).to eql(10)
    end

    it "setting values with the same name in the parent context do not affect the child context" do
      @klass.x = 10
      expect(@klass.x).to eql(10)
      expect(@klass.blah.x).to eql(5)
    end

    it "setting the entire context to a hash with default value overridden sets the value" do
      @klass.blah = { x: 10 }
      expect(@klass.blah.x).to eql(10)
    end

    it "setting the entire context to a hash sets non-default values" do
      @klass.blah = { y: 10 }
      expect(@klass.blah.x).to eql(5)
      expect(@klass.blah.y).to eql(10)
    end

    it "setting the entire context to a hash deletes any non-default values and resets default values" do
      @klass.blah.x = 10
      @klass.blah.y = 10
      @klass.blah = { z: 10 }
      expect(@klass.blah.x).to eql(5)
      expect(@klass.blah.y).to be_nil
      expect(@klass.blah.z).to eql(10)
    end

    it "setting the context values in a block overrides the default values" do
      @klass.blah do
        x 10
        y 20
      end
      expect(@klass.blah.x).to eq 10
      expect(@klass.blah.y).to eq 20
    end

    it "setting the context values in a yielded block overrides the default values" do
      @klass.blah do |b|
        b.x = 10
        b.y = 20
      end
      expect(@klass.blah.x).to eq 10
      expect(@klass.blah.y).to eq 20
    end

    it "after reset of the parent class, children are reset" do
      @klass.blah.x = 10
      expect(@klass.blah.x).to eql(10)
      @klass.reset
      expect(@klass.blah.x).to eql(5)
    end

    it "save should not save anything for it by default" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ blah: { x: 5 } })
    end

    it "saves any new values that are set in the context" do
      @klass.blah.x = 10
      expect((saved = @klass.save)).to eql({ blah: { x: 10 } })
      @klass.reset
      expect(@klass.blah.x).to eql(5)
      @klass.restore(saved)
      expect(@klass.blah.x).to eql(10)
      expect(@klass.save).to eql({ blah: { x: 10 } })
    end

    # this tests existing (somewhat bizarre) behavior of mixlib-config where testing to
    # see if a key exists is equivalent to testing if the key has been set -- we can still
    # retrieve the default value if it was set.  the code in chef/chef which merges
    # knife config values into cli values will be sensitive to this behavior.
    it "defaults values do not show up when querying with #has_key?" do
      expect(@klass.blah.key?(:x)).to be false
      expect(@klass.blah.x).to be 5
    end

    it "if we assign the values, they show up when querying with #has_key?" do
      @klass.blah.x = 5
      expect(@klass.blah.key?(:x)).to be true
    end
  end

  describe "When a configurable exists with a nested context" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        config_context(:blah) do
          config_context(:yarr) do
            default :x, 5
            default :y, 6
          end
        end
        configurable :x
      end
    end

    it "configurable defaults in that context work" do
      expect(@klass.blah.yarr.x).to eql(5)
      expect(@klass.blah.yarr.y).to eql(6)
    end

    it "after setting values in the context, the values remain set" do
      @klass.blah.yarr.x = 10
      @klass.blah.yarr.y = 11
      expect(@klass.blah.yarr.x).to eql(10)
      expect(@klass.blah.yarr.y).to eql(11)
    end

    it "setting values with the same name in the parent context do not affect the child context" do
      @klass.x = 10
      expect(@klass.x).to eql(10)
      expect(@klass.blah.yarr.x).to eql(5)
    end

    it "after reset of the parent class, children are reset" do
      @klass.blah.yarr.x = 10
      @klass.blah.yarr.y = 11
      expect(@klass.blah.yarr.x).to eql(10)
      expect(@klass.blah.yarr.y).to eql(11)
      @klass.reset
      expect(@klass.blah.yarr.x).to eql(5)
      expect(@klass.blah.yarr.y).to eql(6)
    end

    it "save should not save anything for it by default" do
      expect(@klass.save).to eql({})
    end

    it "save with include_defaults should save all defaults" do
      expect(@klass.save(true)).to eql({ blah: { yarr: { x: 5, y: 6 } } })
    end

    it "saves any new values that are set in the context" do
      @klass.blah.yarr.x = 10
      @klass.blah.yarr.y = 11
      expect((saved = @klass.save)).to eql({ blah: { yarr: { x: 10, y: 11 } } })
      @klass.reset
      expect(@klass.blah.yarr.x).to eql(5)
      expect(@klass.blah.yarr.y).to eql(6)
      @klass.restore(saved)
      expect(@klass.blah.yarr.x).to eql(10)
      expect(@klass.blah.yarr.y).to eql(11)
      expect(@klass.save).to eql({ blah: { yarr: { x: 10, y: 11 } } })
    end

    it "restores defaults not included in saved data" do
      @klass.restore( blah: { yarr: { x: 10 } } )
      expect(@klass.blah.yarr.x).to eql(10)
      expect(@klass.blah.yarr.y).to eql(6)
    end

    it "removes added properties not included in saved state" do
      @klass.blah.yarr.z = 12
      @klass.restore( blah: { yarr: { x: 10 } } )
      expect(@klass.blah.yarr.x).to eql(10)
      expect(@klass.blah.yarr.z).to eql(nil)
    end

    it "can set a config context from another context" do
      @klass.blah.blyme = { x: 7 }
      blyme = @klass.blah.blyme
      @klass.blah.yarr.x = 12
      @klass.blah.yarr = blyme
      expect(@klass.blah.yarr.x).to eql(7)
    end
  end

  describe "When a config_context with no defaulted values exists" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        config_context(:blah) do
          configurable(:x)
        end
      end
    end

    it "has_key? finds the subcontext" do
      expect(@klass.key?(:blah)).to be true
    end

    it "key? finds the subcontext" do
      expect(@klass.key?(:blah)).to be true
    end

    it "save does not save the hash for the config_context" do
      expect(@klass.save).to eql({})
    end

    it "save with defaults saves the hash for the config_context" do
      expect(@klass.save(true)).to eql({ blah: {} })
    end
  end

  describe "When a config_context with no configurables exists" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        config_context(:blah)
      end
    end

    it "save does not save the hash for the config_context" do
      expect(@klass.save).to eql({})
    end

    it "save with defaults saves the hash for the config_context" do
      expect(@klass.save(true)).to eql({ blah: {} })
    end
  end

  describe "When a nested context has strict mode on" do
    class StrictClass2
      extend ::Mixlib::Config
      config_context :c do
        config_strict_mode true
        default :x, 1
      end
    end

    it "The parent class allows you to set arbitrary config options" do
      StrictClass2.y = 10
    end

    it "The nested class does not allow you to set arbitrary config options" do
      expect(lambda { StrictClass2.c.y = 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end
  end

  describe "When strict mode is on but a nested context has strict mode unspecified" do
    class StrictClass3
      extend ::Mixlib::Config
      config_strict_mode true
      default :x, 1
      config_context :c
    end

    it "The parent class does not allow you to set arbitrary config options" do
      expect(lambda { StrictClass3.y = 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end

    it "The nested class does not allow you to set arbitrary config options" do
      expect(lambda { StrictClass3.y = 10 }).to raise_error(Mixlib::Config::UnknownConfigOptionError, "Cannot set unsupported config value y.")
    end
  end

  describe "When a config_context is opened twice" do
    before :each do
      @klass = Class.new
      @klass.extend(::Mixlib::Config)
      @klass.class_eval do
        config_context(:blah) do
          default :x, 10
        end
        config_context(:blah) do
          default :y, 20
        end
      end
    end

    it "Both config_context blocks are honored" do
      @klass.blah.x == 10
      @klass.blah.y == 20
    end
  end

  it "When a config_context is opened in place of a regular configurable, an error is raised" do
    klass = Class.new
    klass.extend(::Mixlib::Config)
    expect(lambda do
      klass.class_eval do
        default :blah, 10
        config_context(:blah) do
          default :y, 20
        end
      end
    end).to raise_error(Mixlib::Config::ReopenedConfigurableWithConfigContextError)
  end

  it "When a config_context is opened in place of a regular configurable, an error is raised" do
    klass = Class.new
    klass.extend(::Mixlib::Config)
    expect(lambda do
      klass.class_eval do
        config_context(:blah) do
          default :y, 20
        end
        default :blah, 10
      end
    end).to raise_error(Mixlib::Config::ReopenedConfigContextWithConfigurableError)
  end

  describe "config context lists" do
    let(:klass) do
      klass = Class.new
      klass.extend ::Mixlib::Config
      klass.instance_eval do
        config_context_list(:tests, :test) do
          default :y, 20
        end
      end
      klass
    end
    it "defines list methods when declaring a config_context_list" do
      expect(klass.methods).to include :test
      expect(klass.methods).to include :tests
    end

    it "creates a new item each time the singular list is called" do
      klass.test do
        y 40
      end
      klass.test do
        y 50
      end
      expect(klass.tests.length).to be 2
      expect(klass.tests.first.y).to be 40
      expect(klass.tests.last.y).to be 50
    end

    it "can save the config list" do
      klass.test do
        y 40
      end
      klass.test do
        y 50
      end
      expect(klass.save).to eq({
        tests: [
          { y: 40 },
          { y: 50 },
        ],
      })
    end

    it "can restore the config list from a hash" do
      hash = {
        tests: [
          { y: 40 },
          { y: 50 },
        ],
      }
      klass.restore(hash)
      expect(klass.tests.length).to be 2
      expect(klass.tests.first.y).to be 40
      expect(klass.tests.last.y).to be 50
    end
  end

  describe "config context hashes" do
    let(:klass) do
      klass = Class.new
      klass.extend ::Mixlib::Config
      klass.instance_eval do
        config_context_hash(:tests, :test) do
          default :y, 20
        end
      end
      klass
    end

    it "defines list methods when declaring a config_context_hash" do
      expect(klass.methods).to include :test
      expect(klass.methods).to include :tests
    end

    context "when called with a new key each time" do
      it "creates a new item each time" do
        klass.test :one do
          y 40
        end
        klass.test :two do
          y 50
        end
        expect(klass.tests.length).to be 2
        expect(klass.tests[:one].y).to be 40
        expect(klass.tests[:two].y).to be 50
      end
    end
    context "when called with the same key" do
      it "modifies the existing value" do
        klass.test :only do
          y 40
        end
        klass.test :only do
          y 50
        end
        expect(klass.tests.length).to be 1
        expect(klass.tests[:only].y).to be 50
      end
    end

    it "can save the config hash" do
      klass.test :one do
        y 40
      end
      klass.test :two do
        y 50
      end
      expect(klass.save).to eq({
        tests: {
          one: { y: 40 },
          two: { y: 50 },
        },
      })
    end

    it "can restore the config hash from a hash" do
      hash = {
        tests: {
          one: { y: 40 },
          two: { y: 50 },
        },
      }
      klass.restore(hash)
      expect(klass.tests.length).to be 2
      expect(klass.tests[:one].y).to be 40
      expect(klass.tests[:two].y).to be 50
    end
  end

  describe ".from_yaml" do
    let(:yaml) do
      <<~EOH
        ---
        foo:
          - bar
          - baz
          - matazz
        alpha: beta
      EOH
    end

    it "turns YAML into method-style setting" do
      allow(File).to receive(:exists?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(IO).to receive(:read).with("config.yml").and_return(yaml)

      expect(lambda do
        ConfigIt.from_file("config.yml")
      end).to_not raise_error

      expect(ConfigIt.foo).to eql(%w{ bar baz matazz })
      expect(ConfigIt.alpha).to eql("beta")
    end

    it "does not deserialize arbitrary Ruby objects from YAML" do
      malicious_yaml = "--- !ruby/object:Gem::Requirement\nfoo: bar\n"
      allow(IO).to receive(:read).with("config.yml").and_return(malicious_yaml)

      expect do
        ConfigIt.from_file("config.yml")
      end.to raise_error(Psych::DisallowedClass)
    end
  end

  describe ".from_json" do
    let(:json) do
      <<~EOH
        {
          "foo": [
            "bar",
            "baz",
            "matazz"
          ],
          "alpha": "beta"
        }
      EOH
    end

    it "turns JSON into method-style setting" do
      allow(File).to receive(:exists?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(IO).to receive(:read).with("config.json").and_return(json)

      expect(lambda do
        ConfigIt.from_file("config.json")
      end).to_not raise_error

      expect(ConfigIt.foo).to eql(%w{ bar baz matazz })
      expect(ConfigIt.alpha).to eql("beta")
    end
  end

  describe ".from_toml" do
    let(:toml) do
      <<~EOH
        foo = ["bar", "baz", "matazz"]
        alpha = "beta"
      EOH
    end

    it "turns TOML into method-style setting" do
      allow(File).to receive(:exists?).and_return(true)
      allow(File).to receive(:readable?).and_return(true)
      allow(IO).to receive(:read).with("config.toml").and_return(toml)

      expect(lambda do
        ConfigIt.from_file("config.toml")
      end).to_not raise_error

      expect(ConfigIt.foo).to eql(%w{ bar baz matazz })
      expect(ConfigIt.alpha).to eql("beta")
    end
  end

  describe ".from_hash" do
    context "when contexts in the hash are defined" do
      let(:hash) do
        {
          "alpha" => "beta",
          gamma: "delta",
          "foo" => %w{ bar baz matazz},
          "bar" => { "baz" => { "fizz" => "buzz" } },
          "windows_path" => 'C:\Windows Has Awful\Paths',
        }
      end
      it "configures the config object from a hash" do
        ConfigIt.config_context :bar do
          config_context :baz do
            default :fizz, "quux"
          end
        end
        ConfigIt.from_hash(hash)
        expect(ConfigIt.foo).to eql(%w{ bar baz matazz })
        expect(ConfigIt.alpha).to eql("beta")
        expect(ConfigIt.gamma).to eql("delta")
        expect(ConfigIt[:bar][:baz][:fizz]).to eql("buzz")
        expect(ConfigIt.windows_path).to eql('C:\Windows Has Awful\Paths')
      end
    end
    context "when configurable and hash is defined" do
      before :each do
        @klass = Class.new
        @klass.extend(::Mixlib::Config)
        @klass.class_eval do
          configurable(:environment) do |c|
            c.defaults_to({})
          end
        end
      end

      let(:hash) do
        {
          environment: { "GEM_PATH" => "SOME_PATH" },
        }
      end
      it "configures the config object from a hash" do
        hash_config = @klass.from_hash(hash)
        expect(hash_config[:environment]).to eql({ "GEM_PATH" => "SOME_PATH" })
      end
    end
    context "when contexts in the hash are undefined and strict disabled" do
      before do
        ConfigIt.strict_mode = true
      end
      let(:hash) do
        {
          "brrr" => { "baz" => { "fizz" => "buzz" } },
          "windows_path" => 'C:\Windows Has Awful\Paths',
        }
      end
      it "configures the config object, creating contexts as needed" do
        ConfigIt.from_hash(hash)
        expect(ConfigIt[:brrr][:baz][:fizz]).to eql("buzz")
        expect(ConfigIt.windows_path).to eql('C:\Windows Has Awful\Paths')
      end
    end
  end
end
