require 'spec_helper'

describe OneWorker do
  subject { one_worker }

  let(:one_worker) { OneWorker.new }
  let(:oda) { double('oda') }

  describe '.output_type_specific_data' do
    context 'with output type apel' do
      before :example do
        Settings.output['output_type'] = 'apel-0.2'
        Settings.output.apel['endpoint'] = 'machine.hogwarts.co.uk'
        Settings.output.apel['site_name'] = 'Hogwarts'
        Settings.output.apel['cloud_type'] = 'OpenNebula'
        Settings.output.apel['cloud_compute_service'] = 'CloudComputeServiceValue'
      end

      let(:output_type_specific_data) { {'endpoint' => 'machine.hogwarts.co.uk', 'site_name' => 'Hogwarts', 'cloud_type' => 'OpenNebula', 'cloud_compute_service' => 'CloudComputeServiceValue'} }

      it 'returns data specific for apel output type in form of hash' do
        expect(subject.output_type_specific_data).to eq(output_type_specific_data)
      end
    end

    context 'with output type pbs' do
      before :example do
        Settings.output['output_type'] = 'pbs-0.1'
        Settings.output.pbs['realm'] = 'REALM'
        Settings.output.pbs['queue'] = 'cloud'
        Settings.output.pbs['scratch_type'] = 'local'
        Settings.output.pbs['host_identifier'] = 'on_localhost'
      end

      let(:output_type_specific_data) { {'realm' => 'REALM', 'pbs_queue' => 'cloud', 'scratch_type' => 'local', 'host' => 'on_localhost'} }

      it 'returns data specific for pbs output type in form of hash' do
        expect(subject.output_type_specific_data).to eq(output_type_specific_data)
      end
    end
  end

  describe '.create_user_map' do
    it 'returns user map' do
      expect(subject).to receive(:create_map).with(OpenNebula::UserPool, anything, anything) { 'map' }
      expect(subject.create_user_map(oda)).to eq('map')
    end
  end

  describe '.create_image_map' do
    it 'returns image map' do
      expect(subject).to receive(:create_map).with(OpenNebula::ImagePool, anything, anything) { 'map' }
      expect(subject.create_image_map(oda)).to eq('map')
    end
  end

  describe '.create_cluster_map' do
    it 'returns cluster map' do
      expect(subject).to receive(:create_map).with(OpenNebula::ClusterPool, anything, anything) { 'map' }
      expect(subject.create_cluster_map(oda)).to eq('map')
    end
  end

  describe '.create_map' do
    let(:pool_type) { double('pool_type') }
    let(:mapping) { double('mapping') }

    context 'without any error during data retrieval' do
      before :example do
        expect(oda).to receive(:mapping).with(pool_type, mapping)
      end

      it 'returns requested map' do
        subject.create_map(pool_type, mapping, oda)
      end
    end

    context 'with error during data retrieval' do
      before :example do
        expect(oda).to receive(:mapping).and_raise(Errors::ResourceRetrievalError)
      end

      it 'raises an error' do
        expect { subject.create_map(pool_type, mapping, oda) }.to raise_error(RuntimeError)
      end
    end
  end

  describe '.load_vm' do
    context 'without error' do
      before :example do
        expect(oda).to receive(:vm).with(5) { vm }
      end

      let(:vm) { double('vm') }

      it 'returns vm with given ID' do
        expect(subject.load_vm(5, oda)).to eq(vm)
      end
    end

    context 'with error' do
      before :example do
        expect(oda).to receive(:vm).and_raise(Errors::ResourceRetrievalError)
      end

      it 'returns nil' do
        expect(subject.load_vm(5, oda)).to be_nil
      end
    end
  end

  describe 'write_data' do
    before :example do
      expect(OneWriter).to receive(:new).with(data, output, anything) { ow }
    end

    let(:data) { double('data') }
    let(:output) { double('output') }
    let(:ow) { double('one_writer') }

    context 'without error' do
      before :example do
        expect(ow).to receive(:write)
      end

      it 'calls OneWriter.write with specified data and output directory' do
        subject.write_data(data, output)
      end
    end

    context 'with error' do
      before :example do
        expect(ow).to receive(:write).and_raise(Errors::ResourceRetrievalError)
      end

      it 'raises a RuntimeError' do
        expect { subject.write_data(data, output) }.to raise_error(RuntimeError)
      end
    end
  end

  describe 'process_vm' do
    before :example do
      Settings.output['output_type'] = 'apel-0.2'
      Settings.output.apel['endpoint'] = 'machine.hogwarts.co.uk'
      Settings.output.apel['site_name'] = 'site-name-from-config'
      Settings.output.apel['cloud_type'] = 'OpenNebula'
      Settings.output.apel['cloud_compute_service'] = nil

      allow(vm).to receive(:state_str) { 'DONE' }
    end

    let(:vm) do
      xml = File.read("#{GEM_DIR}/mock/#{filename}")
      OpenNebula::XMLElement.new(OpenNebula::XMLElement.build_xml(xml, 'VM'))
    end

    let(:data) do
      data = {}

      data['endpoint'] = 'machine.hogwarts.co.uk'
      data['site_name'] = 'site-name-from-cluster'
      data['cloud_type'] = 'OpenNebula'
      data['cloud_compute_service'] = nil

      data['vm_uuid'] = '36551'
      data['start_time'] = '1383741160'
      data['end_time'] = '1383742270'
      data['machine_name'] = 'one-36551'
      data['user_id'] = '120'
      data['group_id'] = '0'
      data['user_name'] = 'uname'
      data['group_name'] = 'gname'
      data['status_code'] = '6'
      data['status'] = 'DONE'
      data['cpu_count'] = '1'
      data['network_inbound'] = '43557888'
      data['network_outbound'] = '376832'
      data['memory'] = '1736960'
      data['number_of_public_ips'] = 1
      history = []
      rec = {}
      rec['start_time'] = '1383741169'
      rec['end_time'] = '1383741259'
      rec['rstart_time'] = '0'
      rec['rend_time'] = '0'
      rec['seq'] = '0'
      rec['hostname'] = 'supermachine1.somewhere.com'
      history << rec
      rec = {}
      rec['start_time'] = '1383741589'
      rec['end_time'] = '1383742270'
      rec['rstart_time'] = '1383741674'
      rec['rend_time'] = '1383742270'
      rec['seq'] = '1'
      rec['hostname'] = 'supermachine2.somewhere.com'
      history << rec
      data['history'] = history
      data['disks'] = [{'size' => '10240'}, {'size' => '42368'}]

      data['user_dn'] = '/Dn=FrOm/CN=DN/CN=TeMpLaTe'
      data['image_name'] = 'https://appdb.egi.eu/store/vo/image/image_name_from_VMCATCHER_EVENT_AD_MPURI_tag/'

      data['benchmark_type'] = nil
      data['benchmark_value'] = nil

      data['oneacct_export_version'] = ::OneacctExporter::VERSION

      data
    end

    let(:user_map) { {'120' => '/Dn=FrOm/CN=DN/CN=MaP'} }
    let(:image_map) { {'31' => 'image_name_from_map'} }
    let(:cluster_map) { {'100' => 'site-name-from-cluster'}}
    let(:benchmark_map) { {'11' => {}}  }

    context 'with apel specific data' do
      let(:filename) { 'one_worker_vm_dn01.xml' }

      it 'returns correct vm data with apel specific data' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with pbs specific data' do
      before :example do
        Settings.output['output_type'] = 'pbs-0.1'
        Settings.output.pbs['realm'] = 'REALM'
        Settings.output.pbs['queue'] = 'cloud'
        Settings.output.pbs['scratch_type'] = 'local'
        Settings.output.pbs['host_identifier'] = 'on_localhost'

        data['realm'] = 'REALM'
        data['pbs_queue'] = 'cloud'
        data['scratch_type'] = 'local'
        data['host'] = 'on_localhost'

        data.delete 'endpoint'
        data.delete 'cloud_type'
        data.delete 'cloud_compute_service'
      end

      let(:filename) { 'one_worker_vm_dn01.xml' }

      it 'returns correct vm data with pbs specific data' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with user\'s dn in template' do
      let(:filename) { 'one_worker_vm_dn01.xml' }

      it 'returns correct vm data with user\'s dn from template' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with user\'s dn in map' do
      let(:filename) { 'one_worker_vm_dn02.xml' }

      before :example do
        data['user_dn'] = '/Dn=FrOm/CN=DN/CN=MaP'
      end

      it 'returns correct vm data with user\'s dn from map' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with image name in VMCATCHER_EVENT_AD_MPURI tag' do
      let(:filename) { 'one_worker_vm_image_name01.xml' }

      it 'returns correct vm data with image name from VMCATCHER_EVENT_AD_MPURI tag' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with image name in map' do
      let(:filename) { 'one_worker_vm_image_name02.xml' }

      before :example do
        data['image_name'] = 'image_name_from_map'
      end

      it 'returns correct vm data with image name from map' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    #TODO should be moved into tests for mixin method
    context 'with image name in USER_TEMPLATE/OCCI_COMPUTE_MIXINS tag' do
      let(:filename) { 'one_worker_vm_image_name03.xml' }

      before :example do
        data['image_name'] = 'http://occi.localhost/occi/infrastructure/os_tpl#image_name_from_USER_TEMPLATE_OCCI_COMPUTE_MIXINS'
      end

      it 'returns correct vm data with image name from USER_TEMPLATE/OCCI_COMPUTE_MIXINS tag' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    #TODO should be moved into tests for mixin method
    context 'with image name in USER_TEMPLATE/OCCI_MIXIN tag' do
      let(:filename) { 'one_worker_vm_image_name04.xml' }

      before :example do
        data['image_name'] = 'http://occi.localhost/occi/infrastructure/os_tpl#image_name_from_USER_TEMPLATE_OCCI_MIXIN'
      end

      it 'returns correct vm data with image name from USER_TEMPLATE/OCCI_MIXIN tag' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    #TODO should be moved into tests for mixin method
    context 'with image name in TEMPLATE/OCCI_MIXIN tag' do
      let(:filename) { 'one_worker_vm_image_name05.xml' }

      before :example do
        data['image_name'] = 'http://occi.localhost/occi/infrastructure/os_tpl#image_name_from_TEMPLATE_OCCI_MIXIN'
      end

      it 'returns correct vm data with image name from TEMPLATE/OCCI_MIXIN tag' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'with image name as image id' do
      let(:filename) { 'one_worker_vm_image_name06.xml' }

      before :example do
        data['image_name'] = '42'
      end

      it 'returns correct vm data with image name as image id' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'without site-name on cluster (APEL)' do
      let(:filename) { 'one_worker_vm_dn01.xml' }
      let(:cluster_map) { {} }

      before :example do
        data['site_name'] = 'site-name-from-config'
      end

      it 'returns correct vm data with site-name from configuration file' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end

    context 'without site-name on cluster (NON APEL)' do
      let(:filename) { 'one_worker_vm_dn01.xml' }
      let(:cluster_map) { {} }

      before :example do
        Settings.output['output_type'] = 'pbs-0.1'
        Settings.output.pbs['realm'] = 'REALM'
        Settings.output.pbs['queue'] = 'cloud'
        Settings.output.pbs['scratch_type'] = 'local'
        Settings.output.pbs['host_identifier'] = 'on_localhost'

        data['realm'] = 'REALM'
        data['pbs_queue'] = 'cloud'
        data['scratch_type'] = 'local'
        data['host'] = 'on_localhost'

        data.delete 'endpoint'
        data.delete 'cloud_type'
        data.delete 'cloud_compute_service'
        data.delete 'site_name'
      end

      it 'returns correct vm data without any site-name' do
        expect(subject.process_vm(vm, user_map, image_map, cluster_map, benchmark_map)).to eq(data)
      end
    end
  end

  describe 'history_records' do
    let(:vm) do
      xml = File.read("#{GEM_DIR}/mock/#{filename}")
      OpenNebula::XMLElement.new(OpenNebula::XMLElement.build_xml(xml, 'VM'))
    end

    let(:history) do
      history = []
      rec = {}
      rec['start_time'] = '1383741169'
      rec['end_time'] = '1383741259'
      rec['rstart_time'] = '0'
      rec['rend_time'] = '0'
      rec['seq'] = '0'
      rec['hostname'] = 'supermachine1.somewhere.com'
      history << rec
      rec = {}
      rec['start_time'] = '1383741589'
      rec['end_time'] = '1383742270'
      rec['rstart_time'] = '1383741674'
      rec['rend_time'] = '1383742270'
      rec['seq'] = '1'
      rec['hostname'] = 'supermachine2.somewhere.com'
      history << rec

      history
    end

    context 'with correct history records in vm' do
      let(:filename) { 'one_worker_vm_dn01.xml' }

      it 'returns history records for vm' do
        expect(subject.history_records(vm)).to eq(history)
      end
    end

    context 'with no history records' do
      let(:filename) { 'one_worker_vm_empty_history_records.xml' }

      it 'returns emtpy array' do
        expect(subject.history_records(vm)).to be_empty
      end
    end
  end

  describe 'disk_records' do
    let(:vm) do
      xml = File.read("#{GEM_DIR}/mock/#{filename}")
      OpenNebula::XMLElement.new(OpenNebula::XMLElement.build_xml(xml, 'VM'))
    end

    let(:disks) do
      disks = []
      disk = {}
      disk['size'] = '10240'
      disks << disk
      disk = {}
      disk['size'] = '42368'
      disks << disk

      disks
    end

    context 'with correct disk records in vm' do
      let(:filename) { 'one_worker_vm_dn01.xml' }

      it 'returns history records for vm' do
        expect(subject.disk_records(vm)).to eq(disks)
      end
    end

    context 'with no disk records' do
      let(:filename) { 'one_worker_vm_empty_disk_records.xml' }

      it 'returns emtpy array' do
        expect(subject.disk_records(vm)).to be_empty
      end
    end
  end

  describe ".number_of_public_ips" do
    let(:vm) do
      xml = File.read("#{GEM_DIR}/mock/#{filename}")
      OpenNebula::XMLElement.new(OpenNebula::XMLElement.build_xml(xml, 'VM'))
    end

    context "with multiple NICs (multiple IPs with duplicates) " do
      let(:filename) { 'one_worker_vm_number_of_public_ips_01.xml' }
      it "returns the correct number of public IPs" do
        expect(subject.number_of_public_ips(vm)).to eq 8
      end
    end
    context "with no NICs" do
      let(:filename) { 'one_worker_vm_number_of_public_ips_02.xml' }
      it "returns 0, the correct number of IPs" do
        expect(subject.number_of_public_ips(vm)).to eq 0
      end
    end
    context "with single NIC (multiple IPs with duplicates)" do
      let(:filename) { 'one_worker_vm_number_of_public_ips_03.xml' }
      it "returns the correct number of public IPs" do
        expect(subject.number_of_public_ips(vm)).to eq 8
      end
    end
  end

  describe '.perform' do
    before :example do
      Settings.output['output_type'] = 'unknown'
      allow(OneDataAccessor).to receive(:new) { oda }
      allow(subject).to receive(:create_user_map) { 'user_map' }
      allow(subject).to receive(:create_image_map) { 'image_map' }
      allow(subject).to receive(:create_cluster_map) { 'cluster_map' }
      allow(oda).to receive(:benchmark_map) { 'benchmark_map' }
      allow(subject).to receive(:load_vm).with('10', oda).and_return('10')
      allow(subject).to receive(:load_vm).with('20', oda).and_return('20')
      allow(subject).to receive(:load_vm).with('30', oda).and_return('30')
      allow(subject).to receive(:process_vm).with('10', anything, anything, anything, anything).and_return('data_vm1')
      allow(subject).to receive(:process_vm).with('20', anything, anything, anything, anything).and_return('data_vm2')
      allow(subject).to receive(:process_vm).with('30', anything, anything, anything, anything).and_return('data_vm3')
    end

    let(:vms) { '10|20|30' }
    let(:file_number) { 42 }

    context 'with valid vms' do
      it 'writes vm data' do
        expect(subject).to receive(:write_data).with(['data_vm1', 'data_vm2', 'data_vm3'], file_number)
        subject.perform(vms, file_number)
      end
    end

    context 'with one vm not loaded correctly' do
      before :example do
        allow(subject).to receive(:load_vm).with('20', oda).and_return(nil)
      end

      it 'writes data of the correct vms' do
        expect(subject).to receive(:write_data).with(['data_vm1', 'data_vm3'], file_number)
        subject.perform(vms, file_number)
      end
    end

    context 'with apel data validator' do
      before :example do
        Settings.output['output_type'] = 'apel-0.2'
      end

      let(:validator) { double('validator') }

      context 'and all vm valid' do
        it 'uses apel data validator to validate all vms and all passes' do
          expect(DataValidators::ApelDataValidator).to receive(:new).and_return(validator).exactly(3).times
          expect(validator).to receive(:validate_data).with('data_vm1').and_return('valid_data_vm1')
          expect(validator).to receive(:validate_data).with('data_vm2').and_return('valid_data_vm2')
          expect(validator).to receive(:validate_data).with('data_vm3').and_return('valid_data_vm3')
          expect(subject).to receive(:write_data).with(['valid_data_vm1', 'valid_data_vm2', 'valid_data_vm3'], file_number)
          subject.perform(vms, file_number)
        end
      end

      context 'and all vm valid but one' do
        it 'uses apel data validator to validate all vms and all but one passes' do
          expect(DataValidators::ApelDataValidator).to receive(:new).and_return(validator).exactly(3).times
          expect(validator).to receive(:validate_data).with('data_vm1').and_return('valid_data_vm1')
          expect(validator).to receive(:validate_data).with('data_vm2').and_raise(Errors::ValidationError)
          expect(validator).to receive(:validate_data).with('data_vm3').and_return('valid_data_vm3')
          expect(subject).to receive(:write_data).with(['valid_data_vm1', 'valid_data_vm3'], file_number)
          subject.perform(vms, file_number)
        end
      end
    end

    context 'with apel data validator' do
      before :example do
        Settings.output['output_type'] = 'pbs-0.1'
      end

      let(:validator) { double('validator') }

      context 'and all vm valid' do
        it 'uses apel data validator to validate all vms and all passes' do
          expect(DataValidators::PbsDataValidator).to receive(:new).and_return(validator).exactly(3).times
          expect(validator).to receive(:validate_data).with('data_vm1').and_return('valid_data_vm1')
          expect(validator).to receive(:validate_data).with('data_vm2').and_return('valid_data_vm2')
          expect(validator).to receive(:validate_data).with('data_vm3').and_return('valid_data_vm3')
          expect(subject).to receive(:write_data).with(['valid_data_vm1', 'valid_data_vm2', 'valid_data_vm3'], file_number)
          subject.perform(vms, file_number)
        end
      end

      context 'and all vm valid but one' do
        it 'uses apel data validator to validate all vms and all but one passes' do
          expect(DataValidators::PbsDataValidator).to receive(:new).and_return(validator).exactly(3).times
          expect(validator).to receive(:validate_data).with('data_vm1').and_return('valid_data_vm1')
          expect(validator).to receive(:validate_data).with('data_vm2').and_raise(Errors::ValidationError)
          expect(validator).to receive(:validate_data).with('data_vm3').and_return('valid_data_vm3')
          expect(subject).to receive(:write_data).with(['valid_data_vm1', 'valid_data_vm3'], file_number)
          subject.perform(vms, file_number)
        end
      end
    end
  end
end