require File.dirname(__FILE__) + "/../spec_helper"
require "active_support/time"

module IceCube
  describe Rule, "from_ical" do
    it "should return a IceCube DailyRule class for a basic daily rule" do
      rule = IceCube::Rule.from_ical "FREQ=DAILY"
      expect(rule.class).to eq(IceCube::DailyRule)
    end

    it "should return a IceCube WeeklyRule class for a basic monthly rule" do
      rule = IceCube::Rule.from_ical "FREQ=WEEKLY"
      expect(rule.class).to eq(IceCube::WeeklyRule)
    end

    it "should return a IceCube MonthlyRule class for a basic monthly rule" do
      rule = IceCube::Rule.from_ical "FREQ=MONTHLY"
      expect(rule.class).to eq(IceCube::MonthlyRule)
    end

    it "should return a IceCube YearlyRule class for a basic yearly rule" do
      rule = IceCube::Rule.from_ical "FREQ=YEARLY"
      expect(rule.class).to eq(IceCube::YearlyRule)
    end

    it "should be able to parse a .day rule" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYDAY=MO,TU")
      expect(rule).to eq(IceCube::Rule.daily.day(:monday, :tuesday))
    end

    it "should be able to parse a .day_of_week rule" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYDAY=-1TU,-2TU")
      expect(rule).to eq(IceCube::Rule.daily.day_of_week(tuesday: [-1, -2]))
    end

    it "should be able to parse both .day and .day_of_week rules" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYDAY=MO,-1TU,-2TU")
      expect(rule).to eq(IceCube::Rule.daily.day_of_week(tuesday: [-1, -2]).day(:monday))
    end

    it "should be able to parse a .day_of_month rule" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYMONTHDAY=23")
      expect(rule).to eq(IceCube::Rule.daily.day_of_month(23))
    end

    it "should be able to parse a .day_of_year rule" do
      rule = IceCube::Rule.from_ical("FREQ=YEARLY;BYYEARDAY=100,200")
      expect(rule).to eq(IceCube::Rule.yearly.day_of_year(100, 200))
    end

    it "should be able to serialize a .month_of_year rule" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYMONTH=1,4")
      expect(rule).to eq(IceCube::Rule.daily.month_of_year(:january, :april))
    end

    it "should be able to split to a combination of day_of_week and day (day_of_week has priority)" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYDAY=TU,MO,1MO,-1MO")
      expect(rule).to eq(IceCube::Rule.daily.day(:tuesday).day_of_week(monday: [1, -1]))
    end

    it "should be able to parse of .day_of_week rule with multiple days" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;BYDAY=WE,1MO,-1MO,2TU")
      expect(rule).to eq(IceCube::Rule.daily.day_of_week(monday: [1, -1], tuesday: [2]).day(:wednesday))
    end

    it "should be able to parse a rule with an until date" do
      t = Time.now.utc
      rule = IceCube::Rule.from_ical("FREQ=WEEKLY;UNTIL=#{t.strftime("%Y%m%dT%H%M%SZ")}")
      expect(rule.to_s).to eq(IceCube::Rule.weekly.until(t).to_s)
    end

    it "should be able to parse a rule with a count date" do
      rule = IceCube::Rule.from_ical("FREQ=WEEKLY;COUNT=5")
      expect(rule).to eq(IceCube::Rule.weekly.count(5))
    end

    it "should be able to parse a rule with an interval" do
      rule = IceCube::Rule.from_ical("FREQ=DAILY;INTERVAL=2")
      expect(rule).to eq(IceCube::Rule.daily.interval(2))
    end

    it "should be able to parse week start (WKST)" do
      rule = IceCube::Rule.from_ical("FREQ=WEEKLY;INTERVAL=2;WKST=MO")
      expect(rule).to eq(IceCube::Rule.weekly(2, :monday))
    end

    it "should return no occurrences after daily interval with count is over" do
      schedule = IceCube::Schedule.new(Time.now)
      schedule.add_recurrence_rule(IceCube::Rule.from_ical("FREQ=DAILY;COUNT=5"))
      expect(schedule.occurrences_between(Time.now + (IceCube::ONE_DAY * 7), Time.now + (IceCube::ONE_DAY * 14)).count).to eq(0)
    end
  end

  describe Schedule, "from_ical" do
    ical_string = <<-ICAL.gsub(/^\s*/, "")
  DTSTART:20130314T201500Z
  DTEND:20130314T201545Z
  RRULE:FREQ=WEEKLY;BYDAY=TH;UNTIL=20130531T100000Z
    ICAL

    ical_string_with_multiple_exdates_and_rdates = <<-ICAL.gsub(/^\s*/, "")
  DTSTART;TZID=America/Denver:20130731T143000
  DTEND;TZID=America/Denver:20130731T153000
  RRULE:FREQ=WEEKLY;UNTIL=20140730T203000Z;BYDAY=MO,WE,FR
  EXDATE;TZID=America/Denver:20130823T143000
  EXDATE;TZID=America/Denver:20130812T143000
  EXDATE;TZID=America/Denver:20130807T143000
  RDATE;TZID=America/Denver:20150812T143000
  RDATE;TZID=America/Denver:20150807T143000
    ICAL

    ical_string_with_multiple_rules = <<-ICAL.gsub(/^\s*/, "")
  DTSTART;TZID=CDT:20151005T195541
  RRULE:FREQ=WEEKLY;BYDAY=MO,TU
  RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU;BYDAY=FR
    ICAL

    def sorted_ical(ical)
      ical.split("\n").sort.map { |field|
        k, v = field.split(":")
        v = v.split(";").sort.join(";") if k == "RRULE"

        "#{k}:#{v}"
      }.join("\n")
    end

    describe "instantiation" do
      it "loads an ICAL string" do
        expect(IceCube::Schedule.from_ical(ical_string)).to be_a(IceCube::Schedule)
      end
    end

    describe "daily frequency" do
      it "matches simple daily" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily)

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily.count(4))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily(4))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals and counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily(4).count(10))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles until dates" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily.until(start_time + (IceCube::ONE_DAY * 15)))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end
    end

    describe "weekly frequency" do
      it "matches simple weekly" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.weekly)

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles weekdays" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday, :thursday))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.weekly(2))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals and counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.weekly(2).count(4))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals and counts on given weekdays" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.weekly(2).day(:monday, :wednesday).count(4))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end
    end

    describe "monthly frequency" do
      it "matches simple monthly" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.monthly)

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.monthly(2))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals and counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.monthly(2).count(5))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals and counts on specific days" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.monthly(2).day_of_month(1, 15).count(5))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end
    end

    describe "yearly frequency" do
      it "matches simple yearly" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly)

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles intervals" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly(2))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles a specific day" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly.day_of_year(15))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles specific days" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly.day_of_year(1, 15, -1))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly.count(5))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles specific months" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:january, :december))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles specific months and counts" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:january, :december).count(15))

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end
    end

    describe "exceptions" do
      it "handles single EXDATE lines, single RDATE lines" do
        start_time = Time.now

        schedule = IceCube::Schedule.new(start_time)
        schedule.add_recurrence_rule(IceCube::Rule.daily)
        schedule.add_exception_time(Time.now + (IceCube::ONE_DAY * 2))
        schedule.add_recurrence_time(Time.now + IceCube::ONE_DAY * 4)

        ical = schedule.to_ical
        expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
      end

      it "handles multiple EXDATE / RDATE lines" do
        schedule = IceCube::Schedule.from_ical ical_string_with_multiple_exdates_and_rdates
        expect(schedule.exception_times.count).to eq(3)
        expect(schedule.recurrence_times.count).to eq(2)
      end

      it "should raise ArgumentError when parsing an invalid rule type" do
        str = "FREQ=FAKE"
        expect { Rule.from_ical(str) }.to raise_error(ArgumentError, "Invalid rule frequency type: Fake")
      end

      it "should raise ArgumentError when parsing an invalid validation type" do
        str = "FREQ=DAILY;FAKE=23"
        expect { Rule.from_ical(str) }.to raise_error(ArgumentError, "Invalid rule validation type: FAKE")
      end
    end

    describe "multiple rules" do
      it "handles multiple recurrence rules" do
        schedule = IceCube::Schedule.from_ical ical_string_with_multiple_rules
        expect(schedule.recurrence_rules.count).to eq(2)
      end
    end

    describe "invalid ical data" do
      shared_examples_for("an invalid ical string") do
        it do
          expect {
            IceCube::Schedule.from_ical(ical_str)
          }.to raise_error(ArgumentError) # TODO replace with real ad
        end
      end

      describe "empty rules" do
        let(:ical_str) { "RRULE::" }
        it_behaves_like "an invalid ical string"
      end

      describe "invalid rules" do
        let(:ical_str) { "RRULE::A" }
        it_behaves_like "an invalid ical string"
      end

      describe "incomplete rule" do
        let(:ical_str) { "RRULE:FREQ" }
        it_behaves_like "an invalid ical string"
      end

      describe "invalid rule with invalid sensitive key" do
        let(:ical_str) { "RRULE:FREQ=WEKLY;WKST=SU" }
        it_behaves_like "an invalid ical string"
      end

      describe "invalid rule with invalid value" do
        let(:ical_str) { "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;WKST=SE" }
        it_behaves_like "an invalid ical string"
      end

      describe "invalid rule with invalid key" do
        let(:ical_str) { "RRULE:FREQ=WEEKLY;BDAY=MO,WE,FR;WKST=SU" }
        it_behaves_like "an invalid ical string"
      end

      describe "invalid rule with attempt to execute code" do
        let(:ical_str) { "RRULE:FREQ=to_yaml" }
        it_behaves_like "an invalid ical string"
      end
    end
  end
end
