require 'spec_helper'

require 'flapjack/data/entity'
require 'flapjack/data/entity_check'
require 'flapjack/data/tag_set'

describe Flapjack::Data::EntityCheck, :redis => true do

  let(:name)  { 'abc-123' }
  let(:check) { 'ping' }

  let(:half_an_hour) { 30 * 60 }

  before(:each) do
    Flapjack::Data::Contact.add({'id'         => '362',
                                 'first_name' => 'John',
                                 'last_name'  => 'Johnson',
                                 'email'      => 'johnj@example.com' },
                                 :redis       => @redis)

    Flapjack::Data::Entity.add({'id'   => '5000',
                                'name' => name,
                                'contacts' => ['362']},
                               :redis => @redis)
  end

  it "is created for an event id" do
    ec = Flapjack::Data::EntityCheck.for_event_id("#{name}:ping", :redis => @redis)
    ec.should_not be_nil
    ec.entity.should_not be_nil
    ec.entity.name.should_not be_nil
    ec.entity.name.should == name
    ec.check.should_not be_nil
    ec.check.should == 'ping'
  end

  it "is created for an entity name" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, 'ping', :redis => @redis)
    ec.should_not be_nil
    ec.entity.should_not be_nil
    ec.entity.name.should_not be_nil
    ec.entity.name.should == name
    ec.check.should_not be_nil
    ec.check.should == 'ping'
  end

  it "is created for an entity id" do
    ec = Flapjack::Data::EntityCheck.for_entity_id(5000, 'ping', :redis => @redis)
    ec.should_not be_nil
    ec.entity.should_not be_nil
    ec.entity.name.should_not be_nil
    ec.entity.name.should == name
    ec.check.should_not be_nil
    ec.check.should == 'ping'
  end

  it "is created for an entity object" do
    e = Flapjack::Data::Entity.find_by_name(name, :redis => @redis)
    ec = Flapjack::Data::EntityCheck.for_entity(e, 'ping', :redis => @redis)
    ec.should_not be_nil
    ec.entity.should_not be_nil
    ec.entity.name.should_not be_nil
    ec.entity.name.should == name
    ec.check.should_not be_nil
    ec.check.should == 'ping'
  end

  it "is not created for a missing entity" do
    expect {
      Flapjack::Data::EntityCheck.for_entity(nil, 'ping', :redis => @redis)
    }.to raise_error
  end

  it "raises an error if not created with a redis connection handle" do
    expect {
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, 'ping')
    }.to raise_error
  end

  context "maintenance" do

    it "returns that it is in unscheduled maintenance" do
      @redis.set("#{name}:#{check}:unscheduled_maintenance", Time.now.to_i.to_s)

      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.should be_in_unscheduled_maintenance
    end

    it "returns that it is not in unscheduled maintenance" do
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.should_not be_in_unscheduled_maintenance
    end

    it "returns that it is in scheduled maintenance" do
      @redis.set("#{name}:#{check}:scheduled_maintenance", Time.now.to_i.to_s)

      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.should be_in_scheduled_maintenance
    end

    it "returns that it is not in scheduled maintenance" do
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.should_not be_in_scheduled_maintenance
    end

    it "returns its current maintenance period" do
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.current_maintenance(:scheduled => true).should be_nil

      t = Time.now.to_i

      ec.create_unscheduled_maintenance(t, half_an_hour, :summary => 'oops')
      ec.current_maintenance.should == {:start_time => t,
                                        :duration => half_an_hour,
                                        :summary => 'oops'}
    end

    it "creates an unscheduled maintenance period" do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_unscheduled_maintenance(t, half_an_hour, :summary => 'oops')

      ec.should be_in_unscheduled_maintenance

      umps = ec.maintenances(nil, nil, :scheduled => false)
      umps.should_not be_nil
      umps.should be_an(Array)
      umps.should have(1).unscheduled_maintenance_period
      umps[0].should be_a(Hash)

      start_time = umps[0][:start_time]
      start_time.should_not be_nil
      start_time.should be_an(Integer)
      start_time.should == t

      duration = umps[0][:duration]
      duration.should_not be_nil
      duration.should be_a(Float)
      duration.should == half_an_hour

      summary = @redis.get("#{name}:#{check}:#{t}:unscheduled_maintenance:summary")
      summary.should_not be_nil
      summary.should == 'oops'
    end

    it "creates an unscheduled maintenance period and ends the current one early", :time => true do
      t = Time.now.to_i
      later_t = t + (15 * 60)
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_unscheduled_maintenance(t, half_an_hour, :summary => 'oops')
      Delorean.time_travel_to( Time.at(later_t) )
      ec.create_unscheduled_maintenance(later_t, half_an_hour, :summary => 'spoo')

      ec.should be_in_unscheduled_maintenance

      umps = ec.maintenances(nil, nil, :scheduled => false)
      umps.should_not be_nil
      umps.should be_an(Array)
      umps.should have(2).unscheduled_maintenance_periods
      umps[0].should be_a(Hash)

      start_time = umps[0][:start_time]
      start_time.should_not be_nil
      start_time.should be_an(Integer)
      start_time.should == t

      duration = umps[0][:duration]
      duration.should_not be_nil
      duration.should be_a(Float)
      duration.should == (15 * 60)

      start_time_curr = umps[1][:start_time]
      start_time_curr.should_not be_nil
      start_time_curr.should be_an(Integer)
      start_time_curr.should == later_t

      duration_curr = umps[1][:duration]
      duration_curr.should_not be_nil
      duration_curr.should be_a(Float)
      duration_curr.should == half_an_hour
    end

    it "ends an unscheduled maintenance period" do
      t = Time.now.to_i
      later_t = t + (15 * 60)
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

      ec.create_unscheduled_maintenance(t, half_an_hour, :summary => 'oops')
      ec.should be_in_unscheduled_maintenance

      Delorean.time_travel_to( Time.at(later_t) )
      ec.should be_in_unscheduled_maintenance
      ec.end_unscheduled_maintenance(later_t)
      ec.should_not be_in_unscheduled_maintenance

      umps = ec.maintenances(nil, nil, :scheduled => false)
      umps.should_not be_nil
      umps.should be_an(Array)
      umps.should have(1).unscheduled_maintenance_period
      umps[0].should be_a(Hash)

      start_time = umps[0][:start_time]
      start_time.should_not be_nil
      start_time.should be_an(Integer)
      start_time.should == t

      duration = umps[0][:duration]
      duration.should_not be_nil
      duration.should be_a(Float)
      duration.should == (15 * 60)
    end

    it "creates a scheduled maintenance period for a future time" do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(t + (60 * 60),
        half_an_hour, :summary => "30 minutes")

      smps = ec.maintenances(nil, nil, :scheduled => true)
      smps.should_not be_nil
      smps.should be_an(Array)
      smps.should have(1).scheduled_maintenance_period
      smps[0].should be_a(Hash)

      start_time = smps[0][:start_time]
      start_time.should_not be_nil
      start_time.should be_an(Integer)
      start_time.should == (t + (60 * 60))

      duration = smps[0][:duration]
      duration.should_not be_nil
      duration.should be_a(Float)
      duration.should == half_an_hour
    end

    # TODO this should probably enforce that it starts in the future
    it "creates a scheduled maintenance period covering the current time" do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(t - (60 * 60),
        2 * (60 * 60), :summary => "2 hours")

      smps = ec.maintenances(nil, nil, :scheduled => true)
      smps.should_not be_nil
      smps.should be_an(Array)
      smps.should have(1).scheduled_maintenance_period
      smps[0].should be_a(Hash)

      start_time = smps[0][:start_time]
      start_time.should_not be_nil
      start_time.should be_an(Integer)
      start_time.should == (t - (60 * 60))

      duration = smps[0][:duration]
      duration.should_not be_nil
      duration.should be_a(Float)
      duration.should == 2 * (60 * 60)
    end

    it "removes a scheduled maintenance period for a future time" do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(t + (60 * 60),
        2 * (60 * 60), :summary => "2 hours")

      ec.end_scheduled_maintenance(t + (60 * 60))

      smps = ec.maintenances(nil, nil, :scheduled => true)
      smps.should_not be_nil
      smps.should be_an(Array)
      smps.should be_empty
    end

    # maint period starts an hour from now, goes for two hours -- at 30 minutes into
    # it we stop it, and its duration should be 30 minutes
    it "shortens a scheduled maintenance period covering a current time", :time => true do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(t + (60 * 60),
        2 * (60 * 60), :summary => "2 hours")

      Delorean.time_travel_to( Time.at(t + (90 * 60)) )

      ec.end_scheduled_maintenance(t + (60 * 60))

      smps = ec.maintenances(nil, nil, :scheduled => true)
      smps.should_not be_nil
      smps.should be_an(Array)
      smps.should_not be_empty
      smps.should have(1).item
      smps.first[:duration].should == (30 * 60)
    end

    it "does not alter or remove a scheduled maintenance period covering a past time", :time => true do
      t = Time.now.to_i
      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(t + (60 * 60),
        2 * (60 * 60), :summary => "2 hours")

      Delorean.time_travel_to( Time.at(t + (6 * (60 * 60)) ))

      ec.end_scheduled_maintenance(t + (60 * 60))

      smps = ec.maintenances(nil, nil, :scheduled => true)
      smps.should_not be_nil
      smps.should be_an(Array)
      smps.should_not be_empty
      smps.should have(1).item
      smps.first[:duration].should == 2 * (60 * 60)
    end

    it "returns a list of scheduled maintenance periods" do
      t = Time.now.to_i
      five_hours_ago = t - (60 * 60 * 5)
      three_hours_ago = t - (60 * 60 * 3)

      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_scheduled_maintenance(five_hours_ago, half_an_hour,
        :summary => "first")
      ec.create_scheduled_maintenance(three_hours_ago, half_an_hour,
        :summary => "second")

      smp = ec.maintenances(nil, nil, :scheduled => true)
      smp.should_not be_nil
      smp.should be_an(Array)
      smp.should have(2).scheduled_maintenance_periods
      smp[0].should == {:start_time => five_hours_ago,
                        :end_time   => five_hours_ago + half_an_hour,
                        :duration   => half_an_hour,
                        :summary    => "first"}
      smp[1].should == {:start_time => three_hours_ago,
                        :end_time   => three_hours_ago + half_an_hour,
                        :duration   => half_an_hour,
                        :summary    => "second"}
    end

    it "returns a list of unscheduled maintenance periods" do
      t = Time.now.to_i
      five_hours_ago = t - (60 * 60 * 5)
      three_hours_ago = t - (60 * 60 * 3)

      ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
      ec.create_unscheduled_maintenance(five_hours_ago,
        half_an_hour, :summary => "first")
      ec.create_unscheduled_maintenance(three_hours_ago,
        half_an_hour, :summary => "second")

      ump =  ec.maintenances(nil, nil, :scheduled => false)
      ump.should_not be_nil
      ump.should be_an(Array)
      ump.should have(2).unscheduled_maintenance_periods
      ump[0].should == {:start_time => five_hours_ago,
                        :end_time   => five_hours_ago + half_an_hour,
                        :duration   => half_an_hour,
                        :summary    => "first"}
      ump[1].should == {:start_time => three_hours_ago,
                        :end_time   => three_hours_ago + half_an_hour,
                        :duration   => half_an_hour,
                        :summary    => "second"}
    end

  end

  it "returns its state" do
    @redis.hset("check:#{name}:#{check}", 'state', 'ok')

    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    state = ec.state
    state.should_not be_nil
    state.should == 'ok'
  end

  it "updates state" do
    @redis.hset("check:#{name}:#{check}", 'state', 'ok')

    old_timestamp = @redis.hget("check:#{name}:#{check}", 'last_update')

    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.update_state('critical')

    state = @redis.hget("check:#{name}:#{check}", 'state')
    state.should_not be_nil
    state.should == 'critical'

    new_timestamp = @redis.hget("check:#{name}:#{check}", 'last_update')
    new_timestamp.should_not == old_timestamp
  end

  it "updates enabled checks" do
    ts = Time.now.to_i
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.last_update = ts

    saved_check_ts = @redis.zscore("current_checks:#{name}", check)
    saved_check_ts.should_not be_nil
    saved_check_ts.should == ts
    saved_entity_ts = @redis.zscore("current_entities", name)
    saved_entity_ts.should_not be_nil
    saved_entity_ts.should == ts
  end

  it "exposes that it is enabled" do
    @redis.zadd("current_checks:#{name}", Time.now.to_i, check)
    @redis.zadd("current_entities", Time.now.to_i, name)
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    e = ec.enabled?
    e.should be_true
  end

  it "exposes that it is disabled" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    e = ec.enabled?
    e.should be_false
  end

  it "disables checks" do
    @redis.zadd("current_checks:#{name}", Time.now.to_i, check)
    @redis.zadd("current_entities", Time.now.to_i, name)
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.disable!

    saved_check_ts = @redis.zscore("current_checks:#{name}", check)
    saved_entity_ts = @redis.zscore("current_entities", name)
    saved_check_ts.should be_nil
    saved_entity_ts.should be_nil
  end

  it "does not update state with invalid value" do
    @redis.hset("check:#{name}:#{check}", 'state', 'ok')

    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.update_state('silly')

    state = @redis.hget("check:#{name}:#{check}", 'state')
    state.should_not be_nil
    state.should == 'ok'
  end

  it "does not update state with a repeated state value" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.update_state('critical', :summary => 'small problem')
    changed_at = @redis.hget("check:#{name}:#{check}", 'last_change')
    summary = @redis.hget("check:#{name}:#{check}", 'summary')

    ec.update_state('critical', :summary => 'big problem')
    new_changed_at = @redis.hget("check:#{name}:#{check}", 'last_change')
    new_summary = @redis.hget("check:#{name}:#{check}", 'summary')

    changed_at.should_not be_nil
    new_changed_at.should_not be_nil
    new_changed_at.should == changed_at

    summary.should_not be_nil
    new_summary.should_not be_nil
    new_summary.should_not == summary
    summary.should == 'small problem'
    new_summary.should == 'big problem'
  end

  def time_before(t, min, sec = 0)
    t - ((60 * min) + sec)
  end

  it "returns a list of historical states for a time range" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    t = Time.now.to_i
    ec.update_state('ok', :timestamp => time_before(t, 5), :summary => 'a')
    ec.update_state('critical', :timestamp => time_before(t, 4), :summary => 'b')
    ec.update_state('ok', :timestamp => time_before(t, 3), :summary => 'c')
    ec.update_state('critical', :timestamp => time_before(t, 2), :summary => 'd')
    ec.update_state('ok', :timestamp => time_before(t, 1), :summary => 'e')

    states = ec.historical_states(time_before(t, 4), t)
    states.should_not be_nil
    states.should be_an(Array)
    states.should have(4).data_hashes
    states[0][:summary].should == 'b'
    states[1][:summary].should == 'c'
    states[2][:summary].should == 'd'
    states[3][:summary].should == 'e'
  end

  it "returns a list of historical unscheduled maintenances for a time range" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    t = Time.now.to_i
    ec.update_state('ok', :timestamp => time_before(t, 5), :summary => 'a')
    ec.update_state('critical', :timestamp => time_before(t, 4), :summary => 'b')
    ec.update_state('ok', :timestamp => time_before(t, 3), :summary => 'c')
    ec.update_state('critical', :timestamp => time_before(t, 2), :summary => 'd')
    ec.update_state('ok', :timestamp => time_before(t, 1), :summary => 'e')

    states = ec.historical_states(time_before(t, 4), t)
    states.should_not be_nil
    states.should be_an(Array)
    states.should have(4).data_hashes
    states[0][:summary].should == 'b'
    states[1][:summary].should == 'c'
    states[2][:summary].should == 'd'
    states[3][:summary].should == 'e'
  end

  it "returns a list of historical scheduled maintenances for a time range" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    t = Time.now.to_i

    ec.create_scheduled_maintenance(time_before(t, 180),
      half_an_hour, :summary => "a")
    ec.create_scheduled_maintenance(time_before(t, 120),
      half_an_hour, :summary => "b")
    ec.create_scheduled_maintenance(time_before(t, 60),
      half_an_hour, :summary => "c")

    sched_maint_periods = ec.maintenances(time_before(t, 150), t,
      :scheduled => true)
    sched_maint_periods.should_not be_nil
    sched_maint_periods.should be_an(Array)
    sched_maint_periods.should have(2).data_hashes
    sched_maint_periods[0][:summary].should == 'b'
    sched_maint_periods[1][:summary].should == 'c'
  end

  it "returns that it has failed" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    @redis.hset("check:#{name}:#{check}", 'state', 'warning')
    ec.should be_failed

    @redis.hset("check:#{name}:#{check}", 'state', 'critical')
    ec.should be_failed

    @redis.hset("check:#{name}:#{check}", 'state', 'unknown')
    ec.should be_failed
  end

  it "returns that it has not failed" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    @redis.hset("check:#{name}:#{check}", 'state', 'ok')
    ec.should_not be_failed

    @redis.hset("check:#{name}:#{check}", 'state', 'acknowledgement')
    ec.should_not be_failed
  end

  it "returns a status summary" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)

    t = Time.now.to_i
    ec.update_state('ok', :timestamp => time_before(t, 5), :summary => 'a')
    ec.update_state('critical', :timestamp => time_before(t, 4), :summary => 'b')
    ec.update_state('ok', :timestamp => time_before(t, 3), :summary => 'c')
    ec.update_state('critical', :timestamp => time_before(t, 2), :summary => 'd')

    summary = ec.summary
    summary.should == 'd'
  end

  it "returns timestamps for its last notifications" do
    t = Time.now.to_i
    @redis.set("#{name}:#{check}:last_problem_notification", t - 30)
    @redis.set("#{name}:#{check}:last_acknowledgement_notification", t - 15)
    @redis.set("#{name}:#{check}:last_recovery_notification", t)

    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    ec.last_notification_for_state(:problem)[:timestamp].should == t - 30
    ec.last_notification_for_state(:acknowledgement)[:timestamp].should == t - 15
    ec.last_notification_for_state(:recovery)[:timestamp].should == t
  end

  it "finds all related contacts" do
    ec = Flapjack::Data::EntityCheck.for_entity_name(name, check, :redis => @redis)
    contacts = ec.contacts
    contacts.should_not be_nil
    contacts.should be_an(Array)
    contacts.should have(1).contact
    contacts.first.name.should == 'John Johnson'
  end

  it "generates ephemeral tags for itself" do
    ec = Flapjack::Data::EntityCheck.for_entity_name('foo-app-01.example.com', 'Disk / Utilisation', :redis => @redis)
    tags = ec.tags
    tags.should_not be_nil
    tags.should be_a(Flapjack::Data::TagSet)
    ['foo-app-01', 'example.com', 'disk', '/', 'utilisation'].to_set.subset?(tags).should be_true
  end

end