#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.  The
# ASF licenses this file to you 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.

$:.unshift File.join(File.dirname(__FILE__), '..')
require "deltacloud/test_setup.rb"

INSTANCES = "/instances"

describe 'Deltacloud API instances collection' do
  include Deltacloud::Test::Methods
  need_collection :instances
  #make sure we have at least one running instance to test
  if collection_supported :instances
    #keep track of what we create for deletion after tests:
    @@created_resources = {:instances=>[], :keys=>[], :images=>[], :firewalls=>[]}
    image_id = get_a("image")
    res = post(INSTANCES, :image_id=>image_id)
    unless res.code == 201
      raise Exception.new("Failed to create instance from image_id #{image_id}")
    end
    @@my_instance_id = (res.xml/'instance')[0][:id]
    @@created_resources[:instances] << @@my_instance_id
    #currently supported providers transition from PENDING to either RUNNING or STOPPED
    attempts = 0
    begin
      res = get(INSTANCES+"/"+@@my_instance_id)
      attempts += 1
      state = (res.xml/'instance/state').text
      if state == "STOPPED"
        start_res = post(INSTANCES+"/"+@@my_instance_id+"/start", "")
        attempts = 0
      elsif state == "PENDING"
        sleep(10)
      end
    end while attempts < 30 and state != "RUNNING"
  end

  #stop/destroy the resources we created for the tests
  MiniTest::Unit.after_tests {
puts "CLEANING UP... resources for deletion: #{@@created_resources.inspect}"
    #instances:
    #first try to stop them all. They may still be in another state, so keep trying for a while.
    @@created_resources[:instances].each_index do |i|
      attempts = 0
      begin
        stop_res = post(INSTANCES+"/"+@@created_resources[:instances][i]+"/stop", "")
        @@created_resources[:instances][i] = nil if stop_res.code == 202
      rescue Exception => e
        sleep(10)
        attempts += 1
        retry if (attempts <= 5)
      end
    end
    @@created_resources[:instances].compact!
    #then destroy them. They may still be still stopping, so keep trying for a while.
    (0..20).each do
      @@created_resources[:instances].each_index do |i|
        attempts = 0
        begin
          destroy_res = delete(INSTANCES+"/"+@@created_resources[:instances][i])
          @@created_resources[:instances][i] = nil if destroy_res.code == 204
        rescue Exception => e
          if e.http_code == 404 # the stop operation may have destroyed it already
            @@created_resources[:instances][i] = nil
          elsif (attempts <= 5)
            attempts += 1
            sleep(10)
            retry
          end
        end
      end
      @@created_resources[:instances].compact!
      break if @@created_resources[:instances].empty?
      puts "sleeping 10"
      sleep(10)
    end
    @@created_resources.delete(:instances) if @@created_resources[:instances].empty?
    #keys
    [:keys, :images, :firewalls].each do |col|
      @@created_resources[col].each do |k|
        attempts = 0
        begin
          res = delete("/#{col}/#{k}")
          @@created_resources[col].delete(k) if res.code == 204
        rescue Exception => e
          sleep(10)
          attempts += 1
          retry if (attempts <=5)
        end
      end
      @@created_resources.delete(col) if @@created_resources[col].empty?
    end
puts "CLEANUP attempt finished... resources looks like: #{@@created_resources.inspect}"
    raise Exception.new("Unable to delete all created resources - please check: #{@@created_resources.inspect}") unless @@created_resources.empty?
  }

  def each_instance_xml(&block)
    res = get(INSTANCES)
    (res.xml/'instances/instance').each do |r|
      instance_res = get(INSTANCES + '/' + r[:id])
      yield instance_res.xml
    end
  end

  def wait_until_running_or_stopped(res, instance_id)
    attempts = 0
    state = (res.xml/'instance/state').text
    while (attempts < 30 and not ["RUNNING","STOPPED"].include?(state)) do
      sleep(10)
      res = get(INSTANCES+"/"+instance_id)
      state = (res.xml/'instance/state').text
      attempts += 1
    end
  end

  #Run the 'common' tests for all collections defined in common_tests_collections.rb
  CommonCollectionsTest::run_collection_and_member_tests_for("instances")

  #Now run the instances-specific tests:

  it 'must have a legal "state" element defined for each instance in collection, or no "state" at all' do
    res = get(INSTANCES)
    (res.xml/'instances/instance').each do |r|
      # provider may not return state for each instance in collection because of performance reasons
      (r/'state').first.must_match /(RUNNING|STOPPED|PENDING)/ unless (r/'state').empty?
    end
  end

  it 'must have the "owner_id" element for each instance and it should match with the one in collection' do
    res = get(INSTANCES)
    (res.xml/'instances/instance').each do |r|
      instance_res = get(INSTANCES + '/' + r[:id])
      (instance_res.xml/'owner_id').wont_be_empty
      (instance_res.xml/'owner_id').first.text.must_equal((r/'owner_id').first.text)
    end
  end

  it 'each instance must link to the realm that was used during instance creation' do
    each_instance_xml do |instance_xml|
      (instance_xml/'realm').wont_be_empty
      (instance_xml/'realm').size.must_equal 1
      (instance_xml/'realm').first[:id].wont_be_nil
      (instance_xml/'realm').first[:href].wont_be_nil
      (instance_xml/'realm').first[:href].must_match /\/#{(instance_xml/'realm').first[:id]}$/
    end
  end

  it 'each instance must link to the image that was used to during instance creation' do
    each_instance_xml do |instance_xml|
      (instance_xml/'image').wont_be_empty
      (instance_xml/'image').size.must_equal 1
      (instance_xml/'image').first[:id].wont_be_nil
      (instance_xml/'image').first[:href].wont_be_nil
      (instance_xml/'image').first[:href].must_match /\/#{(instance_xml/'image').first[:id]}$/
    end
  end

  it 'each instance must link to the hardware_profile that was used to during instance creation' do
    each_instance_xml do |instance_xml|
      (instance_xml/'hardware_profile').wont_be_empty
      (instance_xml/'hardware_profile').size.must_equal 1
      (instance_xml/'hardware_profile').first[:id].wont_be_nil
      (instance_xml/'hardware_profile').first[:href].wont_be_nil
      (instance_xml/'hardware_profile').first[:href].must_match /\/#{(instance_xml/'hardware_profile').first[:id]}$/
    end
  end

  it 'each (NON-STOPPED) instance should advertise the public and private addresses of the instance' do
    each_instance_xml do |instance_xml|
      #skip this instance if it is in STOPPED state
      next if (instance_xml/'instance/state').text == "STOPPED"
      (instance_xml/'public_addresses').wont_be_empty
      (instance_xml/'public_addresses').size.must_equal 1
      (instance_xml/'public_addresses/address').each do |a|
        a[:type].wont_be_nil
        a.text.strip.wont_be_empty
      end
      (instance_xml/'private_addresses').wont_be_empty
      (instance_xml/'private_addresses').size.must_equal 1
      (instance_xml/'private_addresses/address').each do |a|
        a[:type].wont_be_nil
        a.text.strip.wont_be_empty
      end
    end
  end

  it 'each instance should advertise the storage volumes used by the instance' do
      each_instance_xml do |i|
        (i/'storage_volumes').wont_be_empty
      end
  end

  it 'each instance should advertise the list of actions that can be executed for each instance' do
    each_instance_xml do |instance_xml|
      (instance_xml/'actions/link').each do |l|
        l[:href].wont_be_nil
        l[:href].must_match /^http/
        l[:method].wont_be_nil
        l[:rel].wont_be_nil
      end
    end
  end

  it 'should allow to create new instance using image without realm' do
    #random image and create instance
    image_id = get_a("image")
    image_id.wont_be_nil
    res = post(INSTANCES, :image_id=>image_id)
    res.code.must_equal 201
    res.headers[:location].wont_be_nil
    created_instance_id = (res.xml/'instance')[0][:id]
    #GET the instance
    res = get(INSTANCES+"/"+created_instance_id)
    res.code.must_equal 200
    (res.xml/'instance').first[:id].must_equal created_instance_id
    (res.xml/'instance/image').first[:id].must_equal image_id
    #confirm instance progresses from PENDING to initial (non-error) state.
    wait_until_running_or_stopped(res, created_instance_id)
    #mark it for stopping/destroying after tests run:
    @@created_resources[:instances] << created_instance_id
  end

  it 'should allow to create new instance using image and realm' do
    #random image, realm and create instance
    image_id = get_a("image")
    image_id.wont_be_nil
    realm_id = get_a("realm")
    realm_id.wont_be_nil
    res = post(INSTANCES, :image_id=>image_id, :realm_id=>realm_id)
    res.code.must_equal 201
    res.headers[:location].wont_be_nil
    created_instance_id = (res.xml/'instance')[0][:id]
    #GET the instance
    res = get(INSTANCES+"/"+created_instance_id)
    res.code.must_equal 200
    (res.xml/'instance').first[:id].must_equal created_instance_id
    (res.xml/'instance/image').first[:id].must_equal image_id
    (res.xml/'instance/realm').first[:id].must_equal realm_id
    #confirm instance progresses from PENDING to initial (non-error) state.
    wait_until_running_or_stopped(res, created_instance_id)
    #mark it for stopping/destroying after tests run:
    @@created_resources[:instances] << created_instance_id
  end

  it 'should allow to create new instance using image, realm and hardware_profile' do
    #random image, realm, hardware_profile and create instance
    image_id = get_a("image")
    image_id.wont_be_nil
    #check if this image defines compatible hw_profiles:
    res = get("/images/"+image_id)
    if (res.xml/'image/hardware_profiles').empty?
      hwp_id = get_a("hardware_profile")
    else
      hwp_id = (res.xml/'image/hardware_profiles/hardware_profile').to_a.choice[:id]
    end
    hwp_id.wont_be_nil
    #random realm:
    realm_id = get_a("realm")
    realm_id.wont_be_nil
    res = post(INSTANCES, :image_id=>image_id, :realm_id=>realm_id, :hwp_id => hwp_id)
    res.code.must_equal 201
    res.headers[:location].wont_be_nil
    created_instance_id = (res.xml/'instance')[0][:id]
    #GET the instance
    res = get(INSTANCES+"/"+created_instance_id)
    res.code.must_equal 200
    (res.xml/'instance').first[:id].must_equal created_instance_id
    (res.xml/'instance/image').first[:id].must_equal image_id
    (res.xml/'instance/realm').first[:id].must_equal realm_id
    (res.xml/'instance/hardware_profile').first[:id].must_equal hwp_id
    #confirm instance progresses from PENDING to initial (non-error) state.
    wait_until_running_or_stopped(res, created_instance_id)
    #mark it for stopping/destroying after tests run:
    @@created_resources[:instances] << created_instance_id
  end

#snapshot (make image of running instance)

  it 'should allow to snapshot running instance if supported by provider' do
    res = get(INSTANCES+"/"+@@my_instance_id)
    #confirm instance is running at all
    (res.xml/'instance/state').text.must_match /RUNNING/
    #check if created instance allows creating image
    instance_actions = (res.xml/'actions/link').to_a.inject([]){|actions, current| actions << current[:rel]; actions}
    #check if the create_image action is available for stopped instances
    unless instance_actions.include?("create_image")
      stop_res = post(INSTANCES+"/"+@@my_instance_id+"/stop", "")
      res = get(INSTANCES+"/"+@@my_instance_id)
      wait_until_running_or_stopped(res, @@my_instance_id)
      instance_actions = (res.xml/'actions/link').to_a.inject([]){|actions, current| actions << current[:rel]; actions}
    end
    skip "no create image support for instance #{@@my_instance_id}" unless instance_actions.include?("create_image")
    #create image
    res = post("/images", :instance_id => @@my_instance_id, :name => random_name)
    res.code.must_equal 201
    my_image_id = (res.xml/'image')[0][:id]
    #mark for deletion later:
    @@created_resources[:images] << my_image_id
    res = get(INSTANCES+"/"+@@my_instance_id)
    #start instance if it was stopped
    if (res.xml/'instance/state').text  == "STOPPED"
      start_res = post(INSTANCES+"/"+@@my_instance_id+"/start", "")
      res = get(INSTANCES+"/"+@@my_instance_id)
      wait_until_running_or_stopped(res, @@my_instance_id)
    end
  end

#
#create with key

  describe "create instance with auth key" do

    need_collection :keys
    need_feature :instances, :authentication_key

      it 'should allow specification of auth key for created instance when supported' do
        #create a key to use
        key_name = random_name
        key_res = post("/keys", :name=>key_name)
        key_res.code.must_equal 201
        key_id = (key_res.xml/'key')[0][:id]
        #create instance with this key:
        image_id = get_a("image")
        res = post(INSTANCES, :image_id => image_id, :keyname => key_id)
        res.code.must_equal 201
        instance_id = (res.xml/'instance')[0][:id]
        #check the key:
        key_used = (res.xml/'instance/authentication/login/keyname')[0].text
        key_used.must_equal key_id
        #mark them for deletion after tests run:
        @@created_resources[:instances] << instance_id
        @@created_resources[:keys] << key_id
    end

  end

#specify user name (feature)
  describe "create instance with user defined name" do

    need_feature :instances, :user_name

    it 'should allow specification of name for created instance when supported' do
      instance_name = random_name
      image_id = get_a("image")
      res = post(INSTANCES, :image_id => image_id, :name => instance_name)
      res.code.must_equal 201
      instance_id = (res.xml/'instance')[0][:id]
      #check the name:
      created_name = (res.xml/'instance/name')[0].text
      created_name.must_equal instance_name
      #confirm instance progresses from PENDING to initial (non-error) state.
      wait_until_running_or_stopped(res, instance_id)
      #mark for deletion:
      @@created_resources[:instances] << instance_id
    end
  end

#create with firewall (feature)
  describe "create instance with firewall" do

    need_collection :firewalls
    need_feature :instances, :firewalls

    it 'should be able to create instance using specified firewall' do
        #create a firewall to use
        fw_name = random_name
        fw_res = post("/firewalls", :name=>fw_name, :description=>"firewall created for instances API test on #{Time.now}")
        fw_res.code.must_equal 201
        fw_id = (fw_res.xml/'firewall')[0][:id]
        ((fw_res.xml/'firewall/name')[0].text).must_equal fw_name
        #create instance with this firewall:
        image_id = get_a("image")
        res = post(INSTANCES, :image_id => image_id, :firewalls1 => fw_id)
        res.code.must_equal 201
        instance_id = (res.xml/'instance')[0][:id]
        #check the firewall:
        fw_used = (res.xml/'instance/firewalls/firewall')[0][:id]
        fw_used.must_equal fw_id
        #mark for deletion:
        @@created_resources[:instances] << instance_id
        @@created_resources[:firewalls] << fw_id
    end

  end
end
