#
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright (c) 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/immutable_collections"

shared_examples_for "ImmutableMash module" do |param|
  let(:copy) { @immutable_mash.send(param) }

  it "converts an immutable mash to a new mutable hash" do
    expect(copy).to be_is_a(Hash)
  end

  it "converts an immutable nested mash to a new mutable hash" do
    expect(copy["top_level_4"]["level2"]).to be_is_a(Hash)
  end

  it "converts an immutable nested array to a new mutable array" do
    expect(copy["top_level_2"]).to be_instance_of(Array)
  end

  it "should create a mash with the same content" do
    expect(copy).to eq(@immutable_mash)
  end

  it "should allow mutation" do
    expect { copy["m"] = "m" }.not_to raise_error
  end
end

shared_examples_for "ImmutableArray module" do |param|
  let(:copy) { @immutable_nested_array.send(param) }

  it "converts an immutable array to a new mutable array" do
    expect(copy).to be_instance_of(Array)
  end

  it "converts an immutable nested array to a new mutable array" do
    expect(copy[1]).to be_instance_of(Array)
  end

  it "converts an immutable nested mash to a new mutable hash" do
    expect(copy[2]).to be_is_a(Hash)
  end

  it "should create an array with the same content" do
    expect(copy).to eq(@immutable_nested_array)
  end

  it "should allow mutation" do
    expect { copy << "m" }.not_to raise_error
  end
end

shared_examples_for "Immutable#to_yaml" do
  it "converts an immutable array to a new valid YAML mutable string" do
    expect { YAML.parse(copy) }.not_to raise_error
  end

  it "should create a YAML string with content" do
    # Roundtrip the test string through YAML to compensate for some changes in libyaml-0.2.5
    # See: https://github.com/yaml/libyaml/pull/186
    expected = YAML.dump(YAML.load(parsed_yaml))

    expect(copy).to eq(expected)
  end
end

describe Chef::Node::ImmutableMash do
  before do
    @data_in = { "top" => { "second_level" => "some value" },
                 "top_level_2" => %w{array of values},
                 "top_level_3" => [{ "hash_array" => 1, "hash_array_b" => 2 }],
                 "top_level_4" => { "level2" => { "key" => "value" } },
    }
    @immutable_mash = Chef::Node::ImmutableMash.new(@data_in)
  end

  it "does not have any unaudited methods" do
    unaudited_methods = Hash.instance_methods - Object.instance_methods - Chef::Node::Mixin::ImmutablizeHash::DISALLOWED_MUTATOR_METHODS - Chef::Node::Mixin::ImmutablizeHash::ALLOWED_METHODS
    expect(unaudited_methods).to be_empty
  end

  it "element references like regular hash" do
    expect(@immutable_mash[:top][:second_level]).to eq("some value")
  end

  it "element references like a regular Mash" do
    expect(@immutable_mash[:top_level_2]).to eq(%w{array of values})
  end

  it "converts Hash-like inputs into ImmutableMash's" do
    expect(@immutable_mash[:top]).to be_a(Chef::Node::ImmutableMash)
  end

  it "converts array inputs into ImmutableArray's" do
    expect(@immutable_mash[:top_level_2]).to be_a(Chef::Node::ImmutableArray)
  end

  it "converts arrays of hashes to ImmutableArray's of ImmutableMashes" do
    expect(@immutable_mash[:top_level_3].first).to be_a(Chef::Node::ImmutableMash)
  end

  it "converts nested hashes to ImmutableMashes" do
    expect(@immutable_mash[:top_level_4]).to be_a(Chef::Node::ImmutableMash)
    expect(@immutable_mash[:top_level_4][:level2]).to be_a(Chef::Node::ImmutableMash)
  end

  # we only ever absorb VividMashes from other precedence levels, which already have
  # been coerced to only have string keys, so we do not need to do that work twice (performance).
  it "does not call convert_value like Mash/VividMash" do
    @mash = Chef::Node::ImmutableMash.new({ test: "foo", "test2" => "bar" })
    expect(@mash[:test]).to eql("foo")
    expect(@mash["test2"]).to eql("bar")
  end

  %w{to_h to_hash dup}.each do |immutable_meth|
    describe "#{immutable_meth}" do
      include_examples "ImmutableMash module", description
    end
  end

  describe "to_yaml" do
    let(:copy) { @immutable_mash.to_yaml }
    let(:parsed_yaml) { "---\ntop:\n  second_level: some value\ntop_level_2:\n- array\n- of\n- values\ntop_level_3:\n- hash_array: 1\n  hash_array_b: 2\ntop_level_4:\n  level2:\n    key: value\n" }

    include_examples "Immutable#to_yaml"
  end

  %i{
    []=
    clear
    default=
    default_proc=
    delete
    delete_if
    keep_if
    merge!
    update
    reject!
    replace
    select!
    shift
    write
    write!
    unlink
    unlink!
  }.each do |mutator|
    it "doesn't allow mutation via `#{mutator}'" do
      expect { @immutable_mash.send(mutator) }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
    end
  end

  it "returns a mutable version of itself when duped" do
    mutable = @immutable_mash.dup
    mutable[:new_key] = :value
    expect(mutable[:new_key]).to eq(:value)
  end

end

describe Chef::Node::ImmutableArray do

  before do
    @immutable_array = Chef::Node::ImmutableArray.new(%w{foo bar baz} + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ])
    immutable_mash = Chef::Node::ImmutableMash.new({ "m" => "m" })
    @immutable_nested_array = Chef::Node::ImmutableArray.new(["level1", @immutable_array, immutable_mash])
  end

  ##
  # Note: other behaviors, such as immutibilizing input data, are tested along
  # with ImmutableMash, above
  ###

  %i{
    <<
    []=
    clear
    collect!
    compact!
    default=
    default_proc=
    delete
    delete_at
    delete_if
    fill
    flatten!
    insert
    keep_if
    map!
    merge!
    pop
    push
    reject!
    reverse!
    replace
    select!
    shift
    slice!
    sort!
    sort_by!
    uniq!
    unshift
  }.each do |mutator|
    it "does not allow mutation via `#{mutator}" do
      expect { @immutable_array.send(mutator) }.to raise_error(Chef::Exceptions::ImmutableAttributeModification)
    end
  end

  it "does not have any unaudited methods" do
    unaudited_methods = Array.instance_methods - Object.instance_methods - Chef::Node::Mixin::ImmutablizeArray::DISALLOWED_MUTATOR_METHODS - Chef::Node::Mixin::ImmutablizeArray::ALLOWED_METHODS
    expect(unaudited_methods).to be_empty
  end

  it "can be duped even if some elements can't" do
    @immutable_array.dup
  end

  it "returns a mutable version of itself when duped" do
    mutable = @immutable_array.dup
    mutable[0] = :value
    expect(mutable[0]).to eq(:value)
  end

  %w{to_a to_array dup}.each do |immutable_meth|
    describe "#{immutable_meth}" do
      include_examples "ImmutableArray module", description
    end
  end

  describe "to_yaml" do
    let(:copy) { @immutable_nested_array.to_yaml }
    let(:parsed_yaml) { "---\n- level1\n- - foo\n  - bar\n  - baz\n  - 1\n  - 2\n  - 3\n  -\n  - true\n  - false\n  - - el\n    - 0\n    -\n- m: m\n" }

    include_examples "Immutable#to_yaml"
  end

  describe "#[]" do
    it "works with array slices" do
      expect(@immutable_array[1, 2]).to eql(%w{bar baz})
    end
  end
end