require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

class AwesomeHash < DeepHash ; end

describe DeepHash do
  before(:each) do
    @deep_hash = DeepHash.new.merge!({ :nested_1 => { :nested_2 => { :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => 'val1b' })
    @hash = { "str_key" => "strk_val", :sym_key => "symk_val"}
    @sub  = AwesomeHash.new("str_key" => "strk_val", :sym_key => "symk_val")
  end

  describe "#initialize" do
    it 'adopts a Hash when given' do
      deep_hash = DeepHash.new(@hash)
      @hash.each{|k,v| deep_hash[k].should == v }
      deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false
    end

    it 'converts all pure Hash values into DeepHashes if param is a Hash' do
      deep_hash = DeepHash.new :sym_key => @hash
      deep_hash[:sym_key].should be_an_instance_of(DeepHash)
      # sanity check
      deep_hash[:sym_key][:sym_key].should == "symk_val"
    end

    it 'does not convert Hash subclass values into DeepHashes' do
      deep_hash = DeepHash.new :sub => @sub
      deep_hash[:sub].should be_an_instance_of(AwesomeHash)
    end

    it 'converts all value items if value is an Array' do
      deep_hash = DeepHash.new :arry => { :sym_key => [@hash] }
      deep_hash[:arry].should be_an_instance_of(DeepHash)
      # sanity check
      deep_hash[:arry][:sym_key].first[:sym_key].should == "symk_val"
    end

    it 'delegates to superclass constructor if param is not a Hash' do
      deep_hash = DeepHash.new("dash berlin")

      deep_hash["unexisting key"].should == "dash berlin"
    end
  end # describe "#initialize"

  describe "#update" do
    it 'converts all keys into symbols when param is a Hash' do
      deep_hash = DeepHash.new(@hash)
      deep_hash.update("starry" => "night")

      deep_hash.keys.any?{|key| key.is_a?(String) }.should be_false
    end

    it 'converts all Hash values into DeepHashes if param is a Hash' do
      deep_hash = DeepHash.new :hash => @hash
      deep_hash.update(:hash => { :sym_key => "is buggy in Ruby 1.8.6" })

      deep_hash[:hash].should be_an_instance_of(DeepHash)
    end
  end # describe "#update"

  describe '#[]=' do
    it 'symbolizes keys' do
      @deep_hash['leaf_at_top'] = :fedora
      @deep_hash['new']         = :unseen
      @deep_hash.should == {:nested_1 => {:nested_2 => {:leaf_3 => "val3"}, :leaf_2 => "val2"}, :leaf_at_top => :fedora, :new => :unseen}
    end
    it 'deep-sets dotted vals, replacing values' do
      @deep_hash['moon.man'] = :cheesy
      @deep_hash[:moon][:man].should == :cheesy
    end
    it 'deep-sets dotted vals, creating new values' do
      @deep_hash['moon.cheese.type'] = :tilsit
      @deep_hash[:moon][:cheese][:type].should == :tilsit
    end
    it 'deep-sets dotted vals, auto-vivifying intermediate hashes' do
      @deep_hash['this.that.the_other'] = :fuhgeddaboudit
      @deep_hash[:this][:that][:the_other].should == :fuhgeddaboudit
    end
    it 'converts all Hash value into DeepHash' do
      deep_hash = DeepHash.new :hash => @hash
      deep_hash[:hash] = { :sym_key => "is buggy in Ruby 1.8.6" }
      deep_hash[:hash].should be_an_instance_of(DeepHash)
    end
  end

  describe '#[]' do
    it 'deep-gets dotted vals' do
      hsh = { :hat => :cat, :basket => :lotion, :moon => { :man => :smiling, :cheese => {:type => :tilsit} } }
      @deep_hash = Configliere::Param.new hsh.dup
      @deep_hash['moon.man'].should == :smiling
      @deep_hash['moon.cheese.type'].should == :tilsit
      @deep_hash['moon.cheese.smell'].should be_nil
      @deep_hash['moon.non.existent.interim.values'].should be_nil
      @deep_hash['moon.non'].should be_nil
      if (RUBY_VERSION >= '1.9') then lambda{ @deep_hash['hat.cat'] }.should raise_error(TypeError)
      else                            lambda{ @deep_hash['hat.cat'] }.should raise_error(NoMethodError, 'undefined method `[]\' for :cat:Symbol') end
      @deep_hash.should == hsh # shouldn't change from reading (specifically, shouldn't autovivify)
    end
  end

  def arrays_should_be_equal arr1, arr2
    arr1.sort_by{|s| s.to_s }.should == arr2.sort_by{|s| s.to_s }
  end

  describe "#to_hash" do
    it 'returns instance of Hash' do
      DeepHash.new(@hash).to_hash.should be_an_instance_of(Hash)
    end

    it 'preserves keys' do
      deep_hash = DeepHash.new(@hash)
      converted  = deep_hash.to_hash
      arrays_should_be_equal deep_hash.keys, converted.keys
    end

    it 'preserves value' do
      deep_hash = DeepHash.new(@hash)
      converted = deep_hash.to_hash
      arrays_should_be_equal deep_hash.values, converted.values
    end
  end

  describe '#compact' do
    it 'removes nils but not empties or falsehoods' do
      DeepHash.new({ 1 => nil }).compact.should == {}
      DeepHash.new({ 1 => nil, 2 => false, 3 => {}, 4 => "", :remains => true }).compact!.should == { 2 => false, 3 => {}, 4 => "", :remains => true }
    end

    it 'leaves original alone' do
      deep_hash = DeepHash.new({ 1 => nil, :remains => true })
      deep_hash.compact.should == { :remains => true }
      deep_hash.should == { 1 => nil, :remains => true }
    end
  end

  describe '#compact!' do
    it 'removes nils but not empties or falsehoods' do
      DeepHash.new({ 1 => nil}).compact!.should == {}
      DeepHash.new({ 1 => nil, 2 => false, 3 => {}, 4 => "", :remains => true }).compact!.should == { 2 => false, 3 => {}, 4 => "", :remains => true }
    end

    it 'modifies in-place' do
      deep_hash = DeepHash.new({ 1 => nil, :remains => true })
      deep_hash.compact!.should == { :remains => true }
      deep_hash.should == { :remains => true }
    end
  end

  describe '#slice' do
    before do
      @deep_hash = DeepHash.new({ :a => 'x', :b => 'y', :c => 10 })
    end

    it 'returns a new hash with only the given keys' do
      @deep_hash.slice(:a, :b).should == { :a => 'x', :b => 'y' }
      @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
    end

    it 'with bang replaces the hash with only the given keys' do
      @deep_hash.slice!(:a, :b).should == { :c => 10 }
      @deep_hash.should == { :a => 'x', :b => 'y' }
    end

    it 'ignores an array key' do
      @deep_hash.slice([:a, :b], :c).should == { :c => 10 }
      @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
    end

    it 'with bang ignores an array key' do
      @deep_hash.slice!([:a, :b], :c).should == { :a => 'x', :b => 'y' }
      @deep_hash.should == { :c => 10 }
    end

    it 'uses splatted keys individually' do
      @deep_hash.slice(*[:a, :b]).should == { :a => 'x', :b => 'y' }
      @deep_hash.should == { :a => 'x', :b => 'y', :c => 10 }
    end

    it 'with bank uses splatted keys individually' do
      @deep_hash.slice!(*[:a, :b]).should == { :c => 10 }
      @deep_hash.should == { :a => 'x', :b => 'y' }
    end
  end

  describe '#extract' do
    before do
      @deep_hash = DeepHash.new({ :a => 'x', :b => 'y', :c => 10 })
    end

    it 'replaces the hash with only the given keys' do
      @deep_hash.extract!(:a, :b).should == { :a => 'x', :b => 'y' }
      @deep_hash.should == { :c => 10 }
    end

    it 'leaves the hash empty if all keys are gone' do
      @deep_hash.extract!(:a, :b, :c).should == { :a => 'x', :b => 'y', :c => 10 }
      @deep_hash.should == {}
    end

    it 'gets values for all given keys even if missing' do
      @deep_hash.extract!(:bob, :c).should == { :bob => nil, :c => 10 }
      @deep_hash.should == { :a => 'x', :b => 'y' }
    end

    it 'is OK when empty' do
      DeepHash.new.slice!(:a, :b, :c).should == {}
    end

    it 'returns an instance of the same class' do
      @deep_hash.slice(:a).should be_a(DeepHash)
    end
  end

  describe 'assert_valid_keys' do
    before do
      @deep_hash = DeepHash.new({ :failure => "stuff", :funny => "business" })
    end

    it 'is true and does not raise when valid' do
      @deep_hash.assert_valid_keys([ :failure, :funny ]).should be_nil
      @deep_hash.assert_valid_keys(:failure, :funny).should be_nil
    end

    it 'fails when invalid' do
      @deep_hash[:failore] = @deep_hash.delete(:failure)
      lambda{ @deep_hash.assert_valid_keys([ :failure, :funny ]) }.should raise_error(ArgumentError, "Unknown key(s): failore")
      lambda{ @deep_hash.assert_valid_keys(:failure, :funny)     }.should raise_error(ArgumentError, "Unknown key(s): failore")
    end
  end

  describe "#delete" do
    it 'converts Symbol key into String before deleting' do
      deep_hash = DeepHash.new(@hash)

      deep_hash.delete(:sym_key)
      deep_hash.key?("hash").should be_false
    end

    it 'works with String keys as well' do
      deep_hash = DeepHash.new(@hash)

      deep_hash.delete("str_key")
      deep_hash.key?("str_key").should be_false
    end
  end

  describe "#merge" do
    before(:each) do
      @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
    end

    it 'returns instance of DeepHash' do
      @deep_hash.should be_an_instance_of(DeepHash)
    end

    it 'merges in give Hash' do
      @deep_hash["no"].should == "in between"
    end
  end

  describe "#fetch" do
    before(:each) do
      @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
    end

    it 'converts key before fetching' do
      @deep_hash.fetch("no").should == "in between"
    end

    it 'returns alternative value if key lookup fails' do
      @deep_hash.fetch("flying", "screwdriver").should == "screwdriver"
    end
  end


  describe "#values_at" do
    before(:each) do
      @deep_hash = DeepHash.new(@hash).merge(:no => "in between")
    end

    it 'is indifferent to whether keys are strings or symbols' do
      @deep_hash.values_at("sym_key", :str_key, :no).should == ["symk_val", "strk_val", "in between"]
    end
  end

  describe "#symbolize_keys" do
    it 'with bang returns the deep_hash itself' do
      deep_hash = DeepHash.new(@hash)
      deep_hash.symbolize_keys!.object_id.should == deep_hash.object_id
    end

    it 'returns a dup of itself' do
      deep_hash = DeepHash.new(@hash)
      deep_hash.symbolize_keys.should == deep_hash
    end
  end

  describe "#reverse_merge" do
    before do
      @defaults  = { :a => "x", :b => "y", :c => 10 }.freeze
      @deep_hash = DeepHash.new({ :a => 1, :b => 2 })
    end

    it 'merges defaults into options, creating a new hash' do
      @deep_hash.reverse_merge(@defaults).should == { :a => 1, :b => 2, :c => 10 }
      @deep_hash.should == { :a => 1, :b => 2 }
    end

    it 'with bang merges! defaults into options, replacing options' do
      @deep_hash.reverse_merge!(@defaults).should == { :a => 1, :b => 2, :c => 10 }
      @deep_hash.should == { :a => 1, :b => 2, :c => 10 }
    end
  end

  describe "#deep_merge!" do
    it "merges two subhashes when they share a key" do
      @deep_hash.deep_merge!(:nested_1 => { :nested_2  => { :leaf_3_also  => "val3a" } })
      @deep_hash.should == { :nested_1 => { :nested_2  => { :leaf_3_also  => "val3a", :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => 'val1b' }
    end
    it "merges two subhashes when they share a symbolized key" do
      @deep_hash.deep_merge!(:nested_1 => { "nested_2" => { "leaf_3_also" => "val3a" } })
      @deep_hash.should == { :nested_1 => { :nested_2  => { :leaf_3_also  => "val3a", :leaf_3 => "val3" }, :leaf_2 => "val2" }, :leaf_at_top => "val1b" }
    end
    it "preserves values in the original" do
      @deep_hash.deep_merge! :other_key => "other_val"
      @deep_hash[:nested_1][:leaf_2].should == "val2"
      @deep_hash[:other_key].should == "other_val"
    end
    # it "converts all Hash values into DeepHashes if param is a Hash"  do
    #   @deep_hash.deep_merge!({:nested_1 => { :nested_2 => { :leaf_3_also => "val3a" } }, :other1 => { "other2" => "other_val2" }})
    #   @deep_hash[:nested_1].should be_an_instance_of(DeepHash)
    #   @deep_hash[:nested_1][:nested_2].should be_an_instance_of(DeepHash)
    #   @deep_hash[:other1].should be_an_instance_of(DeepHash)
    # end
    it "replaces values from the given DeepHash" do
      @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => "new_val3" }, :leaf_2 => { "other2" => "other_val2" }})
      @deep_hash[:nested_1][:nested_2][:leaf_3].should == 'new_val3'
      @deep_hash[:nested_1][:leaf_2].should == { :other2 => "other_val2" }
    end

    it "replaces values from the given DeepHash" do
      @deep_hash.deep_merge!(:nested_1 => { :nested_2 => { :leaf_3 => [] }, :leaf_2 => nil }, :leaf_at_top => '')
      @deep_hash[:nested_1][:nested_2][:leaf_3].should == []
      @deep_hash[:nested_1][:leaf_2].should == "val2"
      @deep_hash[:leaf_at_top].should == ""
    end
  end

  describe "#deep_set" do
    it 'should set a new value (single arg)' do
      @deep_hash.deep_set :new_key, 'new_val'
      @deep_hash[:new_key].should == 'new_val'
    end
    it 'should set a new value (multiple args)' do
      @deep_hash.deep_set :nested_1, :nested_2, :new_key, 'new_val'
      @deep_hash[:nested_1][:nested_2][:new_key].should == 'new_val'
    end
    it 'should replace an existing value (single arg)' do
      @deep_hash.deep_set :leaf_at_top, 'new_val'
      @deep_hash[:leaf_at_top].should == 'new_val'
    end
    it 'should replace an existing value (multiple args)' do
      @deep_hash.deep_set :nested_1, :nested_2, 'new_val'
      @deep_hash[:nested_1][:nested_2].should == 'new_val'
    end
    it 'should auto-vivify intermediate hashes' do
      @deep_hash.deep_set :one, :two, :three, :four, 'new_val'
      @deep_hash[:one][:two][:three][:four].should == 'new_val'
    end
  end

  describe "#deep_delete" do
    it 'should remove the key from the array (multiple args)' do
      @deep_hash.deep_delete(:nested_1)
      @deep_hash[:nested_1].should be_nil
      @deep_hash.should == { :leaf_at_top => 'val1b'}
    end
    it 'should remove the key from the array (multiple args)' do
      @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3)
      @deep_hash[:nested_1][:nested_2][:leaf_3].should be_nil
      @deep_hash.should == {:leaf_at_top => "val1b", :nested_1 => {:leaf_2 => "val2", :nested_2 => {}}}
    end
    it 'should return the value if present (single args)' do
      returned_val = @deep_hash.deep_delete(:leaf_at_top)
      returned_val.should == 'val1b'
    end
    it 'should return the value if present (multiple args)' do
      returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :leaf_3)
      returned_val.should == 'val3'
    end
    it 'should return nil if the key is absent (single arg)' do
      returned_val = @deep_hash.deep_delete(:nested_1, :nested_2, :missing_key)
      returned_val.should be_nil
    end
    it 'should return nil if the key is absent (multiple args)' do
      returned_val = @deep_hash.deep_delete(:missing_key)
      returned_val.should be_nil
    end
  end

end