# 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 "pedant/rspec/cookbook_util"
require "pedant/rspec/environment_util"

describe "Depsolver API endpoint", :depsolver do
  include Pedant::RSpec::CookbookUtil
  include Pedant::RSpec::EnvironmentUtil

  # Cookbook tests are parameterized to support common testing of both
  # /cookbooks and /cookbook_artifacts, so we need to specify that we want to
  # talk to /cookbooks. Have to define this as a method rather than a `let`
  # because we access it in `before(:all)` hooks
  def cookbook_url_base
    "cookbooks"
  end

  shared(:env) { "test_depsolver_env" }
  shared(:no_cookbooks_env) { "test_depsolver_no_cookbooks_env" }
  shared(:cookbook_name) { "foo" }
  shared(:cookbook_version) { "1.2.3" }
  let(:cookbook_name2) { "bar" }
  let(:cookbook_version2) { "2.0.0" }

  # We run all the depsolver tests in a newly created environment
  before(:all) {
    # Make sure we are testing an environment that has some
    # constraints even, if they don't actually constrain.
    the_env = new_environment(env)
    the_env["cookbook_versions"] = {
      "qux" => "> 4.0.0",
      "foo" => ">= 0.1.0",
      "bar" => "< 4.0.0",
    }
    add_environment(admin_user, the_env)

    no_cb_env = new_environment(no_cookbooks_env)
    no_cb_env["cookbook_versions"] = {
      "foo" => "= 400.0.0",
      "bar" => "> 400.0.0",
    }
    add_environment(admin_user, no_cb_env)
  }

  after(:all) {
    delete_environment(admin_user, env)
    delete_environment(admin_user, no_cookbooks_env)
  }

  context "POST /environments/:env/cookbook_versions" do

    context "empty and error cases" do
      before(:all) {
        make_cookbook(admin_user, cookbook_name, cookbook_version)
      }

      after(:all) {
        delete_cookbook(admin_user, cookbook_name, cookbook_version)
      }

      it "returns 400 with an empty payload", :validation do
        payload = ""
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                       status: 400,
                                       body_exact: {
                                           "error" => ["invalid JSON"],
                                       },
                                      })
          end
      end

      it "returns 400 with an non-json payload", :validation do
        payload = "this_is_not_json"
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                       status: 400,
                                       body_exact: {
                                           "error" => ["invalid JSON"],
                                       },
                                      })
          end
      end

      let(:environment_name) { "not@environment" }

      it "returns 404 with an invalid environment" do
        payload = "{\"run_list\":[]}"
        post(api_url("/environments/#{environment_name}/cookbook_versions"), normal_user,
          payload: payload) do |response|
            response.should look_like environment_not_found_response
          end
      end

      it "returns 400 with non-Array as run_list value", :validation do
        payload = "{\"run_list\":\"#{cookbook_name}\"}"
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                       status: 400,
                                       body_exact: {
                                           "error" => ["Field 'run_list' is not a valid run list"],
                                       },
                                      })
          end
      end

      it "returns 400 with malformed JSON", :validation do
        payload = "{\"run_list\": "
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 400,
                                        body_exact: {
                                          "error" => ["invalid JSON"],
                                        },
                                      })
          end
      end

      it "returns Error with an malformed item in run_list (int)", :validation do
        payload = "{\"run_list\": [12]}"
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 400,
                                        body_exact: {
                                          "error" => ["Field 'run_list' is not a valid run list"],
                                        },
                                      })
          end
      end

      it "returns 200 with an empty run_list" do
        payload = "{\"run_list\":[]}"
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                       status: 200,
                                       body_exact: {},

                                      })
          end
      end

      it "returns 412 with a non-existent cookbook in _default environment" do
        not_exist = "this_does_not_exist"
        payload = "{\"run_list\":[\"#{not_exist}\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no such cookbook #{not_exist}.",
          "non_existent_cookbooks" => [ not_exist ],
          "cookbooks_with_no_versions" => [],
        }
        post(api_url("/environments/_default/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      it "returns 412 with a non-existent cookbook" do
        not_exist = "this_does_not_exist"
        payload = "{\"run_list\":[\"#{not_exist}\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no such cookbook #{not_exist}.",
          "non_existent_cookbooks" => [ not_exist ],
          "cookbooks_with_no_versions" => [],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      it "returns 412 with an existing cookbook filtered out by environment" do
        payload = "{\"run_list\":[\"#{cookbook_name}\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no versions match the constraints on cookbook (foo >= 0.0.0).",
          "non_existent_cookbooks" => [],
          "cookbooks_with_no_versions" => ["(foo >= 0.0.0)"],
        }
        post(api_url("/environments/#{no_cookbooks_env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      it "returns 412 and both cookbooks with more than one non-existent cookbook" do
        not_exist1 = "this_does_not_exist"
        not_exist2 = "also_this_one"
        payload = "{\"run_list\":[\"#{not_exist1}\", \"#{not_exist2}\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no such cookbooks #{not_exist1}, #{not_exist2}.",
          "non_existent_cookbooks" => [ not_exist1, not_exist2 ],
          "cookbooks_with_no_versions" => [],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      it "returns 412 when there is a runlist entry specifying version that doesn't exist" do
        missing_version_payload = "{\"run_list\":[\"#{cookbook_name}@#{cookbook_version2}\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no versions match the constraints on cookbook (#{cookbook_name} = #{cookbook_version2}).",
          "non_existent_cookbooks" => [],
          "cookbooks_with_no_versions" => ["(#{cookbook_name} = #{cookbook_version2})"],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: missing_version_payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end


      # TODO: See if it is possible to get both errors out for non-existent runlist items and
      # cookbooks which have no versions matching constraints
      it "returns 412 and both errors non-existent and no versions cookbooks" do
        not_exist = "this_does_not_exist"
        payload = "{\"run_list\":[\"#{not_exist}\", \"#{cookbook_name}@2.0.0\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no such cookbook #{not_exist}.",
          "non_existent_cookbooks" => [ not_exist ],
          "cookbooks_with_no_versions" => [],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

    end # empty and error cases context

    context "dependency error cases (one cookbook)" do
      let(:payload) { "{\"run_list\":[\"#{cookbook_name}\"]}" }

      # We create the cookbook in the test with a specific dependency.
      # Clean it up here
      after(:each) {
        delete_cookbook(admin_user, cookbook_name, cookbook_version)
      }

      # TODO: need more detailed depsolver output to construct error message
      it "returns 412 when there is a dep that doesn't exist" do
        not_exist_name = "this_does_not_exist"
        not_exist_version = "0.0.0"
        opts = { dependencies: { not_exist_name => ">= #{not_exist_version}" } }
        make_cookbook(admin_user, cookbook_name, cookbook_version, opts)
        error_hash = {
          "message" => "Unable to satisfy constraints on package this_does_not_exist, " +
            "which does not exist, due to solution constraint (foo >= 0.0.0). " +
            "Solution constraints that may result in a constraint on this_does_not_exist: " +
            "[(foo = 1.2.3) -> (this_does_not_exist >= 0.0.0)]",
          "unsatisfiable_run_list_item" => "(foo >= 0.0.0)",
          "non_existent_cookbooks" => ["this_does_not_exist"],
          "most_constrained_cookbooks" => [],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end
    end # dependency error cases (one cookbook)

    context "dependency error cases (two cookbooks)" do
      let(:payload) { "{\"run_list\":[\"#{cookbook_name}\"]}" }

      # We create the cookbooks in the test with a specific dependency.
      # Clean them up here
      after(:each) {
        delete_cookbook(admin_user, cookbook_name, cookbook_version)
        delete_cookbook(admin_user, cookbook_name2, cookbook_version2)
      }

      it "returns 412 and both entries when there are runlist entries specifying versions that don't exist" do
        make_cookbook(admin_user, cookbook_name, cookbook_version)
        make_cookbook(admin_user, cookbook_name2, cookbook_version2)
        missing_version_payload = "{\"run_list\":[\"#{cookbook_name2}@2.0.0\", \"#{cookbook_name}@3.0.0\"]}"
        error_hash = {
          "message" => "Run list contains invalid items: no versions match the constraints on cookbook (foo = 3.0.0).",
          "non_existent_cookbooks" => [],
          "cookbooks_with_no_versions" => ["(foo = 3.0.0)"],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: missing_version_payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      # TODO: need more detailed depsolver output to construct error message
      it "returns 412 when there is a dep that doesn't have new enough version" do
        opts = { dependencies: { cookbook_name2 => "> #{cookbook_version2}" } }
        make_cookbook(admin_user, cookbook_name, cookbook_version, opts)
        make_cookbook(admin_user, cookbook_name2, cookbook_version2)
        error_hash = {
          "message" =>
            "Unable to satisfy constraints on package bar due to solution constraint " +
              "(foo >= 0.0.0). Solution constraints that may result in a constraint on bar: " +
              "[(foo = 1.2.3) -> (bar > 2.0.0)]",
          "unsatisfiable_run_list_item" => "(foo >= 0.0.0)",
          "non_existent_cookbooks" => [],
          "most_constrained_cookbooks" => ["bar = 2.0.0 -> []"],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

      # TODO: need more detailed depsolver output to construct error message
      it "returns 412 when there is an impossible dependency" do
        opts1 = { dependencies: { cookbook_name2 => "> 2.0.0" } }
        opts2 = { dependencies: { cookbook_name => "> 3.0.0" } }
        make_cookbook(admin_user, cookbook_name, cookbook_version, opts1)
        make_cookbook(admin_user, cookbook_name2, cookbook_version2, opts2)
        error_hash = {
          "message" => "Unable to satisfy constraints on package bar due to " +
            "solution constraint (foo >= 0.0.0). Solution constraints " +
            "that may result in a constraint on bar: [(foo = 1.2.3) -> (bar > 2.0.0)]",
          "unsatisfiable_run_list_item" => "(foo >= 0.0.0)",
          "non_existent_cookbooks" => [],
          "most_constrained_cookbooks" => ["bar = 2.0.0 -> [(foo > 3.0.0)]"],
        }
        post(api_url("/environments/#{env}/cookbook_versions"), admin_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 412,
                                        body_exact: {
                                          "error" => [error_hash],
                                        },
                                      })
          end
      end

    end # dependency error cases (two cookbooks)

    context "success cases" do
      let(:payload) { "{\"run_list\":[\"#{cookbook_name}\"]}" }

      before(:all) {
        make_cookbook(admin_user, cookbook_name, cookbook_version)
      }

      after(:all) {
        delete_cookbook(admin_user, cookbook_name, cookbook_version)
      }

      it "returns 200 with a minimal good cookbook", :smoke do

        cb = retrieved_cookbook(cookbook_name, cookbook_version)

        meta = cb["metadata"]
        meta.delete("attributes")
        meta.delete("long_description")
        cb["metadata"] = meta

        post(api_url("/environments/#{env}/cookbook_versions"), normal_user,
          payload: payload) do |response|
            response.should look_like({
                                        status: 200,
                                        body: {
                                          cookbook_name => cb,
                                        },
                                      })
          end
      end

      context "CHEF-3813: Return cookbook dependency metadata when a cookbook has dependencies" do

        before :all do
          make_cookbook(admin_user, "foo", "1.0.0")
          make_cookbook(admin_user, "bar", "2.0.0", { dependencies: { "foo" => "> 0.0.0" } })
          make_cookbook(admin_user, "baz", "3.0.0")
          make_cookbook(admin_user, "quux", "4.0.0", { dependencies: { "bar" => "= 2.0.0", "baz" => "= 3.0.0" } })
        end

        after :all do
          delete_cookbook(admin_user, "foo", "1.0.0")
          delete_cookbook(admin_user, "bar", "2.0.0")
          delete_cookbook(admin_user, "baz", "3.0.0")
          delete_cookbook(admin_user, "quux", "4.0.0")
        end

        it "returns dependencies" do
          post(api_url("/environments/_default/cookbook_versions"), normal_user,
            payload: { "run_list" => ["quux"] }) do |response|

              response.should have_status_code 200

              json = parse(response)

              json["foo"]["metadata"]["dependencies"].should eq({})
              json["bar"]["metadata"]["dependencies"].should eq({ "foo" => "> 0.0.0" })
              json["baz"]["metadata"]["dependencies"].should eq({})
              json["quux"]["metadata"]["dependencies"].should eq({ "bar" => "= 2.0.0", "baz" => "= 3.0.0" })
            end
        end
      end

      context "with datestamps in cookbooks and environments" do
        before :each do
          make_cookbook(admin_user, "datestamp", "1.2.20130730201745")
          datestamp_env = new_environment("datestamp_env")
          datestamp_env["cookbook_versions"] = {
            "datestamp" => ">= 1.2.20130730200000",
          }
          add_environment(admin_user, datestamp_env)
        end

        after :each do
          delete_cookbook(admin_user, "datestamp", "1.2.20130730201745")
          delete_environment(admin_user, "datestamp_env")
        end

        it "returns the correct solution" do
          post(api_url("/environments/datestamp_env/cookbook_versions"), admin_user,
            payload: { "run_list" => ["datestamp"] }) do |response|
              response.should have_status_code 200
            end
        end
      end

      # TODO: Check with a cookbook that has some files in segments since we also get
      # back URLs
    end # success cases context
  end # global context
end # describe
