# frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

describe CalendarEvent do
  before(:once) do
    Account.find_or_create_by!(id: 0).update(name: "Dummy Root Account", workflow_state: "deleted", root_account_id: nil)
  end

  it "sanitizes description" do
    course_model
    @c = CalendarEvent.new
    @c.description = "<a href='#' onclick='alert(12);'>only this should stay</a>"
    @c.context_id = @course.id
    @c.context_type = "Course"
    @c.save!
    expect(@c.description).to eql("<a href=\"#\">only this should stay</a>")
  end

  describe "default_values" do
    before(:once) do
      course_model
      @original_start_at = Time.zone.at(1_220_443_500) # 3 Sep 2008 12:05pm (UTC)
      @original_end_at = @original_start_at + 2.hours

      # Create the initial event
      @event = calendar_event_model(
        start_at: @original_start_at,
        end_at: @original_end_at,
        time_zone_edited: "Mountain Time (US & Canada)"
      )
    end

    it "gets localized start_at" do
      df = "%Y-%m-%d %H:%M"
      expect(@event.start_at.strftime(df)).to eq "2008-09-03 12:05"
      expect(@event.zoned_start_at.strftime(df)).to eq "2008-09-03 06:05"
    end

    it "populates missing dates" do
      event_1 = calendar_event_model
      event_1.start_at = @original_start_at
      event_1.end_at = nil
      event_1.send(:populate_missing_dates)
      expect(event_1.end_at).to eql(event_1.start_at)

      event_2 = calendar_event_model
      event_2.start_at = nil
      event_2.end_at = @original_end_at
      event_2.send(:populate_missing_dates)
      expect(event_2.start_at).to eql(event_2.end_at)

      event_3 = calendar_event_model
      event_3.start_at = @original_end_at
      event_3.end_at = @original_start_at
      event_3.send(:populate_missing_dates)
      expect(event_3.end_at).to eql(event_3.start_at)
    end

    it "populates all day flag" do
      midnight = Time.zone.at(1_361_862_000) # 2013-02-26 00:00:00

      event_1 = calendar_event_model(time_zone_edited: "Mountain Time (US & Canada)")
      event_1.start_at = event_1.end_at = midnight
      event_1.send(:populate_all_day_flag)
      expect(event_1.all_day?).to be_truthy
      expect(event_1.all_day_date.strftime("%Y-%m-%d")).to eq "2013-02-26"

      event_2 = calendar_event_model(time_zone_edited: "Mountain Time (US & Canada)")
      event_2.start_at = @original_start_at
      event_2.end_at = @original_end_at
      event_2.send(:populate_all_day_flag)
      expect(event_2.all_day?).to be_falsey

      event_3 = calendar_event_model(
        start_at: midnight,
        end_at: midnight + 1.hour,
        time_zone_edited: "Mountain Time (US & Canada)"
      )
      event_3.start_at = midnight
      event_3.end_at = midnight + 30.minutes
      event_3.all_day = true
      event_3.send(:populate_all_day_flag)
      expect(event_3.all_day?).to be_truthy
      expect(event_3.end_at).to eql(event_3.start_at)
    end

    it "retains all day flag when date is changed (calls :default_values)" do
      # Flag the event as all day
      @event.update({ start_at: @original_start_at, end_at: @original_end_at, all_day: true })
      expect(@event.all_day?).to be_truthy
      expect(@event.all_day_date.strftime("%Y-%m-%d")).to eq "2008-09-03"
      expect(@event.zoned_start_at.strftime("%H:%M")).to eq "00:00"
      expect(@event.end_at).to eql(@event.zoned_start_at)

      # Change the date but keep the all day flag as true
      @event.update({ start_at: @event.start_at - 1.day, end_at: @event.end_at - 1.day, all_day: true })
      expect(@event.all_day?).to be_truthy
      expect(@event.all_day_date.strftime("%Y-%m-%d")).to eq "2008-09-02"
      expect(@event.zoned_start_at.strftime("%H:%M")).to eq "00:00"
      expect(@event.end_at).to eql(@event.zoned_start_at)
    end

    it "sets blackout_date to false by default" do
      expect(@event.blackout_date).to be_falsey
    end
  end

  describe "default_values during update" do
    before(:once) do
      course_model
      @original_start_at = Time.zone.at(1_220_443_500) # 3 Sep 2008 12:05pm (UTC)
      @original_end_at = @original_start_at + 2.hours
      @midnight = Time.zone.at(1_220_421_600)
      @event = calendar_event_model(
        start_at: @original_start_at,
        end_at: @original_end_at,
        time_zone_edited: "Mountain Time (US & Canada)"
      )
    end

    it "does not retain all day flag when times are changed (calls :default_values)" do
      # set the start and end to midnight and make sure all_day is automatically set
      @event.update({ start_at: @midnight, end_at: @midnight })
      expect(@event.all_day?).to be_truthy
      expect(@event.all_day_date.strftime("%Y-%m-%d")).to eq "2008-09-03"

      # set the start and end to different times and then make sure all_day is automatically unset
      @event.update({ start_at: @original_start_at, end_at: @original_end_at })
      expect(@event.all_day?).to be_falsey
    end
  end

  context "ical" do
    describe "to_ics" do
      it "does not fail for null times" do
        calendar_event_model(start_at: "", end_at: "")
        res = @event.to_ics
        expect(res).not_to be_nil
        expect(res.include?("DTSTART")).to be false
      end

      it "does not return data for null times" do
        calendar_event_model(start_at: "", end_at: "")
        res = @event.to_ics(in_own_calendar: false)
        expect(res).to be_nil
      end

      it "returns string data for events with times" do
        Time.zone = "UTC"
        calendar_event_model(start_at: "Sep 3 2008 11:55am", end_at: "Sep 3 2008 12:00pm")
        # force known value so we can check serialization
        @event.updated_at = Time.zone.at(1_220_443_500) # 3 Sep 2008 12:05pm (UTC)
        res = @event.to_ics
        expect(res).not_to be_nil
        expect(res.include?("DTSTART:20080903T115500Z")).not_to be_nil
        expect(res.include?("DTEND:20080903T120000Z")).not_to be_nil
        expect(res.include?("DTSTAMP:20080903T120500Z")).not_to be_nil
      end

      it "returns string data for events with times in correct tz" do
        Time.zone = "Alaska" # -0800
        calendar_event_model(start_at: "Sep 3 2008 11:55am", end_at: "Sep 3 2008 12:00pm")
        # force known value so we can check serialization
        @event.updated_at = Time.zone.at(1_220_472_300) # 3 Sep 2008 12:05pm (AKDT)
        res = @event.to_ics
        expect(res).not_to be_nil
        expect(res.include?("DTSTART:20080903T195500Z")).not_to be_nil
        expect(res.include?("DTEND:20080903T200000Z")).not_to be_nil
        expect(res.include?("DTSTAMP:20080903T200500Z")).not_to be_nil
      end

      it "returns data for events with times" do
        Time.zone = "UTC"
        calendar_event_model(start_at: "Sep 3 2008 11:55am", end_at: "Sep 3 2008 12:00pm")
        # force known value so we can check serialization
        @event.updated_at = Time.zone.at(1_220_443_500) # 3 Sep 2008 12:05pm (UTC)
        res = @event.to_ics(in_own_calendar: false)
        expect(res).not_to be_nil
        expect(res.dtstart.tz_utc).to be true
        expect(res.dtstart.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 11:55am").utc.strftime("%Y-%m-%dT%H:%M:00")
        expect(res.dtend.tz_utc).to be true
        expect(res.dtend.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 12:00pm").utc.strftime("%Y-%m-%dT%H:%M:00")
        expect(res.dtstamp.tz_utc).to be true
        expect(res.dtstamp.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 12:05pm").utc.strftime("%Y-%m-%dT%H:%M:00")
      end

      it "returns data for events with times in correct tz" do
        Time.zone = "Alaska" # -0800
        calendar_event_model(start_at: "Sep 3 2008 11:55am", end_at: "Sep 3 2008 12:00pm")
        # force known value so we can check serialization
        @event.updated_at = Time.zone.at(1_220_472_300) # 3 Sep 2008 12:05pm (AKDT)
        res = @event.to_ics(in_own_calendar: false)
        expect(res).not_to be_nil
        expect(res.dtstart.tz_utc).to be true
        expect(res.dtstart.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 11:55am").utc.strftime("%Y-%m-%dT%H:%M:00")
        expect(res.dtend.tz_utc).to be true
        expect(res.dtend.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 12:00pm").utc.strftime("%Y-%m-%dT%H:%M:00")
        expect(res.dtend.tz_utc).to be true
        expect(res.dtstamp.strftime("%Y-%m-%dT%H:%M:%S")).to eq Time.zone.parse("Sep 3 2008 12:05pm").utc.strftime("%Y-%m-%dT%H:%M:00")
      end

      it "does not fail with no date for all_day event" do
        res = calendar_event_model(all_day: true).to_ics
        expect(res).not_to be_nil
      end

      it "returns string dates for all_day events" do
        calendar_event_model(start_at: "Sep 3 2008 12:00am")
        expect(@event.all_day).to be(true)
        expect(@event.end_at).to eql(@event.start_at)
        res = @event.to_ics
        expect(res.include?("DTSTART;VALUE=DATE:20080903")).not_to be_nil
        expect(res.include?("DTEND;VALUE=DATE:20080903")).not_to be_nil
      end

      it "returns a plain-text description" do
        calendar_event_model(start_at: "Sep 3 2008 12:00am", description: <<~HTML)
          <p>
            This assignment is due December 16th. <b>Please</b> do the reading.
            <br/>
            <a href="www.example.com">link!</a>
          </p>
        HTML
        ev = @event.to_ics(in_own_calendar: false)
        expect(ev.description).to match_ignoring_whitespace("This assignment is due December 16th. Please do the reading.  [link!](www.example.com)")
        expect(ev.x_alt_desc.first).to eq @event.description
      end

      context "with double testing with disable adding uuid verifier in api ff" do
        before do
          attachment_model(context: course_factory)
        end

        double_testing_with_disable_adding_uuid_verifier_in_api_ff do
          it "does not add verifiers to files unless course or attachment is public" do
            html = %(<div><a href="/courses/#{@course.id}/files/#{@attachment.id}/download?wrap=1">here</a></div>)
            calendar_event_model(start_at: "Sep 3 2008 12:00am", description: html, saving_user: @teacher)
            ev = @event.to_ics(in_own_calendar: false)
            expect(ev.description).to_not include("verifier")

            @attachment.file_state = "public"
            @attachment.save!

            AdheresToPolicy::Cache.clear
            ev = @event.to_ics(in_own_calendar: false)
            expect(ev.description).to include("verifier") unless disable_adding_uuid_verifier_in_api

            @attachment.file_state = "hidden"
            @attachment.save!
            @course.offer
            @course.is_public = true
            @course.save!

            AdheresToPolicy::Cache.clear
            ev = @event.to_ics(in_own_calendar: false)
            expect(ev.description).to include("verifier") unless disable_adding_uuid_verifier_in_api
          end
        end
      end

      it "works with media comments in course section events" do
        course_model
        @course.offer
        @course.is_public = true

        @course.media_objects.create!(media_id: "0_12345678")
        event = @course.default_section.calendar_events.create!(start_at: "Sep 3 2008 12:00am",
                                                                description: %(<p><a id="media_comment_0_12345678" class="instructure_inline_media_comment video_comment" href="/media_objects/0_12345678">media comment</a></p>))
        event.effective_context_code = @course.asset_string
        event.save!

        ics = event.to_ics
        expect(ics.gsub(/\s+/, "")).to include("/courses/#{@course.id}/media_download?entryId=0_12345678")
      end

      it "adds a course code to the summary of an event that has a course as an effective_context" do
        course_model
        calendar_event_model(start_at: "Sep 3 2008 12:00am")
        @event.effective_context_code = @course.asset_string
        ics = @event.to_ics
        expect(ics).to include("SUMMARY:#{@event.title} [#{@course.course_code}]")
      end

      it "adds a course code to the summary of an event that has a course as its effective_context's context" do
        course_model
        group(context: @course)
        calendar_event_model(start_at: "Sep 3 2008 12:00am")
        @event.effective_context_code = @group.asset_string
        ics = @event.to_ics
        expect(ics).to include("SUMMARY:#{@event.title} [#{@course.course_code}]")
      end

      context "discussion with checkpoints" do
        before :once do
          course_model
          @course.account.enable_feature!(:discussion_checkpoints)
          @required_replies = 2
          @reply_to_topic, @reply_to_entry, @topic = graded_discussion_topic_with_checkpoints(
            context: @course,
            reply_to_entry_required_count: @required_replies
          )
        end

        def url_for_checkpoint(checkpoint)
          asset_string = checkpoint.context.asset_string
          start_at = checkpoint.due_at
          tag_name_id = "assignment_#{checkpoint.parent_assignment_id}"
          "https://localhost/calendar?include_contexts=#{asset_string}&month=#{start_at.try(:strftime, "%m")}&year=#{start_at.try(:strftime, "%Y")}##{tag_name_id}"
        end

        it "generates correct titles for discussion checkpoints" do
          expect(@reply_to_topic.to_ics).to include("SUMMARY:#{@topic.title} Reply to Topic")
          expect(@reply_to_entry.to_ics).to include("SUMMARY:#{@topic.title} Required Replies (#{@required_replies})")
        end

        it "generates correct urls for discussion checkpoints" do
          expect(@reply_to_topic.to_ics.match(/URI([\s\S]*?)END/)).not_to be_nil
          expect(@reply_to_entry.to_ics.match(/URI([\s\S]*?)END/)).not_to be_nil
        end
      end
    end
  end

  context "root account" do
    it "sets the root_account when the context is an appointment_group" do
      course = Course.create!
      ag = AppointmentGroup.create(title: "test", contexts: [course])
      ag.publish!
      appointment = ag.appointments.create(start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00")
      expect(appointment.root_account_id).to eq ag.context.root_account_id
    end

    it "sets the root_account when the context is a course" do
      course = Course.create!
      appointment = CalendarEvent.create!(title: "test", context: course)
      expect(appointment.root_account_id).to eq course.root_account_id
    end

    it "sets the root_account when the context is a user if the effective context is set and is not a user" do
      user = User.create!
      course = Course.create!
      appointment = CalendarEvent.create!(title: "test", context: user, effective_context_code: course.asset_string)
      expect(appointment.root_account_id).to eq course.root_account_id
    end

    it "sets the root_account_id to 0 when the context is a user and there is no effective context" do
      user = User.create!
      appointment = CalendarEvent.create!(title: "test", context: user)
      expect(appointment.root_account_id).to eq 0
    end

    it "sets the root_account when the context is a group" do
      account = Account.create!
      group = Group.create!(context: account)
      appointment = CalendarEvent.create!(title: "test", context: group)
      expect(appointment.root_account_id).to eq group.root_account_id
    end

    it "sets the root_account when the context is a course_section" do
      course = Course.create!
      section = course.course_sections.create!
      appointment = CalendarEvent.create!(title: "test", context: section)
      expect(appointment.root_account_id).to eq section.root_account_id
    end

    it "sets the root_account when assignment_group is not yet assigned to a course context" do
      course = Course.create!
      ag = AppointmentGroup.create(
        title: "test",
        contexts: [course],
        new_appointments: {
          appt01: [Time.zone.now, Time.zone.now]
        }
      )
      expect(ag.appointments[0].root_account_id).to eq course.root_account_id
    end
  end

  context "for_user_and_context_codes" do
    before :once do
      course_with_student(active_all: true)
      @student = @user
      @e1 = @course.calendar_events.create!
      @e2 = @student.calendar_events.create!
      @e3 = Course.create.calendar_events.create!
    end

    it "returns events explicitly tied to the contexts" do
      expect(CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string]).sort_by(&:id))
        .to eql [@e1]

      expect(CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string, @student.asset_string]).sort_by(&:id))
        .to eql [@e1, @e2]
    end

    it "returns events implicitly tied to the contexts (via effective_context_string)" do
      @teacher = user_factory
      @course.enroll_teacher(@teacher).accept!
      course1 = @course
      course_with_teacher(user: @teacher)
      course2, @course = @course, course1
      g1 = AppointmentGroup.create!(title: "foo", contexts: [course1, course2])
      g1.publish!
      ae1 = g1.appointments.create!
      a1 = ae1.reserve_for(@student, @student)
      g2 = AppointmentGroup.create!(title: "foo", contexts: [@course], sub_context_codes: [@course.default_section.asset_string])
      g2.publish!
      ae2 = g2.appointments.create!
      a2 = ae2.reserve_for(@student, @student)
      g3 = AppointmentGroup.create!(title: "foo", contexts: [@course])
      g3.publish!
      ae3 = g3.appointments.create!
      pe = @course.calendar_events.create!
      section = @course.default_section
      se = pe.child_events.build
      se.context = section
      se.save!

      expect(CalendarEvent.for_user_and_context_codes(@student, [@student.asset_string]).sort_by(&:id))
        .to eql [@e2] # none of the appointments even though they technically are on the user

      expect(CalendarEvent.for_user_and_context_codes(@student, [section.asset_string]).sort_by(&:id))
        .to eql [] # none of the appointments even though they technically are on the section

      expect(CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string, @student.asset_string]).sort_by(&:id))
        .to eql [@e1, @e2, a1, a2, pe, se]

      expect(CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string]).sort_by(&:id))
        .to eql [@e1, a1, a2, pe, se]

      expect(CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string]).events_without_child_events.sort_by(&:id))
        .to eql [@e1, a1, a2, se]

      expect(CalendarEvent.for_user_and_context_codes(@student, [g1.asset_string, g2.asset_string, g3.asset_string]).sort_by(&:id))
        .to eql [ae1, ae2, ae3]

      expect(CalendarEvent.for_user_and_context_codes(@teacher, [g1.asset_string, g2.asset_string, g3.asset_string]).events_with_child_events.sort_by(&:id))
        .to eql [ae1, ae2]
    end
  end

  context "notifications" do
    before :once do
      Notification.create(name: "New Event Created", category: "TestImmediately")
      Notification.create(name: "Event Date Changed", category: "TestImmediately")
      course_with_student(active_all: true)
      @teacher = user_factory(active_all: true)
      @course.enroll_teacher(@teacher).accept!
      communication_channel(@student, { username: "test_channel_email_#{user_factory.id}@test.com", active_cc: true })
    end

    context "with calendar event created" do
      before :once do
        course_with_student(active_all: true)
        course_with_observer(active_all: true, active_cc: true, associated_user_id: @student.id, course: @course)
        @event1 = @course.calendar_events.build(title: "test")
        @event1.updating_user = @teacher
        @event1.save!
      end

      context "creation notification" do
        before :once do
          @users = @event1.messages_sent["New Event Created"].map(&:user_id)
        end

        it "sends to participants", priority: "1" do
          expect(@event1.messages_sent).to include("New Event Created")
          expect(@users).to include(@student.id)
        end

        it "has correct URL" do
          @event1.messages_sent["New Event Created"].each do |message|
            expect(message.url).to include "/courses/#{@course.id}/calendar_events/#{@event1.id}"
          end
        end

        it "does not send to creating teacher" do
          expect(@users).not_to include(@teacher.id)
        end

        it "sends to observers" do
          expect(@users).to include(@observer.id)
        end
      end

      context "with event date edited" do
        before :once do
          @event1.update(start_at: Time.zone.now, end_at: Time.zone.now)
        end

        context "edit notification" do
          before :once do
            @users = @event1.messages_sent["Event Date Changed"].map(&:user_id)
          end

          it "sends to participants", priority: "1" do
            expect(@event1.messages_sent).to include("Event Date Changed")
            expect(@users).to include(@student.id)
          end

          it "has correct url" do
            @event1.messages_sent["Event Date Changed"].each do |message|
              expect(message.url).to include "/courses/#{@course.id}/calendar_events/#{@event1.id}"
            end
          end

          it "does not send to editing teacher" do
            expect(@users).not_to include(@teacher.id)
          end

          it "sends to observers" do
            expect(@users).to include(@observer.id)
          end
        end
      end
    end

    it "does not send notifications to participants if hidden" do
      course_with_student(active_all: true)
      event = @course.calendar_events.build(title: "test", child_event_data: [{ start_at: "2012-01-01", end_at: "2012-01-02", context_code: @course.default_section.asset_string }])
      event.updating_user = @teacher
      event.save!
      expect(event.messages_sent).to be_empty

      event.update_attribute(:child_event_data, [{ start_at: "2012-01-02", end_at: "2012-01-03", context_code: @course.default_section.asset_string }])
      expect(event.messages_sent).to be_empty
    end
  end

  context "appointments" do
    before :once do
      course_with_student(active_all: true)
      @student1 = @user
      @other_section = @course.course_sections.create!
      @other_course = Course.create!
      @ag = AppointmentGroup.create(title: "test", contexts: [@course])
      @ag.publish!
      @appointment = @ag.appointments.create(start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00")
    end

    context "notifications" do
      before do
        Notification.create(name: "Appointment Canceled By User", category: "TestImmediately")
        Notification.create(name: "Appointment Deleted For User", category: "TestImmediately")
        Notification.create(name: "Appointment Reserved By User", category: "TestImmediately")
        Notification.create(name: "Appointment Reserved For User", category: "TestImmediately")

        @teacher = user_factory(active_all: true)
        @course.enroll_teacher(@teacher).accept!

        student_in_course(course: @course, active_all: true)
        @student2 = @user

        c1 = group_category
        @group = c1.groups.create(context: @course)
        @group.users << @student1 << @student2

        @ag2 = AppointmentGroup.create!(title: "test", contexts: [@course], sub_context_codes: [c1.asset_string])
        @ag2.publish!
        @appointment2 = @ag2.appointments.create(start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00")

        course_with_observer(active_all: true, course: @course, associated_user_id: @student1.id)

        [@teacher, @student1, @student2, @observer].each do |user|
          communication_channel(user, { username: "test_channel_email_#{user.id}@test.com", active_cc: true })
        end

        @expected_users = [@teacher.id, @student1.id, @student2.id, @observer.id].sort
      end

      def message_recipients_for(notification_name)
        Message.where(notification_id: BroadcastPolicy.notification_finder.by_name(notification_name), user_id: @expected_users).pluck(:user_id).sort
      end

      it "includes course_ids from appointment_groups" do
        reservation = @appointment2.reserve_for(@group, @student1)
        expect(reservation.course_broadcast_data).to eql({ root_account_id: @course.root_account_id, course_ids: [@course.id] })
      end

      it "includes multiple course_ids" do
        course2 = @course.root_account.courses.create!(name: "course2", workflow_state: "available")
        course2.enroll_teacher(@teacher).accept!
        ag = AppointmentGroup.create!(title: "test", contexts: [@course, course2])
        appointment = ag.appointments.create!(start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00")
        reservation = appointment.reserve_for(@student1, @student1)
        expect(reservation.course_broadcast_data).to eql({ root_account_id: @course.root_account_id, course_ids: [@course.id, course2.id] })
      end

      it "notifies all participants except the person reserving", priority: "1" do
        @appointment2.reserve_for(@group, @student1)
        expect(message_recipients_for("Appointment Reserved For User")).to eq @expected_users - [@student1.id, @teacher.id]
      end

      it "notifies all participants except the person canceling the reservation" do
        reservation = @appointment2.reserve_for(@group, @student1)
        reservation.updating_user = @student1
        reservation.destroy
        expect(message_recipients_for("Appointment Deleted For User")).to eq @expected_users - [@student1.id, @teacher.id]
      end

      it "notifies participants if teacher deletes the appointment time slot", priority: "1" do
        @appointment2.reserve_for(@group, @student1)
        @appointment2.updating_user = @teacher
        @appointment2.destroy
        expect(message_recipients_for("Appointment Deleted For User")).to eq @expected_users - [@teacher.id]
      end

      it "notifies all participants when the the time slot is canceled", priority: "1" do
        @appointment2.reserve_for(@group, @student1)
        @appointment2.updating_user = @teacher
        user_evt = CalendarEvent.where(context_type: "Group").first
        user_evt.updating_user = @teacher
        user_evt.destroy
        expect(message_recipients_for("Appointment Deleted For User")).to eq @expected_users - [@teacher.id]
      end

      it "notifies admins and observers when a user reserves", priority: "1" do
        reservation = @appointment.reserve_for(@student1, @student1)
        messages = reservation.messages_sent["Appointment Reserved By User"]
        expect(messages).to be_present
        expect(messages.first.root_account_id).to eq Account.default.id
        expect(messages.map(&:user_id).sort.uniq).to eql (@course.instructors.map(&:id) + [@observer.id]).sort
      end

      it "notifies admins and observers when a user reserves a group appointment" do
        reservation = @appointment2.reserve_for(@group, @student1)
        expect(reservation.messages_sent).to include("Appointment Reserved By User")
        expect(reservation.messages_sent["Appointment Reserved By User"].map(&:user_id).sort.uniq).to eql (@course.instructors.map(&:id) + [@observer.id]).sort
      end

      it "notifies admins and observers when a user cancels", priority: "1" do
        reservation = @appointment.reserve_for(@student1, @student1)
        reservation.updating_user = @student1
        reservation.destroy
        expect(reservation.messages_sent).to include("Appointment Canceled By User")
        expect(reservation.messages_sent["Appointment Canceled By User"].map(&:user_id).sort.uniq).to eql (@course.instructors.map(&:id) + [@observer.id]).sort
      end
    end

    it "updates the appointment group when a related event is deleted" do
      ag = AppointmentGroup.create!(
        title: "testing...",
        contexts: [@course],
        participants_per_appointment: 2,
        new_appointments: [
          ["2012-01-01 13:00:00", "2012-01-01 14:00:00"],
          ["2012-01-02 13:00:00", "2012-01-02 14:00:00"]
        ]
      )

      expect(AppointmentGroup.find(ag.id).start_at).to eq("2012-01-01 13:00:00")
      ag.appointments.first.destroy
      expect(AppointmentGroup.find(ag.id).start_at).to eq("2012-01-02 13:00:00")
    end

    it "allows multiple participants in an appointment, up to the limit" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   participants_per_appointment: 2,
                                   new_appointments: [["2012-01-01 13:00:00", "2012-01-01 14:00:00"]])
      ag.publish!
      appointment = ag.appointments.first

      student_in_course(course: @course, active_all: true)
      @other_student = @user
      student_in_course(course: @course, active_all: true)
      @unlucky_student = @user

      expect(appointment.reserve_for(@student1, @student1)).not_to be_nil
      expect(appointment.reserve_for(@other_student, @other_student)).not_to be_nil
      expect { appointment.reserve_for(@unlucky_student, @unlucky_student) }.to raise_error(CalendarEvent::ReservationError)
    end

    it "gives preference to the calendar's appointment limit" do
      ag = AppointmentGroup.create!(
        title: "testing...",
        contexts: [@course],
        participants_per_appointment: 2,
        new_appointments: [["2012-01-01 13:00:00", "2012-01-01 14:00:00"]]
      )
      ag.publish!
      appointment = ag.appointments.first
      appointment.participants_per_appointment = 3
      appointment.save!

      s1, s2, s3 = Array.new(3) do
        student_in_course(course: @course, active_all: true)
        @user
      end

      expect(appointment.reserve_for(@student1, @student1)).not_to be_nil
      expect(appointment.reserve_for(s1, s1)).not_to be_nil
      expect(appointment.reserve_for(s2, s2)).not_to be_nil
      expect { appointment.reserve_for(s3, s3) }.to raise_error(CalendarEvent::ReservationError)

      # should be able to unset the participant limit too
      appointment.participants_per_appointment = nil
      appointment.save!
      expect(appointment.reserve_for(s3, s3)).not_to be_nil
    end

    it "reverts to the appointment group's participant_limit when appropriate" do
      ag = AppointmentGroup.create!(
        title: "testing...",
        contexts: [@course],
        participants_per_appointment: 2,
        new_appointments: [["2012-01-01 13:00:00", "2012-01-01 14:00:00"]]
      )
      ag.publish!

      appointment = ag.appointments.first
      appointment.participants_per_appointment = 3
      appointment.save!
      expect(appointment.participants_per_appointment).to be 3

      appointment.participants_per_appointment = 2
      appointment.save!
      expect(appointment.override_participants_per_appointment?).to be_falsey
      expect(appointment.participants_per_appointment).to be 2
    end

    it "does not let participants exceed max_appointments_per_participant" do
      ag = AppointmentGroup.create(
        title: "test",
        contexts: [@course],
        max_appointments_per_participant: 1,
        new_appointments: [["2012-01-01 12:00:00", "2012-01-01 13:00:00"], ["2012-01-01 13:00:00", "2012-01-01 14:00:00"]]
      )
      ag.publish!

      appointment = ag.appointments.first
      appointment2 = ag.appointments.last

      appointment.reserve_for(@student1, @student1)
      expect { appointment2.reserve_for(@student1, @student1) }.to raise_error(CalendarEvent::ReservationError)
    end

    it "cancels existing reservations if cancel_existing = true and the appointment is in the future" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   max_appointments_per_participant: 1,
                                   new_appointments: [[1.hour.from_now, 2.hours.from_now], [3.hours.from_now, 4.hours.from_now]])
      ag.publish!
      appointment = ag.appointments.first
      appointment2 = ag.appointments.last

      r1 = appointment.reserve_for(@student1, @student1)
      expect { appointment2.reserve_for(@student1, @student1, cancel_existing: true) }.not_to raise_error
      expect(r1.reload).to be_deleted
    end

    it "refuses to cancel existing reservations if cancel_existing = true and the appointment is in the past" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   max_appointments_per_participant: 1,
                                   new_appointments: [[2.hours.ago, 1.hour.ago], [1.hour.from_now, 2.hours.from_now]])
      ag.publish!
      appointment = ag.appointments.first
      appointment2 = ag.appointments.last

      r1 = appointment.reserve_for(@student1, @student1)
      expect { appointment2.reserve_for(@student1, @student1, cancel_existing: true) }.to raise_error(CalendarEvent::ReservationError)
      expect(r1.reload).not_to be_deleted
    end

    it "saves comments with appointment" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   max_appointments_per_participant: 1,
                                   new_appointments: [["2012-01-01 12:00:00",
                                                       "2012-01-01 13:00:00"],
                                                      ["2012-01-01 13:00:00",
                                                       "2012-01-01 14:00:00"]])
      ag.publish!
      appointment = ag.appointments.first
      r1 = appointment.reserve_for(@student1, @student1, comments: "my appointment notes")
      r1.reload
      expect(r1.comments).to eq("my appointment notes")
    end

    it "enforces the section" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course.course_sections.create],
                                   new_appointments: [["2012-01-01 12:00:00", "2012-01-01 13:00:00"]])
      ag.publish!
      appointment = ag.appointments.first

      expect { appointment.reserve_for(@student1, @student1) }.to raise_error(CalendarEvent::ReservationError)
    end

    it "enforces the group category" do
      teacher = user_factory(active_all: true)
      @course.enroll_teacher(teacher).accept!
      c1 = group_category
      g1 = c1.groups.create(context: @course)
      c2 = group_category(name: "bar")
      g2 = c2.groups.create(context: @course)

      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   sub_context_codes: [c1.asset_string],
                                   new_appointments: [["2012-01-01 12:00:00", "2012-01-01 13:00:00"]])
      appointment = ag.appointments.first
      ag.publish!

      expect { appointment.reserve_for(g2, teacher) }.to raise_error(CalendarEvent::ReservationError)
      expect { appointment.reserve_for(g1, teacher) }.not_to raise_error
    end

    it "only accepts users with StudentEnrollments as valid user participants" do
      expect(@ag.eligible_participant?(@student1)).to be_truthy
      expect { @appointment.reserve_for(@student1, @student1) }.not_to raise_error

      # both a student and a teacher
      student_in_course(course: @course, active_all: true)
      @user.teacher_enrollments.create!(course: @course)
      expect(@ag.eligible_participant?(@user)).to be_truthy
      expect { @appointment.reserve_for(@user, @user) }.not_to raise_error

      # just a teacher
      user_factory(active_all: true)
      @course.enroll_teacher(@user).accept!
      expect(@ag.eligible_participant?(@user)).to be_falsey
      expect { @appointment.reserve_for(@user, @user) }.to raise_error(CalendarEvent::ReservationError)
    end

    it "locks the appointment once it is reserved" do
      expect(@appointment).to be_active
      expect(@appointment.reserve_for(@student1, @student1)).not_to be_nil
      expect(@appointment).to be_locked
    end

    it "unlocks the appointment when the last reservation is canceled" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   participants_per_appointment: 2,
                                   new_appointments: [["2012-01-01 13:00:00", "2012-01-01 14:00:00"]])
      appointment = ag.appointments.first
      student_in_course(course: @course, active_all: true)
      @other_student = @user

      expect(appointment).to be_active
      r1 = appointment.reserve_for(@student1, @student1).reload
      expect(appointment).to be_locked
      r2 = appointment.reserve_for(@other_student, @other_student).reload
      r2.destroy
      expect(appointment.reload).to be_locked
      r1.destroy
      expect(appointment.reload).to be_active
    end

    it "copies the group attributes to the initial appointments" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   description: "hello world",
                                   new_appointments: [["2012-01-01 12:00:00", "2012-01-01 13:00:00"]])
      e = ag.appointments.first
      expect(e.title).to eql "test"
      expect(e.description).to eql "hello world"
    end

    it "copies changed group attributes to existing appointments" do
      @ag.update(title: "changed!", description: "test123")
      e = @ag.appointments.first.reload
      expect(e.title).to eql "changed!"
      expect(e.description).to eql "test123"
    end

    it "does not copy group description if appointment is overridden" do
      @appointment.description = "pizza party"
      @appointment.save!

      @ag.description = "boring meeting"
      @ag.save!

      expect(@appointment.description).to eq "pizza party"
      expect(@ag.description).to eq "boring meeting"
    end

    it "copies the group attributes to subsequent appointments" do
      ag = AppointmentGroup.create(title: "test", contexts: [@course])
      ag.update(
        title: "haha",
        new_appointments: [["2012-01-01 12:00:00", "2012-01-01 13:00:00"]]
      )
      e = ag.appointments.first
      expect(e.title).to eql "haha"
    end

    it "ignores changes to locked attributes on the appointment" do
      @appointment.update(start_at: "2012-01-01 12:30:00", title: "you wish")
      expect(@appointment.title).to eql "test"
      expect(@appointment.start_at).to eql Time.parse("2012-01-01 12:30:00Z")
    end

    it "allows a user to re-reserve a slot after canceling" do
      ag = AppointmentGroup.create(title: "test",
                                   contexts: [@course],
                                   participants_per_appointment: 1,
                                   new_appointments: [["2012-01-01 13:00:00", "2012-01-01 14:00:00"]])
      appointment = ag.appointments.first

      r1 = appointment.reserve_for(@student1, @student1).reload
      expect(ag.reload.available_slots).to be 0
      r1.destroy
      expect(ag.reload.available_slots).to be 1
      expect { appointment.reserve_for(@student1, @student1) }.not_to raise_error
      expect(ag.reload.available_slots).to be 0
    end

    it "always allows editing the description on an appointment" do
      @appointment.update_attribute :workflow_state, "locked"
      @appointment.description = "bacon"
      @appointment.save!
      expect(@appointment.description).to eq "bacon"
    end
  end

  context "child_events" do
    it "deletes child events when deleting the parent" do
      calendar_event_model(start_at: "Sep 3 2008", title: "some event")
      child = @event.child_events.build
      child.context = user_factory
      child.save!

      @event.destroy

      expect(@event.reload).to be_deleted
      expect(child.reload).to be_deleted
    end

    it "deletes the parent event after the last child event is deleted" do
      calendar_event_model
      sec2 = @course.course_sections.create! name: "sec2"
      child1 = @event.child_events.create! context: @course.default_section, start_at: 1.day.from_now
      child2 = @event.child_events.create! context: sec2, start_at: 2.days.from_now
      child1.destroy
      expect(@event.reload).to be_active
      child2.destroy
      expect(@event.reload).to be_deleted
    end

    context "bulk updating" do
      before :once do
        course_with_teacher(active_all: true)
      end

      it "validates child events" do
        expect do
          @course.calendar_events.create! title: "ohai",
                                          child_event_data: [
                                            { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string }
                                          ]
        end.to raise_error(/Can't update child events unless an updating_user is set/)

        expect do
          event = @course.calendar_events.build title: "ohai",
                                                child_event_data: [
                                                  { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: "invalid_1" }
                                                ]
          event.updating_user = @user
          event.save!
        end.to raise_error(/Invalid child event context/)

        expect do
          other_section = Course.create!.default_section
          event = @course.calendar_events.build title: "ohai",
                                                child_event_data: [
                                                  { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: other_section.asset_string }
                                                ]
          event.updating_user = @user
          event.save!
        end.to raise_error(/Invalid child event context/)

        expect do
          event = @course.calendar_events.build title: "ohai",
                                                child_event_data: [
                                                  { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
                                                  { start_at: "2012-01-01 13:00:00", end_at: "2012-01-01 14:00:00", context_code: @course.default_section.asset_string }
                                                ]
          event.updating_user = @user
          event.save!
        end.to raise_error(/Duplicate child event contexts/)
      end

      it "allows user to update child_events only where they have permissions" do
        section1 = @course.default_section
        section2 = @course.course_sections.create!
        admin = account_admin_user

        event = @course.calendar_events.build title: "ohai",
                                              child_event_data: [
                                                { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: section1.asset_string },
                                                { start_at: "2012-01-01 13:00:00", end_at: "2012-01-01 14:00:00", context_code: section2.asset_string }
                                              ]
        event.updating_user = admin
        event.save!

        teacher = user_factory(active_all: true)
        limited_teacher_role = custom_teacher_role("Limited teacher", account: Account.default)
        RoleOverride.create!(context: Account.default, permission: "manage_calendar", role: limited_teacher_role, enabled: false)
        @course.enroll_teacher(teacher, enrollment_state: :active, section: section1)
        @course.enroll_teacher(teacher, role: limited_teacher_role, enrollment_state: :active, section: section2)
        teacher.enrollments.update_all(limit_privileges_to_course_section: true)

        event.child_event_data = [
          { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: section1.asset_string },
          { start_at: "2012-01-01 13:00:00", end_at: "2012-01-01 14:00:00", context_code: section2.asset_string }
        ]
        event.updating_user = teacher
        event.save!

        expect do
          event.child_event_data = [
            { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: section1.asset_string },
            { start_at: "2012-01-02 13:00:00", end_at: "2012-01-02 14:00:00", context_code: section2.asset_string }
          ]
          event.updating_user = teacher
          event.save!
        end.to raise_error(/Invalid child event context/)
      end

      it "creates child events" do
        s2 = @course.course_sections.create!
        e1 = @course.calendar_events.build title: "ohai",
                                           child_event_data: [
                                             { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
                                             { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: s2.asset_string },
                                           ]
        e1.updating_user = @user
        e1.save!

        e1.reload
        events = e1.child_events.sort_by(&:id)
        expect(events.map(&:context_code)).to eql [@course.default_section.asset_string, s2.asset_string]
        expect(events.map(&:effective_context_code).uniq).to eql [@course.asset_string]
        expect(e1.start_at).to eql events.first.start_at
        expect(e1.end_at).to eql events.last.end_at
      end

      it "updates child events" do
        s2 = @course.course_sections.create!
        s3 = @course.course_sections.create!
        e1 = @course.calendar_events.build title: "ohai",
                                           child_event_data: [
                                             { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
                                             { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: s2.asset_string },
                                           ]
        e1.updating_user = @user
        e1.save!
        e1.reload
        events1 = e1.child_events.sort_by(&:id)

        e1.update child_event_data: [
          { start_at: "2012-01-01 13:00:00", end_at: "2012-01-01 14:00:00", context_code: @course.default_section.asset_string },
          { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: s3.asset_string },
        ]
        e1.reload
        events2 = e1.child_events.sort_by(&:id)
        expect(events2.size).to be 2

        expect(events1.first.reload).to eql events2.first
        expect(events1.last.reload).to be_deleted
      end

      it "does not try to migrate resources to section context" do
        attachment_with_context(@course)
        s2 = @course.course_sections.create!
        e1 = @course.calendar_events.build title: "ohai",
                                           description: "<img src='/courses/#{@course.id}/files/#{@attachment.id}/preview'>",
                                           child_event_data: [
                                             { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
                                             { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: s2.asset_string },
                                           ]
        e1.updating_user = @user
        e1.saving_user = @user
        e1.save!
        e1.child_event_data = [
          { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
          { start_at: "2012-01-02 22:00:00", end_at: "2012-01-02 23:00:00", context_code: s2.asset_string },
        ]
        expect { e1.save! }.not_to raise_error
      end

      it "deletes all child events" do
        s2 = @course.course_sections.create!
        @course.course_sections.create!
        e1 = @course.calendar_events.build title: "ohai",
                                           child_event_data: [
                                             { start_at: "2012-01-01 12:00:00", end_at: "2012-01-01 13:00:00", context_code: @course.default_section.asset_string },
                                             { start_at: "2012-01-02 12:00:00", end_at: "2012-01-02 13:00:00", context_code: s2.asset_string },
                                           ]
        e1.updating_user = @user
        e1.save!
        e1.reload
        e1.update remove_child_events: true
        expect(e1.child_events.reload).to be_empty
      end

      it "unsets all_day when deleting child events" do
        s2 = @course.course_sections.create!
        e1 = @course.calendar_events.create!({
                                               title: "foo",
                                               start_at: "2020-10-29T00:00:00.000Z",
                                               end_at: "2020-10-29T00:00:00.000Z",
                                               updating_user: @user,
                                               child_event_data: [
                                                 { start_at: "2020-10-27T10:00:00.000Z", end_at: "2020-10-27T11:00:00.000Z", context_code: @course.default_section.asset_string },
                                                 { start_at: "2020-10-27T14:00:00.000Z", end_at: "2020-10-27T15:00:00.000Z", context_code: s2.asset_string }
                                               ]
                                             })
        e1 = CalendarEvent.find(e1.id)
        e1.update({
                    start_at: "2020-10-27T10:00:00.000Z",
                    end_at: "2020-10-27T15:00:00.000Z",
                    remove_child_events: true
                  })
        expect(e1.reload).not_to be_all_day
      end
    end

    context "cascading" do
      it "copies cascaded attributes when creating a child event" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event", important_dates: true)
        child = @event.child_events.build
        child.context = user_factory
        child.save!
        expect(child.start_at).to be_nil
        expect(child.title).to eql @event.title
        expect(child.important_dates).to eql @event.important_dates
      end

      it "updates cascaded attributes on the child events whenever the parent is updated" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event", important_dates: false)
        child = @event.child_events.build
        child.context = user_factory
        child.save!
        child.reload
        orig_start_at = child.start_at

        @event.title = "asdf"
        @event.important_dates = true
        @event.start_at = Time.now.utc
        @event.save!
        expect(child.reload.title).to eql "asdf"
        expect(child.important_dates).to be true
        expect(child.start_at).to eql orig_start_at
      end

      it "disregards attempted changes to cascaded attributes" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event")
        child = @event.child_events.build
        child.context = user_factory
        child.save!
        child.reload
        orig_start_at = child.start_at

        child.title = "asdf"
        child.start_at = Time.now.utc
        child.save!
        expect(child.title).to eql "some event"
        expect(child.start_at).not_to eql orig_start_at
      end
    end

    context "locking" do
      it "copies all attributes when creating a locked child event" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event")
        child = @event.child_events.build
        child.context = user_factory
        child.workflow_state = :locked
        child.save!
        expect(child.start_at).to eql @event.start_at
        expect(child.title).to eql @event.title
      end

      it "updates locked child events whenever the parent is updated" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event")
        child = @event.child_events.build
        child.context = user_factory
        child.workflow_state = :locked
        child.save!

        @event.title = "asdf"
        @event.save!
        expect(child.reload.title).to eql "asdf"
      end

      it "disregards attempted changes to locked attributes" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event")
        child = @event.child_events.build
        child.context = user_factory
        child.workflow_state = :locked
        child.save!

        child.title = "asdf"
        child.save!
        expect(child.title).to eql "some event"
      end

      it "unlocks events when the last child is deleted" do
        calendar_event_model(start_at: "Sep 3 2008", title: "some event")
        @event.workflow_state = :locked
        @event.save!
        child = @event.child_events.build
        child.context = user_factory
        child.workflow_state = :locked
        child.save!

        child.destroy
        expect(@event.reload).to be_active
        expect(child.reload).to be_deleted
      end
    end
  end

  context "web_conference" do
    before(:once) do
      %w[big_blue_button wimba].each do |name|
        plugin = PluginSetting.create!(name:)
        plugin.update_attribute(:settings, { key: "value" })
      end
    end

    let_once(:course) { course_model }
    let_once(:course2) { course_model }
    let_once(:group1) { group(context: course) }
    let_once(:group2) { group(context: course) }

    def conference(context:, user: @user, type: "BigBlueButton")
      WebConference.create!(context:, user:, conference_type: type)
    end

    before do
      @course = course
    end

    it "can have no conference" do
      calendar_event_model
      expect(@event).to be_valid
    end

    it "sets the conference to nil when the calendar event is destroyed" do
      event = course.calendar_events.create! title: "Foo", web_conference: conference(context: course)
      expect(event.web_conference.id).to eq WebConference.last.id
      event.destroy!
      expect(event.web_conference).to be_nil
    end

    context "after_save callbacks" do
      it "keeps title of conference in sync with event" do
        event = course.calendar_events.create! title: "Foo", web_conference: conference(context: course)
        event.update! title: "updated title"
        expect(event.web_conference.reload.title).to eq "updated title"
      end

      it "keeps date of conference in sync with event" do
        event = course.calendar_events.create! title: "Foo", web_conference: conference(context: course)
        start_at = 3.days.from_now
        event.reload.update!(start_at:)
        expect(event.web_conference.reload.user_settings[:scheduled_date]).to eq start_at
      end

      it "does not fail when conference does not support scheduled_date" do
        event = course.calendar_events.create! title: "Foo", web_conference: conference(context: course, type: "Wimba")
        start_at = 3.days.from_now
        event.reload.update!(start_at:)
        expect(event.reload.start_at).to eq start_at
        expect(event.web_conference.reload.user_settings).not_to have_key(:scheduled_date)
      end
    end

    context "when event has course context" do
      it "can have a conference from the same course" do
        event = course.calendar_events.build title: "Foo", web_conference: conference(context: course)
        expect(event).to be_valid
      end

      it "cannot have a conference from a different course" do
        event = course.calendar_events.build title: "Foo", web_conference: conference(context: course2)
        expect(event).not_to be_valid
      end
    end

    context "when event has group context" do
      it "can have a conference from the same group" do
        event = group1.calendar_events.build title: "Foo", web_conference: conference(context: group1)
        expect(event).to be_valid
      end

      it "cannot have a conference from a different group" do
        event = group1.calendar_events.build title: "Foo", web_conference: conference(context: group2)
        expect(event).not_to be_valid
      end
    end

    context "when event has course section context" do
      it "can have a conference from the course" do
        section = add_section("foo", course:)
        event = section.calendar_events.build title: "Foo", web_conference: conference(context: course)
        expect(event).to be_valid
      end

      it "can not have have a conference from a different course" do
        section = add_section("foo", course:)
        event = section.calendar_events.build title: "Foo", web_conference: conference(context: course2)
        expect(event).not_to be_valid
      end
    end
  end

  describe "valid_ranges scope" do
    it "filters out calendar events with unpopulated start_at or end_at fields" do
      course_model
      common_fields = { context_id: @course.id, context_type: "Course" }
      @ce1 = CalendarEvent.create(common_fields.merge({ start_at: 1.day.ago, end_at: nil, description: "nil end_at" }))
                          .update_columns(end_at: nil)
      @ce2 = CalendarEvent.create(common_fields.merge({ start_at: nil, end_at: 1.day.from_now, description: "nil start_at" }))
                          .update_columns(start_at: nil)
      @ce3 = CalendarEvent.create(common_fields.merge({ start_at: nil, end_at: nil, description: "nil range" }))
      @ce4 = CalendarEvent.create(common_fields.merge({ start_at: 1.day.ago, end_at: 1.day.from_now, description: "populated range" }))

      expect(CalendarEvent.valid_ranges.length).to eq 1
      expect(CalendarEvent.valid_ranges).to include(@ce4)
    end
  end
end
