# -*- coding: utf-8 -*-
# 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/auth_headers_util'
require 'pedant/rspec/environment_util'

describe "Environments API Endpoint", :environments do
  include Pedant::RSpec::EnvironmentUtil
  include Pedant::RSpec::AuthHeadersUtil

  # include roles for testing w/ environments roles method
  #include Pedant::RSpec::RoleUtil

  let(:requestor) { admin_user }

  # Deal with subtly different error messages/codes in one place

  let(:new_environment_name) { 'pedant_testing_environment' }
  let(:non_existent_environment_name) { 'pedant_dummy_environment' }

  context 'POST /environments' do
    let(:request_method)  { :POST }
    let(:request_url)     { api_url '/environments' }
    let(:request_payload) { full_environment(new_environment_name) }

    let(:expected_response) { resource_created_exact_response }
    let(:created_resource)  { { "uri" => api_url("/environments/#{new_environment_name}") } }
    let(:persisted_resource) { get_environment requestor, new_environment_name }

    respects_maximum_payload_size

    context 'with no additional environments' do
      before(:each) { delete_environment(admin_user, new_environment_name) }
      after(:each) { delete_environment(admin_user, new_environment_name) }


      def self.should_create_an_environment(&additional_examples)
        should_respond_with 201, 'and a correct path'

        it 'should persist the environment' do
          response.should have_status_code 201

          # Verify that it comes back properly
          persisted_resource.should look_like http_200_response.with(body_exact: full_environment(new_environment_name))
        end

        instance_eval(&additional_examples) if additional_examples
      end

      context 'when creating a valid environment', :smoke do
        before(:each) { get_environment(admin_user, new_environment_name).should have_status_code 404 }
        let(:request_payload) { new_environment(new_environment_name) }

        should_create_an_environment
      end # when creating a valid environment


      context 'when handling the payload', :pedantic do
        context 'with authentication headers' do
          # Unconverted Auth Header DSL
          let(:method) { request_method }
          let(:url)    { request_url }
          let(:body)   { request_payload }
          let(:response_should_be_successful) do
            response.
              should look_like({
              :status => 201,
              :body_exact => {
              "uri" => api_url("/environments/#{new_environment_name}")
            }
            })
          end

          let(:success_user) { admin_user }
          let(:failure_user) { outside_user }

          include_context 'handles authentication headers correctly'
        end # with authentication headers

        context 'when unable to accept application/json' do
          let(:request_headers) { { "Accept" => "application/xml" } }
          let(:expected_response) { not_acceptable_response }

          should_respond_with 406
        end

        context 'when sending something other than application/json' do
          let(:request_headers) { { "Content-Type" => "application/xml" } }
          let(:request_payload) { '<environment name="blah"></environment>' }

          skip "webmachine not sucking" do
            should_respond_with 415
          end
        end

        context 'with unparsable JSON payload', :validation do
          let(:request_payload) { '{"hi' }
          let(:expected_response) { bad_request_response }

          should_respond_with 400
        end

        context 'with empty request payload', :validation do
          let(:request_payload) { '' }
          let(:expected_response) { bad_request_response }

          should_respond_with 400
        end
      end

      context 'when creating another "_default" environment' do
        let(:request_payload) { new_environment '_default' }
        let(:expected_response) { conflict_exact_response }
        let(:conflict_error_message) { [ "Environment already exists" ] }

        should_respond_with 409
      end

      context 'when validating', :pedantic do
        include_context "environment_body_util"

        context 'with invalid body' do
          def self.should_respond_with_bad_request(message, _payload)
            context message, :validation do
              let(:request_payload) { _payload }
              should_respond_with_error 400, incorrect_json_type_body_msg
            end
          end

          context 'with an array', :validation do
            let(:request_payload) { '["name","blah"]' }
            should_respond_with_error 400, incorrect_json_type_body_msg
          end

          should_respond_with_bad_request 'with a string', '"environment"'
          should_respond_with_bad_request 'with a number', '-1'
          should_respond_with_bad_request 'with a floating number', '9.9'
          should_respond_with_bad_request 'with a boolean', 'true'
          should_respond_with_bad_request 'with a null', 'null'

          fails_with_value("blah", "anyoldvalue", "Invalid key blah in request body")
        end

        context 'for name' do
          fails_with_value("name", :delete, "Field 'name' missing", nil, false)
          fails_with_value("name", "", "Field 'name' invalid", nil, false)
          fails_with_value("name", "abc!123", "Field 'name' invalid", nil, false)
          fails_with_value("name", "abc 123", "Field 'name' invalid", nil, false)

          # TODO: Looks like we're losing the null character with the
          # ejson:decode call which is eating it silently
          # OSC is not using ejson anymore.
          # Test works on OSC, but unknown if it works on the other platforms
          context 'ejson:decode eats nulls', :skip do
            fails_with_value("name", "abc\u0000123",
                             "Field 'name' invalid")
          end

          fails_with_value("name", "大爆発",
                           "Field 'name' invalid", nil, false)
          fails_with_value("name", 1999, incorrect_json_type_name_msg, nil, false)
          fails_with_value("name", nil, incorrect_json_type_name_msg)
          fails_with_value("name", true, incorrect_json_type_name_msg, nil, false)
          fails_with_value("name", [], incorrect_json_type_name_msg, nil, false)
          fails_with_value("name", {}, incorrect_json_type_name_msg, nil, false)
        end

        context "with weird environment name" do
          let(:new_environment_name) { "ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-0123456789" }
          let(:request_payload) { default_payload }
          let(:expected_response) { created_response }
          let(:created_resource) { { "uri" => api_url("/environments/#{new_environment_name}") } }

          should_respond_with 201, 'and the environment url'
        end

        context "for description" do
          succeeds_with_value("description", :delete)
          succeeds_with_value("description", "")
          succeeds_with_value("description", "normal text")
          succeeds_with_value("description", "これは日本語だ")
          succeeds_with_value("description",
                              "\u0048\u0065\u006c\u006c\u006f\u0020\u65e5\u672c!")

          fails_with_value("description", 1999, "Field 'description' invalid", nil, false)
        end

        context "for json_class" do
          succeeds_with_value("json_class", "Chef::Environment")
          succeeds_with_value("json_class", :delete)
          fails_with_value("json_class", "", incorrect_json_type_class_msg)
          fails_with_value("json_class", 1999, incorrect_json_type_class_msg, nil, false)
          fails_with_value("json_class", "notaclass", incorrect_json_type_class_msg, nil, false)
        end

        context "for chef_type" do
          succeeds_with_value("chef_type", "environment")
          succeeds_with_value("chef_type", :delete)

          fails_with_value("chef_type", "", incorrect_json_type_type_msg)
          fails_with_value("chef_type", 1999, incorrect_json_type_type_msg)
          fails_with_value("chef_type", "notaclass", incorrect_json_type_type_msg)
        end

        context "for default_attributes" do
          succeeds_with_value("default_attributes", :delete)
          succeeds_with_value("default_attributes", {})
          succeeds_with_value("default_attributes", {"key1" => "value1",
                              "key2" => "value2"})

          fails_with_value("default_attributes", 1999, "Field 'default_attributes' is not a hash", nil, false)
          fails_with_value("default_attributes", "hello",
                           "Field 'default_attributes' is not a hash", nil, false)

          context "for keys" do
            succeeds_with_value("default_attributes", {"key" => "value"})
            succeeds_with_value("default_attributes", {"鍵" => "value"})

            succeeds_with_value("default_attributes", {"" => "value"})

            fails_with_value("default_attributes", '{null:"value"}',
                             "Field 'default_attributes' is not a hash", nil, false)
            fails_with_value("default_attributes", '{99:"value"}',
                              "Field 'default_attributes' is not a hash", nil, false)
            fails_with_value("default_attributes", '{"key":"value","key":"value"}',
                             "Field 'default_attributes' is not a hash", nil, false)
          end

          context "for values" do
            succeeds_with_value("default_attributes", {"key" => ""})
            succeeds_with_value("default_attributes", {"key" => nil})
            succeeds_with_value("default_attributes", {"key" => 99})
            succeeds_with_value("default_attributes", {"key" => "値"})
          end
        end

        context "for override_attributes" do
          succeeds_with_value("override_attributes", :delete)
          succeeds_with_value("override_attributes", {})
          succeeds_with_value("override_attributes", {"key1" => "value1",
                              "key2" => "value2"})

          # These cause internal server errors in Ruby
          fails_with_value("override_attributes", 1999,
                           "Field 'override_attributes' is not a hash", nil, false)
          fails_with_value("override_attributes", "hello",
                           "Field 'override_attributes' is not a hash", nil, false)
          context "for keys" do
            succeeds_with_value("override_attributes", {"key" => "value"})
            succeeds_with_value("override_attributes", {"鍵" => "value"})

            succeeds_with_value("override_attributes", {"" => "value"})

            # These cause internal server errors in Ruby
            fails_with_value("override_attributes", '{null:"value"}',
                             "Field 'override_attributes' is not a hash", nil, false)
            fails_with_value("override_attributes", '{99:"value"}',
                             "Field 'override_attributes' is not a hash", nil, false)
            fails_with_value("override_attributes", '{"key":"value","key":"value"}',
                             "Field 'override_attributes' is not a hash", nil, false)
          end

          context "for values" do
            succeeds_with_value("override_attributes", {"key" => ""})
            succeeds_with_value("override_attributes", {"key" => nil})
            succeeds_with_value("override_attributes", {"key" => 99})
            succeeds_with_value("override_attributes", {"key" => "値"})
          end
        end

        context "for cookbooks" do
          succeeds_with_value("cookbook_versions", :delete)
          succeeds_with_value("cookbook_versions", {})

          # These cause internal server errors in Ruby
          fails_with_value("cookbook_versions", 1999,
                           "Field 'cookbook_versions' is not a hash", nil, false)
          fails_with_value("cookbook_versions", "hello",
                           "Field 'cookbook_versions' is not a hash", nil, false)

          context "for cookbook names" do
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.0.0"})

            fails_with_value("cookbook_versions", {"the cookbook" => ">= 1.0.0"},
                             "Invalid key 'the cookbook' for cookbook_versions", nil, false)
            fails_with_value("cookbook_versions", {"料理書" => ">= 1.0.0"},
                             "Invalid key '料理書' for cookbook_versions", nil, false)
            fails_with_value("cookbook_versions", {"" => ">= 1.0.0"},
                             "Invalid key '' for cookbook_versions", nil, false)
            fails_with_value("cookbook_versions", '{null:">= 1.0.0"}',
                             "Field 'cookbook_versions' is not a hash", nil, false)
            fails_with_value("cookbook_versions", '{1999:">= 1.0.0"}',
                             "Field 'cookbook_versions' is not a hash", nil, false)
          end

          context "for versions" do
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "<= 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "> 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "< 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "= 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "~> 1.0.0"})
            succeeds_with_value("cookbook_versions", {"cookbook" => "1.0.0"})

            fails_with_value("cookbook_versions", {"cookbook" => nil},
                             "Invalid value 'null' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => [">= 1.0.0"]},
                             "Invalid value '[huh]' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => []},
                             "Invalid value '[huh]' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1.0.0.0"},
                             "Invalid value '>= 1.0.0.0' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1,0,0"},
                             "Invalid value '>= 1,0,0' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1.a.b"},
                             "Invalid value '>= 1.a.b' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1.0rc1"},
                             "Invalid value '>= 1.0rc1' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">=1.0.0"},
                             "Invalid value '>=1.0.0' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => " >= 1.0.0"},
                             "Invalid value ' >= 1.0.0' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ">=  1.0.0"},
                             "Invalid value '>=  1.0.0' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => [">= 1.0", ">= 2.0"]},
                             'Invalid value \'[<<">= 1.0">>,<<">= 2.0">>]\'' +
                             ' for cookbook_versions')
            fails_with_value("cookbook_versions", {"cookbook" => 1},
                             "Invalid value '1' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => 1.1},
                             "Invalid value '1.1' for cookbook_versions")
            fails_with_value("cookbook_versions", {"cookbook" => ""},
                             "Invalid value '' for cookbook_versions")

            # test version integer constraints
            # datestamp version
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.2.20130730201745"})

            # negative version
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1.-2.3"},
                             "Invalid value '>= 1.-2.3' for cookbook_versions")

            # 4-byte version
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.2.2147483647"})

            # 4-byte overflow version
            succeeds_with_value("cookbook_versions", {"cookbook" => ">= 1.2.2147483669"})

            # 8-byte overflow version
            fails_with_value("cookbook_versions", {"cookbook" => ">= 1.2.9223372036854775849"},
                             "Invalid value '>= 1.2.9223372036854775849' for cookbook_versions")

          end
        end
      end # when validating

      context 'when creating an environment without json_class' do
        before(:each) { get_environment(admin_user, new_environment_name).should have_status_code 404 }

        let(:request_payload) { new_environment(new_environment_name).except('json_class') }

        should_create_an_environment
      end # when creating an environment without json_class

    end # with no additional environments

    context 'with non-default environments' do

      before(:each) { add_environment(admin_user, full_environment(new_environment_name)) }
      after(:each)  { delete_environment(admin_user, new_environment_name) }

      context 'with the same name as another non-default environment' do
        let(:request_payload) { new_environment(new_environment_name) }
        let(:expected_response) { conflict_exact_response }
        let(:conflict_error_message) { ["Environment already exists"] }

        should_respond_with 409
      end

    end # with non-default environments
  end # POST /environments
end # Environments Endpoint
