require File.dirname(__FILE__) + "/../spec_helper"
require "benchmark"

describe IceCube::Schedule do
  include IceCube

  it "yields itself for configuration" do
    t1 = Time.utc(2013, 2, 12, 12, 34, 56)
    schedule = IceCube::Schedule.new do |s|
      s.start_time = t1
    end
    expect(schedule.start_time).to eq(t1)
  end

  it "initializes with a start_time" do
    t1 = Time.local(2013, 2, 14, 0, 32, 0)
    schedule = IceCube::Schedule.new(t1)
    expect(schedule.start_time).to be_a Time
    expect(schedule.start_time).to eq(t1)
  end

  it "converts initialized DateTime to Time", expect_warnings: true do
    dt = DateTime.new(2013, 2, 14, 0, 32, 0)
    schedule = IceCube::Schedule.new(dt)
    expect(schedule.start_time).to be_a Time
    expect(schedule.start_time).to eq(Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec))
  end

  describe :next_occurrence do
    it "should not raise an exception when calling next occurrence with no remaining occurrences" do
      schedule = IceCube::Schedule.new Time.now
      expect { schedule.next_occurrence }.not_to raise_error
    end

    it "should not skip ahead a day when called with a date" do
      schedule = IceCube::Schedule.new(Time.utc(2014, 1, 1, 12, 34, 56)) do |s|
        s.add_recurrence_rule IceCube::Rule.hourly
      end
      next_hour = schedule.next_occurrence(Date.new(2014, 1, 2))
      expect(next_hour).to eq Time.utc(2014, 1, 2, 0o0, 34, 56)
    end
  end

  describe :duration do
    it "should be based on end_time" do
      start = Time.now
      schedule = IceCube::Schedule.new(start)
      expect(schedule.duration).to eq(0)
      schedule.end_time = start + 3600
      expect(schedule.duration).to eq(3600)
    end

    it "should give precedence to :end_time option" do
      start = Time.now
      conflicting_options = {end_time: start + 600, duration: 1200}
      schedule = IceCube::Schedule.new(start, conflicting_options)
      expect(schedule.duration).to eq(600)
    end
  end

  describe :occurring_at? do
    it "should not capture multiple days when called with a date" do
      schedule = IceCube::Schedule.new do |s|
        s.start_time = Time.utc(2013, 12, 31, 23, 59, 50)
        s.duration = 20
        s.add_recurrence_rule IceCube::Rule.daily(2)
      end
      expect(schedule.occurring_at?(Date.new(2014, 1, 1))).to eq true
      expect(schedule.occurring_at?(Date.new(2014, 1, 2))).to eq false
    end
  end

  describe :recurrence_times do
    it "should start empty" do
      expect(IceCube::Schedule.new.recurrence_times).to be_empty
    end

    it "should include added times" do
      schedule = IceCube::Schedule.new(t0 = Time.now)
      schedule.add_recurrence_time(t1 = t0 + 3600)
      expect(schedule.recurrence_times).to eq([t1])
    end

    it "can include start time" do
      schedule = IceCube::Schedule.new(t0 = Time.now)
      schedule.add_recurrence_time(t0)
      expect(schedule.recurrence_times).to eq([t0])
    end
  end

  describe :conflicts_with? do
    it "should raise an error if both are not terminating" do
      schedules = 2.times.map do
        schedule = IceCube::Schedule.new(Time.now)
        schedule.rrule IceCube::Rule.daily
        schedule
      end
      expect do
        schedules.first.conflicts_with?(schedules.last)
      end.to raise_error(ArgumentError)
    end

    it "should not raise error if both are non-terminating closing time present" do
      schedule1 = IceCube::Schedule.new Time.now
      schedule1.rrule IceCube::Rule.weekly
      schedule2 = IceCube::Schedule.new Time.now
      schedule2.rrule IceCube::Rule.weekly
      expect do
        schedule1.conflicts_with?(schedule2, Time.now + IceCube::ONE_DAY)
      end.not_to raise_error
    end

    it "should not raise an error if one is non-terminating" do
      schedule1 = IceCube::Schedule.new Time.now
      schedule1.rrule IceCube::Rule.weekly
      schedule2 = IceCube::Schedule.new Time.now
      schedule2.rrule IceCube::Rule.weekly.until(Time.now)
      expect do
        schedule1.conflicts_with?(schedule2)
      end.not_to raise_error
    end

    it "should not raise an error if the other is non-terminating" do
      schedule1 = IceCube::Schedule.new Time.now
      schedule1.rrule IceCube::Rule.weekly.until(Time.now)
      schedule2 = IceCube::Schedule.new Time.now
      schedule2.rrule IceCube::Rule.weekly
      expect do
        schedule1.conflicts_with?(schedule2)
      end.not_to raise_error
    end

    it "should return true if conflict is present" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time)
      schedule1.rrule IceCube::Rule.daily
      schedule2 = IceCube::Schedule.new(start_time)
      schedule2.rrule IceCube::Rule.daily
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_DAY)
      expect(conflict).to be_truthy
    end

    it "should return false if conflict is not present" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time)
      schedule1.rrule IceCube::Rule.weekly.day(:tuesday)
      schedule2 = IceCube::Schedule.new(start_time)
      schedule2.rrule IceCube::Rule.weekly.day(:monday)
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_DAY)
      expect(conflict).to be_falsey
    end

    it "should return true if conflict is present based on duration" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_DAY + 1)
      schedule1.rrule IceCube::Rule.weekly.day(:monday)
      schedule2 = IceCube::Schedule.new(start_time)
      schedule2.rrule IceCube::Rule.weekly.day(:tuesday)
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK)
      expect(conflict).to be_truthy
    end

    it "should return true if conflict is present based on duration - other way" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time)
      schedule1.rrule IceCube::Rule.weekly.day(:tuesday)
      schedule2 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_DAY + 1)
      schedule2.rrule IceCube::Rule.weekly.day(:monday)
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK)
      expect(conflict).to be_truthy
    end

    it "should return false if conflict is past closing_time" do
      start_time = Time.local(2011, 1, 1, 12) # Sunday
      schedule1 = IceCube::Schedule.new(start_time)
      schedule1.rrule IceCube::Rule.weekly.day(:friday)
      schedule2 = IceCube::Schedule.new(start_time)
      schedule2.rrule IceCube::Rule.weekly.day(:friday)
      expect(schedule2.conflicts_with?(schedule1, start_time + IceCube::ONE_WEEK))
        .to be_truthy
      expect(schedule2.conflicts_with?(schedule1, start_time + IceCube::ONE_DAY))
        .to be_falsey
    end

    it "should return false if conflict is not present based on duration" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.rrule IceCube::Rule.weekly.day(:monday)
      schedule2 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule2.rrule IceCube::Rule.weekly.day(:tuesday)
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK)
      expect(conflict).to be_falsey
    end

    it "should return false if conflict is not present on same day based on duration" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.rrule IceCube::Rule.daily
      schedule2 = IceCube::Schedule.new(start_time + 3600, duration: IceCube::ONE_HOUR)
      schedule2.rrule IceCube::Rule.daily
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK)
      expect(conflict).to be_falsey
    end

    it "should return true if conflict is present on same day based on duration" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.rrule IceCube::Rule.daily
      schedule2 = IceCube::Schedule.new(start_time + 600, duration: IceCube::ONE_HOUR)
      schedule2.rrule IceCube::Rule.daily
      conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK)
      expect(conflict).to be_truthy
    end

    it "should return true if conflict is present and no recurrence" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.add_recurrence_time(start_time)
      schedule2 = IceCube::Schedule.new(start_time + 600, duration: IceCube::ONE_HOUR)
      schedule2.add_recurrence_time(start_time + 600)
      conflict = schedule1.conflicts_with?(schedule2)
      expect(conflict).to be_truthy
      conflict = schedule2.conflicts_with?(schedule1)
      expect(conflict).to be_truthy
    end

    it "should return false if conflict is not present and no recurrence" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.add_recurrence_time(start_time)
      schedule2 = IceCube::Schedule.new(start_time + IceCube::ONE_HOUR, duration: IceCube::ONE_HOUR)
      schedule2.add_recurrence_time(start_time + IceCube::ONE_HOUR)
      conflict = schedule1.conflicts_with?(schedule2)
      expect(conflict).to be_falsey
      conflict = schedule2.conflicts_with?(schedule1)
      expect(conflict).to be_falsey
    end

    it "should return false if conflict is not present and single recurrence" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.add_recurrence_time(start_time)
      schedule2 = IceCube::Schedule.new(start_time + IceCube::ONE_HOUR, duration: IceCube::ONE_HOUR)
      schedule2.rrule IceCube::Rule.daily
      conflict = schedule1.conflicts_with?(schedule2)
      expect(conflict).to be_falsey
      conflict = schedule2.conflicts_with?(schedule1)
      expect(conflict).to be_falsey
    end

    it "should return true if conflict is present and single recurrence" do
      start_time = Time.now
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.add_recurrence_time(start_time)
      schedule2 = IceCube::Schedule.new(start_time + 600, duration: IceCube::ONE_HOUR)
      schedule2.rrule IceCube::Rule.daily
      conflict = schedule1.conflicts_with?(schedule2)
      expect(conflict).to be_truthy
      conflict = schedule2.conflicts_with?(schedule1)
      expect(conflict).to be_truthy
    end

    it "should return false if conflict is not present and single recurrence and time originally specified as Time" do
      start_time = Time.local(2020, 9, 21, 11, 30, 0)
      schedule1 = IceCube::Schedule.new(start_time, duration: IceCube::ONE_HOUR)
      schedule1.add_recurrence_time(start_time)
      schedule2 = IceCube::Schedule.new(start_time + IceCube::ONE_HOUR, duration: IceCube::ONE_HOUR)
      schedule2.add_recurrence_time(start_time + IceCube::ONE_HOUR)
      conflict = schedule1.conflicts_with?(schedule2)
      expect(conflict).to be_falsey
      conflict = schedule2.conflicts_with?(schedule1)
      expect(conflict).to be_falsey
    end
  end

  describe :each do
    it "should be able to yield occurrences for a schedule" do
      schedule = IceCube::Schedule.new
      schedule.add_recurrence_rule IceCube::Rule.daily
      i = 0
      answers = []
      schedule.each_occurrence do |time|
        answers << time
        i += 1
        break if i > 9
      end
      expect(answers).to eq(schedule.first(10))
    end

    it "should return self" do
      schedule = IceCube::Schedule.new
      expect(schedule.each_occurrence { |s| }).to eq(schedule)
    end

    it "should stop itself when hitting the end of a schedule" do
      schedule = IceCube::Schedule.new(t0 = Time.now)
      t1 = t0 + 24 * IceCube::ONE_DAY
      schedule.add_recurrence_time t1
      answers = []
      schedule.each_occurrence { |t| answers << t }
      expect(answers).to eq([t0, t1])
    end
  end

  describe :all_occurrences_enumerator do
    it "should be equivalent to all_occurrences in terms of arrays" do
      schedule = IceCube::Schedule.new(Time.now, duration: IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.daily.until(Time.now + 3 * IceCube::ONE_DAY)
      expect(schedule.all_occurrences).to match_array(schedule.all_occurrences_enumerator.to_a)
    end
  end

  describe :remaining_occurrences_enumerator do
    it "should be equivalent to remaining_occurrences in terms of arrays" do
      schedule = IceCube::Schedule.new(Time.now, duration: IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.daily.until(Time.now + 3 * IceCube::ONE_DAY)
      expect(schedule.remaining_occurrences).to match_array(schedule.remaining_occurrences_enumerator.to_a)
    end
  end

  describe :all_occurrences do
    it "has end times for each occurrence" do
      schedule = IceCube::Schedule.new(Time.now, duration: IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.daily.until(Time.now + 3 * IceCube::ONE_DAY)
      schedule.all_occurrences.all? { |o| expect(o.end_time).to eq(o + IceCube::ONE_HOUR) }
    end

    it "should include its start time when empty" do
      schedule = IceCube::Schedule.new(t0 = Time.now)
      expect(schedule.all_occurrences).to eq([t0])
    end

    it "should have one occurrence with one recurrence time at start_time" do
      schedule = IceCube::Schedule.new(t0 = Time.local(2012, 12, 12, 12, 12, 12))
      schedule.add_recurrence_time t0
      expect(schedule.all_occurrences).to eq([t0])
    end

    it "should have two occurrences with a recurrence time after start_time" do
      schedule = IceCube::Schedule.new(t0 = Time.local(2012, 12, 12, 12, 12, 12))
      schedule.add_recurrence_time t1 = Time.local(2013, 1, 13, 1, 13, 1)
      expect(schedule.all_occurrences).to eq([t0, t1])
    end

    it "should return an error if there is nothing to stop it" do
      schedule = IceCube::Schedule.new
      schedule.rrule IceCube::Rule.daily
      expect do
        schedule.all_occurrences
      end.to raise_error(ArgumentError)
    end

    it "should consider count limits separately for multiple rules" do
      schedule = IceCube::Schedule.new
      schedule.rrule IceCube::Rule.minutely.count(3)
      schedule.rrule IceCube::Rule.daily.count(3)
      expect(schedule.all_occurrences.size).to eq(5)
    end
  end

  describe :next_occurrences do
    let(:nonsense) { IceCube::Rule.monthly.day_of_week(monday: [1]).day_of_month(31) }

    it "should be able to calculate next occurrences ignoring excluded times" do
      start_time = Time.now
      schedule = IceCube::Schedule.new start_time
      schedule.rrule IceCube::Rule.daily(1)
      schedule.extime start_time + IceCube::ONE_DAY
      occurrences = schedule.next_occurrences(2, start_time) # 3 occurrences in the next year
      expect(occurrences).to eq([
        start_time + IceCube::ONE_DAY * 2,
        start_time + IceCube::ONE_DAY * 3
      ])
    end

    it "should be empty if nothing is found before closing time" do
      schedule = IceCube::Schedule.new(t0 = Time.utc(2013, 1, 1)) do |s|
        next_year = Date.new(t0.year + 1, t0.month, t0.day)
        s.add_recurrence_rule nonsense.until(next_year)
      end
      trap_infinite_loop_beyond(24)
      expect(schedule.next_occurrences(1)).to be_empty
    end

    it "should not skip ahead a day when called with a date" do
      schedule = IceCube::Schedule.new(Time.utc(2014, 1, 1, 12, 34, 56)) do |s|
        s.add_recurrence_rule IceCube::Rule.hourly
      end
      next_hours = schedule.next_occurrences(2, Date.new(2014, 1, 2))
      expect(next_hours).to eq [Time.utc(2014, 1, 2, 0o0, 34, 56),
        Time.utc(2014, 1, 2, 0o1, 34, 56)]
    end
  end

  describe :next_occurrence do
    it "should be able to calculate the next occurrence past an exception time" do
      start_time = Time.now
      schedule = IceCube::Schedule.new start_time
      schedule.rrule IceCube::Rule.daily(1)
      schedule.extime start_time + IceCube::ONE_DAY
      occurrence = schedule.next_occurrence(start_time) # 3 occurrences in the next year
      expect(occurrence).to eq(start_time + IceCube::ONE_DAY * 2)
    end

    it "should respect time zone info for a local future time [#115]" do
      start_time = Time.local(Time.now.year + 1, 7, 1, 0, 0, 0)
      compare_time_zone_info(start_time)
    end

    it "should respect time zone info for a local past time [#115]" do
      start_time = Time.local(Time.now.year - 1, 7, 1, 0, 0, 0)
      compare_time_zone_info(start_time)
    end

    it "should respect time zone info for a utc past time [#115]" do
      start_time = Time.utc(Time.now.year - 1, 7, 1, 0, 0, 0)
      compare_time_zone_info(start_time)
    end

    it "should respect time zone info for a utc future time [#115]" do
      start_time = Time.utc(Time.now.year + 1, 7, 1, 0, 0, 0)
      compare_time_zone_info(start_time)
    end

    it "should respect time zone info for a offset past time [#115]" do
      start_time = Time.utc(Time.now.year - 1, 7, 1, 0, 0, 0).localtime("-05:00")
      compare_time_zone_info(start_time)
    end

    it "should respect time zone info for a offset future time [#115]" do
      start_time = Time.utc(Time.now.year + 1, 7, 1, 0, 0, 0).localtime("-05:00")
      compare_time_zone_info(start_time)
    end
  end

  describe :spans do
    it "should find occurrence in past with duration beyond the start time" do
      t0 = Time.utc(2015, 10, 1, 15, 31)
      schedule = IceCube::Schedule.new(t0, duration: 2 * IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.daily
      next_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, spans: true)
      expect(next_occ).to eq(t0)
    end

    it "should include occurrence in past with duration beyond the start time" do
      t0 = Time.utc(2015, 10, 1, 15, 31)
      schedule = IceCube::Schedule.new(t0, duration: 2 * IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.daily.count(2)
      occs = schedule.next_occurrences(10, t0 + IceCube::ONE_HOUR, spans: true)
      expect(occs).to eq([t0, t0 + IceCube::ONE_DAY])
    end

    it "should allow duration span on remaining_occurrences" do
      t0 = Time.utc(2015, 10, 1, 0o0, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_DAY)
      schedule.add_recurrence_rule IceCube::Rule.daily.count(3)
      occs = schedule.remaining_occurrences(t0 + IceCube::ONE_DAY + IceCube::ONE_HOUR, spans: true)
      expect(occs).to eq([t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY])
    end

    it "should include occurrences with duration spanning the requested start time" do
      t0 = Time.utc(2015, 10, 1, 15, 31)
      schedule = IceCube::Schedule.new(t0, duration: 30 * IceCube::ONE_DAY)
      long_event = schedule.remaining_occurrences_enumerator(t0 + IceCube::ONE_DAY, spans: true).take(1)
      expect(long_event).to eq([t0])
    end

    it "should find occurrences between including previous one with duration spanning start" do
      t0 = Time.utc(2015, 10, 1, 10, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_HOUR)
      schedule.add_recurrence_rule IceCube::Rule.hourly.count(10)
      occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR + 1, t0 + 3 * IceCube::ONE_HOUR + 1, spans: true)
      expect(occs.length).to eq(3)
    end

    it "should include long occurrences starting before and ending after" do
      t0 = Time.utc(2015, 10, 1, 0o0, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_DAY)
      occs = schedule.occurrences_between(t0 + IceCube::ONE_HOUR, t0 + IceCube::ONE_DAY - IceCube::ONE_HOUR, spans: true)
      expect(occs).to eq([t0])
    end

    it "should not find occurrence with duration ending on start time" do
      t0 = Time.utc(2015, 10, 1, 12, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_HOUR)
      expect(schedule.occurs_between?(t0 + IceCube::ONE_HOUR, t0 + 2 * IceCube::ONE_HOUR, spans: true)).to be_falsey
    end

    it "should quickly fetch a future time from a recurring schedule" do
      t0 = Time.utc(2000, 10, 1, 0o0, 0o0)
      t1 = Time.utc(2015, 10, 1, 12, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_HOUR - 1)
      schedule.add_recurrence_rule IceCube::Rule.hourly
      occ = nil
      timing = Benchmark.realtime do
        occ = schedule.remaining_occurrences_enumerator(t1, spans: true).take(1)
      end
      expect(timing).to be < 0.1
      expect(occ).to eq([t1])
    end

    it "should not include occurrence ending on start time" do
      t0 = Time.utc(2015, 10, 1, 10, 0o0)
      schedule = IceCube::Schedule.new(t0, duration: IceCube::ONE_HOUR / 2)
      schedule.add_recurrence_rule IceCube::Rule.minutely(30).count(6)
      third_occ = schedule.next_occurrence(t0 + IceCube::ONE_HOUR, spans: true)
      expect(third_occ).to eq(t0 + IceCube::ONE_HOUR)
    end
  end

  describe :previous_occurrence do
    it "returns the previous occurrence for a time in the schedule" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily
      previous = schedule.previous_occurrence(t0 + 2 * IceCube::ONE_DAY)
      expect(previous).to eq(t0 + IceCube::ONE_DAY)
    end

    it "returns nil given the start time" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily
      previous = schedule.previous_occurrence(t0)
      expect(previous).to be_nil
    end

    it "should not skip back a day when called with a date" do
      schedule = IceCube::Schedule.new(Time.utc(2014, 1, 1, 12, 34, 56)) do |s|
        s.add_recurrence_rule IceCube::Rule.hourly
      end
      prev_hour = schedule.previous_occurrence(Date.new(2014, 1, 2))
      expect(prev_hour).to eq Time.utc(2014, 1, 1, 23, 34, 56)
    end
  end

  describe :previous_occurrences do
    it "returns an array of previous occurrences from a given time" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily
      previous = schedule.previous_occurrences(2, t0 + 3 * IceCube::ONE_DAY)
      expect(previous).to eq([t0 + IceCube::ONE_DAY, t0 + 2 * IceCube::ONE_DAY])
    end

    it "limits the returned occurrences to a given count" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily
      previous = schedule.previous_occurrences(999, t0 + 2 * IceCube::ONE_DAY)
      expect(previous).to eq([t0, t0 + IceCube::ONE_DAY])
    end

    it "returns empty array given the start time" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily
      previous = schedule.previous_occurrences(2, t0)
      expect(previous).to eq([])
    end

    it "should not skip back a day when called with a date" do
      schedule = IceCube::Schedule.new(Time.utc(2014, 1, 1, 12, 34, 56)) do |s|
        s.add_recurrence_rule IceCube::Rule.hourly
      end
      prev_hours = schedule.previous_occurrences(2, Date.new(2014, 1, 2))
      expect(prev_hours).to eq [Time.utc(2014, 1, 1, 22, 34, 56),
        Time.utc(2014, 1, 1, 23, 34, 56)]
    end
  end

  describe :last do
    it "returns the last occurrence for a terminating schedule" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      t1 = Time.utc(2013, 5, 31, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily.until(t1 + 1)
      expect(schedule.last).to eq(t1)
    end

    it "returns an array of occurrences given a number" do
      t0 = Time.utc(2013, 5, 18, 12, 34)
      t1 = Time.utc(2013, 5, 31, 12, 34)
      schedule = IceCube::Schedule.new(t0)
      schedule.add_recurrence_rule IceCube::Rule.daily.until(t1 + 1)
      expect(schedule.last(2)).to eq([t1 - IceCube::ONE_DAY, t1])
    end

    it "raises an error for a non-terminating schedule" do
      schedule = IceCube::Schedule.new
      schedule.add_recurrence_rule IceCube::Rule.daily
      expect { schedule.last }.to raise_error(ArgumentError)
    end
  end

  describe :start_time= do
    it "should modify start date in rrule_occurrence_heads when changed" do
      schedule = IceCube::Schedule.new(Time.now - 1000)
      schedule.rrule IceCube::Rule.daily
      schedule.start_time = Time.now
      expect(Time.now - schedule.first.start_time).to be < 100
    end
  end

  describe :recurrence_rules do
    it "should not include rules for single occurrences" do
      schedule = IceCube::Schedule.new Time.now
      schedule.add_recurrence_time Time.now
      expect(schedule.rrules).to be_empty
    end
  end

  describe :remove_recurrence_rule do
    it "should be able to one rule based on the comparator" do
      schedule = IceCube::Schedule.new Time.now
      schedule.rrule IceCube::Rule.daily
      schedule.rrule IceCube::Rule.daily(2)
      schedule.remove_recurrence_rule schedule.rrules.first
      expect(schedule.rrules.count).to eq(1)
    end

    it "should be able to remove multiple rules based on the comparator" do
      schedule = IceCube::Schedule.new Time.now
      schedule.rrule IceCube::Rule.daily
      schedule.rrule IceCube::Rule.daily
      schedule.remove_recurrence_rule schedule.rrules.first
      expect(schedule.rrules).to be_empty
    end

    it "should return the rule that was removed" do
      schedule = IceCube::Schedule.new Time.now
      rule = IceCube::Rule.daily
      schedule.rrule rule
      rule2 = schedule.remove_recurrence_rule rule
      expect([rule]).to eq(rule2)
    end

    it "should return [] if nothing was removed" do
      schedule = IceCube::Schedule.new Time.now
      rule = IceCube::Rule.daily
      expect(schedule.remove_recurrence_rule(rule)).to eq([])
    end
  end

  describe :remove_recurrence_time do
    it "should be able to remove a recurrence date from a schedule" do
      time = Time.now
      schedule = IceCube::Schedule.new(time)
      schedule.add_recurrence_time time
      schedule.remove_recurrence_time time
      expect(schedule.recurrence_times).to be_empty
    end

    it "should return the time that was removed" do
      schedule = IceCube::Schedule.new Time.now
      time = Time.now
      schedule.rtime time
      expect(schedule.remove_rtime(time)).to eq(time)
    end

    it "should return nil if the date was not in the schedule" do
      schedule = IceCube::Schedule.new Time.now
      expect(schedule.remove_recurrence_time(Time.now)).to be_nil
    end
  end

  describe :remove_exception_time do
    it "should be able to remove a exception date from a schedule" do
      time = Time.now
      schedule = IceCube::Schedule.new(time)
      schedule.extime time
      schedule.remove_exception_time time
      expect(schedule.exception_times).to be_empty
    end

    it "should return the date that was removed" do
      schedule = IceCube::Schedule.new Time.now
      time = Time.now
      schedule.extime time
      expect(schedule.remove_extime(time)).to eq(time)
    end

    it "should return nil if the date was not in the schedule" do
      schedule = IceCube::Schedule.new Time.now
      expect(schedule.remove_exception_time(Time.now)).to be_nil
    end
  end

  describe :occurs_on? do
    subject(:schedule) { IceCube::Schedule.new(start_time) }

    shared_examples "occurring on a given day" do
      WORLD_TIME_ZONES.each do |zone|
        context "in #{zone}", system_time_zone: zone do
          specify "should determine if it occurs on a given Date" do
            expect(schedule.occurs_on?(Date.new(2010, 7, 1))).to be_falsey
            expect(schedule.occurs_on?(Date.new(2010, 7, 2))).to be_truthy
            expect(schedule.occurs_on?(Date.new(2010, 7, 3))).to be_falsey
          end

          specify "should determine if it occurs on the day of a given UTC Time" do
            expect(schedule.occurs_on?(Time.utc(2010, 7, 1, 23, 59, 59))).to be_falsey
            expect(schedule.occurs_on?(Time.utc(2010, 7, 2, 0, 0, 1))).to be_truthy
            expect(schedule.occurs_on?(Time.utc(2010, 7, 2, 23, 59, 59))).to be_truthy
            expect(schedule.occurs_on?(Time.utc(2010, 7, 3, 0, 0, 1))).to be_falsey
          end

          specify "should determine if it occurs on the day of a given local Time" do
            expect(schedule.occurs_on?(Time.local(2010, 7, 1, 23, 59, 59))).to be_falsey
            expect(schedule.occurs_on?(Time.local(2010, 7, 2, 0, 0, 1))).to be_truthy
            expect(schedule.occurs_on?(Time.local(2010, 7, 2, 23, 59, 59))).to be_truthy
            expect(schedule.occurs_on?(Time.local(2010, 7, 3, 0, 0, 1))).to be_falsey
          end

          specify "should determine if it occurs on the day of a given non-local Time" do
            expect(schedule.occurs_on?(Time.new(2010, 7, 1, 23, 59, 59, "+11:15"))).to be_falsey
            expect(schedule.occurs_on?(Time.new(2010, 7, 2, 0, 0, 1, "+11:15"))).to be_truthy
            expect(schedule.occurs_on?(Time.new(2010, 7, 2, 23, 59, 59, "+11:15"))).to be_truthy
            expect(schedule.occurs_on?(Time.new(2010, 7, 3, 0, 0, 1, "+11:15"))).to be_falsey
          end

          specify "should determine if it occurs on the day of a given ActiveSupport::Time", requires_active_support: true do
            Time.zone = "Pacific/Honolulu"
            expect(schedule.occurs_on?(Time.zone.parse("2010-07-01 23:59:59"))).to be_falsey
            expect(schedule.occurs_on?(Time.zone.parse("2010-07-02 00:00:01"))).to be_truthy
            expect(schedule.occurs_on?(Time.zone.parse("2010-07-02 23:59:59"))).to be_truthy
            expect(schedule.occurs_on?(Time.zone.parse("2010-07-03 00:00:01"))).to be_falsey
          end
        end
      end
    end

    shared_examples :occurs_on? do
      context "starting from a UTC Time" do
        let(:start_time) { Time.utc(2010, 7, 2, 10, 0, 0) }
        include_examples "occurring on a given day"
      end

      context "starting from a local Time" do
        let(:start_time) { Time.local(2010, 7, 2, 10, 0, 0) }
        include_examples "occurring on a given day"
      end

      context "starting from a non-local Time" do
        let(:start_time) { Time.local(2010, 7, 2, 10, 0, 0, false, "-2:30") }
        include_examples "occurring on a given day"
      end

      context "starting from an ActiveSupport::Time", requires_active_support: true do
        let(:start_time) { Time.new(2010, 7, 2, 10, 0, 0, "-07:00").in_time_zone("America/Vancouver") }
        include_examples "occurring on a given day"
      end
    end

    context "with a recurrence rule limited by count" do
      before { schedule.add_recurrence_rule IceCube::Rule.daily.count(1) }
      include_examples :occurs_on?
    end

    context "with a recurrence rule limited by until" do
      before { schedule.add_recurrence_rule IceCube::Rule.daily.until(start_time) }
      include_examples :occurs_on?
    end

    context "with a single recurrence time" do
      before { schedule.add_recurrence_time(start_time) }
      include_examples :occurs_on?
    end

    context "across DST" do
      let(:start_time) { Time.local(2010, 3, 2, 0, 0, 0) }
      before { schedule.add_recurrence_rule(IceCube::Rule.monthly) }
      it "determines local midnight with time change" do
        expect(schedule.occurs_on?(Date.new(2010, 7, 2))).to be_truthy
      end
    end

    it "should be true for multiple rtimes" do
      schedule = IceCube::Schedule.new(Time.local(2010, 7, 10, 16))
      schedule.add_recurrence_time(Time.local(2010, 7, 11, 16))
      schedule.add_recurrence_time(Time.local(2010, 7, 12, 16))
      schedule.add_recurrence_time(Time.local(2010, 7, 13, 16))

      expect(schedule.occurs_on?(Date.new(2010, 7, 11))).to be_truthy
      expect(schedule.occurs_on?(Date.new(2010, 7, 12))).to be_truthy
      expect(schedule.occurs_on?(Date.new(2010, 7, 13))).to be_truthy
    end
  end

  def compare_time_zone_info(start_time)
    schedule = IceCube::Schedule.new(start_time)
    schedule.rrule IceCube::Rule.yearly(1)
    occurrence = schedule.next_occurrence

    expect(occurrence.dst?).to eq(start_time.dst?) if start_time.respond_to? :dst?
    expect(occurrence.utc?).to eq(start_time.utc?) if start_time.respond_to? :utc?
    expect(occurrence.zone).to eq(start_time.zone)
    expect(occurrence.utc_offset).to eq(start_time.utc_offset)
  end

  def trap_infinite_loop_beyond(iterations)
    expect_any_instance_of(IceCube::ValidatedRule).to receive(:finds_acceptable_time?)
      .at_most(iterations).times.and_call_original
  end
end
