require 'spec_helper'

describe "Puppet::Util::Windows::Service", :if => Puppet.features.microsoft_windows? do
  require 'puppet/util/windows'

  before(:each) do
    allow(Puppet::Util::Windows::Error).to receive(:format_error_code)
      .with(anything)
      .and_return("fake error!")
  end

  def service_state_str(state)
    Puppet::Util::Windows::Service::SERVICE_STATES[state].to_s
  end

  # The following should emulate a successful call to the private function
  # query_status that returns the value of query_return. This should give
  # us a way to mock changes in service status.
  #
  # Everything else is stubbed, the emulation of the successful call is really
  # just an expectation of subject::SERVICE_STATUS_PROCESS.new in sequence that
  # returns the value passed in as a param
  def expect_successful_status_query_and_return(query_return)
    expect(subject::SERVICE_STATUS_PROCESS).to receive(:new).and_return(query_return)
  end

  def expect_successful_status_queries_and_return(*query_returns)
    query_returns.each do |query_return|
      expect_successful_status_query_and_return(query_return)
    end
  end

  # The following should emulate a successful call to the private function
  # query_config that returns the value of query_return. This should give
  # us a way to mock changes in service configuration.
  #
  # Everything else is stubbed, the emulation of the successful call is really
  # just an expectation of subject::QUERY_SERVICE_CONFIGW.new in sequence that
  # returns the value passed in as a param
  def expect_successful_config_query_and_return(query_return)
    expect(subject::QUERY_SERVICE_CONFIGW).to receive(:new).and_return(query_return)
  end

  def expect_successful_config_query2_and_return(param, query_return)
    expect(param).to receive(:new).and_return(query_return)
  end

  let(:subject)      { Puppet::Util::Windows::Service }
  let(:pointer) { double() }
  let(:mock_service_name) { double() }
  let(:service) { double() }
  let(:scm) { double() }
  let(:timeout) { 30 }

  before do
    allow(subject).to receive(:QueryServiceStatusEx).and_return(1)
    allow(subject).to receive(:QueryServiceConfigW).and_return(1)
    allow(subject).to receive(:QueryServiceConfig2W).and_return(1)
    allow(subject).to receive(:ChangeServiceConfigW).and_return(1)
    allow(subject).to receive(:ChangeServiceConfig2W).and_return(1)
    allow(subject).to receive(:OpenSCManagerW).and_return(scm)
    allow(subject).to receive(:OpenServiceW).and_return(service)
    allow(subject).to receive(:CloseServiceHandle)
    allow(subject).to receive(:EnumServicesStatusExW).and_return(1)
    allow(subject).to receive(:wide_string)
    allow(subject::SERVICE_STATUS_PROCESS).to receive(:new)
    allow(subject::QUERY_SERVICE_CONFIGW).to receive(:new)
    allow(subject::SERVICE_STATUS).to receive(:new).and_return({:dwCurrentState => subject::SERVICE_RUNNING})
    allow(FFI).to receive(:errno).and_return(0)
    allow(FFI::MemoryPointer).to receive(:new).and_yield(pointer)
    allow(pointer).to receive(:read_dword)
    allow(pointer).to receive(:write_dword)
    allow(pointer).to receive(:size)
    allow(subject).to receive(:sleep)
  end

  describe "#exists?" do
    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.exists?(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }

      it "returns false if it fails to open because the service does not exist" do
        allow(FFI).to receive(:errno).and_return(Puppet::Util::Windows::Service::ERROR_SERVICE_DOES_NOT_EXIST)

        expect(subject.exists?(mock_service_name)).to be false
      end

      it "raises a puppet error if it fails to open for some other reason" do
        expect{ subject.exists?(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      it "returns true" do
        expect(subject.exists?(mock_service_name)).to be true
      end
    end
  end

  # This shared example contains the unit tests for the wait_on_pending_state
  # helper as used by service actions like #start and #stop. Before including
  # this shared example, be sure to mock out any intermediate calls prior to
  # the pending transition, and make sure that the post-condition _after_ those
  # intermediate calls leaves the service in the pending state. Before including
  # this example in your tests, be sure to define the following variables in a `let`
  # context:
  #     * action -- The service action
  shared_examples "a service action waiting on a pending transition" do |pending_state|
    pending_state_str = Puppet::Util::Windows::Service::SERVICE_STATES[pending_state].to_s

    final_state = Puppet::Util::Windows::Service::FINAL_STATES[pending_state]
    final_state_str = Puppet::Util::Windows::Service::SERVICE_STATES[final_state].to_s

    it "raises a Puppet::Error if the service query fails" do
      expect(subject).to receive(:QueryServiceStatusEx).and_return(FFI::WIN32_FALSE)

      expect { subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
    end

    it "raises a Puppet::Error if the service unexpectedly transitions to a state other than #{pending_state_str} or #{final_state_str}" do
      invalid_state = (subject::SERVICE_STATES.keys - [pending_state, final_state]).first

      expect_successful_status_query_and_return(dwCurrentState: invalid_state)

      expect { subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
    end

    it "waits for at least 1 second if the wait_hint/10 is < 1 second" do
      expect_successful_status_queries_and_return(
        { :dwCurrentState => pending_state, :dwWaitHint => 0, :dwCheckPoint => 1 },
        { :dwCurrentState => final_state }
      )

      expect(subject).to receive(:sleep).with(1)

      subject.send(action, mock_service_name, timeout: timeout)
    end

    it "waits for at most 10 seconds if wait_hint/10 is > 10 seconds" do
      expect_successful_status_queries_and_return(
        { :dwCurrentState => pending_state, :dwWaitHint => 1000000, :dwCheckPoint => 1 },
        { :dwCurrentState => final_state }
      )

      expect(subject).to receive(:sleep).with(10)

      subject.send(action, mock_service_name, timeout: timeout)
    end

    it "does not raise an error if the service makes any progress while transitioning to #{final_state_str}" do
      expect_successful_status_queries_and_return(
        # The three "pending_state" statuses simulate the scenario where the service
        # makes some progress during the transition right when Puppet's about to
        # time out.
        { :dwCurrentState => pending_state, :dwWaitHint => 100000, :dwCheckPoint => 1 },
        { :dwCurrentState => pending_state, :dwWaitHint => 100000, :dwCheckPoint => 1 },
        { :dwCurrentState => pending_state, :dwWaitHint => 100000, :dwCheckPoint => 2 },

        { :dwCurrentState => final_state }
      )

      expect { subject.send(action, mock_service_name, timeout: timeout) }.to_not raise_error
    end

    it "raises a Puppet::Error if it times out while waiting for the transition to #{final_state_str}" do
      31.times do
        expect_successful_status_query_and_return(
          dwCurrentState: pending_state,
          dwWaitHint: 10000,
          dwCheckPoint: 1
        )
      end

      expect { subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
    end
  end

  # This shared example contains the unit tests for the transition_service_state
  # helper, which is the helper that all of our service actions like #start, #stop
  # delegate to. Including these tests under a shared example lets us include them in each of
  # those service action's unit tests. Before including this example in your tests, be
  # sure to define the following variables in a `let` context:
  #     * initial_state         -- The initial state of the service prior to performing the state
  #                                transition
  #
  #     * mock_state_transition -- A lambda that mocks the state transition. This should mock
  #                                any code in the block that's passed to the
  #                                transition_service_state helper
  #
  # See the unit tests for the #start method to see how this shared example's
  # included.
  #
  shared_examples "a service action that transitions the service state" do |action, valid_initial_states, pending_state, final_state|
    valid_initial_states_str = valid_initial_states.map do |state|
      Puppet::Util::Windows::Service::SERVICE_STATES[state]
    end.join(', ')
    pending_state_str = Puppet::Util::Windows::Service::SERVICE_STATES[pending_state].to_s
    final_state_str = Puppet::Util::Windows::Service::SERVICE_STATES[final_state].to_s

    it "noops if the service is already in the #{final_state} state" do
      expect_successful_status_query_and_return(dwCurrentState: final_state)

      expect { subject.send(action, mock_service_name, timeout: timeout) }.to_not raise_error
    end

    # invalid_initial_states will be empty for the #stop action
    invalid_initial_states = Puppet::Util::Windows::Service::SERVICE_STATES.keys - valid_initial_states - [final_state]
    unless invalid_initial_states.empty?
      it "raises a Puppet::Error if the service's initial state is not one of #{valid_initial_states_str}" do
        invalid_initial_state = invalid_initial_states.first
        expect_successful_status_query_and_return(dwCurrentState: invalid_initial_state)

        expect{ subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when there's a pending transition to the #{final_state} state" do
      before(:each) do
        expect_successful_status_query_and_return(dwCurrentState: pending_state)
      end

      include_examples "a service action waiting on a pending transition", pending_state do
        let(:action) { action }
      end
    end

    # If the service action accepts an unsafe pending state as one of the service's
    # initial states, then we need to test that the action waits for the service to
    # transition from that unsafe pending state before doing anything else.
    unsafe_pending_states = valid_initial_states & Puppet::Util::Windows::Service::UNSAFE_PENDING_STATES
    unless unsafe_pending_states.empty?
      unsafe_pending_state = unsafe_pending_states.first
      unsafe_pending_state_str = Puppet::Util::Windows::Service::SERVICE_STATES[unsafe_pending_state]

      context "waiting for a service with #{unsafe_pending_state_str} as its initial state" do
        before(:each) do
          # This mocks the status query to return the 'final_state' by default. Otherwise,
          # we will fail the tests in the latter parts of the code where we wait for the
          # service to finish transitioning to the 'final_state'.
          allow(subject::SERVICE_STATUS_PROCESS).to receive(:new).and_return(dwCurrentState: final_state)

          # Set our service's initial state
          expect_successful_status_query_and_return(dwCurrentState: unsafe_pending_state)

          mock_state_transition.call
        end

        include_examples "a service action waiting on a pending transition", unsafe_pending_state do
          let(:action) { action }
        end
      end
    end

    # reads e.g. "waiting for the service to transition to the SERVICE_RUNNING state after executing the 'start' action"
    #
    # NOTE: This is really unit testing the wait_on_state_transition helper
    context "waiting for the service to transition to the #{final_state_str} state after executing the '#{action}' action" do
      before(:each) do
        # Set our service's initial state prior to performing the state transition
        expect_successful_status_query_and_return(dwCurrentState: initial_state)

        mock_state_transition.call
      end

      it "raises a Puppet::Error if the service query fails" do
        expect(subject).to receive(:QueryServiceStatusEx).and_return(FFI::WIN32_FALSE)

        expect { subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end

      it "waits, then queries again until it transitions to #{final_state_str}" do
        expect_successful_status_queries_and_return(
          { :dwCurrentState => initial_state },
          { :dwCurrentState => initial_state },
          { :dwCurrentState => final_state }
        )

        expect(subject).to receive(:sleep).with(1).twice

        subject.send(action, mock_service_name, timeout: timeout)
      end

      context "when it transitions to the #{pending_state_str} state" do
        before(:each) do
          expect_successful_status_query_and_return(dwCurrentState: pending_state)
        end

        include_examples "a service action waiting on a pending transition", pending_state do
          let(:action) { action }
        end
      end

      it "raises a Puppet::Error if it times out while waiting for the transition to #{final_state_str}" do
        31.times do
          expect_successful_status_query_and_return(dwCurrentState: initial_state)
        end

        expect { subject.send(action, mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end
  end

  describe "#start" do
    # rspec will still try to load the tests even though
    # the :if => Puppet.features.microsoft_windows? filter
    # is passed-in to the top-level describe block on
    # non-Windows platforms; it just won't run them. However
    # on these platforms, the loading will fail because this
    # test uses a shared example that references variables
    # from the Windows::Service module when building the unit
    # tests, which is only available on Windows platforms.
    # Thus, we add the next here to ensure that rspec does not
    # attempt to load our test code. This is OK for us to do
    # because we do not want to run these tests on non-Windows
    # platforms.
    next unless Puppet.features.microsoft_windows?

    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      # Can't use rspec's subject here because that
      # can only be referenced inside an 'it' block.
      service = Puppet::Util::Windows::Service
      valid_initial_states = [
        service::SERVICE_STOP_PENDING,
        service::SERVICE_STOPPED,
        service::SERVICE_START_PENDING
      ]
      final_state = service::SERVICE_RUNNING

      include_examples "a service action that transitions the service state", :start, valid_initial_states, service::SERVICE_START_PENDING, final_state do
        let(:initial_state) { subject::SERVICE_STOPPED }
        let(:mock_state_transition) do
          lambda do
            allow(subject).to receive(:StartServiceW).and_return(1)
          end
        end
      end

      it "raises a Puppet::Error if StartServiceW returns false" do
        expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_STOPPED)

        expect(subject).to receive(:StartServiceW).and_return(FFI::WIN32_FALSE)

        expect { subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end

      it "starts the service" do
        expect_successful_status_queries_and_return(
          { dwCurrentState: subject::SERVICE_STOPPED },
          { dwCurrentState: subject::SERVICE_RUNNING }
        )

        expect(subject).to receive(:StartServiceW).and_return(1)

        subject.start(mock_service_name, timeout: timeout)
      end
    end
  end

  describe "#stop" do
    next unless Puppet.features.microsoft_windows?

    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      service = Puppet::Util::Windows::Service
      valid_initial_states = service::SERVICE_STATES.keys - [service::SERVICE_STOPPED]
      final_state = service::SERVICE_STOPPED

      include_examples "a service action that transitions the service state", :stop, valid_initial_states, service::SERVICE_STOP_PENDING, final_state do
        let(:initial_state) { subject::SERVICE_RUNNING }
        let(:mock_state_transition) do
          lambda do
            allow(subject).to receive(:ControlService).and_return(1)
          end
        end
      end

      it "raises a Puppet::Error if ControlService returns false" do
        expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_RUNNING)

        allow(subject).to receive(:ControlService).and_return(FFI::WIN32_FALSE)

        expect { subject.stop(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end

      it "stops the service" do
        expect_successful_status_queries_and_return(
          { dwCurrentState: subject::SERVICE_RUNNING },
          { dwCurrentState: subject::SERVICE_STOPPED }
        )

        expect(subject).to receive(:ControlService).and_return(1)

        subject.stop(mock_service_name, timeout: timeout)
      end
    end
  end

  describe "#resume" do
    next unless Puppet.features.microsoft_windows?

    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.start(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      service = Puppet::Util::Windows::Service
      valid_initial_states = [
        service::SERVICE_PAUSE_PENDING,
        service::SERVICE_PAUSED,
        service::SERVICE_CONTINUE_PENDING
      ]
      final_state = service::SERVICE_RUNNING

      include_examples "a service action that transitions the service state", :resume, valid_initial_states, service::SERVICE_CONTINUE_PENDING, final_state do
        let(:initial_state) { service::SERVICE_PAUSED }
        let(:mock_state_transition) do
          lambda do
            # We need to mock the status query because in the block for #resume, we
            # wait for the service to enter the SERVICE_PAUSED state prior to
            # performing the transition (in case it is in SERVICE_PAUSE_PENDING).
            expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_PAUSED)

            allow(subject).to receive(:ControlService).and_return(1)
          end
        end
      end

      context "waiting for the SERVICE_PAUSE_PENDING => SERVICE_PAUSED transition to finish before resuming it" do
        before(:each) do
          # This mocks the status query to return the SERVICE_RUNNING state by default.
          # Otherwise, we will fail the tests in the latter parts of the code where we
          # wait for the service to finish transitioning to the 'SERVICE_RUNNING' state.
          allow(subject::SERVICE_STATUS_PROCESS).to receive(:new).and_return(dwCurrentState: subject::SERVICE_RUNNING)

          expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_PAUSE_PENDING)

          allow(subject).to receive(:ControlService).and_return(1)
        end

        include_examples "a service action waiting on a pending transition", service::SERVICE_PAUSE_PENDING do
          let(:action) { :resume }
        end
      end

      it "raises a Puppet::Error if ControlService returns false" do
        expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_PAUSED)
        expect_successful_status_query_and_return(dwCurrentState: subject::SERVICE_PAUSED)

        allow(subject).to receive(:ControlService).and_return(FFI::WIN32_FALSE)

        expect { subject.resume(mock_service_name, timeout: timeout) }.to raise_error(Puppet::Error)
      end

      it "resumes the service" do
        expect_successful_status_queries_and_return(
          { dwCurrentState: subject::SERVICE_PAUSED },
          { dwCurrentState: subject::SERVICE_PAUSED },
          { dwCurrentState: subject::SERVICE_RUNNING }
        )

        expect(subject).to receive(:ControlService).and_return(1)

        subject.resume(mock_service_name, timeout: timeout)
      end
    end
  end

  describe "#service_state" do
    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.service_state(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.service_state(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      it "raises Puppet::Error if the result of the query is empty" do
        expect_successful_status_query_and_return({})
        expect{subject.service_state(mock_service_name)}.to raise_error(Puppet::Error)
      end

      it "raises Puppet::Error if the result of the query is an unknown state" do
        expect_successful_status_query_and_return({:dwCurrentState => 999})
        expect{subject.service_state(mock_service_name)}.to raise_error(Puppet::Error)
      end

      # We need to guard this section explicitly since rspec will always
      # construct all examples, even if it isn't going to run them.
      if Puppet.features.microsoft_windows?
        {
          :SERVICE_STOPPED => Puppet::Util::Windows::Service::SERVICE_STOPPED,
          :SERVICE_PAUSED => Puppet::Util::Windows::Service::SERVICE_PAUSED,
          :SERVICE_STOP_PENDING => Puppet::Util::Windows::Service::SERVICE_STOP_PENDING,
          :SERVICE_PAUSE_PENDING => Puppet::Util::Windows::Service::SERVICE_PAUSE_PENDING,
          :SERVICE_RUNNING => Puppet::Util::Windows::Service::SERVICE_RUNNING,
          :SERVICE_CONTINUE_PENDING => Puppet::Util::Windows::Service::SERVICE_CONTINUE_PENDING,
          :SERVICE_START_PENDING => Puppet::Util::Windows::Service::SERVICE_START_PENDING,
        }.each do |state_name, state|
          it "queries the service and returns #{state_name}" do
            expect_successful_status_query_and_return({:dwCurrentState => state})
            expect(subject.service_state(mock_service_name)).to eq(state_name)
          end
        end
      end
    end
  end

  describe "#service_start_type" do
    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.service_start_type(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.service_start_type(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      # We need to guard this section explicitly since rspec will always
      # construct all examples, even if it isn't going to run them.
      if Puppet.features.microsoft_windows?
        {
          :SERVICE_AUTO_START => Puppet::Util::Windows::Service::SERVICE_AUTO_START,
          :SERVICE_BOOT_START => Puppet::Util::Windows::Service::SERVICE_BOOT_START,
          :SERVICE_SYSTEM_START => Puppet::Util::Windows::Service::SERVICE_SYSTEM_START,
          :SERVICE_DEMAND_START => Puppet::Util::Windows::Service::SERVICE_DEMAND_START,
          :SERVICE_DISABLED => Puppet::Util::Windows::Service::SERVICE_DISABLED,
        }.each do |start_type_name, start_type|
          it "queries the service and returns the service start type #{start_type_name}" do
            expect_successful_config_query_and_return({:dwStartType => start_type})
            if start_type_name == :SERVICE_AUTO_START
              expect_successful_config_query2_and_return(subject::SERVICE_DELAYED_AUTO_START_INFO, {:fDelayedAutostart => 0})
            end
            expect(subject.service_start_type(mock_service_name)).to eq(start_type_name)
          end
        end
      end

      it "raises a puppet error if the service query fails" do
        expect(subject).to receive(:QueryServiceConfigW)
        expect(subject).to receive(:QueryServiceConfigW).and_return(FFI::WIN32_FALSE)
        expect{ subject.service_start_type(mock_service_name) }.to raise_error(Puppet::Error)
      end
    end
  end

  describe "#set_startup_configuration" do
    let(:status_checks) { sequence('status_checks') }

    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.set_startup_configuration(mock_service_name, options: {startup_type: :SERVICE_DEMAND_START}) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service cannot be opened" do
      let(:service) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.set_startup_configuration(mock_service_name, options: {startup_type: :SERVICE_DEMAND_START}) }.to raise_error(Puppet::Error)
      end
    end

    context "when the service can be opened" do
      it "Raises an error on an unsuccessful change" do
        expect(subject).to receive(:ChangeServiceConfigW).and_return(FFI::WIN32_FALSE)
        expect{ subject.set_startup_configuration(mock_service_name, options: {startup_type: :SERVICE_DEMAND_START}) }.to raise_error(Puppet::Error)
      end
    end
  end

  describe "#services" do
    let(:pointer_sequence) { sequence('pointer_sequence') }

    context "when the service control manager cannot be opened" do
      let(:scm) { FFI::Pointer::NULL_HANDLE }
      it "raises a puppet error" do
        expect{ subject.services }.to raise_error(Puppet::Error)
      end
    end

    context "when the service control manager is open" do
      let(:cursor) { [ 'svc1', 'svc2', 'svc3' ] }
      let(:svc1name_ptr) { double() }
      let(:svc2name_ptr) { double() }
      let(:svc3name_ptr) { double() }
      let(:svc1displayname_ptr) { double() }
      let(:svc2displayname_ptr) { double() }
      let(:svc3displayname_ptr) { double() }
      let(:svc1) { { :lpServiceName => svc1name_ptr, :lpDisplayName => svc1displayname_ptr, :ServiceStatusProcess => 'foo' } }
      let(:svc2) { { :lpServiceName => svc2name_ptr, :lpDisplayName => svc2displayname_ptr, :ServiceStatusProcess => 'foo' } }
      let(:svc3) { { :lpServiceName => svc3name_ptr, :lpDisplayName => svc3displayname_ptr, :ServiceStatusProcess => 'foo' } }

      it "Raises an error if EnumServicesStatusExW fails" do
        expect(subject).to receive(:EnumServicesStatusExW)
        expect(subject).to receive(:EnumServicesStatusExW).and_return(FFI::WIN32_FALSE)
        expect{ subject.services }.to raise_error(Puppet::Error)
      end

      it "Reads the buffer using pointer arithmetic to create a hash of service entries" do
        # the first read_dword is for reading the bytes required, let that return 3 too.
        # the second read_dword will actually read the number of services returned
        expect(pointer).to receive(:read_dword).twice.and_return(3)
        expect(FFI::Pointer).to receive(:new).with(subject::ENUM_SERVICE_STATUS_PROCESSW, pointer).and_return(cursor)
        expect(subject::ENUM_SERVICE_STATUS_PROCESSW).to receive(:new).with('svc1').and_return(svc1)
        expect(subject::ENUM_SERVICE_STATUS_PROCESSW).to receive(:new).with('svc2').and_return(svc2)
        expect(subject::ENUM_SERVICE_STATUS_PROCESSW).to receive(:new).with('svc3').and_return(svc3)
        expect(svc1name_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('svc1')
        expect(svc2name_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('svc2')
        expect(svc3name_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('svc3')
        expect(svc1displayname_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('service 1')
        expect(svc2displayname_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('service 2')
        expect(svc3displayname_ptr).to receive(:read_arbitrary_wide_string_up_to).and_return('service 3')
        expect(subject.services).to eq({
          'svc1' => { :display_name => 'service 1', :service_status_process => 'foo' },
          'svc2' => { :display_name => 'service 2', :service_status_process => 'foo' },
          'svc3' => { :display_name => 'service 3', :service_status_process => 'foo' }
        })
      end
    end
  end
end
