require 'spec_helper'

require 'puppet/transaction/event_manager'

describe Puppet::Transaction::EventManager do
  include PuppetSpec::Files

  describe "at initialization" do
    it "should require a transaction" do
      expect(Puppet::Transaction::EventManager.new("trans").transaction).to eq("trans")
    end
  end

  it "should delegate its relationship graph to the transaction" do
    transaction = double('transaction')
    manager = Puppet::Transaction::EventManager.new(transaction)

    expect(transaction).to receive(:relationship_graph).and_return("mygraph")

    expect(manager.relationship_graph).to eq("mygraph")
  end

  describe "when queueing events" do
    before do
      @manager = Puppet::Transaction::EventManager.new(@transaction)

      @resource = Puppet::Type.type(:file).new :path => make_absolute("/my/file")

      @graph = double('graph', :matching_edges => [], :resource => @resource)
      allow(@manager).to receive(:relationship_graph).and_return(@graph)

      @event = Puppet::Transaction::Event.new(:name => :foo, :resource => @resource)
    end

    it "should store all of the events in its event list" do
      @event2 = Puppet::Transaction::Event.new(:name => :bar, :resource => @resource)
      @manager.queue_events(@resource, [@event, @event2])

      expect(@manager.events).to include(@event)
      expect(@manager.events).to include(@event2)
    end

    it "should queue events for the target and callback of any matching edges" do
      edge1 = double("edge1", :callback => :c1, :source => double("s1"), :target => double("t1", :c1 => nil))
      edge2 = double("edge2", :callback => :c2, :source => double("s2"), :target => double("t2", :c2 => nil))

      expect(@graph).to receive(:matching_edges).with(@event, anything).and_return([edge1, edge2])

      expect(@manager).to receive(:queue_events_for_resource).with(@resource, edge1.target, edge1.callback, [@event])
      expect(@manager).to receive(:queue_events_for_resource).with(@resource, edge2.target, edge2.callback, [@event])

      @manager.queue_events(@resource, [@event])
    end

    it "should queue events for the changed resource if the resource is self-refreshing and not being deleted" do
      allow(@graph).to receive(:matching_edges).and_return([])

      expect(@resource).to receive(:self_refresh?).and_return(true)
      expect(@resource).to receive(:deleting?).and_return(false)
      expect(@manager).to receive(:queue_events_for_resource).with(@resource, @resource, :refresh, [@event])

      @manager.queue_events(@resource, [@event])
    end

    it "should not queue events for the changed resource if the resource is not self-refreshing" do
      allow(@graph).to receive(:matching_edges).and_return([])

      expect(@resource).to receive(:self_refresh?).and_return(false)
      allow(@resource).to receive(:deleting?).and_return(false)
      expect(@manager).not_to receive(:queue_events_for_resource)

      @manager.queue_events(@resource, [@event])
    end

    it "should not queue events for the changed resource if the resource is being deleted" do
      allow(@graph).to receive(:matching_edges).and_return([])

      expect(@resource).to receive(:self_refresh?).and_return(true)
      expect(@resource).to receive(:deleting?).and_return(true)
      expect(@manager).not_to receive(:queue_events_for_resource)

      @manager.queue_events(@resource, [@event])
    end

    it "should ignore edges that don't have a callback" do
      edge1 = double("edge1", :callback => :nil, :source => double("s1"), :target => double("t1", :c1 => nil))

      expect(@graph).to receive(:matching_edges).and_return([edge1])

      expect(@manager).not_to receive(:queue_events_for_resource)

      @manager.queue_events(@resource, [@event])
    end

    it "should ignore targets that don't respond to the callback" do
      edge1 = double("edge1", :callback => :c1, :source => double("s1"), :target => double("t1"))

      expect(@graph).to receive(:matching_edges).and_return([edge1])

      expect(@manager).not_to receive(:queue_events_for_resource)

      @manager.queue_events(@resource, [@event])
    end

    it "should dequeue events for the changed resource if an event with invalidate_refreshes is processed" do
      @event2 = Puppet::Transaction::Event.new(:name => :foo, :resource => @resource, :invalidate_refreshes => true)

      allow(@graph).to receive(:matching_edges).and_return([])

      expect(@manager).to receive(:dequeue_events_for_resource).with(@resource, :refresh)

      @manager.queue_events(@resource, [@event, @event2])
    end
  end

  describe "when queueing events for a resource" do
    before do
      @transaction = double('transaction')
      @manager = Puppet::Transaction::EventManager.new(@transaction)
    end

    it "should do nothing if no events are queued" do
      @manager.queued_events(double("target")) { |callback, events| raise "should never reach this" }
    end

    it "should yield the callback and events for each callback" do
      target = double("target")

      2.times do |i|
        @manager.queue_events_for_resource(double("source", :info => nil), target, "callback#{i}", ["event#{i}"])
      end

      @manager.queued_events(target) { |callback, events| }
    end

    it "should use the source to log that it's scheduling a refresh of the target" do
      target = double("target")
      source = double('source')
      expect(source).to receive(:info)

      @manager.queue_events_for_resource(source, target, "callback", ["event"])

      @manager.queued_events(target) { |callback, events| }
    end
  end

  describe "when processing events for a given resource" do
    before do
      @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil)
      @manager = Puppet::Transaction::EventManager.new(@transaction)
      allow(@manager).to receive(:queue_events)

      @resource = Puppet::Type.type(:file).new :path => make_absolute("/my/file")
      @event = Puppet::Transaction::Event.new(:name => :event, :resource => @resource)

      @resource.class.send(:define_method, :callback1) {}
      @resource.class.send(:define_method, :callback2) {}
    end

    it "should call the required callback once for each set of associated events" do
      expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event]).and_yield(:callback2, [@event])

      expect(@resource).to receive(:callback1)
      expect(@resource).to receive(:callback2)

      @manager.process_events(@resource)
    end

    it "should set the 'restarted' state on the resource status" do
      expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])

      allow(@resource).to receive(:callback1)

      @manager.process_events(@resource)

      expect(@transaction.resource_status(@resource)).to be_restarted
    end

    it "should have an event on the resource status" do
      expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])

      allow(@resource).to receive(:callback1)

      @manager.process_events(@resource)

      expect(@transaction.resource_status(@resource).events.length).to eq(1)
    end

    it "should queue a 'restarted' event generated by the resource" do
      expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])

      allow(@resource).to receive(:callback1)

      expect(@resource).to receive(:event).with({:message => "Triggered 'callback1' from 1 event", :status => 'success', :name => 'callback1'})
      expect(@resource).to receive(:event).with({:name => :restarted, :status => "success"}).and_return("myevent")
      expect(@manager).to receive(:queue_events).with(@resource, ["myevent"])

      @manager.process_events(@resource)
    end

    it "should log that it restarted" do
      expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])

      allow(@resource).to receive(:callback1)

      expect(@resource).to receive(:notice).with(/Triggered 'callback1'/)

      @manager.process_events(@resource)
    end

    describe "and the events include a noop event and at least one non-noop event" do
      before do
        allow(@event).to receive(:status).and_return("noop")
        @event2 = Puppet::Transaction::Event.new(:name => :event, :resource => @resource)
        @event2.status = "success"
        expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event, @event2])
        @resource.class.send(:define_method, :callback1) {}
      end

      it "should call the callback" do

        expect(@resource).to receive(:callback1)

        @manager.process_events(@resource)
      end
    end

    describe "and the events are all noop events" do
      before do
        allow(@event).to receive(:status).and_return("noop")
        allow(@resource).to receive(:event).and_return(Puppet::Transaction::Event.new)
        expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])
        @resource.class.send(:define_method, :callback1) {}
      end

      it "should log" do
        expect(@resource).to receive(:notice).with(/Would have triggered 'callback1'/)

        @manager.process_events(@resource)
      end

      it "should not call the callback" do
        expect(@resource).not_to receive(:callback1)

        @manager.process_events(@resource)
      end

      it "should queue a new noop event generated from the resource" do
        event = Puppet::Transaction::Event.new
        expect(@resource).to receive(:event).with({:status => "noop", :name => :noop_restart}).and_return(event)
        expect(@manager).to receive(:queue_events).with(@resource, [event])

        @manager.process_events(@resource)
      end
    end

    describe "and the resource has noop set to true" do
      before do
        allow(@event).to receive(:status).and_return("success")
        allow(@resource).to receive(:event).and_return(Puppet::Transaction::Event.new)
        allow(@resource).to receive(:noop?).and_return(true)
        expect(@manager).to receive(:queued_events).with(@resource).and_yield(:callback1, [@event])
        @resource.class.send(:define_method, :callback1) {}
      end

      it "should log" do
        expect(@resource).to receive(:notice).with(/Would have triggered 'callback1'/)

        @manager.process_events(@resource)
      end

      it "should not call the callback" do
        expect(@resource).not_to receive(:callback1)

        @manager.process_events(@resource)
      end

      it "should queue a new noop event generated from the resource" do
        event = Puppet::Transaction::Event.new
        expect(@resource).to receive(:event).with({:status => "noop", :name => :noop_restart}).and_return(event)
        expect(@manager).to receive(:queue_events).with(@resource, [event])

        @manager.process_events(@resource)
      end
    end

    describe "and the callback fails" do
      before do
        @resource.class.send(:define_method, :callback1) { raise "a failure" }

        expect(@manager).to receive(:queued_events).and_yield(:callback1, [@event])
      end

      it "should emit an error and log but not fail" do
        expect(@resource).to receive(:err).with('Failed to call callback1: a failure').and_call_original

        @manager.process_events(@resource)

        expect(@logs).to include(an_object_having_attributes(level: :err, message: 'a failure'))
      end

      it "should set the 'failed_restarts' state on the resource status" do
        @manager.process_events(@resource)
        expect(@transaction.resource_status(@resource)).to be_failed_to_restart
      end

      it "should set the 'failed' state on the resource status" do
        @manager.process_events(@resource)
        expect(@transaction.resource_status(@resource)).to be_failed
      end

      it "should record a failed event on the resource status" do
        @manager.process_events(@resource)

        expect(@transaction.resource_status(@resource).events.length).to eq(1)
        expect(@transaction.resource_status(@resource).events[0].status).to eq('failure')
      end

      it "should not queue a 'restarted' event" do
        expect(@manager).not_to receive(:queue_events)
        @manager.process_events(@resource)
      end

      it "should set the 'restarted' state on the resource status" do
        @manager.process_events(@resource)
        expect(@transaction.resource_status(@resource)).not_to be_restarted
      end
    end
  end

  describe "when queueing then processing events for a given resource" do
    before do
      @catalog = Puppet::Resource::Catalog.new
      @target = Puppet::Type.type(:exec).new(name: 'target', path: ENV['PATH'])
      @resource = Puppet::Type.type(:exec).new(name: 'resource', path: ENV['PATH'], notify: @target)
      @catalog.add_resource(@resource, @target)

      @manager = Puppet::Transaction::EventManager.new(Puppet::Transaction.new(@catalog, nil, nil))

      @event  = Puppet::Transaction::Event.new(:name => :notify, :resource => @target)
      @event2 = Puppet::Transaction::Event.new(:name => :service_start, :resource => @target, :invalidate_refreshes => true)
    end

    it "should succeed when there's no invalidated event" do
      @manager.queue_events(@target, [@event2])
    end

    describe "and the events were dequeued/invalidated" do
      before do
        expect(@resource).to receive(:info).with(/Scheduling refresh/)
        expect(@target).to receive(:info).with(/Unscheduling/)
      end

      it "should not run an event or log" do
        expect(@target).not_to receive(:notice).with(/Would have triggered 'refresh'/)
        expect(@target).not_to receive(:refresh)

        @manager.queue_events(@resource, [@event])
        @manager.queue_events(@target, [@event2])
        @manager.process_events(@resource)
        @manager.process_events(@target)
      end
    end
  end
end
