#
#
# Author:: Prabhu Das (<prabhu.das@clogeny.com>)
# Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
# Author:: Siddheshwar More (<siddheshwar.more@clogeny.com>)
# Author:: Ameya Varade (<ameya.varade@clogeny.com>)
# Author:: Lance Albertson (<lance@osuosl.org>)
# 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 File.expand_path("../spec_helper", __dir__)
require "chef/knife/openstack_server_create"
require "support/shared_examples_for_servercreatecommand"
require "support/shared_examples_for_command"

describe Chef::Knife::Cloud::OpenstackServerCreate do
  create_instance = Chef::Knife::Cloud::OpenstackServerCreate.new
  create_instance.define_singleton_method(:post_connection_validations) {}

  it_behaves_like Chef::Knife::Cloud::Command, Chef::Knife::Cloud::OpenstackServerCreate.new
  it_behaves_like Chef::Knife::Cloud::ServerCreateCommand, create_instance

  describe "#create_service_instance" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
    end

    it "return OpenstackService instance" do
      expect(@instance.create_service_instance).to be_an_instance_of(Chef::Knife::Cloud::OpenstackService)
    end

    it "has custom_arguments as its option" do
      expect(@instance.options.include? :custom_attributes).to be true
    end
  end

  describe "#validate_params!" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      allow(@instance.ui).to receive(:error)
      @instance.config[:connection_protocol] = "ssh"
      @instance.config[:identity_file] = "identity_file"
      @instance.config[:image_os_type] = "linux"
      @instance.config[:openstack_ssh_key_id] = "openstack_ssh_key"
      @instance.config[:openstack_region] = "test-region"
    end

    it "run successfully on all params exist" do
      expect { @instance.validate_params! }.to_not raise_error
    end
  end

  describe "#before_exec_command" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      allow(@instance.ui).to receive(:error)
      @instance.config[:chef_node_name] = "chef_node_name"
      @instance.config[:image] = "image"
      @instance.config[:flavor] = "flavor"
      @instance.config[:openstack_security_groups] = "openstack_security_groups"
      @instance.config[:server_create_timeout] = "server_create_timeout"
      @instance.config[:openstack_ssh_key_id] = "openstack_ssh_key"
      @instance.config[:network_ids] = "test_network_id"
      allow(@instance.config[:network_ids]).to receive(:map).and_return(@instance.config[:network_ids])
      @instance.config[:metadata] = "foo=bar"
    end

    it "set create_options" do
      @instance.service = double
      allow(@instance.service).to receive(:get_image).and_return(get_mock_resource("image"))
      allow(@instance.service).to receive(:get_flavor).and_return(get_mock_resource("flavor"))
      expect(@instance.service).to receive(:create_server_dependencies)
      expect(@instance).to receive(:post_connection_validations)
      @instance.before_exec_command
      expect(@instance.create_options[:server_def][:name]).to be == @instance.config[:chef_node_name]
      expect(@instance.create_options[:server_def][:image_ref]).to be == @instance.config[:image]
      expect(@instance.create_options[:server_def][:security_groups]).to be == @instance.config[:openstack_security_groups]
      expect(@instance.create_options[:server_def][:flavor_ref]).to be == @instance.config[:flavor]
      expect(@instance.create_options[:server_def][:nics]).to be == @instance.config[:network_ids]
      expect(@instance.create_options[:server_def][:metadata]).to be == @instance.config[:metadata]
      expect(@instance.create_options[:server_def][:region]).to be == @instance.config[:openstack_region]
      expect(@instance.create_options[:server_create_timeout]).to be == @instance.config[:server_create_timeout]
    end

    it "doesn't set user data in server_def if user_data not specified" do
      @instance.service = double("Chef::Knife::Cloud::OpenstackService", create_server_dependencies: nil)
      allow(@instance.service).to receive(:get_image).and_return(get_mock_resource("image"))
      allow(@instance.service).to receive(:get_flavor).and_return(get_mock_resource("flavor"))
      expect(@instance).to receive(:post_connection_validations)
      @instance.before_exec_command
      expect(@instance.create_options[:server_def]).to_not include(:user_data)
    end

    it "sets user data" do
      user_data = "echo 'hello world' >> /tmp/user_data.txt"
      @instance.config[:user_data] = user_data
      @instance.service = double("Chef::Knife::Cloud::OpenstackService", create_server_dependencies: nil)
      allow(@instance.service).to receive(:get_image).and_return(get_mock_resource("image"))
      allow(@instance.service).to receive(:get_flavor).and_return(get_mock_resource("flavor"))
      expect(@instance).to receive(:post_connection_validations)
      @instance.before_exec_command
      expect(@instance.create_options[:server_def][:user_data]).to be == user_data
    end

    context "with multiple network_ids specified" do
      before(:each) do
        @instance.service = double
        allow(@instance.service).to receive(:get_image).and_return(get_mock_resource("image"))
        allow(@instance.service).to receive(:get_flavor).and_return(get_mock_resource("flavor"))
        expect(@instance.service).to receive(:create_server_dependencies)
        @instance.config[:network_ids] = "test_network_id1,test_network_id2"
        allow(@instance.config[:network_ids]).to receive(:map).and_return(@instance.config[:network_ids].split(","))
        expect(@instance).to receive(:post_connection_validations)
      end

      it "creates the server_def with multiple nic_ids." do
        @instance.before_exec_command
        expect(@instance.create_options[:server_def][:nics]).to be == %w{test_network_id1 test_network_id2}
      end
    end

    it "ensures default value for metadata" do
      options = @instance.options
      expect(options[:metadata][:default]).to be_nil
    end
  end

  describe "#after_exec_command" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      allow(@instance).to receive(:msg_pair)
    end

    it "don't set openstack_floating_ip on missing openstack_floating_ip option" do
      # default openstack_floating_ip is '-1'
      @instance.config[:openstack_floating_ip] = "-1"
      @instance.service = Chef::Knife::Cloud::Service.new(config: @instance.config)
      @instance.server = double
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.1.1" }] })
      expect(@instance).to receive(:bootstrap)
      @instance.after_exec_command
    end

    it "set openstack_floating_ip on openstack_floating_ip option" do
      @instance.config[:openstack_floating_ip] = nil
      @instance.service = Chef::Knife::Cloud::Service.new(config: @instance.config)
      @instance.server = double

      @network = double
      @ports = ["id" => "test",
                "fixed_ips" => ["ip_address" => "127.0.1.1"]]
      allow(@network).to receive(:list_ports).and_return(body: { "ports" => @ports })
      @floating_ips = ["id" => "test",
                       "fixed_ips" => ["ip_address" => "127.0.1.1"]]
      allow(@network).to receive(:list_floating_ips).and_return(body: { "floatingips" => @floating_ips })
      allow(@network).to receive(:associate_floating_ip)
      allow(@instance.service).to receive(:network).and_return(@network)

      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.1.1" }] })
      expect(@instance).to receive(:bootstrap)
      allow(@instance.service).to receive(:connection).and_return(double)
      free_floating = Object.new
      free_floating.define_singleton_method(:fixed_ip) { return nil }
      free_floating.define_singleton_method(:ip) { return "127.0.0.1" }
      expect(@instance.service.connection).to receive(:addresses).and_return([free_floating])
      @instance.after_exec_command
    end

    it "raise error on unavailability of free_floating ip" do
      @instance.config[:openstack_floating_ip] = nil
      @instance.service = Chef::Knife::Cloud::Service.new(config: @instance.config)
      allow(@instance.ui).to receive(:fatal)
      @instance.server = double
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.1.1" }] })
      expect(@instance).to_not receive(:bootstrap)
      allow(@instance.service).to receive(:connection).and_return(double)
      free_floating = Object.new
      free_floating.define_singleton_method(:fixed_ip) { return "127.0.0.1" }
      expect(@instance.service.connection).to receive(:addresses).and_return([free_floating])
      expect { @instance.after_exec_command }.to raise_error(Chef::Knife::Cloud::CloudExceptions::ServerSetupError, "Unable to assign a Floating IP from allocated IPs.")
    end
  end

  describe "#before_bootstrap" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      @instance.server = double
      # default bootstrap_network is public
      @instance.config[:bootstrap_network] = "public"
      # default no network is true
      @instance.config[:network] = true
      @instance.config[:ssh_password] = "config_ssh_password"
    end

    after(:each) do
      @instance.config.delete(:ssh_password)
    end

    context "when no-network option specified" do
      before(:each) { @instance.config[:network] = false }

      it "set public ip as a bootstrap ip if both public and private ip available" do
        allow(@instance.server).to receive(:addresses).and_return({ "private" => [{ "version" => 4, "addr" => "127.0.0.1" }], "public" => [{ "version" => 4, "addr" => "127.0.0.2" }] })
        @instance.before_bootstrap
        expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.2"
      end

      it "set private-ip as a bootstrap ip if private ip is available" do
        allow(@instance.server).to receive(:addresses).and_return({ "private" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
        @instance.before_bootstrap
        expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.1"
      end

      it "set available ip as a bootstrap ip if no public, private ip available" do
        allow(@instance.server).to receive(:addresses).and_return({ 1 => [{ "version" => 4, "addr" => "127.0.0.1" }] })
        @instance.before_bootstrap
        expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.1"
      end
    end

    it "set bootstrap_ip" do
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.1"
    end

    it "set private-ip as a bootstrap-ip if private-network option set" do
      allow(@instance.server).to receive(:addresses).and_return({ "private" => [{ "version" => 4, "addr" => "127.0.0.1" }], "public" => [{ "version" => 4, "addr" => "127.0.0.2" }] })
      @instance.config[:private_network] = true
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.1"
    end

    it "raise error on nil bootstrap_ip" do
      allow(@instance.ui).to receive(:error)

      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => nil }] })
      expect { @instance.before_bootstrap }.to raise_error(Chef::Knife::Cloud::CloudExceptions::BootstrapError, "No IP address available for bootstrapping.")
    end

    it "set public ip as default bootstrap network is public" do
      allow(@instance.server).to receive(:addresses).and_return({ "private" => [{ "version" => 4, "addr" => "127.0.0.1" }], "public" => [{ "version" => 4, "addr" => "127.0.0.2" }] })
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.2"
    end

    it "configures the bootstrap to use alternate network" do
      allow(@instance.server).to receive(:addresses).and_return({ "foo" => [{ "version" => 1, "addr" => "127.0.0.1" }], "private" => [{ "version" => 4, "addr" => "127.0.0.2" }], "public" => [{ "version" => 4, "addr" => "127.0.0.3" }] })
      @instance.config[:bootstrap_network] = "foo"
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_ip_address]).to be == "127.0.0.1"
    end

    it "configures the bootstrap to use the server password" do
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      @instance.config.delete(:ssh_password)
      server_password = "adFRjk1089"
      allow(@instance.server).to receive(:password).and_return(server_password)
      @instance.before_bootstrap
      expect(@instance.config[:ssh_password]).to be == server_password
    end

    it "configures the bootstrap to use the config ssh password" do
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      server_password = "config_ssh_password"
      @instance.config[:ssh_password] = server_password
      expect(@instance.server).to_not receive(:password)
      @instance.before_bootstrap
      expect(@instance.config[:ssh_password]).to be == server_password
    end

    it "configures the default private bootstrap network to use 'private'" do
      allow(@instance.server).to receive(:addresses).and_return({ "private" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      @instance.config[:private_network] = true
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_network]).to be == "private"
    end

    it "configures the bootstrap to use alternate private network" do
      allow(@instance.server).to receive(:addresses).and_return({ "secure" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      @instance.config[:bootstrap_network] = "secure"
      @instance.config[:private_network] = true
      @instance.before_bootstrap
      expect(@instance.config[:bootstrap_network]).to be == "secure"
    end

    it "set openstack ohai hint" do
      allow(@instance.server).to receive(:addresses).and_return({ "public" => [{ "version" => 4, "addr" => "127.0.0.1" }] })
      @instance.before_bootstrap
      expect(@instance.config[:hints]).to be == { "openstack" => {} }
    end
  end

  describe "#post_connection_validations" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      allow(@instance.ui).to receive(:error)
    end

    it "raise error on invalid image" do
      allow(@instance).to receive(:is_flavor_valid?).and_return(true)
      allow(@instance).to receive(:is_floating_ip_valid?).and_return(true)
      expect(@instance).to receive(:is_image_valid?).and_return(false)
      expect { @instance.post_connection_validations }.to raise_error(Chef::Knife::Cloud::CloudExceptions::ValidationError, " You have not provided a valid image ID. Please note the options for this value are -I or --image..")
    end

    it "raise error on invalid flavor" do
      allow(@instance).to receive(:is_image_valid?).and_return(true)
      allow(@instance).to receive(:is_floating_ip_valid?).and_return(true)
      expect(@instance).to receive(:is_flavor_valid?).and_return(false)
      expect { @instance.post_connection_validations }.to raise_error(Chef::Knife::Cloud::CloudExceptions::ValidationError, " You have not provided a valid flavor ID. Please note the options for this value are -f or --flavor..")
    end

    it "raise error on invalid floating IP" do
      allow(@instance).to receive(:is_flavor_valid?).and_return(true)
      allow(@instance).to receive(:is_image_valid?).and_return(true)
      expect(@instance).to receive(:is_floating_ip_valid?).and_return(false)
      expect { @instance.post_connection_validations }.to raise_error(Chef::Knife::Cloud::CloudExceptions::ValidationError, " You have either requested an invalid floating IP address or none are available..")
    end
  end

  describe "#is_floating_ip_valid?" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      # Default value
      @instance.config[:openstack_floating_ip] = "-1"
      @instance.service = double
    end

    it "returns true for default" do
      expect(@instance.is_floating_ip_valid?).to be true
    end

    it "returns false if no floating IPs" do
      @instance.config.delete(:openstack_floating_ip)
      expect(@instance.service).to receive_message_chain(:connection, :addresses).and_return([])
      expect(@instance.is_floating_ip_valid?).to be false
    end

    context "when floating ip requested without value" do
      it "returns true if fixed_ip is nil" do
        @instance.config[:openstack_floating_ip] = nil
        obj = Object.new
        obj.define_singleton_method(:fixed_ip) { nil }
        expect(@instance.service).to receive_message_chain(:connection, :addresses).and_return([obj])
        expect(@instance.is_floating_ip_valid?).to be true
      end
    end

    context "when floating ip requested with value" do
      before { @instance.config[:openstack_floating_ip] = "127.0.0.1" }
      after { @instance.config.delete(:openstack_floating_ip) }

      it "returns true if requested floating IP is exist" do
        obj = Object.new
        obj.define_singleton_method(:ip) { return "127.0.0.1" }
        expect(@instance.service).to receive_message_chain(:connection, :addresses).and_return([obj])
        expect(@instance.is_floating_ip_valid?).to be true
      end

      it "returns false if requested floating IP does not exist" do
        obj = Object.new
        obj.define_singleton_method(:ip) { return "127.0.1.1" }
        expect(@instance.service).to receive_message_chain(:connection, :addresses).and_return([obj])
        expect(@instance.is_floating_ip_valid?).to be false
      end
    end
  end

  describe "#is_image_valid?" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      @instance.service = double
      @instance.config[:image] = "image_id"
    end

    after(:each) do
      @instance.config.delete(:image)
    end

    it "returns false on invalid image" do
      expect(@instance.service).to receive_message_chain(:get_image).and_return(nil)
      expect(@instance.is_image_valid?).to be false
    end

    it "returns true on valid image" do
      expect(@instance.service).to receive_message_chain(:get_image).and_return("image")
      expect(@instance.is_image_valid?).to be true
    end
  end

  describe "#is_flavor_valid?" do
    before(:each) do
      @instance = Chef::Knife::Cloud::OpenstackServerCreate.new
      @instance.service = double
      @instance.config[:flavor] = "flavor"
    end

    after(:each) do
      @instance.config.delete(:flavor)
    end

    it "returns false on invalid flavor" do
      expect(@instance.service).to receive_message_chain(:get_flavor).and_return(nil)
      expect(@instance.is_flavor_valid?).to be false
    end

    it "returns true on valid flavor" do
      expect(@instance.service).to receive_message_chain(:get_flavor).and_return("flavor")
      expect(@instance.is_flavor_valid?).to be true
    end
  end
end
