require 'spec_helper'
require 'puppet/resource/status'

describe Puppet::Resource::Status do
  include PuppetSpec::Files

  let(:resource) { Puppet::Type.type(:file).new(:path => make_absolute("/my/file")) }
  let(:containment_path) { ["foo", "bar", "baz"] }
  let(:status) { Puppet::Resource::Status.new(resource) }

  before do
    allow(resource).to receive(:pathbuilder).and_return(containment_path)
  end

  it "should compute type and title correctly" do
    expect(status.resource_type).to eq("File")
    expect(status.title).to eq(make_absolute("/my/file"))
  end

  [:file, :line, :evaluation_time].each do |attr|
    it "should support #{attr}" do
      status.send(attr.to_s + "=", "foo")
      expect(status.send(attr)).to eq("foo")
    end
  end

  [:skipped, :failed, :restarted, :failed_to_restart, :changed, :out_of_sync, :scheduled].each do |attr|
    it "should support #{attr}" do
      status.send(attr.to_s + "=", "foo")
      expect(status.send(attr)).to eq("foo")
    end

    it "should have a boolean method for determining whether it was #{attr}" do
      status.send(attr.to_s + "=", "foo")
      expect(status).to send("be_#{attr}")
    end
  end

  it "should accept a resource at initialization" do
    expect(Puppet::Resource::Status.new(resource).resource).not_to be_nil
  end

  it "should set its source description to the resource's path" do
    expect(resource).to receive(:path).and_return("/my/path")
    expect(Puppet::Resource::Status.new(resource).source_description).to eq("/my/path")
  end

  it "should set its containment path" do
  expect(Puppet::Resource::Status.new(resource).containment_path).to eq(containment_path)
  end

  [:file, :line].each do |attr|
    it "should copy the resource's #{attr}" do
      expect(resource).to receive(attr).and_return("foo")
      expect(Puppet::Resource::Status.new(resource).send(attr)).to eq("foo")
    end
  end

  it "should copy the resource's tags" do
    resource.tag('foo', 'bar')
    status = Puppet::Resource::Status.new(resource)
    expect(status).to be_tagged("foo")
    expect(status).to be_tagged("bar")
  end

  it "should always convert the resource to a string" do
    expect(resource).to receive(:to_s).and_return("foo")
    expect(Puppet::Resource::Status.new(resource).resource).to eq("foo")
  end

  it 'should set the provider_used correctly' do
    expected_name = if Puppet::Util::Platform.windows?
                      'windows'
                    else
                      'posix'
                    end
    expect(status.provider_used).to eq(expected_name)
  end

  it "should support tags" do
    expect(Puppet::Resource::Status.ancestors).to include(Puppet::Util::Tagging)
  end

  it "should create a timestamp at its creation time" do
    expect(status.time).to be_instance_of(Time)
  end

  it "should support adding events" do
    event = Puppet::Transaction::Event.new(:name => :foobar)
    status.add_event(event)
    expect(status.events).to eq([event])
  end

  it "should use '<<' to add events" do
    event = Puppet::Transaction::Event.new(:name => :foobar)
    expect(status << event).to equal(status)
    expect(status.events).to eq([event])
  end

  it "fails and records a failure event with a given message" do
    status.fail_with_event("foo fail")
    event = status.events[0]

    expect(event.message).to eq("foo fail")
    expect(event.status).to eq("failure")
    expect(event.name).to eq(:resource_error)
    expect(status.failed?).to be_truthy
  end

  it "fails and records a failure event with a given exception" do
    error = StandardError.new("the message")
    expect(resource).to receive(:log_exception).with(error, "Could not evaluate: the message")
    expect(status).to receive(:fail_with_event).with("the message")

    status.failed_because(error)
  end

  it "should count the number of successful events and set changed" do
    3.times{ status << Puppet::Transaction::Event.new(:status => 'success') }
    expect(status.change_count).to eq(3)

    expect(status.changed).to eq(true)
    expect(status.out_of_sync).to eq(true)
  end

  it "should not start with any changes" do
    expect(status.change_count).to eq(0)

    expect(status.changed).to eq(false)
    expect(status.out_of_sync).to eq(false)
  end

  it "should not treat failure, audit, or noop events as changed" do
    ['failure', 'audit', 'noop'].each do |s| status << Puppet::Transaction::Event.new(:status => s) end
    expect(status.change_count).to eq(0)
    expect(status.changed).to eq(false)
  end

  it "should not treat audit events as out of sync" do
    status << Puppet::Transaction::Event.new(:status => 'audit')
    expect(status.out_of_sync_count).to eq(0)
    expect(status.out_of_sync).to eq(false)
  end

  ['failure', 'noop', 'success'].each do |event_status|
    it "should treat #{event_status} events as out of sync" do
      3.times do status << Puppet::Transaction::Event.new(:status => event_status) end
      expect(status.out_of_sync_count).to eq(3)
      expect(status.out_of_sync).to eq(true)
    end
  end

  context 'when serializing' do
    let(:status) do
      s = Puppet::Resource::Status.new(resource)
      s.file = '/foo.rb'
      s.line = 27
      s.evaluation_time = 2.7
      s.tags = %w{one two}
      s << Puppet::Transaction::Event.new(:name => :mode_changed, :status => 'audit')
      s.failed = false
      s.changed = true
      s.out_of_sync = true
      s.skipped = false
      s.provider_used = 'provider_used_class_name'
      s.failed_to_restart = false
      s
    end

    it 'should round trip through json' do
      expect(status.containment_path).to eq(containment_path)

      tripped = Puppet::Resource::Status.from_data_hash(JSON.parse(status.to_json))

      expect(tripped.title).to eq(status.title)
      expect(tripped.containment_path).to eq(status.containment_path)
      expect(tripped.file).to eq(status.file)
      expect(tripped.line).to eq(status.line)
      expect(tripped.resource).to eq(status.resource)
      expect(tripped.resource_type).to eq(status.resource_type)
      expect(tripped.provider_used).to eq(status.provider_used)
      expect(tripped.evaluation_time).to eq(status.evaluation_time)
      expect(tripped.tags).to eq(status.tags)
      expect(tripped.time).to eq(status.time)
      expect(tripped.failed).to eq(status.failed)
      expect(tripped.changed).to eq(status.changed)
      expect(tripped.out_of_sync).to eq(status.out_of_sync)
      expect(tripped.skipped).to eq(status.skipped)
      expect(tripped.failed_to_restart).to eq(status.failed_to_restart)

      expect(tripped.change_count).to eq(status.change_count)
      expect(tripped.out_of_sync_count).to eq(status.out_of_sync_count)
      expect(events_as_hashes(tripped)).to eq(events_as_hashes(status))
    end

    it 'to_data_hash returns value that is instance of to Data' do
      expect(Puppet::Pops::Types::TypeFactory.data.instance?(status.to_data_hash)).to be_truthy
    end

    def events_as_hashes(report)
      report.events.collect do |e|
        {
          :audited => e.audited,
          :property => e.property,
          :previous_value => e.previous_value,
          :desired_value => e.desired_value,
          :historical_value => e.historical_value,
          :message => e.message,
          :name => e.name,
          :status => e.status,
          :time => e.time,
        }
      end
    end
  end
end
