require File.expand_path("../../test_helper", __FILE__)

class VMTest < Test::Unit::TestCase
  setup do
    @klass = VirtualBox::VM
    @interface = mock("interface")
    @parent = mock("parent")
  end

  context "class methods" do
    context "retrieving all machines" do
      should "return an array of VM objects" do
        vms = mock("vms")
        global = mock("global")
        global.expects(:vms).returns(vms)
        VirtualBox::Global.expects(:global).returns(global)
        assert_equal vms, VirtualBox::VM.all
      end
    end

    context "finding a VM" do
      setup do
        @lib = mock("lib")
        @virtualbox = mock("virtualbox")
        VirtualBox::Lib.stubs(:lib).returns(@lib)
        @lib.stubs(:virtualbox).returns(@virtualbox)
      end

      def mock_vm(uuid, name=nil)
        vm = mock("vm-#{uuid}")
        vm.stubs(:uuid).returns(uuid)
        vm.stubs(:name).returns(name)
        vm
      end

      should "return nil if it doesn't exist" do
        @virtualbox.expects(:find_machine).raises(VirtualBox::Exceptions::ObjectNotFoundException)
        assert_nil @klass.find("bar")
      end

      should "return the matching vm if it is found" do
        @virtualbox.expects(:find_machine).returns(mock_vm("foo"))
        assert !@klass.find("foo").nil?
      end
    end

    context "importing" do
      setup do
        @path = "foo.rb"
        @appliance = mock("appliance")
        @virtual_system = mock("virtual_system")
        @appliance.stubs(:virtual_systems).returns([@virtual_system])

        @name = :foo
        @virtual_system.stubs(:descriptions).returns({
          :name => { :auto => @name }
        })
      end

      should "create a new appliance with path, import, and return VM" do
        result = mock("result")
        proc = mock("proc")
        VirtualBox::Appliance.expects(:new).with(@path).returns(@appliance)
        @appliance.expects(:import).yields(proc)
        @klass.expects(:find).with(@name).returns(result)
        proc.expects(:call)

        value = @klass.import(@path) do |proc|
          proc.call
        end

        assert_equal result, value
      end
    end

    context "populating relationships" do
      setup do
        @caller = mock("caller")
      end

      should "call populate_array_relationship for arrays" do
        @klass.expects(:populate_array_relationship).with(@caller, []).once
        @klass.populate_relationship(@caller, [])
      end

      should "call populate_single_relationship for non-arrays" do
        @klass.expects(:populate_single_relationship).with(@caller, nil).once
        @klass.populate_relationship(@caller, nil)
      end
    end

    context "populating single relationships" do
      setup do
        @machine = mock("interface")
      end

      should "return a new machine" do
        result = mock("result")
        @klass.expects(:new).with(@machine).returns(result)
        assert_equal result, @klass.populate_single_relationship(nil, @machine)
      end
    end

    context "populating array relationship" do
      setup do
        @instance = mock("instance")

        @klass.stubs(:new).returns(@instance)
      end

      should "return a proxied collection" do
        result = @klass.populate_array_relationship(nil, [])
        assert result.is_a?(VirtualBox::Proxies::Collection)
      end

      should "call new for every machine" do
        machines = []
        5.times { |i| machines << mock("m#{i}") }

        expected_result = []
        new_seq = sequence("new_seq")
        machines.each do |machine|
          expected_value = "instance-#{machine.inspect}"
          @klass.expects(:new).with(machine).in_sequence(new_seq).returns(expected_value)
          expected_result << expected_value
        end

        assert_equal expected_result, @klass.populate_array_relationship(nil, machines)
      end
    end
  end

  context "initializing" do
    should "load attributes from the machine" do
      @klass.any_instance.expects(:initialize_attributes).with(@interface).once
      @klass.new(@interface)
    end
  end

  context "initializing attributes" do
    setup do
      @klass.any_instance.stubs(:load_interface_attributes)
      @klass.any_instance.stubs(:populate_relationships)
    end

    should "not load interface attribtues" do
      @klass.any_instance.expects(:load_interface_attributes).with(@interface).never
      @klass.new(@interface)
    end

    should "not populate relationships" do
      @klass.any_instance.expects(:populate_relationships).with(@interface).never
      @klass.new(@interface)
    end

    should "not be dirty" do
      @instance = @klass.new(@interface)
      assert !@instance.changed?
    end

    should "be existing record" do
      @instance = @klass.new(@interface)
      assert !@instance.new_record?
    end
  end

  context "instance methods" do
    setup do
      @klass.any_instance.stubs(:initialize_attributes)
      @instance = @klass.new(@interface)
    end

    def setup_session_mocks
      @parent = mock("parent")
      @session = mock("session")
      @lib = mock("lib")
      @progress = mock("progress")

      @session.stubs(:close)
      @progress.stubs(:wait_for_completion)
      @lib.stubs(:session).returns(@session)
      @uuid = :foo

      VirtualBox::Lib.stubs(:lib).returns(@lib)
      @interface.stubs(:parent).returns(@parent)
      @instance.stubs(:imachine).returns(@interface)
      @instance.stubs(:uuid).returns(@uuid)
      @instance.stubs(:running).returns(false)
    end

    context "reloading" do
      setup do
        @instance.stubs(:initialize_attributes)
      end

      should "just reload the attributes" do
        @instance.expects(:initialize_attributes).with(@interface).once
        @instance.reload
      end

      should "return itself" do
        assert @instance.equal?(@instance.reload)
      end
    end

    context "destroying" do
      setup do
        @instance.stubs(:uuid).returns(:foo)

        @interface_parent = mock("interface_parent")
        @interface.stubs(:parent).returns(@interface_parent)
      end

      should "do a full destroy and destroy the media" do
        destroy_seq = sequence("destroy_seq")
        media = [1,2,3]
        progress = mock("progress")
        @interface.expects(:unregister).with(:full).once.returns(media).in_sequence(destroy_seq)
        @interface.expects(:delete).with(media).once.returns(progress).in_sequence(destroy_seq)

        @instance.destroy
      end

      should "not destroy media if there aren't any" do
        @interface.expects(:unregister).with(:full).once.returns([])
        @interface.expects(:delete).never

        @instance.destroy
      end
    end

    context "state" do
      setup do
        @state = mock("state")
        @interface = mock("interface")
        @instance.stubs(:read_attribute).with(:state).returns(@state)
        @instance.stubs(:interface).returns(@interface)
      end

      should "reload and return the state" do
        @instance.expects(:load_interface_attribute).with(:state, @instance.interface).once
        assert_equal @state, @instance.state
      end

      should "not reload the state if suppress is given" do
        @instance.expects(:load_interface_attribute).never
        assert_equal @state, @instance.state(true)
      end
    end

    context "starting" do
      setup do
        setup_session_mocks

        @instance.stubs(:running?).returns(false)
        @session.stubs(:state).returns(:open)
      end

      should "launch the VM with the given mode" do
        start_seq = sequence('start_seq')
        mode = "foo"
        @interface.expects(:launch_vm_process).with(@session, mode, "").once.returns(@progress).in_sequence(start_seq)
        @progress.expects(:wait).in_sequence(start_seq)
        @session.expects(:unlock_machine).in_sequence(start_seq)
        assert @instance.start(mode)
      end

      should "return false if state is running" do
        @instance.expects(:running?).returns(true)
        assert !@instance.start(nil)
      end
    end

    context "controlling" do
      setup do
        setup_session_mocks

        @console = mock("console")
        @console.stubs(:send)
        @session.stubs(:console).returns(@console)
        @session.stubs(:state).returns(:open)

        @instance.stubs(:with_open_session).yields(@session)

        @method = :foo
      end

      should "get an existing, session, send the command, and close" do
        method = :foo
        control_seq = sequence("control_seq")
        @instance.expects(:with_open_session).with(:shared).yields(@session).in_sequence(control_seq)
        @console.expects(:send).with(@method).once.in_sequence(control_seq)

        @instance.control(@method)
      end

      should "wait for completion if an IProgress is returned" do
        progress = mock("IProgress")
        progress.stubs(:is_a?).with(VirtualBox::COM::Util.versioned_interface(:Progress)).returns(true)
        progress.expects(:wait).once
        @console.expects(:send).with(@method).returns(progress)
        @instance.control(@method)
      end

      should "forward other args" do
        @console.expects(:send).with(@method, 1, 2, 3).once
        @instance.control(@method, 1, 2, 3)
      end
    end

    context "control helpers" do
      should "call the proper control method" do
        methods = {
          :shutdown => :power_button,
          :stop => :power_down,
          :pause => :pause,
          :resume => :resume,
          :save_state => :save_state,
          :discard_state => [:discard_saved_state, true]
        }

        methods.each do |method, control|
          control = [control] unless control.is_a?(Array)
          @instance.expects(:control).with(*control).once
          @instance.send(method)
        end
      end
    end

    context "saving" do
      setup do
        @session = mock("session")
        @session.stubs(:machine).returns(@parent)

        @locked_interface = mock("locked_interface")

        @instance.stubs(:saved?).returns(false)
        @instance.stubs(:valid?).returns(true)
        @instance.stubs(:with_open_session)
      end

      should "open the session, save, and close" do
        save_seq = sequence("save_seq")
        @instance.expects(:with_open_session).once.yields(@session).in_sequence(save_seq)
        @session.expects(:machine).returns(@locked_interface).in_sequence(save_seq)
        @instance.expects(:save_interface_attribute).with(:boot_order, @locked_interface).in_sequence(save_seq)
        @instance.expects(:save_changed_interface_attributes).with(@locked_interface).in_sequence(save_seq)
        @instance.expects(:save_relationships).in_sequence(save_seq)

        @instance.save
      end

      should "raise an exception if saved" do
        @instance.stubs(:saved?).returns(true)

        assert_raises(VirtualBox::Exceptions::ReadonlyVMStateException) {
          @instance.save
        }
      end

      should "return false if not valid" do
        @instance.stubs(:valid?).returns(false)
        assert !@instance.save
      end

      should "return true if save succeeds" do
        assert @instance.save
      end
    end

    context "opening a session [direct]" do
      setup do
        setup_session_mocks

        @locked_interface = mock("locked_interface")
        @locked_interface.stubs(:state).returns(:powered_off)
        @session.stubs(:machine).returns(@locked_interface)
        @session.stubs(:state).returns(:closed)
        @interface.stubs(:lock_machine)
      end

      should "close the session if an exception is raised" do
        @locked_interface.expects(:save_settings).raises(Exception)
        @session.expects(:unlock_machine).once

        assert_raises(Exception) do
          @instance.with_open_session do
            # After this point, state should be open
            @session.stubs(:state).returns(:open)
          end
        end
      end

      should "open the session, save, and close" do
        save_seq = sequence("save_seq")
        @proc = Proc.new {}

        @interface.expects(:lock_machine).with(@session, :write).in_sequence(save_seq)
        @proc.expects(:call).with(@session).once.in_sequence(save_seq)
        @locked_interface.expects(:save_settings).once.in_sequence(save_seq)
        @session.expects(:unlock_machine).in_sequence(save_seq)

        @instance.with_open_session do |session|
          @proc.call(session)
        end
      end

      should "open the session with the shared type and NOT save settings" do
        @interface.expects(:lock_machine).with(@session, :shared)
        @session.expects(:unlock_machine)
        @locked_interface.expects(:save_settings).never

        @instance.with_open_session(:shared)
      end

      should "not save settings when the state is saved" do
        @locked_interface.stubs(:state).returns(:saved)

        save_seq = sequence("save_seq")
        @interface.expects(:lock_machine).with(@session, :write).in_sequence(save_seq)
        @locked_interface.expects(:save_settings).never
        @session.expects(:unlock_machine).in_sequence(save_seq)

        @instance.with_open_session { |session| }
      end

      should "only open the session and close once" do
        open_seq = sequence("open_seq")

        @interface.expects(:lock_machine).with(@session, :write).in_sequence(open_seq)
        @locked_interface.expects(:save_settings).once.in_sequence(open_seq)
        @session.expects(:unlock_machine).once.in_sequence(open_seq)

        @instance.with_open_session do |session|
          session.stubs(:state).returns(:open)

          @instance.with_open_session do |subsession|
            assert_equal session, subsession
          end
        end
      end
    end

    context "state methods" do
      should "check the proper results" do
        methods = {
          :starting? => :starting,
          :running? => :running,
          :powered_off? => :powered_off,
          :paused? => :paused,
          :saved? => :saved,
          :aborted? => :aborted
        }

        methods.each do |method, value|
          @instance.stubs(:state).returns(value)
          assert @instance.send(method)

          @instance.stubs(:state).returns(:nope)
          assert !@instance.send(method)
        end
      end
    end

    context "exporting" do
      setup do
        @path = "foo.rb"
        @appliance = mock("appliance")
        @appliance.stubs(:path=)
        @appliance.stubs(:add_machine)

        VirtualBox::Appliance.stubs(:new).returns(@appliance)
      end

      should "create a new appliance with path and export" do
        result = mock("result")
        options = mock("options")
        VirtualBox::Appliance.expects(:new).returns(@appliance)
        @appliance.expects(:path=).with(@path)
        @appliance.expects(:add_machine).with(@instance, options)
        @appliance.expects(:export)

        @instance.export(@path, options)
      end

      should "forward any block to the appliance export method" do
        proc = mock("proc")
        @appliance.expects(:export).yields(proc)
        proc.expects(:call)

        @instance.export(@path) do |yielded_proc|
          yielded_proc.call
        end
      end
    end

    context "taking a snapshot" do
      setup do
        setup_session_mocks

        @progress = mock("progress")
        @progress.stubs(:wait)

        @console = mock("console")
        @console.stubs(:take_snapshot).returns(@progress)
        @session.stubs(:console).returns(@console)

        @instance.stubs(:with_open_session).yields(@session)
      end

      should "take a snapshot on the console and wait" do
        name = "foo"
        description = "baz"
        @console.expects(:take_snapshot).with(name, description).returns(@progress)
        @instance.take_snapshot(name, description)
      end

      should "wait and pass in the given block, if given" do
        foo = mock("foo")
        @progress.expects(:wait).yields(foo)
        foo.expects(:called).once

        @instance.take_snapshot(nil, nil) do |obj|
          obj.called
        end
      end
    end

    context "root snapshot" do
      should "return nil if the current snapshot is nil" do
        @instance.stubs(:current_snapshot).returns(nil)
        assert_nil @instance.root_snapshot
      end

      should "return the proper root snapshot" do
        root = mock("root_snapshot")
        root.stubs(:parent).returns(nil)

        parent = mock("parent")
        parent.stubs(:parent).returns(root)

        snapshot = mock("snapshot")
        snapshot.stubs(:parent).returns(parent)

        @instance.stubs(:current_snapshot).returns(snapshot)
        assert_equal root, @instance.root_snapshot
      end
    end

    context "finding a snapshot" do
      should "return nil if there is no root snapshot" do
        @instance.stubs(:current_snapshot).returns(nil)
        assert_nil @instance.find_snapshot("foo")
      end

      should "return nil if the snapshot is not found" do
        snapshot = mock("snapshot")
        snapshot.stubs(:name).returns("wrong")
        snapshot.stubs(:uuid).returns(nil)
        snapshot.stubs(:children).returns([])
        @instance.stubs(:root_snapshot).returns(snapshot)

        assert_nil @instance.find_snapshot("bar")
      end

      # TODO: Testing traversing the snapshot tree. Too many mocks :S
    end

    context "getting the boot order" do
      setup do
        @max = 4
        @global = mock("global")
        @sys_props = mock("system_properties")

        @sys_props.stubs(:max_boot_position).returns(@max)
        @global.stubs(:system_properties).returns(@sys_props)
        VirtualBox::Global.stubs(:global).returns(@global)
      end

      should "get the boot order for each up to max" do
        expected = (1..@max).inject([]) do |acc, pos|
          result = mock("p#{pos}")
          @interface.expects(:get_boot_order).with(pos).returns(result)
          acc << result
          acc
        end

        assert_equal expected, @instance.get_boot_order(@interface, nil)
      end
    end

    context "setting the boot order" do
      setup do
        @max = 4
        @global = mock("global")
        @sys_props = mock("system_properties")

        @sys_props.stubs(:max_boot_position).returns(@max)
        @global.stubs(:system_properties).returns(@sys_props)
        VirtualBox::Global.stubs(:global).returns(@global)
      end

      should "set the boot order for each up to max" do
        expected = (1..@max).inject([]) do |acc, pos|
          result = mock("p#{pos}")
          @interface.expects(:set_boot_order).with(pos, result)
          acc << result
          acc
        end

        @instance.set_boot_order(@interface, nil, expected)
      end

      should "set the boot order for max items even if value has less than the proper amount" do
        items = [1, 2]
        items.concat(Array.new(@max - items.size))
        items.each_with_index do |item, i|
          @interface.expects(:set_boot_order).with(i + 1, item).once
        end

        assert_equal @max, items.size # sanity

        @instance.set_boot_order(@interface, nil, items)
      end
    end
  end
end
