#
# Copyright:: Copyright (c) 2014 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 'shared/setup_git_cookbooks'
require 'chef-dk/policyfile_lock.rb'

describe ChefDK::PolicyfileLock do

  def id_to_dotted(sha1_id)
    major = sha1_id[0...14]
    minor = sha1_id[14...28]
    patch = sha1_id[28..40]
    decimal_integers =[major, minor, patch].map {|hex| hex.to_i(16) }
    decimal_integers.join(".")
  end

  # For debugging giant nested hashes...
  def expect_hash_equal(actual, expected)
    expected.each do |key, expected_value|
      expect(actual).to have_key(key)
      if expected_value.kind_of?(Hash)
        expect_hash_equal(actual[key], expected_value)
      else
        expect(actual[key]).to eq(expected_value)
      end
    end
    expect(actual).to eq(expected)
  end

  let(:cache_path) do
    File.expand_path("spec/unit/fixtures/cookbook_cache", project_root)
  end

  let(:relative_paths_root) do
    File.expand_path("spec/unit/fixtures/", project_root)
  end

  let(:policyfile_lock_options) do
    { cache_path: cache_path, relative_paths_root: relative_paths_root }
  end

  describe "when first created" do

    let(:policyfile_lock) do
      described_class.new(policyfile_lock_options)
    end

    it "uses CWD for relative_paths_root if none is given" do
      policyfile_lock = described_class.new
      expect(policyfile_lock.relative_paths_root).to eq(Dir.pwd)
    end

    it "uses the provided option for relative_paths_root" do
      expect(policyfile_lock.relative_paths_root).to eq(relative_paths_root)
    end

    it "uses the provided cache_path" do
      expect(policyfile_lock.cache_path).to eq(cache_path)
    end

  end

  context "when a cookbook is not in the cache" do

    let(:policyfile_lock) do
      ChefDK::PolicyfileLock.build(policyfile_lock_options) do |p|

        p.name = "invalid_cache_key_policyfile"

        p.run_list = [ "recipe[foo]" ]

        p.cached_cookbook("nosuchthing") do |cb|
          cb.cache_key = "nosuchthing-1.0.0"
        end
      end
    end

    it "raises a descriptive error" do
      expect { policyfile_lock.to_lock }.to raise_error(ChefDK::CachedCookbookNotFound)
    end

  end

  context "with a minimal policyfile" do

    let(:policyfile_lock) do
      ChefDK::PolicyfileLock.build(policyfile_lock_options) do |p|

        p.name = "minimal_policyfile"

        p.run_list = [ "recipe[foo]" ]
        p.cached_cookbook("foo") do |cb|
          cb.cache_key = "foo-1.0.0"
        end

      end
    end

    let(:compiled_policyfile) do
      {

        "name" => "minimal_policyfile",

        "run_list" => ["recipe[foo]"],

        "cookbook_locks" => {

          "foo" => {
            "version" => "1.0.0",
            "identifier" => "e4611e9b5ec0636a18979e7dd22537222a2eab47",
            "dotted_decimal_identifier" => id_to_dotted("e4611e9b5ec0636a18979e7dd22537222a2eab47"),
            "cache_key" => "foo-1.0.0",
            "origin" => nil
          },
        }
      }
    end

    it "has a cache path" do
      expect(policyfile_lock.cache_path).to eq(cache_path)
    end

    it "computes a minimal policyfile" do
      expect(policyfile_lock.to_lock).to eq(compiled_policyfile)
    end

  end

  context "with a policyfile containing a local cookbook" do

    include_context "setup git cookbooks"
    include_context "setup git cookbook remote"

    let(:relative_paths_root) do
      tempdir
    end

    let(:policyfile_lock) do
      ChefDK::PolicyfileLock.build(policyfile_lock_options) do |p|

        p.name = "dev_cookbook"

        p.run_list = [ "recipe[bar]" ]
        p.local_cookbook("bar") do |cb|
          cb.source = "bar"
        end

      end
    end

    let(:compiled_policyfile) do
      {

        "name" => "dev_cookbook",

        "run_list" => ["recipe[bar]"],

        "cookbook_locks" => {

          "bar" => {
            "version" => "0.1.0",
            "identifier" => "f7694dbebe4109dfc857af7e2e4475c322c65259",
            "dotted_decimal_identifier" => id_to_dotted("f7694dbebe4109dfc857af7e2e4475c322c65259"),

            "source" => "bar",
            "cache_key" => nil,
            "scm_info" => {
              "scm" => "git",
              "remote" => remote_url,
              "revision" => current_rev,
              "working_tree_clean" => true,
              "published" => true,
              "synchronized_remote_branches"=>["origin/master"]
            },
          },
        }
      }
    end

    it "computes a lockfile including git data" do
      actual_lock = policyfile_lock.to_lock
      expect(actual_lock).to eq(compiled_policyfile)
    end
  end

  context "with a policyfile using custom identifiers" do

    include_context "setup git cookbooks"

    let(:relative_paths_root) do
      tempdir
    end

    let(:policyfile_lock) do
      ChefDK::PolicyfileLock.build(policyfile_lock_options) do |p|

        p.name = "custom_identifier"

        p.run_list = [ "recipe[foo]" ]

        p.cached_cookbook("foo") do |cb|
          cb.cache_key = "foo-1.0.0"

          # Explicitly set the identifier and dotted decimal identifiers to the
          # version number (but it could be anything).
          cb.identifier = "1.0.0"
          cb.dotted_decimal_identifier ="1.0.0"
        end

        p.local_cookbook("bar") do |cb|
          cb.source = "bar"
          cb.identifier = "0.1.0"
          cb.dotted_decimal_identifier = "0.1.0"
        end
      end

    end

    let(:compiled_policyfile) do
      {

        "name" => "custom_identifier",

        "run_list" => ["recipe[foo]"],

        "cookbook_locks" => {

          "foo" => {
            "version" => "1.0.0",
            "identifier" => "1.0.0",
            "dotted_decimal_identifier" => "1.0.0",
            "cache_key" => "foo-1.0.0",
            "origin" => nil
          },

          "bar" => {
            "version" => "0.1.0",
            "identifier" => "0.1.0",
            "dotted_decimal_identifier" => "0.1.0",

            "source" => "bar",
            "cache_key" => nil,
            "scm_info" => {
              "scm" => "git",
              "remote" => nil,
              "revision" => current_rev,
              "working_tree_clean" => true,
              "published" => false,
              "synchronized_remote_branches"=>[]
            },
          },
        }
      }
    end

    it "generates a lockfile with custom identifiers" do
      expect(policyfile_lock.to_lock).to eq(compiled_policyfile)
    end

  end

  context "with a policyfile lock with a mix of cached and local cookbooks" do

    include_context "setup git cookbooks"

    let(:relative_paths_root) do
      tempdir
    end

    let(:policyfile_lock) do

      ChefDK::PolicyfileLock.build(policyfile_lock_options) do |p|

        # Required
        p.name = "basic_example"

        # Required. Should be fully expanded without roles
        p.run_list = ["recipe[foo]", "recipe[bar]", "recipe[baz::non_default]"]

        # A cached_cookbook is stored in the cache directory in a subdirectory
        # given by 'cache_key'. It is assumed to be static (not modified by the
        # user).
        p.cached_cookbook("foo") do |cb|
          cb.cache_key = "foo-1.0.0"

          # Optional attribute that humans can use to understand where a cookbook
          # came from.
          cb.origin = "https://community.getchef.com/api/cookbooks/foo/1.0.0"
        end

        p.local_cookbook("bar") do |cb|
          cb.source = "bar"
        end

        p.cached_cookbook("baz") do |cb|
          cb.cache_key = "baz-f59ee7a5bca6a4e606b67f7f856b768d847c39bb"
          cb.origin = "git://github.com/opscode-cookbooks/bar.git"
        end

        p.cached_cookbook("dep_of_bar") do |cb|
          cb.cache_key = "dep_of_bar-1.2.3"
          cb.origin = "https://chef-server.example.com/cookbooks/dep_of_bar/1.2.3"
        end
      end

    end


    let(:compiled_policyfile) do
      {

        "name" => "basic_example",

        "run_list" => ["recipe[foo]", "recipe[bar]", "recipe[baz::non_default]"],

        "cookbook_locks" => {

          "foo" => {
            "version" => "1.0.0",
            "identifier" => "e4611e9b5ec0636a18979e7dd22537222a2eab47",
            "dotted_decimal_identifier" => id_to_dotted("e4611e9b5ec0636a18979e7dd22537222a2eab47"),
            "origin" => "https://community.getchef.com/api/cookbooks/foo/1.0.0",
            "cache_key" => "foo-1.0.0"
          },

          "bar" => {
            "version" => "0.1.0",
            "identifier" => "f7694dbebe4109dfc857af7e2e4475c322c65259",
            "dotted_decimal_identifier" => id_to_dotted("f7694dbebe4109dfc857af7e2e4475c322c65259"),
            "source" => "bar",
            "cache_key" => nil,

            "scm_info" => {
              "scm" => "git",
              "remote" => nil,
              "revision" => current_rev,
              "working_tree_clean" => true,
              "published" => false,
              "synchronized_remote_branches"=>[]
            },
          },

          "baz" => {
            "version" => "1.2.3",
            "identifier"=>"08c6ac1d202f4d59ad67953559084886f6ba710a",
            "dotted_decimal_identifier" => id_to_dotted("08c6ac1d202f4d59ad67953559084886f6ba710a"),
            "cache_key" => "baz-f59ee7a5bca6a4e606b67f7f856b768d847c39bb",
            "origin" => "git://github.com/opscode-cookbooks/bar.git"
          },

          "dep_of_bar" => {
            "version" => "1.2.3",
            "identifier" => "e6c08ea35bce8009386710d8c9bcd6caa036e8bc",
            "dotted_decimal_identifier" => id_to_dotted("e6c08ea35bce8009386710d8c9bcd6caa036e8bc"),
            "origin" => "https://chef-server.example.com/cookbooks/dep_of_bar/1.2.3",
            "cache_key" => "dep_of_bar-1.2.3",

          },

        },

      }
    end

    it "generates a lockfile with the relevant profile data for each cookbook" do
      generated = policyfile_lock.to_lock
      expect(generated['name']).to eq(compiled_policyfile['name'])
      expect(generated['run_list']).to eq(compiled_policyfile['run_list'])

      generated_locks = generated['cookbook_locks']
      expected_locks = compiled_policyfile['cookbook_locks']

      # test individually so failures are easier to read
      expect(generated_locks['foo']).to eq(expected_locks['foo'])
      expect(generated_locks['bar']).to eq(expected_locks['bar'])
      expect(generated_locks['baz']).to eq(expected_locks['baz'])
      expect(generated_locks['dep_of_bar']).to eq(expected_locks['dep_of_bar'])

      expect(policyfile_lock.to_lock).to eq(compiled_policyfile)
    end

  end

  describe "building a policyfile lock from a policyfile compiler" do

    include_context "setup git cookbooks"

    let(:relative_paths_root) do
      tempdir
    end

    let(:cached_cookbook_spec) do
      double( "ChefDK::Policyfile::CookbookSpec",
              mirrors_canonical_upstream?: true,
              cache_key: "foo-1.0.0",
              uri: "https://supermarket.getchef.com/api/v1/cookbooks/foo/versions/1.0.0/download")
    end

    let(:local_cookbook_spec) do
      double( "ChefDK::Policyfile::CookbookSpec",
              mirrors_canonical_upstream?: false,
              relative_paths_root: relative_paths_root,
              relative_path: "bar")
    end


    let(:policyfile_compiler) do
      double( "ChefDK::PolicyfileCompiler",
              expanded_run_list: %w[foo bar],
              all_cookbook_specs: {"foo" => cached_cookbook_spec, "bar" => local_cookbook_spec})
    end

    let(:policyfile_lock) do
      ChefDK::PolicyfileLock.build_from_compiler(policyfile_compiler, cache_path: cache_path)
    end

    let(:compiled_policyfile) do
      {

        "name" => nil,

        "run_list" => ["foo", "bar"],

        "cookbook_locks" => {

          "foo" => {
            "version" => "1.0.0",
            "identifier" => "e4611e9b5ec0636a18979e7dd22537222a2eab47",
            "dotted_decimal_identifier" => id_to_dotted("e4611e9b5ec0636a18979e7dd22537222a2eab47"),
            "cache_key" => "foo-1.0.0",
            "origin" => cached_cookbook_spec.uri
          },

          "bar" => {
            "version" => "0.1.0",
            "identifier" => "f7694dbebe4109dfc857af7e2e4475c322c65259",
            "dotted_decimal_identifier" => id_to_dotted("f7694dbebe4109dfc857af7e2e4475c322c65259"),
            "source" => "bar",
            "cache_key" => nil,

            "scm_info" => {
              "scm" => "git",
              "remote" => nil,
              "revision" => current_rev,
              "working_tree_clean" => true,
              "published" => false,
              "synchronized_remote_branches"=>[]
            }
          }
        }
      }
    end


    it "adds a cached cookbook lock generator for the compiler's cached cookbook" do
      expect(policyfile_lock.cookbook_locks).to have_key("foo")
      cb_lock = policyfile_lock.cookbook_locks["foo"]
      expect(cb_lock.origin).to eq(cached_cookbook_spec.uri)
      expect(cb_lock.cache_key).to eq(cached_cookbook_spec.cache_key)
    end

    it "adds a local cookbook lock generator for the compiler's local cookbook" do
      expect(policyfile_lock.cookbook_locks).to have_key("bar")
      cb_lock = policyfile_lock.cookbook_locks["bar"]
      expect(cb_lock.source).to eq(local_cookbook_spec.relative_path)
    end

    it "generates a lockfile data structure" do
      expect(policyfile_lock.to_lock).to eq(compiled_policyfile)
    end

  end

end