require 'spec_helper'
require 'vagrant-cloudstack/action/run_instance'
require 'vagrant-cloudstack/config'

require 'vagrant'
require 'fog'

describe VagrantPlugins::Cloudstack::Action::RunInstance do
  let(:action) { VagrantPlugins::Cloudstack::Action::RunInstance.new(app, env) }

  let(:create_servers_parameters) do
    {
      :display_name => DISPLAY_NAME,
      :group => nil,
      :zone_id => ZONE_ID,
      :flavor_id => SERVICE_OFFERING_ID,
      :image_id => TEMPLATE_ID,
      'network_ids' => NETWORK_ID
    }
  end

  let(:fake_job_result) do
    {
      'queryasyncjobresultresponse' => {
        'jobstatus' => 1,
        'jobresult' => {
          'portforwardingrule' => {
            'id' => PORT_FORWARDING_RULE_ID
          },
          'networkacl' => {
            'id' => ACL_ID
          },
          'virtualmachine' => {
            'password' => GENERATED_PASSWORD
          }
        }
      }
    }
  end

  let(:list_public_ip_addresses_response) do
    {
      'listpublicipaddressesresponse' => {
        'count' => 1,
        'publicipaddress' =>
        [
          {
            'id' => PF_IP_ADDRESS_ID,
            'ipaddress' => PF_IP_ADDRESS,
            'associatednetworkid' => NETWORK_ID,
            'associatednetworkname' => NETWORK_NAME,
            'vpcid' => VPC_ID
          }
        ]
      }
    }
  end

  let(:network_type) { NETWORK_TYPE_ADVANCED }
  let(:security_groups_enabled) { SECURITY_GROUPS_DISABLED }
  let(:list_zones_response) do
    {
      'listzonesresponse' => {
        'count' => 1,
        'zone' =>
        [
          {
            'tags' => [],
            'id' => ZONE_ID,
            'name' => ZONE_NAME,
            'networktype' => network_type,
            'securitygroupsenabled' => security_groups_enabled
          }
        ]
      }
    }
  end

  let(:list_service_offerings_response) do
    {
      'listserviceofferingsresponse' => {
        'count' => 1,
        'serviceoffering' => [
          {
            'id' => SERVICE_OFFERING_ID,
            'name' => SERVICE_OFFERING_NAME,
            'displaytext' => "Display version of #{SERVICE_OFFERING_NAME}"
          }
        ]
      }
    }
  end

  let(:list_templates_response) do
    {
      'listtemplatesresponse' => {
        'count' => 1,
        'template' => [
          {
            'id' => TEMPLATE_ID,
            'name' => TEMPLATE_NAME
          }
        ]
      }
    }
  end

  let(:list_networks_response) do
    {
      'listnetworksresponse' => {
        'count' => 1,
        'network' => [
          {
            'id' => NETWORK_ID,
            'name' => NETWORK_NAME,
            'vpcid' => VPC_ID,
            'aclid' => ACL_ID
          }
        ]
      }
    }
  end

  let(:list_network_acl_lists_response) do
    {
      'listnetworkacllistsresponse' => {
        'count' => 3,
        'networkacllist' => [
          {
            'id' => ACL_ID,
            'name' => NETWORK_NAME,
            'vpcid' => VPC_ID
          },
          {
            'id' => '13fa8945-9248-13e5-4afa-525405b8977a', 'name' => 'default_allow',
            'description' => 'Default Network ACL Allow All'
          },
          {
            'id' => '13fa283b-9248-13e5-4afa-525405b8977a', 'name' => 'default_deny',
            'description' => 'Default Network ACL Deny All'
          }
        ]
      }
    }
  end
  let(:list_disk_offerings_response) do
    {
      'listdiskofferingsresponse' => {
        'count' => 1,
        'diskoffering' => [
          {
            'id' => DISK_OFFERING_ID,
            'name' => DISK_OFFERING_NAME
          }
        ]
      }
    }
  end

  let(:create_port_forwarding_rule_parameters) do
    {
      networkid: NETWORK_ID,
      ipaddressid: PF_IP_ADDRESS_ID,
      publicport: 49_152,
      privateport: GUEST_PORT_SSH,
      protocol: 'tcp',
      virtualmachineid: SERVER_ID
    }
  end

  let(:create_port_forwarding_rule_respones) do
    {
      'createportforwardingruleresponse' => {
        'id' => PORT_FORWARDING_RULE_ID,
        'jobid' => JOB_ID
      }
    }
  end

  let(:create_network_acl_request) do
    {
      command: 'createNetworkACL',
      aclid: ACL_ID,
      action: 'Allow',
      protocol: 'tcp',
      cidrlist: PF_TRUSTED_NETWORKS,
      startport: GUEST_PORT_SSH,
      endport: GUEST_PORT_SSH,
      icmpcode: nil,
      icmptype: nil,
      traffictype: 'Ingress'
    }
  end

  let(:createNetworkACL_response) do
    {
      'createnetworkaclresponse' => {
        'id' => '5dcb96b5-7785-463d-9a11-d8388c98e4ee',
        'jobid' => JOB_ID
      }
    }
  end

  let(:create_ssh_key_pair_response) do
    {
      'createsshkeypairresponse' => {
        'keypair' => {
          'privatekey' => "#{SSH_GENERATED_PRIVATE_KEY}\n",
          'name' => SSH_GENERATED_KEY_NAME
        }
      }
    }
  end

  describe 'run_instance' do
    subject { action.call(env) }
    let(:app) { double('Vagrant::Action::Warden') }
    let(:ssh_key) { '/some/path' }

    let(:template_name) { TEMPLATE_NAME }
    let(:machine) { double('Vagrant::Machine') }
    let(:data_dir) { double('Pathname') }
    let(:a_path) { double('Pathname') }
    let(:file) { double('File') }
    let(:communicator) { double('VagrantPlugins::CommunicatorSSH::Communicator') }
    let(:communicator_config) { double('VagrantPlugins::...::...Config') }
    let(:cloudstack_compute) { double('Fog::Compute::Cloudstack') }
    let(:servers) { double('Fog::Compute::Cloudstack::Servers') }
    let(:server) { double('Fog::Compute::Cloudstack::Server') }
    let(:ui) { double('Vagrant::UI::Prefixed') }
    let(:root_path) { double('Pathname') }
    let(:env) do
      {
        root_path: root_path,
        ui: ui,
        machine: machine,
        cloudstack_compute: cloudstack_compute
      }
    end

    let(:cloudstack_zone) do
      instance_double('Fog::Compute::Cloudstack::Zone',
                      id: ZONE_ID,
                      name: ZONE_NAME,
                      network_type: network_type,
                      security_groups_enabled: security_groups_enabled)
    end

    before(:each) do
      allow(app).to receive(:call).and_return(true)
      allow(ui).to receive(:info)
      allow(ui).to receive(:detail)

      allow(machine).to receive(:data_dir).and_return(data_dir)
      allow(data_dir).to receive(:join).and_return(a_path)
      allow(a_path).to receive(:open).and_yield(file)

      allow(machine).to receive(:communicate).and_return(communicator)
      allow(machine).to receive_message_chain(:communicate, :ready?).and_return(true)

      allow(machine).to receive(:provider_config).and_return(provider_config)
      expect(server).to receive(:wait_for).and_return(ready = true)
      allow(server).to receive(:job_id).and_return(JOB_ID)
      allow(server).to receive(:password_enabled).and_return(false)
      expect(cloudstack_compute).to receive(:servers).and_return(servers)
      allow(cloudstack_compute).to receive(:send).with(:list_zones, available: true, name: ZONE_NAME).and_return(list_zones_response)
      allow(cloudstack_compute).to receive(:send).with(:list_service_offerings, listall: true, name: SERVICE_OFFERING_NAME)
        .and_return(list_service_offerings_response)
      allow(cloudstack_compute).to receive(:send)
        .with(:list_templates, zoneid: ZONE_ID, templatefilter: 'executable', listall: true, name: TEMPLATE_NAME)
        .and_return(list_templates_response)
      allow(cloudstack_compute).to receive(:zones).and_return([cloudstack_zone])
      allow(servers).to receive(:create).with(create_servers_parameters).and_return(server)
      expect(server).to receive(:id).and_return(SERVER_ID)
      expect(machine).to receive(:id=).with(SERVER_ID)

      allow(cloudstack_compute).to receive(:volumes).and_return([])
    end

    context 'in basic zone' do
      let(:security_groups) { [] }
      let(:network_name) { nil }
      let(:provider_config) do
        config = VagrantPlugins::Cloudstack::Config.new
        config.domain_config :cloudstack do |cfg|
          cfg.zone_name = ZONE_NAME
          cfg.service_offering_name = SERVICE_OFFERING_NAME
          cfg.template_name = template_name
          cfg.display_name = DISPLAY_NAME
          cfg.ssh_key = ssh_key
          cfg.security_groups = security_groups
          cfg.network_name = network_name
        end
        config.finalize!
        config.get_domain_config(:cloudstack)
      end
      let(:network_type) { NETWORK_TYPE_BASIC }
      let(:security_groups_enabled) { SECURITY_GROUPS_ENABLED }

      let(:create_servers_parameters) do
        {
          display_name: DISPLAY_NAME,
          group: nil,
          zone_id: ZONE_ID,
          flavor_id: SERVICE_OFFERING_ID,
          image_id: TEMPLATE_ID
        }
      end

      context 'a basic configuration' do
        it 'starts a vm' do
          should eq true
        end
      end

      context 'with inline security groups' do
        let(:sg_rule) { { protocol: 'TCP', startport: 23, endport: 23, cidrlist: '0.0.0.0/0' } }

        let(:create_servers_parameters) { super().merge('security_group_ids' => SECURITY_GROUP_ID) }
        let(:security_groups) do
          [
            {
              name: SECURITY_GROUP_NAME, description: SECURITY_GROUP_DESC,
              rules: [sg_rule.merge(type: 'ingress')]
            }
          ]
        end

        before(:each) do
          allow(cloudstack_compute).to receive(:create_security_group)
            .with(name: SECURITY_GROUP_NAME, description: SECURITY_GROUP_DESC)
            .and_return('createsecuritygroupresponse' => { 'securitygroup' => { 'id' => SECURITY_GROUP_ID } })

          allow(cloudstack_compute).to receive(:send).with(:authorize_security_group_ingress,
                                                           { securityGroupId: SECURITY_GROUP_ID }.merge(sg_rule))
            .and_return(
              'authorizesecuritygroupingressresponse' => { 'jobid' => '0b6c2c41-f0c8-43b5-be1d-9d5957873cf9' }
            )
          expect(file).to receive(:write).with("#{SECURITY_GROUP_ID}\n")
        end

        it 'starts a vm' do
          should eq true
        end
      end

      context 'with advanced zone parameters give warnings' do
        let(:network_name) { NETWORK_NAME }

        before(:each) do
          expect(ui).to receive(:warn).with("Network name or id defined but zone Zone Name is of network type 'Basic'"\
            "\nNetwork name or id will be ignored")
        end
        it 'starts a vm' do
          should eq true
        end
      end
    end

    context 'in advanced zone' do
      let(:pf_ip_address) { nil }
      let(:pf_trusted_networks) { nil }
      let(:pf_public_port_randomrange) { { start: 49_152, end: 65_535 } }
      let(:pf_open_firewall) { true }
      let(:disk_offering_name) { nil }

      let(:provider_config) do
        config = VagrantPlugins::Cloudstack::Config.new
        config.domain_config :cloudstack do |cfg|
          cfg.zone_name = ZONE_NAME
          cfg.network_name = NETWORK_NAME
          cfg.service_offering_name = SERVICE_OFFERING_NAME
          cfg.template_name = template_name
          cfg.display_name = DISPLAY_NAME
          cfg.pf_ip_address = pf_ip_address
          cfg.pf_trusted_networks = pf_trusted_networks
          cfg.pf_public_port_randomrange = pf_public_port_randomrange
          cfg.pf_open_firewall = pf_open_firewall
          cfg.ssh_key = ssh_key
          cfg.disk_offering_name = disk_offering_name
        end
        config.finalize!
        config.get_domain_config(:cloudstack)
      end

      let(:winrm_config) { double('VagrantPlugins::VagrantWinRM::WinRMConfig') }

      before(:each) do
        allow(cloudstack_compute).to receive(:query_async_job_result).with(jobid: JOB_ID).and_return(fake_job_result)

        allow(cloudstack_compute).to receive(:send).with(:list_networks, {}).and_return(list_networks_response)
      end

      context 'a basic configuration' do
        it 'starts a vm' do
          should eq true
        end
      end

      context 'with template specified from Vagrant(file)' do
        let(:template_name) { nil }

        before(:each) do
          expect(machine).to receive_message_chain(:config, :vm, :box).and_return(TEMPLATE_NAME)
        end
        it 'starts a vm' do
          should eq true
        end
      end

      context 'with additional data disk' do
        let(:disk_offering_name) { DISK_OFFERING_NAME }
        let(:create_servers_parameters) { super().merge('disk_offering_id' => DISK_OFFERING_ID) }
        let(:volume) { double('Fog::Compute::Cloudstack::Volume') }

        before(:each) do
          allow(cloudstack_compute).to receive(:send).with(:list_disk_offerings, listall: true, name: DISK_OFFERING_NAME)
            .and_return(list_disk_offerings_response)
          expect(cloudstack_compute).to receive(:volumes).and_return([volume])
          allow(volume).to receive(:server_id).and_return(SERVER_ID)
          allow(volume).to receive(:type).and_return('DATADISK')
          allow(volume).to receive(:id).and_return(VOLUME_ID)
          expect(file).to receive(:write).with("#{VOLUME_ID}\n")
          allow(server).to receive(:id).and_return(SERVER_ID)
        end
        it 'starts a vm' do
          should eq true
        end
      end

      context 'with generated password' do
        before(:each) do
          expect(server).to receive(:password_enabled).and_return(true)
          allow(server).to receive(:job_id).and_return(JOB_ID)
          expect(file).to receive(:write).with(GENERATED_PASSWORD)
        end

        it 'starts a vm' do
          should eq true
        end
      end

      context 'with SSH key generation' do
        let(:ssh_key) { nil }
        let(:create_servers_parameters) { super().merge('key_name' => SSH_GENERATED_KEY_NAME) }

        before(:each) do
          expect(ui).to receive(:warn)
            .with('No keypair or ssh_key specified to launch your instance with.' \
                    "\n" + 'Generating a temporary keypair for this instance...')
          expect(cloudstack_compute).to receive(:create_ssh_key_pair).with(/vagacs_#{DISPLAY_NAME}/, nil, nil, nil)
            .and_return(create_ssh_key_pair_response)
          expect(file).to receive(:write).with("#{SSH_GENERATED_PRIVATE_KEY}\n")
          expect(file).to receive(:write).with(SSH_GENERATED_KEY_NAME)
        end

        it 'starts a vm' do
          should eq true
        end
      end

      context 'with autogenerated firewall and port forward' do
        let(:pf_ip_address) { PF_IP_ADDRESS }
        let(:pf_trusted_networks) { PF_TRUSTED_NETWORKS }
        let(:pf_public_port_randomrange) { { start: PF_RANDOM_START, end: PF_RANDOM_START + 1 } }
        let(:pf_open_firewall) { false }

        before(:each) do
          allow(cloudstack_compute).to receive(:send).with(:list_public_ip_addresses, {})
            .and_return(list_public_ip_addresses_response)
          expect(communicator_config).to receive(:port).and_return(nil)
          expect(communicator_config).to receive(:guest_port).and_return(nil)

          allow(machine).to receive_message_chain(:config, :vm, :rdp, :port).and_return(3389)
          allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(nil)
          allow(machine).to receive(:id).and_return(SERVER_ID)

          allow(cloudstack_compute).to receive(:send).with(:list_public_ip_addresses, 'id' => PF_IP_ADDRESS_ID)
            .and_return(list_public_ip_addresses_response)
          allow(cloudstack_compute).to receive(:list_network_acl_lists).with(id: nil)
            .and_return(list_network_acl_lists_response)

          expect(file).to receive(:write).with(PORT_FORWARDING_RULE_ID + "\n")
          expect(file).to receive(:write).with(PF_RANDOM_START.to_s)
          expect(file).to receive(:write).with("#{ACL_ID},networkacl\n")
        end

        context 'for the SSH communicator' do
          before(:each) do
            allow(communicator).to receive_message_chain(:class, :name).and_return(COMMUNICATOR_SSH)
            expect(machine).to receive_message_chain(:config, :ssh).and_return(communicator_config)

            allow(communicator_config).to receive_message_chain(:default, :port).and_return(GUEST_PORT_SSH)
            expect(cloudstack_compute).to receive(:create_port_forwarding_rule)
              .with(create_port_forwarding_rule_parameters)
              .and_return(create_port_forwarding_rule_respones)
            expect(cloudstack_compute).to receive(:request)
              .with(create_network_acl_request)
              .and_return(createNetworkACL_response)
          end
          it 'starts a vm' do
            should eq true
          end

          context 'with a port conflict' do
            let(:pf_public_port_randomrange) { { start: PF_RANDOM_START - 1, end: PF_RANDOM_START + 1 } }

            before(:each) do
              allow(Kernel).to receive(:rand).with((PF_RANDOM_START - 1)...(PF_RANDOM_START + 1))
                .and_return(PF_RANDOM_START - 1, PF_RANDOM_START)
              expect(cloudstack_compute).to receive(:create_port_forwarding_rule)
                .with(create_port_forwarding_rule_parameters.merge(publicport: PF_RANDOM_START - 1))
                .and_raise(
                  Fog::Compute::Cloudstack::Error,
                  'The range specified, CONFLICTINGRANGE, conflicts with rule SOMERULE which has THESAME'
                )
            end

            it 'starts a vm' do
              should eq true
            end
          end
        end

        context 'for the WinRM (and RDP) communicator' do
          PF_RDP_RULE_ID = 'UUID RDP port forwarding rule'.freeze
          PF_RDP_JOB_ID = 'UUID RDP Port Forward Job'.freeze
          FW_RDP_JOB_ID = 'UUID RDP Port Firewall Job'.freeze
          FW_RDP_ACL_ID = 'UUID of RDP ACL'.freeze

          let(:pf_public_port_randomrange) { { start: PF_RANDOM_START, end: PF_RANDOM_START + 2 } }

          let(:create_port_forwarding_rule_parameters) { super().merge(privateport: GUEST_PORT_WINRM) }
          let(:create_port_forwarding_rule_rdp_parameters) do
            create_port_forwarding_rule_parameters.merge(privateport: GUEST_PORT_RDP, publicport: PF_RANDOM_START + 1)
          end

          let(:create_network_acl_winrm_request) do
            create_network_acl_request.merge(startport: GUEST_PORT_WINRM, endport: GUEST_PORT_WINRM)
          end
          let(:create_network_acl_rdp_request) do
            create_network_acl_request.merge(startport: GUEST_PORT_RDP, endport: GUEST_PORT_RDP)
          end

          before(:each) do
            allow(Kernel).to receive(:rand).with(PF_RANDOM_START...(PF_RANDOM_START + 2))
              .and_return(PF_RANDOM_START, PF_RANDOM_START + 1)

            allow(communicator).to receive_message_chain(:class, :name).and_return(COMMUNICATOR_WINRM)
            expect(machine).to receive_message_chain(:config, :winrm).and_return(communicator_config)

            allow(communicator_config).to receive_message_chain(:default, :port).and_return(GUEST_PORT_WINRM)

            expect(cloudstack_compute).to receive(:create_port_forwarding_rule)
              .with(create_port_forwarding_rule_parameters)
              .and_return(create_port_forwarding_rule_respones)

            expect(cloudstack_compute).to receive(:create_port_forwarding_rule)
              .with(create_port_forwarding_rule_rdp_parameters).and_return(
                'createportforwardingruleresponse' =>
                {
                  'id' => PF_RDP_RULE_ID,
                  'jobid' => PF_RDP_JOB_ID
                }
              )
            allow(cloudstack_compute).to receive(:query_async_job_result).with(jobid: PF_RDP_JOB_ID)
              .and_return(fake_job_result.merge(
                            'queryasyncjobresultresponse' => {
                              'jobresult' => {
                                'portforwardingrule' => {
                                  'id' => PF_RDP_RULE_ID
                                }
                              }
                            }
              ))
            expect(file).to receive(:write).with(PF_RDP_RULE_ID + "\n")
            expect(file).to receive(:write).with((PF_RANDOM_START + 1).to_s)
            expect(cloudstack_compute).to receive(:request).with(create_network_acl_winrm_request)
              .and_return(createNetworkACL_response)

            expect(cloudstack_compute).to receive(:request).with(create_network_acl_rdp_request)
              .and_return(
                'createnetworkaclresponse' => {
                  'id' => '5dcb96b5-7785-463d-9a11-d8388c98e4ee',
                  'jobid' => FW_RDP_JOB_ID
                }
              )
            allow(cloudstack_compute).to receive(:query_async_job_result).with(jobid: FW_RDP_JOB_ID)
              .and_return(
                fake_job_result.merge(
                  'queryasyncjobresultresponse' => {
                    'jobstatus' => 1,
                    'jobresult' => {
                      'networkacl' => {
                        'id' => FW_RDP_ACL_ID
                      }
                    }
                  }
                )
              )
            expect(file).to receive(:write).with("#{FW_RDP_ACL_ID},networkacl\n")
          end
          it 'starts a vm' do
            should eq true
          end
        end
      end
    end
  end
end
