# 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 Enrollment do
  subject(:enrollment) { @enrollment }

  before(:once) do
    @user = User.create!
    @course = Course.create!
    @enrollment = StudentEnrollment.new(valid_enrollment_attributes)
  end

  it { is_expected.to be_valid }

  describe "workflow" do
    subject(:enrollment) { @enrollment.tap(&:save!) }

    describe "invited" do
      it { is_expected.to be_invited }

      it "can transition to rejected" do
        enrollment.reject!
        expect(enrollment).to be_rejected
      end

      it "updates the user when rejected" do
        expect(enrollment.user).to receive(:touch).at_least(1).times
        enrollment.reject!
      end

      it "can transition to completed" do
        enrollment.complete!
        expect(enrollment).to be_completed
      end
    end

    describe "creation_pending" do
      subject(:enrollment) do
        @enrollment.tap { |e| e.update!(workflow_state: :creation_pending) }
      end

      it { is_expected.to be_creation_pending }

      it "can transition to invited" do
        enrollment.invite!
        expect(enrollment).to be_invited
      end
    end

    describe "active" do
      subject(:enrollment) do
        @enrollment.tap { |e| e.update!(workflow_state: :active) }
      end

      it { is_expected.to be_active }

      it "can transition to rejected" do
        enrollment.reject!
        expect(enrollment).to be_rejected
      end

      it "updates the user when rejected" do
        expect(enrollment.user).to receive(:touch).at_least(1).times
        enrollment.reject!
      end

      it "can transition to completed" do
        enrollment.complete!
        expect(enrollment).to be_completed
      end
    end

    describe "deleted" do
      subject { @enrollment.tap { |e| e.update!(workflow_state: :deleted) } }

      it { is_expected.to be_deleted }
    end

    describe "rejected" do
      subject(:enrollment) do
        @enrollment.tap { |e| e.update!(workflow_state: :rejected) }
      end

      it { is_expected.to be_rejected }

      it "can transition to invited" do
        enrollment.unreject!
        expect(enrollment).to be_invited
      end
    end

    describe "completed" do
      subject { @enrollment.tap { |e| e.update!(workflow_state: :completed) } }

      it { is_expected.to be_completed }
    end

    describe "inactive" do
      subject { @enrollment.tap { |e| e.update!(workflow_state: :inactive) } }

      it { is_expected.to be_inactive }
    end
  end

  it "is pending if it is invited or creation_pending" do
    enrollment_model(workflow_state: "invited")
    expect(@enrollment).to be_pending
    @enrollment.destroy_permanently!

    enrollment_model(workflow_state: "creation_pending")
    expect(@enrollment).to be_pending
  end

  it "has a context_id as the course_id" do
    expect(@enrollment.course.id).not_to be_nil
    expect(@enrollment.context_id).to eql(@enrollment.course.id)
  end

  it "has a readable_type of Teacher for a TeacherEnrollment" do
    e = TeacherEnrollment.new
    e.type = "TeacherEnrollment"
    expect(e.readable_type).to eql("Teacher")
  end

  it "has a readable_type of Student for a StudentEnrollment" do
    e = StudentEnrollment.new
    e.type = "StudentEnrollment"
    expect(e.readable_type).to eql("Student")
  end

  it "has a readable_type of TaEnrollment for a TA" do
    e = TaEnrollment.new(valid_enrollment_attributes)
    e.type = "TaEnrollment"
    expect(e.readable_type).to eql("TA")
  end

  it "has a defalt readable_type of Student" do
    e = Enrollment.new
    e.type = "Other"
    expect(e.readable_type).to eql("Student")
  end

  describe "#student_or_fake_student?" do
    it "returns true for students" do
      expect(StudentEnrollment.create!(valid_enrollment_attributes).student_or_fake_student?).to be true
    end

    it "returns true for fake students" do
      fake_student_enrollment = @course.enroll_user(@enrollment.user, "StudentViewEnrollment")
      expect(fake_student_enrollment.student_or_fake_student?).to be true
    end

    it "returns false for non-students" do
      expect(TaEnrollment.create!(valid_enrollment_attributes).student_or_fake_student?).to be false
    end
  end

  describe "sis_role" do
    it "returns role_name if present" do
      role = custom_account_role("Assistant Grader", account: Account.default)
      e = TaEnrollment.new
      e.role_id = role.id
      expect(e.sis_role).to eq "Assistant Grader"
    end

    it "returns the sis enrollment type otherwise" do
      c = Account.default.courses.create!
      e = TaEnrollment.new(course: c)
      expect(e.sis_role).to eq "ta"
    end
  end

  describe "#enrollment_state" do
    let_once(:enrollment) { enrollment_model }

    context "when called on a new record" do
      it "raises an error" do
        expect { described_class.new.enrollment_state }
          .to raise_error(RuntimeError, "cannot call enrollment_state on a new record")
      end
    end

    context "when enrollment_state exists" do
      let(:enrollment_state) { EnrollmentState.find_by(enrollment_id: enrollment) }

      it "returns the existing enrollment_state" do
        expect(enrollment.enrollment_state).to eq(enrollment_state)
      end
    end

    context "when enrollment enrollment_state association is nil" do
      let(:non_stale_enrollment_state) { EnrollmentState.find_by(enrollment_id: enrollment) }

      before do
        allow_any_instance_of(ActiveRecord::Associations::HasOneAssociation).to receive(:reload)
      end

      it "returns the non-stale enrollment_state" do
        enrollment.association(:enrollment_state).target = nil

        expect(enrollment.enrollment_state).to eq(non_stale_enrollment_state)
      end
    end
  end

  describe "#destroy" do
    context "with overrides" do
      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        assignment = @course.assignments.create!
        @override = assignment.assignment_overrides.create!
        @override.assignment_override_students.create!(user: @enrollment.user)
      end

      let(:override_student) { @override.assignment_override_students.unscope(:where).find_by(user_id: @enrollment.user) }

      it "does not destroy assignment override students on the user if other enrollments for the user exist in the course" do
        @course.enroll_user(
          @enrollment.user,
          "StudentEnrollment",
          section: @course.course_sections.create!,
          allow_multiple_enrollments: true
        )
        @enrollment.destroy
        expect(override_student).to be_present
        expect(override_student).to be_active
      end

      it "destroys assignment override students on the user if no other enrollments for the user exist in the course" do
        @enrollment.destroy
        expect(override_student).to be_deleted
      end

      context "when the user is a final grader" do
        before(:once) do
          @teacher = User.create!
          @another_teacher = User.create!
          @course.enroll_teacher(@teacher, enrollment_state: "active", allow_multiple_enrollments: true)
          @course.enroll_teacher(@another_teacher, enrollment_state: "active", allow_multiple_enrollments: true)
          2.times { @course.assignments.create!(moderated_grading: true, final_grader: @teacher, grader_count: 2) }
          @course.assignments.create!(moderated_grading: true, final_grader: @another_teacher, grader_count: 2)
        end

        it "removes the user as final grader from all course assignments" do
          expect { @course.enrollments.find_by!(user: @teacher).destroy }.to change {
            @course.assignments.order(:created_at).pluck(:final_grader_id)
          }.from([nil, @teacher.id, @teacher.id, @another_teacher.id]).to([nil, nil, nil, @another_teacher.id])
        end

        it "does not remove the user as final grader from assignments if the user has other active enrollments of the same type" do
          section_one = @course.course_sections.create!
          @course.enroll_teacher(@teacher, active_all: true, allow_multiple_enrollments: true, section: section_one)
          expect { @course.enrollments.find_by!(user: @teacher).destroy }.not_to change {
            @course.assignments.order(:created_at).pluck(:final_grader_id)
          }.from([nil, @teacher.id, @teacher.id, @another_teacher.id])
        end

        it "does not remove the user as final grader from assignments if the user has other active instructor enrollments" do
          @course.enroll_ta(@teacher, active_all: true, allow_multiple_enrollments: true)
          expect { @course.enrollments.find_by!(user: @teacher).destroy }.not_to change {
            @course.assignments.order(:created_at).pluck(:final_grader_id)
          }.from([nil, @teacher.id, @teacher.id, @another_teacher.id])
        end
      end
    end

    it "updates user_account_associations" do
      course_with_teacher(active_all: 1)
      expect(@user.associated_accounts).to eq [Account.default]
      @enrollment.destroy
      expect(@user.associated_accounts.reload).to eq []
    end

    it "removes assignment overrides if they are only linked to this enrollment" do
      course_with_student
      assignment = assignment_model(course: @course)
      ao = AssignmentOverride.new
      ao.assignment = assignment
      ao.title = "ADHOC OVERRIDE"
      ao.workflow_state = "active"
      ao.set_type = "ADHOC"
      ao.save!
      assignment.reload
      override_student = ao.assignment_override_students.build
      override_student.user = @user
      override_student.save!

      expect(ao.workflow_state).to eq("active")
      @user.enrollments.destroy_all

      ao.reload
      expect(ao.workflow_state).to eq("deleted")
    end

    it "destroys associated scores" do
      @enrollment.save
      score = @enrollment.scores.create!
      @enrollment.destroy
      expect(score.reload).to be_deleted

      # ensure callbacks don't run if enrollment was already deleted
      expect(Enrollment).not_to receive(:recompute_final_score_in_singleton)
      @enrollment.destroy
    end
  end

  describe "restoring" do
    before(:once) do
      @course.assignments.create!
      @enrollment.save!
      @enrollment.destroy
    end

    it "restores associated scores that are deleted" do
      @enrollment.restore
      score_workflow = Score.find_by(enrollment_id: @enrollment, course_score: true).workflow_state
      expect(score_workflow).to eq("active")
    end

    it "does not restore scores associated with other enrollments" do
      new_enrollment = StudentEnrollment.create!(user: User.create!, course: @course)
      score = new_enrollment.scores.first
      score.update!(workflow_state: :deleted)
      @enrollment.restore
      expect(score.workflow_state).to eq("deleted")
    end

    it "restores associated scores that are deleted if restored by workflow state" do
      @enrollment.update!(workflow_state: :active)
      score_workflow = Score.find_by(enrollment_id: @enrollment, course_score: true).workflow_state
      expect(score_workflow).to eq("active")
    end

    it "does not restore scores associated with other enrollments if restored by workflow_state" do
      new_enrollment = StudentEnrollment.create!(user: User.create!, course: @course)
      score = new_enrollment.scores.first
      score.update!(workflow_state: :deleted)
      @enrollment.update!(workflow_state: :active)
      expect(score.workflow_state).to eq "deleted"
    end

    it "restores associated scores that are deleted if restored to inactive by workflow state" do
      @enrollment.update!(workflow_state: :inactive)
      score_workflow = Score.find_by(enrollment_id: @enrollment, course_score: true).workflow_state
      expect(score_workflow).to eq("active")
    end
  end

  describe "enrolling into a previously enrolled course" do
    let(:assignment) { @course.assignments.create!(points_possible: 10) }
    let(:section) { @course.course_sections.create! }
    let(:student) { User.create! }
    let(:teacher) do
      user = User.create!
      @course.enroll_teacher(user).accept(true)
      user
    end

    before do
      original_enrollment = @course.enroll_student(student)
      original_enrollment.accept(true)
      assignment.grade_student(student, grade: "10", grader: teacher)
      original_enrollment.destroy!
    end

    it "when there are no active enrollments, grades are recalculated" do
      # Create a new assignment while the student has no enrollment, to prove
      # that scores are being recalculated on enrollment, rather than copied.
      @course.assignments.create!(points_possible: 10)
      new_enrollment = @course.enroll_student(student, allow_multiple_enrollments: true, section:)
      expect(new_enrollment.scores.find_by!(course_score: true).final_score).to be 50.0
    end
  end

  describe "scores and grades" do
    let(:new_student_enrollment) do
      @course.enroll_user(
        @enrollment.user,
        "StudentEnrollment",
        section: @course.course_sections.create!,
        allow_multiple_enrollments: true
      )
    end

    let(:new_fake_student_enrollment) do
      @course.enroll_user(
        @enrollment.user,
        "StudentViewEnrollment",
        section: @course.course_sections.create!,
        allow_multiple_enrollments: true
      )
    end

    describe "effective_current_grade" do
      let_once(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @enrollment.scores.create!(course_score: true, current_score: 88.0)
        @enrollment.scores.create!(grading_period_id: period.id, current_score: 82.0)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      before do
        allow(@course).to receive(:grading_standard_enabled?).and_return true
      end

      it "returns the course current grade" do
        expect(@enrollment.effective_current_grade).to eq "B+"
      end

      it "returns the grading period current grade, if given a grading period" do
        expect(@enrollment.effective_current_grade(grading_period_id: period.id)).to eq "B-"
      end

      it "returns the override grade" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        expect(@enrollment.effective_current_grade).to eq "A"
      end

      it "does not return the override grade if the feature is not allowed" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.effective_current_grade).to eq "B+"
      end

      it "does not return the override grade if the feature is not enabled" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.effective_current_grade).to eq "B+"
      end

      it "returns nil if a grading standard is not enabled" do
        allow(@course).to receive(:grading_standard_enabled?).and_return false
        expect(@enrollment.effective_final_grade).to be_nil
      end
    end

    describe "effective_current_score" do
      let(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      it "returns the course current score" do
        @enrollment.scores.create!(course_score: true, current_score: 88.0)
        expect(@enrollment.effective_current_score).to eq 88.0
      end

      it "returns the grading period current score, if given a grading period" do
        @enrollment.scores.create!(grading_period_id: period.id, current_score: 82.0)
        expect(@enrollment.effective_current_score(grading_period_id: period.id)).to eq 82.0
      end

      it "returns the override score" do
        @enrollment.scores.create!(current_score: 79.0, override_score: 97.0)
        expect(@enrollment.effective_current_score).to eq 97.0
      end

      it "does not return the override score if the feature is not allowed" do
        @enrollment.scores.create!(current_score: 79.0, override_score: 97.0)
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.effective_current_score).to eq 79.0
      end

      it "does not return the override score if the feature is not enabled" do
        @enrollment.scores.create!(current_score: 79.0, override_score: 97.0)
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.effective_current_score).to eq 79.0
      end
    end

    describe "effective_final_grade" do
      let_once(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @enrollment.scores.create!(course_score: true, final_score: 88.0)
        @enrollment.scores.create!(grading_period_id: period.id, final_score: 82.0)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      before do
        allow(@course).to receive(:grading_standard_enabled?).and_return true
      end

      it "returns the course final grade" do
        expect(@enrollment.effective_final_grade).to eq "B+"
      end

      it "returns the grading period final grade, if given a grading period" do
        expect(@enrollment.effective_final_grade(grading_period_id: period.id)).to eq "B-"
      end

      it "returns the override grade" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        expect(@enrollment.effective_final_grade).to eq "A"
      end

      it "does not return the override grade if the feature is not allowed" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.effective_final_grade).to eq "B+"
      end

      it "does not return the override grade if the feature is not enabled" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.effective_final_grade).to eq "B+"
      end

      it "returns nil if a grading standard is not enabled" do
        allow(@course).to receive(:grading_standard_enabled?).and_return false
        expect(@enrollment.effective_final_grade).to be_nil
      end
    end

    describe "effective_final_score" do
      let_once(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @enrollment.scores.create!(course_score: true, final_score: 88.0)
        @enrollment.scores.create!(grading_period_id: period.id, final_score: 82.0)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      it "returns the course final score" do
        expect(@enrollment.effective_final_score).to be 88.0
      end

      it "returns the grading period final score, if given a grading period" do
        expect(@enrollment.effective_final_score(grading_period_id: period.id)).to be 82.0
      end

      it "returns the override score" do
        @enrollment.scores.find_by!(course_score: true).update!(override_score: 97.0)
        expect(@enrollment.effective_current_score).to eq 97.0
      end

      it "does not return the override score if the feature is not allowed" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.disable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.effective_final_score).to be 88.0
      end

      it "does not return the override score if the feature is not enabled" do
        @enrollment.scores.find_by(course_score: true).update!(override_score: 97.0)
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.effective_final_score).to be 88.0
      end
    end

    describe "override_grade" do
      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @course = @enrollment.course
        @score = @enrollment.scores.create!(course_score: true, final_score: 19)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      before do
        @course.enable_feature!(:final_grades_override)
        @course.update!(grading_standard_enabled: true)
        @score.update!(override_score: 99.0)
      end

      it "returns nil if final_grades_override is not allowed" do
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.override_grade).to be_nil
      end

      it "returns nil if final_grades_override is not enabled" do
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.override_grade).to be_nil
      end

      it "returns nil if there is no override grade" do
        @score.update!(override_score: nil)
        expect(@enrollment.override_grade).to be_nil
      end

      it "returns nil if a grading standard is not enabled" do
        @course.update!(grading_standard_enabled: false)
        expect(@enrollment.override_grade).to be_nil
      end

      it "returns the override grade if an override score exists" do
        expect(@enrollment.override_grade).to eq "A"
      end

      it "can return a grading period's override grade" do
        period_group = @course.grading_period_groups.create!
        period = period_group.grading_periods.create!(
          close_date: 1.day.from_now,
          end_date: 1.day.from_now,
          start_date: 1.day.ago,
          title: "period"
        )
        @enrollment.scores.find_by(grading_period: period).update!(override_score: 71.0)
        expect(@enrollment.override_grade(grading_period_id: period.id)).to eq "C-"
      end

      it "optionally accepts a score to use" do
        period_group = @course.grading_period_groups.create!
        period = period_group.grading_periods.create!(
          close_date: 1.day.from_now,
          end_date: 1.day.from_now,
          start_date: 1.day.ago,
          title: "period"
        )
        score = @enrollment.scores.find_by(grading_period: period)
        score.update!(override_score: 71.0)
        expect(@enrollment.override_grade(score:)).to eq "C-"
      end
    end

    describe "override_score" do
      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
        @course = @enrollment.course
        @score = @enrollment.scores.create!(course_score: true, final_score: 19)
        @course.enable_feature!(:final_grades_override)
        @course.update!(allow_final_grade_override: true)
      end

      before do
        @score.update!(override_score: 99.0)
      end

      it "returns nil if final_grades_override is not allowed" do
        @course.update!(allow_final_grade_override: false)
        expect(@enrollment.override_score).to be_nil
      end

      it "returns nil if final_grades_override is not enabled" do
        @course.disable_feature!(:final_grades_override)
        expect(@enrollment.override_score).to be_nil
      end

      it "returns nil if there is no override score" do
        @score.update!(override_score: nil)
        expect(@enrollment.override_score).to be_nil
      end

      it "returns the override score if one exists" do
        expect(@enrollment.override_score).to be 99.0
      end

      it "can return a grading period's override score" do
        period_group = @course.grading_period_groups.create!
        period = period_group.grading_periods.create!(
          close_date: 1.day.from_now,
          end_date: 1.day.from_now,
          start_date: 1.day.ago,
          title: "period"
        )
        @enrollment.scores.find_by(grading_period: period).update!(override_score: 71.0)
        expect(@enrollment.override_score(grading_period_id: period.id)).to be 71.0
      end

      it "optionally accepts a score to use" do
        period_group = @course.grading_period_groups.create!
        period = period_group.grading_periods.create!(
          close_date: 1.day.from_now,
          end_date: 1.day.from_now,
          start_date: 1.day.ago,
          title: "period"
        )
        score = @enrollment.scores.find_by(grading_period: period)
        score.update!(override_score: 71.0)
        expect(@enrollment.override_score(score:)).to eq 71.0
      end
    end

    describe "current scores and grades" do
      before(:once) do
        @enrollment = StudentEnrollment.create!(valid_enrollment_attributes)
      end

      let(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      let(:a_group) { @course.assignment_groups.create!(name: "a group") }

      describe "#computed_current_score" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(current_score: 80.3)
          expect(@enrollment.computed_current_score).to eq 80.3
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(current_score: nil)
          expect(@enrollment.computed_current_score).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(current_score: 80.3, grading_period: period)
          expect(@enrollment.computed_current_score).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(current_score: 80.3)
          score.destroy
          expect(@enrollment.computed_current_score).to be_nil
        end

        it "computes current score for a given grading period id" do
          @enrollment.scores.create!(current_score: 80.3)
          @enrollment.scores.create!(current_score: 70.6, grading_period: period)
          current_score = @enrollment.computed_current_score(grading_period_id: period.id)
          expect(current_score).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          current_score = @enrollment.computed_current_score(grading_period_id: period.id)
          expect(current_score).to be_nil
        end
      end

      describe "#unposted_current_score" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(unposted_current_score: 80.3)
          expect(@enrollment.unposted_current_score).to eq 80.3
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(unposted_current_score: nil)
          expect(@enrollment.unposted_current_score).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(unposted_current_score: 80.3, grading_period: period)
          expect(@enrollment.unposted_current_score).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(unposted_current_score: 80.3)
          score.destroy
          expect(@enrollment.unposted_current_score).to be_nil
        end

        it "computes current score for a given grading period id" do
          @enrollment.scores.create!(current_score: 80.3)
          @enrollment.scores.create!(current_score: 70.6, grading_period: period)
          current_score = @enrollment.computed_current_score(grading_period_id: period.id)
          expect(current_score).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          current_score = @enrollment.computed_current_score(grading_period_id: period.id)
          expect(current_score).to be_nil
        end
      end

      describe "#computed_current_grade" do
        before do
          @course.grading_standard_enabled = true
          @course.save!
        end

        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(current_score: 80.3)
          expect(@enrollment.computed_current_grade).to eq "B-"
        end

        it "ignores grading period grades when passed no arguments" do
          @enrollment.scores.create!(current_score: 80.3, grading_period: period)
          expect(@enrollment.computed_current_grade).to be_nil
        end

        it "ignores grades from soft-deleted scores" do
          score = @enrollment.scores.create!(current_score: 80.3)
          score.destroy
          expect(@enrollment.computed_current_grade).to be_nil
        end

        it "computes current grade for a given grading period id" do
          @enrollment.scores.create!(current_score: 70.6, grading_period: period)
          current_grade = @enrollment.computed_current_grade(grading_period_id: period.id)
          expect(current_grade).to eq "C-"
        end

        it "returns nil if a grading period grade is requested and does not exist" do
          current_grade = @enrollment.computed_current_grade(grading_period_id: period.id)
          expect(current_grade).to be_nil
        end
      end

      describe "#unposted_current_grade" do
        before do
          @course.grading_standard_enabled = true
          @course.save!
        end

        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(unposted_current_score: 80.3)
          expect(@enrollment.unposted_current_grade).to eq "B-"
        end

        it "ignores grading period grades when passed no arguments" do
          @enrollment.scores.create!(unposted_current_score: 80.3, grading_period: period)
          expect(@enrollment.unposted_current_grade).to be_nil
        end

        it "ignores grades from soft-deleted scores" do
          score = @enrollment.scores.create!(unposted_current_score: 80.3)
          score.destroy
          expect(@enrollment.unposted_current_grade).to be_nil
        end

        it "computes current grade for a given grading period id" do
          @enrollment.scores.create!(unposted_current_score: 70.6, grading_period: period)
          unposted_current_grade = @enrollment.unposted_current_grade(grading_period_id: period.id)
          expect(unposted_current_grade).to eq "C-"
        end

        it "returns nil if a grading period grade is requested and does not exist" do
          unposted_current_grade = @enrollment.unposted_current_grade(grading_period_id: period.id)
          expect(unposted_current_grade).to be_nil
        end
      end

      describe "#computed_current_points" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(current_points: 80.3)
          expect(@enrollment.computed_current_points).to eq 80.3
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(current_points: nil)
          expect(@enrollment.computed_current_points).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(current_points: 80.3, grading_period: period)
          expect(@enrollment.computed_current_points).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(current_points: 80.3)
          score.destroy
          expect(@enrollment.computed_current_points).to be_nil
        end

        it "computes current points for a given grading period id" do
          @enrollment.scores.create!(current_points: 80.3)
          @enrollment.scores.create!(current_points: 70.6, grading_period: period)
          current_points = @enrollment.computed_current_points(grading_period_id: period.id)
          expect(current_points).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          current_points = @enrollment.computed_current_points(grading_period_id: period.id)
          expect(current_points).to be_nil
        end
      end

      describe "#computed_final_points" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(final_points: 80.3)
          expect(@enrollment.computed_final_points).to eq 80.3
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(final_points: nil)
          expect(@enrollment.computed_final_points).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(final_points: 80.3, grading_period: period)
          expect(@enrollment.computed_final_points).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(final_points: 80.3)
          score.destroy
          expect(@enrollment.computed_final_points).to be_nil
        end

        it "computes current points for a given grading period id" do
          @enrollment.scores.create!(final_points: 80.3)
          @enrollment.scores.create!(final_points: 70.6, grading_period: period)
          final_points = @enrollment.computed_final_points(grading_period_id: period.id)
          expect(final_points).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          final_points = @enrollment.computed_final_points(grading_period_id: period.id)
          expect(final_points).to be_nil
        end
      end

      describe "#unposted_current_points" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(unposted_current_points: 80.3)
          expect(@enrollment.unposted_current_points).to eq 80.3
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(unposted_current_points: nil)
          expect(@enrollment.unposted_current_points).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(unposted_current_points: 80.3, grading_period: period)
          expect(@enrollment.unposted_current_points).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(unposted_current_points: 80.3)
          score.destroy
          expect(@enrollment.unposted_current_points).to be_nil
        end

        it "computes current points for a given grading period id" do
          @enrollment.scores.create!(unposted_current_points: 80.3)
          @enrollment.scores.create!(unposted_current_points: 70.6, grading_period: period)
          current_points = @enrollment.unposted_current_points(grading_period_id: period.id)
          expect(current_points).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          current_score = @enrollment.unposted_current_points(grading_period_id: period.id)
          expect(current_score).to be_nil
        end
      end

      describe "#find_score" do
        before do
          @course.update!(grading_standard_enabled: true)
          allow(GradeCalculator).to receive(:recompute_final_score)
          @enrollment.scores.create!(current_score: 85.3)
          @enrollment.scores.create!(grading_period: period, current_score: 99.1)
          @enrollment.scores.create!(assignment_group: a_group, current_score: 66.3)
          allow(GradeCalculator).to receive(:recompute_final_score).and_call_original
        end

        it "returns the course score" do
          expect(@enrollment.find_score.current_score).to be 85.3
        end

        it "returns grading period scores" do
          expect(@enrollment.find_score(grading_period_id: period.id).current_score).to be 99.1
        end

        it "returns assignment group scores" do
          expect(@enrollment.find_score(assignment_group_id: a_group.id).current_score).to be 66.3
        end

        it "returns no score when given an invalid grading period id" do
          expect(@enrollment.find_score(grading_period_id: 99_999)).to be_nil
        end

        it "returns no score when given an invalid assignment group id" do
          expect(@enrollment.find_score(assignment_group_id: 8_888_888)).to be_nil
        end

        it "returns no score when given unrecognized id keys" do
          expect(@enrollment.find_score(flavor: "Anchovied Caramel")).to be_nil
        end
      end

      describe "#graded_at" do
        it "uses the updated_at from the associated score object, if one exists" do
          score = @enrollment.scores.create!(current_score: 80.3)
          score.update_attribute(:updated_at, 5.days.from_now)
          expect(@enrollment.graded_at).to eq score.updated_at
        end

        it "ignores grading period scores" do
          @enrollment.scores.create!(current_score: 80.3, grading_period: period)
          expect(@enrollment.graded_at).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(current_score: 80.3)
          score.destroy
          expect(@enrollment.graded_at).to be_nil
        end
      end

      describe "copying overridden scores on restoration" do
        before(:once) do
          teacher = User.create!
          @course.enroll_teacher(teacher, active_all: true)
          assignment = @course.assignments.create!(points_possible: 100)
          assignment.grade_student(@enrollment.user, grade: 95, grader: teacher)
          @enrollment.find_score.update!(override_score: 98)
        end

        it "restores override score from an existing enrollment" do
          new_student_enrollment.find_score.update!(override_score: 88)
          new_student_enrollment.destroy
          new_student_enrollment.update!(workflow_state: :completed)
          expect(new_student_enrollment.find_score.override_score).to eq 98
        end

        it "restores override score from soft-deleted scores when the existing enrollment does not have any scores" do
          @enrollment.scores.update_all(workflow_state: :deleted)
          new_student_enrollment.find_score.update!(override_score: 88)
          new_student_enrollment.destroy
          new_student_enrollment.update!(workflow_state: :completed)
          expect(new_student_enrollment.find_score.override_score).to eq 88
        end
      end

      describe "copying current scores" do
        before(:once) do
          teacher = User.create!
          @course.enroll_teacher(teacher, active_all: true)
          assignment = @course.assignments.create!(points_possible: 100)
          assignment.grade_student(@enrollment.user, grade: 95, grader: teacher)
        end

        context "on creation" do
          it "copies scores over from existing student enrollments to new student enrollments" do
            expect(new_student_enrollment.computed_current_score).to eq(@enrollment.computed_current_score)
          end

          it "copies scores over from existing fake student enrollments to new fake student enrollments" do
            @enrollment.update!(type: "StudentViewEnrollment")
            expect(new_fake_student_enrollment.computed_current_score).to eq(@enrollment.computed_current_score)
          end
        end

        # if a user is being restored to active, the SubmissionLifecycleManager
        # run will kick off a grade calculation, which will update
        # the score objects. To test we're not copying scores, we'll
        # restore to completed for these tests.
        context "on restoration" do
          it "copies scores over from existing student enrollments to restored student enrollments" do
            new_student_enrollment.destroy
            new_student_enrollment.update!(workflow_state: :completed)
            expect(new_student_enrollment.computed_current_score).to eq(@enrollment.computed_current_score)
          end

          it "copies scores over from existing fake student enrollments to restored fake student enrollments" do
            @enrollment.update!(type: "StudentViewEnrollment")
            new_fake_student_enrollment.destroy
            new_fake_student_enrollment.update!(workflow_state: :completed)
            expect(new_fake_student_enrollment.computed_current_score).to eq(@enrollment.computed_current_score)
          end
        end
      end
    end

    describe "final scores and grades" do
      before(:once) do
        @enrollment.save!
      end

      let(:period) do
        group = @course.root_account.grading_period_groups.create!
        group.grading_periods.create!(
          title: "period",
          start_date: "Jan 1, 2015",
          end_date: "Jan 5, 2015"
        )
      end

      describe "#find_score" do
        it "returns the course score when no arg is passed" do
          score = @enrollment.scores.create!(final_score: 80.3)
          @enrollment.scores.create!(final_score: 80.3, grading_period_id: period.id)
          expect(@enrollment.find_score).to eq score
        end

        it "returns the grading period score when grading_period_id is passed" do
          @enrollment.scores.create!(final_score: 80.3)
          score = @enrollment.scores.create!(final_score: 80.3, grading_period_id: period.id)
          expect(@enrollment.find_score(grading_period_id: period.id)).to eq score
        end
      end

      describe "#computed_final_score" do
        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(final_score: 80.3)
          expect(@enrollment.computed_final_score).to eq 80.3
        end

        it "forgets scores on destroyed enrollments" do
          @enrollment.scores.create!(final_score: 55.2)
          @enrollment.destroy
          expect(@enrollment.computed_final_score).to be_nil
        end

        it "uses the value from the associated score object, even if it is nil" do
          @enrollment.scores.create!(final_score: nil)
          expect(@enrollment.computed_final_score).to be_nil
        end

        it "ignores grading period scores when passed no arguments" do
          @enrollment.scores.create!(final_score: 80.3, grading_period: period)
          expect(@enrollment.computed_final_score).to be_nil
        end

        it "ignores soft-deleted scores" do
          score = @enrollment.scores.create!(final_score: 80.3)
          score.destroy
          expect(@enrollment.computed_final_score).to be_nil
        end

        it "computes final score for a given grading period id" do
          @enrollment.scores.create!(final_score: 70.6, grading_period: period)
          final_score = @enrollment.computed_final_score(grading_period_id: period.id)
          expect(final_score).to eq 70.6
        end

        it "returns nil if a grading period score is requested and does not exist" do
          final_score = @enrollment.computed_final_score(grading_period_id: period.id)
          expect(final_score).to be_nil
        end
      end

      describe "#computed_final_grade" do
        before do
          @course.grading_standard_enabled = true
          @course.save!
        end

        it "uses the value from the associated score object, if one exists" do
          @enrollment.scores.create!(final_score: 80.3)
          expect(@enrollment.computed_final_grade).to eq "B-"
        end

        it "ignores grading period grades when passed no arguments" do
          @enrollment.scores.create!(final_score: 80.3, grading_period: period)
          expect(@enrollment.computed_final_grade).to be_nil
        end

        it "ignores grades from soft-deleted scores" do
          score = @enrollment.scores.create!(final_score: 80.3)
          score.destroy
          expect(@enrollment.computed_final_grade).to be_nil
        end

        it "computes final grade for a given grading period id" do
          @enrollment.scores.create!(final_score: 80.3)
          @enrollment.scores.create!(final_score: 70.6, grading_period: period)
          final_grade = @enrollment.computed_final_grade(grading_period_id: period.id)
          expect(final_grade).to eq "C-"
        end

        it "returns nil if a grading period grade is requested and does not exist" do
          final_grade = @enrollment.computed_final_grade(grading_period_id: period.id)
          expect(final_grade).to be_nil
        end
      end

      context "copying final scores on creation" do
        before(:once) do
          teacher = User.create!
          @course.enroll_teacher(teacher, active_all: true)
          assignment = @course.assignments.create!(points_possible: 100)
          assignment.grade_student(@enrollment.user, grade: 95, grader: teacher)
          # create this so the enrollment's current_score differs from its final_score
          @course.assignments.create!(points_possible: 100)
        end

        it "copies scores over from the user's existing student enrollments to new student enrollments" do
          expect(new_student_enrollment.computed_final_score).to eq(@enrollment.computed_final_score)
        end

        it "copies scores over from the user's existing fake student enrollments to new fake student enrollments" do
          @enrollment.update!(type: "StudentViewEnrollment")
          expect(new_fake_student_enrollment.computed_final_score).to eq(@enrollment.computed_final_score)
        end
      end
    end

    describe "restoring enrollments directly from soft-deleted to completed state" do
      before do
        # Create two enrollments for this course
        @enrollment.save!
        user2 = User.create!
        @enrollment2 = StudentEnrollment.create!(user: user2, course: @course)

        # and ensure the course has two assignment groups with one assignment in each group
        @course.assignments.create!(title: "Assignment #1", points_possible: 10)
        group2 = @course.assignment_groups.create!(name: "Assignment Group #2")
        @course.assignments.create!(title: "Assignment #2", points_possible: 10, assignment_group: group2)

        # Soft-delete both enrollments so their corresponding scores are also soft-deleted
        @enrollment.destroy
        @enrollment2.destroy
      end

      it "restores deleted scores belonging to the specific enrollment" do
        expect do
          # Restore an enrollment directly from "deleted" to "completed" state
          @enrollment.workflow_state = "completed"
          @enrollment.save!
        end.to change { @enrollment.reload.scores.size }.from(0).to(3)
      end

      it "does not restore deleted scores belonging to the other enrollment" do
        expect do
          # Restore an enrollment directly from "deleted" to "completed" state
          @enrollment.workflow_state = "completed"
          @enrollment.save!
        end.not_to change { @enrollment2.reload.scores.size }
      end
    end
  end

  it "does not allow an associated_user_id on a non-observer enrollment" do
    observed = User.create!

    @enrollment.type = "ObserverEnrollment"
    @enrollment.associated_user_id = observed.id
    expect(@enrollment).to be_valid

    @enrollment.type = "StudentEnrollment"
    expect(@enrollment).not_to be_valid

    @enrollment.associated_user_id = nil
    expect(@enrollment).to be_valid
  end

  it "does not allow an associated_user_id = user_id" do
    observed = User.create!
    @enrollment.type = "ObserverEnrollment"
    @enrollment.user_id = observed.id
    @enrollment.associated_user_id = observed.id
    expect(@enrollment).to_not be_valid
  end

  it "does not allow an enrollment to be created in a template course" do
    course = course_factory
    course.template = true
    course.save!
    enrollment = course.teacher_enrollments.new
    enrollment.user = user_with_pseudonym
    expect(enrollment).not_to be_valid
    expect(enrollment.errors.to_a).to eq(["Course is a template course"])
  end

  context "permissions" do
    before(:once) do
      course_with_student(active_all: true)
    end

    it "allows post_to_forum permission on a course if date is current" do
      @enrollment.start_at = 2.days.ago
      @enrollment.end_at = 4.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.save!

      expect(@enrollment.reload.state_based_on_date).to eq :active
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(true)
    end

    it "does not allow post_to_forum permission on a course if date in future" do
      @enrollment.start_at = 2.days.from_now
      @enrollment.end_at = 4.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.save!

      expect(@enrollment.reload.state_based_on_date).to eq :accepted
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(false)
    end

    it "does not allow read permission on a course if date inactive" do
      @enrollment.start_at = 2.days.from_now
      @enrollment.end_at = 4.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.save!

      @course.restrict_student_future_view = true
      @course.save!
      expect(@course.grants_right?(@enrollment.user, :read)).to be(false)

      # post to forum comes from role_override; inactive enrollments should not
      # get any permissions form role_override
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(false)
    end

    it "does not allow read permission on a course if explicitly inactive" do
      @enrollment.workflow_state = "inactive"
      @enrollment.save!
      expect(@course.grants_right?(@enrollment.user, :read)).to be(false)
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(false)
    end

    it "allows read, but not post_to_forum on a course if date completed" do
      @enrollment.start_at = 4.days.ago
      @enrollment.end_at = 2.days.ago
      @enrollment.workflow_state = "active"
      @enrollment.save!
      expect(@course.grants_right?(@enrollment.user, :read)).to be(true)
      # post to forum comes from role_override; completed enrollments should not
      # get any permissions form role_override
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(false)
    end

    it "allows read, but not post_to_forum on a course if explicitly completed" do
      @enrollment.workflow_state = "completed"
      @enrollment.save!
      expect(@course.grants_right?(@enrollment.user, :read)).to be(true)
      expect(@course.grants_right?(@enrollment.user, :post_to_forum)).to be(false)
    end

    it "grants read rights to account members with the ability to read_roster" do
      role = Role.get_built_in_role("AccountMembership", root_account_id: Account.default.id)
      user = account_admin_user(role:)
      RoleOverride.create!(context: Account.default,
                           permission: :read_roster,
                           role:,
                           enabled: true)
      @enrollment.save

      expect(@enrollment.user.grants_right?(user, :read)).to be false
      expect(@enrollment.grants_right?(user, :read)).to be true
    end

    it "is able to read grades if the course grants management rights to the enrollment" do
      @new_user = user_model
      @enrollment.save
      expect(@enrollment.grants_right?(@new_user, :read_grades)).to be_falsey
      @course.enroll_teacher(@new_user)
      @enrollment.reload
      AdheresToPolicy::Cache.clear
      expect(@enrollment.grants_right?(@user, :read_grades)).to be_truthy
    end

    it "allows the user itself to read its own grades" do
      expect(@enrollment.grants_right?(@user, :read_grades)).to be_truthy
    end
  end

  context "typed_enrollment" do
    it "allows StudentEnrollment" do
      expect(Enrollment.typed_enrollment("StudentEnrollment")).to eql(StudentEnrollment)
    end

    it "allows TeacherEnrollment" do
      expect(Enrollment.typed_enrollment("TeacherEnrollment")).to eql(TeacherEnrollment)
    end

    it "allows TaEnrollment" do
      expect(Enrollment.typed_enrollment("TaEnrollment")).to eql(TaEnrollment)
    end

    it "allows ObserverEnrollment" do
      expect(Enrollment.typed_enrollment("ObserverEnrollment")).to eql(ObserverEnrollment)
    end

    it "allows DesignerEnrollment" do
      expect(Enrollment.typed_enrollment("DesignerEnrollment")).to eql(DesignerEnrollment)
    end

    it "allows not NothingEnrollment" do
      expect(Enrollment.typed_enrollment("NothingEnrollment")).to be_nil
    end
  end

  context "drop scores" do
    before(:once) do
      course_with_teacher
      course_with_student(course: @course)
      @group = @course.assignment_groups.create!(name: "some group", group_weight: 50, rules: "drop_lowest:1")
      @assignment = @group.assignments.build(title: "some assignments", points_possible: 10)
      @assignment.context = @course
      @assignment.save!
      @assignment2 = @group.assignments.build(title: "some assignment 2", points_possible: 40)
      @assignment2.context = @course
      @assignment2.save!
    end

    it "drops high scores for groups when specified" do
      @enrollment = @user.enrollments.first
      @group.update_attribute(:rules, "drop_highest:1")
      expect(@enrollment.reload.computed_current_score).to be_nil
      @submission = @assignment.grade_student(@user, grade: "9", grader: @teacher)
      expect(@submission[0].score).to be(9.0)
      expect(@enrollment.reload.computed_current_score).to be(90.0)
      @submission2 = @assignment2.grade_student(@user, grade: "20", grader: @teacher)
      expect(@submission2[0].score).to be(20.0)
      expect(@enrollment.reload.computed_current_score).to be(50.0)
      @group.update_attribute(:rules, nil)
      expect(@enrollment.reload.computed_current_score).to be(58.0)
    end

    it "drops low scores for groups when specified" do
      @enrollment = @user.enrollments.first
      expect(@enrollment.reload.computed_current_score).to be_nil
      @submission = @assignment.grade_student(@user, grade: "9", grader: @teacher)
      @submission2 = @assignment2.grade_student(@user, grade: "20", grader: @teacher)
      expect(@submission2[0].score).to be(20.0)
      expect(@enrollment.reload.computed_current_score).to be(90.0)
      @group.update_attribute(:rules, "")
      expect(@enrollment.reload.computed_current_score).to be(58.0)
    end

    it "does not drop the last score for a group, even if the settings say it should be dropped" do
      @enrollment = @user.enrollments.first
      @group.update_attribute(:rules, "drop_lowest:2")
      expect(@enrollment.reload.computed_current_score).to be_nil
      @submission = @assignment.grade_student(@user, grade: "9", grader: @teacher)
      expect(@submission[0].score).to be(9.0)
      expect(@enrollment.reload.computed_current_score).to be(90.0)
      @submission2 = @assignment2.grade_student(@user, grade: "20", grader: @teacher)
      expect(@submission2[0].score).to be(20.0)
      expect(@enrollment.reload.computed_current_score).to be(90.0)
    end
  end

  context "notifications" do
    it "sends out invitations if the course is already published" do
      Notification.create!(name: "Enrollment Registration")
      course_with_teacher(active_all: true)
      user_with_pseudonym
      e = @course.enroll_student(@user)
      expect(e.messages_sent).to include("Enrollment Registration")
    end

    it "does not send out invitations immediately if the course restricts future viewing" do
      Notification.create!(name: "Enrollment Registration")
      course_with_teacher(active_all: true)
      @course.restrict_student_future_view = true
      @course.restrict_enrollments_to_course_dates = true
      @course.start_at = 1.day.from_now
      @course.conclude_at = 3.days.from_now
      @course.save!

      user_with_pseudonym
      e = @course.enroll_student(@user)
      expect(e).to be_inactive
      expect(e.messages_sent).to_not include("Enrollment Registration")

      Timecop.freeze(2.days.from_now) do
        expect(e).to be_invited
        expect_any_instantiation_of(e).to receive(:re_send_confirmation!).once
        run_jobs
      end
    end

    it "does not send out invitations to an observer if the student doesn't receive an invitation (e.g. sis import)" do
      Notification.create!(name: "Enrollment Registration", category: "Registration")

      course_with_teacher(active_all: true)
      student = user_with_pseudonym
      observer = user_with_pseudonym
      add_linked_observer(student, observer)

      @course.enroll_student(student, no_notify: true)
      expect(student.messages).to be_empty
      expect(observer.messages).to be_empty

      course_with_teacher(active_all: true)
      @course.enroll_student(student)
      student.reload
      observer.reload
      expect(student.messages).to_not be_empty
      expect(observer.messages).to be_empty
    end

    it "does not send out invitations to an observer if the course is not published" do
      Notification.create!(name: "Enrollment Registration", category: "Registration")

      course_with_teacher
      student = user_with_pseudonym
      observer = user_with_pseudonym
      add_linked_observer(student, observer)

      @course.enroll_student(student)
      expect(observer.messages).to be_empty
    end

    it "does not send out invitations if the course is not yet published" do
      Notification.create!(name: "Enrollment Registration")
      course_with_teacher
      user_with_pseudonym
      e = @course.enroll_student(@user)
      expect(e.messages_sent).not_to include("Enrollment Registration")
    end

    it "sends out invitations for previously-created enrollments when the course is published" do
      n = Notification.create(name: "Enrollment Registration", category: "Registration")
      course_with_teacher
      user_with_pseudonym
      e = @course.enroll_student(@user)
      expect(e.messages_sent).not_to include("Enrollment Registration")
      expect(@user.pseudonym).not_to be_nil
      @course.offer
      e.reload
      expect(e).to be_invited
      expect(e.user).not_to be_nil
      expect(e.user.pseudonym).not_to be_nil
      expect(Message.last).not_to be_nil
      expect(Message.last.notification).to eql(n)
      expect(Message.last.to).to eql(@user.email)
    end

    it "sends out notifications for enrollment acceptance correctly" do
      teacher = user_with_pseudonym(active_all: true)
      n = Notification.create!(name: "Enrollment Accepted")
      NotificationPolicy.create!(notification: n, communication_channel: @user.communication_channel, frequency: "immediately")
      course_with_teacher(active_all: true, user: teacher)
      student = user_factory
      e = @course.enroll_student(student)
      e.accept!
      expect(teacher.messages).to be_exists
    end

    it "does not send out notifications for enrollment acceptance to admins who are section restricted and in other sections" do
      teacher = user_with_pseudonym(active_all: true)
      n = Notification.create!(name: "Enrollment Accepted")
      NotificationPolicy.create!(notification: n, communication_channel: @user.communication_channel, frequency: "immediately")
      course_with_teacher(active_all: true, user: teacher)
      teacher.enrollments.first.update_attribute(:limit_privileges_to_course_section, true)
      other_section = @course.course_sections.create!
      e1 = @course.enroll_student(user_factory, section: other_section)
      e1.accept!
      expect(teacher.messages).not_to exist
      e2 = @course.enroll_student(user_factory, section: @course.default_section)
      e2.accept!
      expect(teacher.reload.messages).to exist
    end

    context "for course admin temporary enrollments" do
      before(:once) do
        Account.default.enable_feature!(:temporary_enrollments)
        provider = user_factory(active_all: true)
        @recipient = user_with_pseudonym(active_all: true)
        @course = course_with_teacher(active_all: true, user: provider).course
        pairing = TemporaryEnrollmentPairing.create!(root_account: Account.default, created_by: account_admin_user)
        @temp_enrollment = @course.enroll_user(@recipient,
                                               "TeacherEnrollment",
                                               {
                                                 role: teacher_role,
                                                 temporary_enrollment_source_user_id: provider.id,
                                                 temporary_enrollment_pairing_id: pairing.id,
                                               })
      end

      it "does not send out notifications for enrollment acceptance with inactive / future enrollments" do
        @temp_enrollment.update!(start_at: 1.day.from_now, end_at: 1.week.from_now)
        n = Notification.create!(name: "Enrollment Accepted")
        NotificationPolicy.create!(notification: n, communication_channel: @recipient.communication_channel, frequency: "immediately")
        e = @course.enroll_student(user_factory, section: @course.default_section)
        e.accept!
        expect(@recipient.reload.messages).not_to exist
      end

      it "sends out notifications for enrollment acceptance with active temp enrollments" do
        @temp_enrollment.update!(start_at: 1.day.ago, end_at: 1.day.from_now)
        n = Notification.create!(name: "Enrollment Accepted")
        NotificationPolicy.create!(notification: n, communication_channel: @recipient.communication_channel, frequency: "immediately")
        e = @course.enroll_student(user_factory, section: @course.default_section)
        e.accept!
        expect(@recipient.reload.messages).to exist
        expect(@recipient.messages.take.notification).to eq(n)
      end

      it "sends out notifications for enrollment acceptance with non-temporary enrollments" do
        @course.enroll_user(@recipient, "TeacherEnrollment", { role: teacher_role })
        n = Notification.create!(name: "Enrollment Accepted")
        NotificationPolicy.create!(notification: n, communication_channel: @recipient.communication_channel, frequency: "immediately")
        e = @course.enroll_student(user_factory, section: @course.default_section)
        e.accept!
        expect(@recipient.reload.messages).to exist
        expect(@recipient.messages.take.notification).to eq(n)
      end
    end
  end

  it "does not touch observer when set to skip" do
    course_model
    student = user_with_pseudonym
    observer = user_with_pseudonym
    old_time = observer.updated_at
    add_linked_observer(student, observer)
    @course.enrollments.create(user: student, skip_touch_user: true, type: "StudentEnrollment")
    expect(observer.reload.updated_at).to eq old_time
  end

  context "atom" do
    it "uses the course and user name to derive a title" do
      expect(@enrollment.to_atom[:title]).to eql("#{@enrollment.user.name} in #{@enrollment.course.name}")
    end

    it "links to the enrollment" do
      link_path = @enrollment.to_atom[:link]
      expect(link_path).to eql("/courses/#{@enrollment.course.id}/enrollments/#{@enrollment.id}")
    end
  end

  context "recompute_final_score_in_singleton" do
    before(:once) { course_with_student }

    it "raises an exception if called with more than one user" do
      expect { Enrollment.recompute_final_score_in_singleton([@user.id, 5], @course.id) }
        .to raise_error(ArgumentError)
    end

    it "sends later for a single student" do
      expect(Enrollment).to receive(:delay_if_production)
        .with(hash_including(singleton: "Enrollment.recompute_final_score:#{@user.id}:#{@course.id}:"))
        .and_call_original
      expect(Enrollment).to receive(:recompute_final_score).with(@user.id, @course.id)

      Enrollment.recompute_final_score_in_singleton(@user.id, @course.id)
    end
  end

  context "recompute_final_scores" do
    it "only recomputes once per student, per course" do
      course_with_student(active_all: true)
      @c1 = @course
      @s2 = @course.course_sections.create!(name: "s2")
      @course.enroll_student(@user, section: @s2, allow_multiple_enrollments: true)
      expect(@user.student_enrollments.reload.count).to eq 2
      course_with_student(user: @user)
      @c2 = @course
      expect(Enrollment).to receive(:recompute_final_score).with(@user.id, @c1.id)
      expect(Enrollment).to receive(:recompute_final_score).with(@user.id, @c2.id)
      Enrollment.recompute_final_scores(@user.id)
    end
  end

  context "date restrictions" do
    context "accept" do
      def enrollment_availability_test
        @enrollment.start_at = 2.days.ago
        @enrollment.end_at = 2.days.from_now
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:invited)
        @enrollment.accept
        expect(@enrollment.reload.state).to be(:active)
        expect(@enrollment.state_based_on_date).to be(:active)

        @enrollment.start_at = 4.days.ago
        @enrollment.end_at = 2.days.ago
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.reload.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:completed)
        expect(@enrollment.accept).to be_falsey

        @enrollment.start_at = 2.days.from_now
        @enrollment.end_at = 4.days.from_now
        @enrollment.save!
        expect(@enrollment.reload.state).to be(:invited)
        if @enrollment.admin?
          expect(@enrollment.state_based_on_date).to eq(:inactive)
        else
          expect(@enrollment.state_based_on_date).to be(:invited)
          expect(@enrollment.accept).to be_truthy
        end
      end

      def course_section_availability_test(should_be_invited = false)
        @section = @course.course_sections.first
        expect(@section).not_to be_nil
        @enrollment.course_section = @section
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        @section.start_at = 2.days.ago
        @section.end_at = 2.days.from_now
        @section.restrict_enrollments_to_section_dates = true
        @section.save!
        expect(@enrollment.state).to be(:invited)
        @enrollment.accept
        expect(@enrollment.reload.state).to be(:active)
        expect(@enrollment.state_based_on_date).to be(:active)

        @section.start_at = 4.days.ago
        @section.end_at = 2.days.ago
        @section.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.reload.state).to be(:invited)
        if should_be_invited
          expect(@enrollment.state_based_on_date).to be(:invited)
          expect(@enrollment.accept).to be_truthy
        else
          expect(@enrollment.state_based_on_date).to be(:completed)
          expect(@enrollment.accept).to be_falsey
        end

        @section.start_at = 2.days.from_now
        @section.end_at = 4.days.from_now
        @section.save!
        @enrollment.save!
        @enrollment.reload
        if should_be_invited
          expect(@enrollment.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
        else
          expect(@enrollment.state).to be(:invited)
          expect(@enrollment.state_based_on_date).to be(:invited)
          expect(@enrollment.accept).to be_truthy
        end
      end

      def course_availability_test(state_based_state)
        @course.start_at = 2.days.ago
        @course.conclude_at = 2.days.from_now
        @course.restrict_enrollments_to_course_dates = true
        @course.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        @enrollment.accept
        expect(@enrollment.reload.state).to be(:active)
        expect(@enrollment.state_based_on_date).to be(:active)

        @course.start_at = 4.days.ago
        @course.conclude_at = 2.days.ago
        @course.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        @enrollment.accept if @enrollment.invited?
        expect(@enrollment.state_based_on_date).to eql(state_based_state)

        @course.start_at = 2.days.from_now
        @course.conclude_at = 4.days.from_now
        @course.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        @enrollment.reload
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:invited)
        expect(@enrollment.accept).to be_truthy

        @course.complete!
        expect(@enrollment.reload.state).to be(:completed)
        expect(@enrollment.state_based_on_date).to be(:completed)

        @enrollment.workflow_state = "active"
        @enrollment.save!
        expect(@enrollment.state_based_on_date).to be(:completed)
      end

      def enrollment_term_availability_test
        @term = @course.enrollment_term
        expect(@term).not_to be_nil
        @term.start_at = 2.days.ago
        @term.end_at = 2.days.from_now
        @term.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:invited)
        @enrollment.accept
        expect(@enrollment.reload.state).to be(:active)
        expect(@enrollment.state_based_on_date).to be(:active)

        @term.start_at = 4.days.ago
        @term.end_at = 2.days.ago
        @term.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:completed)
        expect(@enrollment.accept).to be_falsey

        @term.start_at = 2.days.from_now
        @term.end_at = 4.days.from_now
        @term.save!
        @enrollment.reload
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:invited)
        expect(@enrollment.accept).to be_truthy
        expect(@enrollment.reload.state_based_on_date).to eql(@enrollment.admin? ? :active : :accepted)
      end

      def enrollment_dates_override_test
        @term = @course.enrollment_term
        expect(@term).not_to be_nil
        @term.save!
        @override = @term.enrollment_dates_overrides.create!(enrollment_type: @enrollment.type, enrollment_term: @term, context: @term.root_account)
        @override.start_at = 2.days.ago
        @override.end_at = 2.days.from_now
        @override.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        @enrollment.accept
        expect(@enrollment.reload.state).to be(:active)
        expect(@enrollment.state_based_on_date).to be(:active)

        @override.start_at = 4.days.ago
        @override.end_at = 2.days.ago
        @override.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        expect(@enrollment.state).to be(:invited)
        expect(@enrollment.state_based_on_date).to be(:completed)

        @override.start_at = 2.days.from_now
        @override.end_at = 4.days.from_now
        @override.save!
        @enrollment.workflow_state = "invited"
        @enrollment.save!
        @enrollment.reload
        expect(@enrollment.state).to be(:invited)
        if @enrollment.admin?
          expect(@enrollment.state_based_on_date).to be(:inactive)
        else
          expect(@enrollment.state_based_on_date).to be(:invited)
          expect(@enrollment.accept).to be_truthy
        end
        @course.restrict_student_future_view = true
        @course.save!
        @enrollment.update_attribute(:workflow_state, "active")
        @override.start_at = nil
        @override.end_at = nil
        @override.save!
        @term.start_at = 2.days.from_now
        @term.end_at = 4.days.from_now
        @term.save!
        expected = @enrollment.admin? ? :active : :inactive
        expect(@enrollment.reload.state_based_on_date).to eql(expected)
      end

      context "as a student" do
        before :once do
          course_with_student(active_all: true)
        end

        it "accepts into the right state based on availability dates on enrollment" do
          enrollment_availability_test
        end

        it "accepts into the right state based on availability dates on course_section" do
          course_section_availability_test
        end

        it "accepts into the right state based on availability dates on course" do
          course_availability_test(:completed)
        end

        it "accepts into the right state based on availability dates on enrollment_term" do
          enrollment_term_availability_test
        end

        it "accepts into the right state based on availability dates on enrollment_dates_override" do
          enrollment_dates_override_test
        end

        it "has the correct state for a half-open past course" do
          @term = @course.enrollment_term
          expect(@term).not_to be_nil
          @term.start_at = nil
          @term.end_at = 2.days.ago
          @term.save!

          @enrollment.workflow_state = "invited"
          @enrollment.save!
          expect(@enrollment.reload.state).to eq :invited
          expect(@enrollment.state_based_on_date).to eq :completed
        end

        it "recomputes scores for the student" do
          expect(Enrollment).to receive(:recompute_final_score).with(@enrollment.user_id, @enrollment.course_id)
          @enrollment.workflow_state = "invited"
          @enrollment.save!
          @enrollment.accept
        end
      end

      context "as a teacher" do
        before :once do
          course_with_teacher(active_all: true)
        end

        it "accepts into the right state based on availability dates on enrollment" do
          enrollment_availability_test
        end

        it "accepts into the right state based on availability dates on course_section" do
          course_section_availability_test(true)
        end

        it "accepts into the right state based on availability dates on course" do
          course_availability_test(:active)
        end

        it "accepts into the right state based on availability dates on enrollment_term" do
          enrollment_term_availability_test
        end

        it "accepts into the right state based on availability dates on enrollment_dates_override" do
          enrollment_dates_override_test
        end

        it "does not attempt to recompute scores since the user is not a student" do
          expect(Enrollment).not_to receive(:recompute_final_score)
          @enrollment.workflow_state = "invited"
          @enrollment.save!
          @enrollment.accept
        end
      end
    end

    shared_examples_for "term and enrollment dates" do
      describe "enrollment dates" do
        it "returns active enrollment" do
          @enrollment.start_at = 2.days.ago
          @enrollment.end_at = 2.days.from_now
          @enrollment.workflow_state = "active"
          @enrollment.save!
          expect(@enrollment.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
        end

        it "returns completed enrollment" do
          @enrollment.start_at = 4.days.ago
          @enrollment.end_at = 2.days.ago
          @enrollment.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:completed)
        end

        it "returns accepted if upcoming and available" do
          @enrollment.start_at = 2.days.from_now
          @enrollment.end_at = 4.days.from_now
          @enrollment.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to eql(@enrollment.admin? ? :inactive : :accepted)
        end

        it "returns inactive for students (accepted for admins) if upcoming and not available" do
          @enrollment.start_at = 2.days.from_now
          @enrollment.end_at = 4.days.from_now
          @enrollment.save!
          @course.restrict_student_future_view = true
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:inactive)
        end
      end

      describe "term dates" do
        before do
          @term = @course.enrollment_term
        end

        it "returns active" do
          @term.start_at = 2.days.ago
          @term.end_at = 2.days.from_now
          @term.save!
          @enrollment.workflow_state = "active"
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
          expect(Enrollment.where(id: @enrollment).active_by_date.first).to eq @enrollment
        end

        it "returns completed" do
          @term.start_at = 4.days.ago
          @term.end_at = 2.days.ago
          @term.reset_touched_courses_flag
          @term.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:completed)
          expect(Enrollment.where(id: @enrollment).active_by_date.first).to be_nil
        end

        it "returns accepted for students (inactive for admins) if upcoming and available" do
          @term.start_at = 2.days.from_now
          @term.end_at = 4.days.from_now
          @term.reset_touched_courses_flag
          @term.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to eql(@enrollment.admin? ? :active : :accepted)
        end

        it "returns inactive for all users if upcoming and not available" do
          @term.start_at = 2.days.from_now
          @term.end_at = 4.days.from_now
          @term.reset_touched_courses_flag
          @term.save!
          @course.restrict_student_future_view = true
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to eql(@enrollment.admin? ? :active : :inactive)
          if @enrollment.student?
            expect(Enrollment.where(id: @enrollment).active_by_date.first).to be_nil
          end
        end
      end

      describe "enrollment_dates_override dates" do
        before do
          @term = @course.enrollment_term
          @override = @term.enrollment_dates_overrides.create!(
            enrollment_type: @enrollment.type,
            enrollment_term: @term,
            context: @term.root_account
          )
        end

        it "infers root account by default" do
          expect(@override.root_account).to eq @term.root_account
        end

        it "allows setting the root account" do
          new_override = @term.enrollment_dates_overrides.create!(
            enrollment_type: @enrollment.type,
            enrollment_term: @term,
            context: @term.root_account,
            root_account: @course.root_account
          )
          expect(new_override.root_account).to eq @course.root_account
        end

        it "returns active" do
          @override.start_at = 2.days.ago
          @override.end_at = 2.days.from_now
          @override.save!
          @enrollment.workflow_state = "active"
          @enrollment.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
        end

        it "returns completed" do
          @override.start_at = 4.days.ago
          @override.end_at = 2.days.ago
          @term.reset_touched_courses_flag
          @override.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:completed)
        end

        it "returns accepted if upcoming and available (and inactive for admins)" do
          @override.start_at = 2.days.from_now
          @override.end_at = 4.days.from_now
          @term.reset_touched_courses_flag
          @override.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to eql(@enrollment.admin? ? :inactive : :accepted)
        end

        it "returns inactive for all users if upcoming and not available" do
          @override.start_at = 2.days.from_now
          @override.end_at = 4.days.from_now
          @term.reset_touched_courses_flag
          @override.save!
          @course.restrict_student_future_view = true
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:inactive)
        end
      end
    end

    context "dates for students" do
      before :once do
        Timecop.freeze(10.minutes.ago) do
          course_with_student(active_all: true)
        end
      end

      it_behaves_like "term and enrollment dates"

      describe "section dates" do
        before do
          @section = @course.course_sections.first
          @section.restrict_enrollments_to_section_dates = true
        end

        it "returns active" do
          @section.start_at = 2.days.ago
          @section.end_at = 2.days.from_now
          @section.save!
          expect(@enrollment.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
        end

        it "returns completed" do
          @section.start_at = 4.days.ago
          @section.end_at = 2.days.ago
          @section.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:completed)
        end

        it "returns accepted if upcoming and available" do
          @section.start_at = 2.days.from_now
          @section.end_at = 4.days.from_now
          @section.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:accepted)
        end

        it "returns inactive if upcoming and not available" do
          @section.start_at = 2.days.from_now
          @section.end_at = 4.days.from_now
          @section.save!
          @course.restrict_student_future_view = true
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:inactive)
        end
      end

      describe "course dates" do
        before do
          @course.restrict_enrollments_to_course_dates = true
        end

        it "returns active" do
          @course.start_at = 2.days.ago
          @course.conclude_at = 2.days.from_now
          @course.save!
          @enrollment.workflow_state = "active"
          @enrollment.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:active)
        end

        it "returns completed" do
          @course.start_at = 4.days.ago
          @course.conclude_at = 2.days.ago
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:completed)
        end

        it "returns accepted if upcoming and available" do
          @course.start_at = 2.days.from_now
          @course.conclude_at = 4.days.from_now
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:accepted)
        end

        it "returns inactive if upcoming and not available" do
          @course.start_at = 2.days.from_now
          @course.conclude_at = 4.days.from_now
          @course.restrict_student_future_view = true
          @course.save!
          expect(@enrollment.reload.state).to be(:active)
          expect(@enrollment.state_based_on_date).to be(:inactive)
        end
      end
    end

    context "dates for teachers" do
      before :once do
        Timecop.freeze(10.minutes.ago) do
          course_with_teacher(active_all: true)
        end
      end

      it_behaves_like "term and enrollment dates"
    end

    it "allows teacher access if both course and term have dates" do
      @teacher_enrollment = course_with_teacher(active_all: 1)
      @student_enrollment = student_in_course(active_all: 1)
      @term = @course.enrollment_term

      expect(@teacher_enrollment.state).to eq :active
      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Course dates completely before Term dates, now in course dates
      @course.start_at = 2.days.ago
      @course.conclude_at = 2.days.from_now
      @course.restrict_enrollments_to_course_dates = true
      @course.save!
      @term.start_at = 4.days.from_now
      @term.end_at = 6.days.from_now
      @term.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Term dates completely before Course dates, now in course dates
      @term.start_at = 6.days.ago
      @term.end_at = 4.days.ago
      @term.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Terms dates superset of course dates, now in both
      @term.start_at = 4.days.ago
      @term.end_at = 4.days.from_now
      @term.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Course dates superset of term dates, now in both
      @course.start_at = 6.days.ago
      @course.conclude_at = 6.days.from_now
      @course.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Course dates superset of term dates, now in beginning non-overlap
      @term.start_at = 2.days.from_now
      @term.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Course dates superset of term dates, now in ending non-overlap
      @term.start_at = 4.days.ago
      @term.end_at = 2.days.ago
      @term.save!

      expect(@teacher_enrollment.state_based_on_date).to eq :active
      expect(@student_enrollment.state_based_on_date).to eq :active

      # Term dates superset of course dates, now in beginning non-overlap
      @term.start_at = 6.days.ago
      @term.end_at = 6.days.from_now
      @term.save!
      @course.start_at = 2.days.from_now
      @course.conclude_at = 4.days.from_now
      @course.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :accepted

      @course.restrict_student_future_view = true
      @course.save!
      expect(@student_enrollment.reload.state_based_on_date).to eq :inactive

      # Term dates superset of course dates, now in ending non-overlap
      @course.start_at = 4.days.ago
      @course.conclude_at = 2.days.ago
      @course.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :completed

      # Course dates completely before term dates, now in term dates
      @course.start_at = 6.days.ago
      @course.conclude_at = 4.days.ago
      @course.save!
      @term.start_at = 2.days.ago
      @term.end_at = 2.days.from_now
      @term.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :completed

      # Course dates completely after term dates, now in term dates
      @course.start_at = 4.days.from_now
      @course.conclude_at = 6.days.from_now
      @course.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :inactive

      # Now between course and term dates, term first
      @term.start_at = 4.days.ago
      @term.end_at = 2.days.ago
      @term.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :completed
      expect(@student_enrollment.reload.state_based_on_date).to eq :inactive

      # Now after both dates
      @course.start_at = 4.days.ago
      @course.conclude_at = 2.days.ago
      @course.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :completed
      expect(@student_enrollment.reload.state_based_on_date).to eq :completed

      # Now before both dates
      @course.start_at = 2.days.from_now
      @course.conclude_at = 4.days.from_now
      @course.save!
      @term.start_at = 2.days.from_now
      @term.end_at = 4.days.from_now
      @term.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :inactive

      # Now between course and term dates, course first
      @course.start_at = 4.days.ago
      @course.conclude_at = 2.days.ago
      @course.save!

      expect(@teacher_enrollment.reload.state_based_on_date).to eq :active
      expect(@student_enrollment.reload.state_based_on_date).to eq :completed
    end

    it "affects the active?/inactive?/completed? predicates" do
      course_with_student(active_all: true)
      @enrollment.start_at = 2.days.ago
      @enrollment.end_at = 2.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.save!
      expect(@enrollment.active?).to be_truthy
      expect(@enrollment.inactive?).to be_falsey
      expect(@enrollment.completed?).to be_falsey

      @enrollment.start_at = 4.days.ago
      @enrollment.end_at = 2.days.ago
      @enrollment.save!
      @enrollment.reload
      expect(@enrollment.active?).to be_falsey
      expect(@enrollment.inactive?).to be_falsey
      expect(@enrollment.completed?).to be_truthy

      @enrollment.start_at = 2.days.from_now
      @enrollment.end_at = 4.days.from_now
      @enrollment.save!
      @enrollment.reload
      expect(@enrollment.active?).to be_falsey
      expect(@enrollment.completed?).to be_falsey
      expect(@enrollment.accepted?).to be_truthy
      @course.restrict_student_future_view = true
      @course.save!
      @enrollment.reload
      expect(@enrollment.inactive?).to be_truthy
    end

    it "does not affect the explicitly_completed? predicate" do
      course_with_student(active_all: true)
      @enrollment.start_at = 2.days.ago
      @enrollment.end_at = 2.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.save!
      expect(@enrollment.explicitly_completed?).to be_falsey

      @enrollment.start_at = 4.days.ago
      @enrollment.end_at = 2.days.ago
      @enrollment.save!
      expect(@enrollment.explicitly_completed?).to be_falsey

      @enrollment.start_at = 2.days.from_now
      @enrollment.end_at = 4.days.from_now
      @enrollment.save!
      expect(@enrollment.explicitly_completed?).to be_falsey

      @enrollment.workflow_state = "completed"
      expect(@enrollment.explicitly_completed?).to be_truthy
    end

    it "affects the completed_at" do
      yesterday = 1.day.ago

      course_with_student(active_all: true)
      @enrollment.start_at = 2.days.ago
      @enrollment.end_at = 2.days.from_now
      @enrollment.workflow_state = "active"
      @enrollment.completed_at = nil
      @enrollment.save!

      expect(@enrollment.completed_at).to be_nil
      @enrollment.completed_at = yesterday
      expect(@enrollment.completed_at).to eq yesterday

      @enrollment.start_at = 4.days.ago
      @enrollment.end_at = 2.days.ago
      @enrollment.completed_at = nil
      @enrollment.save!
      @enrollment.reload

      expect(@enrollment.completed_at).to eq @enrollment.end_at
      @enrollment.completed_at = yesterday
      expect(@enrollment.completed_at).to eq yesterday
    end
  end

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

    it "ungroups the user when the enrollment is deleted" do
      # set up course with two users in one section
      user1 = user_model
      user2 = user_model
      section1 = @course.course_sections.create
      section1.enroll_user(user1, "StudentEnrollment")
      section1.enroll_user(user2, "StudentEnrollment")

      # set up a group without a group category and put both users in it
      group = @course.groups.create
      group.add_user(user1)
      group.add_user(user2)

      # remove user2 from the section (effectively unenrolled from the course)
      user2.enrollments.first.destroy
      group.reload

      # he should be removed from the group
      expect(group.users.size).to eq 1
      expect(group.users).not_to include(user2)
      expect(group).to have_common_section
    end

    it "ungroups the user when the enrollment is rejected" do
      user1 = user_model
      # set up a group without a group category and put both users in it
      group = @course.groups.create
      gm = group.add_user(user1)
      section1 = @course.course_sections.create
      enrollment = section1.enroll_user(user1, "StudentEnrollment")
      enrollment.reject!
      expect(gm.reload).to be_deleted
    end

    it "ungroups the user when a changed enrollment causes conflict" do
      # set up course with two users in one section
      user1 = user_model
      user2 = user_model
      section1 = @course.course_sections.create
      section1.enroll_user(user1, "StudentEnrollment")
      section1.enroll_user(user2, "StudentEnrollment")

      # set up a group category in that course with restricted self sign-up and
      # put both users in one of its groups
      category = group_category
      category.configure_self_signup(true, true)
      category.save
      group = category.groups.create(context: @course)
      group.add_user(user1)
      group.add_user(user2)
      expect(category).not_to have_heterogenous_group

      # move a user to a new section
      section2 = @course.course_sections.create
      enrollment = user2.enrollments.first
      enrollment.course_section = section2
      enrollment.save
      group.reload
      category.reload

      # he should be removed from the group, keeping the group and the category
      # happily satisfying the self sign-up restriction.
      expect(group.users.size).to eq 1
      expect(group.users).not_to include(user2)
      expect(group).to have_common_section
      expect(category).not_to have_heterogenous_group
    end

    it "does not ungroup the user when a the group doesn't care" do
      # set up course with two users in one section
      user1 = user_model
      user2 = user_model
      section1 = @course.course_sections.create
      section1.enroll_user(user1, "StudentEnrollment")
      section1.enroll_user(user2, "StudentEnrollment")

      # set up a group category in that course *without* restrictions on self
      # sign-up and put both users in one of its groups
      category = group_category
      category.configure_self_signup(true, false)
      category.save
      group = category.groups.create(context: @course)
      group.add_user(user1)
      group.add_user(user2)

      # move a user to a new section
      section2 = @course.course_sections.create
      enrollment = user2.enrollments.first
      enrollment.course_section = section2
      enrollment.save
      group.reload
      category.reload

      # he should still be in the group
      expect(group.users.size).to eq 2
      expect(group.users).to include(user2)
    end

    it "ungroups the user from all groups, restricted and unrestricted when completely unenrolling from the course" do
      user1 = user_model name: "Andy"
      user2 = user_model name: "Bruce"

      section1 = @course.course_sections.create name: "Section 1"

      @course.enroll_user(user1, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      @course.enroll_user(user2, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)

      # created category for restricted groups
      res_category = group_category name: "restricted"
      res_category.configure_self_signup(true, true)
      res_category.save

      # created category for unrestricted groups
      unrestricted_category = group_category name: "unrestricted"
      unrestricted_category.configure_self_signup(true, false)
      unrestricted_category.save

      # Group 1 - restricted group
      group1 = res_category.groups.create(name: "Group1", context: @course)
      group1.add_user(user1)
      group1.add_user(user2)

      # Group 2 - unrestricted group
      group2 = unrestricted_category.groups.create(name: "Group2 (Unrestricted)", context: @course)
      group2.add_user(user1)
      group2.add_user(user2)

      user2.enrollments.where(course_section_id: section1.id).first.destroy
      group1.reload
      group2.reload

      expect(group1.users.size).to eq 1
      expect(group2.users.size).to eq 1
      expect(group1.users).not_to include(user2)
      expect(group2.users).not_to include(user2)
      expect(group1).to have_common_section
    end

    it "ungroups the user from the restricted group when deleting enrollment to one section but user is still enrolled in another section" do
      user1 = user_model name: "Andy"
      user2 = user_model name: "Bruce"

      section1 = @course.course_sections.create name: "Section 1"
      section2 = @course.course_sections.create name: "Section 2"

      # we should have more than one student enrolled in section to exercise common_to_section check.
      @course.enroll_user(user1, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      @course.enroll_user(user2, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      # enroll user2 in a second section
      @course.enroll_user(user2, "StudentEnrollment", section: section2, enrollment_state: "active", allow_multiple_enrollments: true)

      # set up a group category for restricted groups
      # and put both users in one of its groups
      category = group_category name: "restricted category"
      category.configure_self_signup(true, true)
      category.save

      # restricted group
      group = category.groups.create(name: "restricted group", context: @course)
      group.add_user(user1)
      group.add_user(user2)

      # remove user2 from the section (effectively unenrolled from a section of the course)
      user2.enrollments.where(course_section_id: section1.id).first.destroy
      group.reload

      # user2 should be removed from the group
      expect(group.users.size).to eq 1
      expect(group.users).not_to include(user2)
      expect(group).to have_common_section
    end

    it "does not ungroup the user from unrestricted group when deleting enrollment to one section but user is still enrolled in another section" do
      user1 = user_model name: "Andy"
      user2 = user_model name: "Bruce"

      section1 = @course.course_sections.create name: "Section 1"
      section2 = @course.course_sections.create name: "Section 2"

      # we should have more than one student enrolled in section to exercise common_to_section check.
      @course.enroll_user(user1, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      @course.enroll_user(user2, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      # enroll user2 in a second section
      @course.enroll_user(user2, "StudentEnrollment", section: section2, enrollment_state: "active", allow_multiple_enrollments: true)

      # set up a group category for unrestricted groups
      unrestricted_category = group_category name: "unrestricted category"
      unrestricted_category.configure_self_signup(true, false)
      unrestricted_category.save

      # unrestricted group
      group = unrestricted_category.groups.create(name: "unrestricted group", context: @course)
      group.add_user(user1)
      group.add_user(user2)

      # remove user2 from the section (effectively unenrolled from a section of the course)
      user2.enrollments.where(course_section_id: section1.id).first.destroy
      group.reload

      # user2 should not be removed from group 2
      expect(group.users.size).to eq 2
      expect(group.users).to include(user2)
      expect(group).not_to have_common_section
    end

    it "does not ungroup the user from restricted group when there's not another user in the group and user is still enrolled in another section" do
      user1 = user_model name: "Andy"

      section1 = @course.course_sections.create name: "Section 1"
      section2 = @course.course_sections.create name: "Section 2"

      # enroll user in two sections
      @course.enroll_user(user1, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      @course.enroll_user(user1, "StudentEnrollment", section: section2, enrollment_state: "active", allow_multiple_enrollments: true)

      # set up a group category for restricted groups
      restricted_category = group_category name: "restricted category"
      restricted_category.configure_self_signup(true, true)
      restricted_category.save

      # restricted group
      group = restricted_category.groups.create(name: "restricted group", context: @course)
      group.add_user(user1)

      # remove user from the section (effectively unenrolled from a section of the course)
      user1.enrollments.where(course_section_id: section1.id).first.destroy
      group.reload

      # he should not be removed from the group
      expect(group.users.size).to eq 1
      expect(group.users).to include(user1)
      expect(group).to have_common_section
    end

    it "ungroups the user even when there's not another user in the group if the enrollment is deleted" do
      # set up course with only one user in one section
      user1 = user_model
      section1 = @course.course_sections.create
      section1.enroll_user(user1, "StudentEnrollment")

      # set up a group category in that course with restricted self sign-up and
      # put the user in one of its groups
      category = group_category
      category.configure_self_signup(true, false)
      category.save
      group = category.groups.create(context: @course)
      group.add_user(user1)

      # remove the user from the section (effectively unenrolled from the course)
      user1.enrollments.first.destroy
      group.reload
      category.reload

      # he should not be in the group
      expect(group.users.size).to eq 0
    end

    it "does not ungroup the user when there's not another user in the group" do
      # set up course with only one user in one section
      user1 = user_model
      section1 = @course.course_sections.create
      section1.enroll_user(user1, "StudentEnrollment")

      # set up a group category in that course with restricted self sign-up and
      # put the user in one of its groups
      category = group_category
      category.configure_self_signup(true, false)
      category.save
      group = category.groups.create(context: @course)
      group.add_user(user1)

      # move a user to a new section
      section2 = @course.course_sections.create
      enrollment = user1.enrollments.first
      enrollment.course_section = section2
      enrollment.save
      group.reload
      category.reload

      # he should still be in the group
      expect(group.users.size).to eq 1
      expect(group.users).to include(user1)
    end

    it "ignores previously deleted memberships" do
      # set up course with a user in one section
      user = user_model
      section1 = @course.course_sections.create
      enrollment = section1.enroll_user(user, "StudentEnrollment")

      # set up a group without a group category and put the user in it
      group = @course.groups.create
      group.add_user(user)

      # mark the membership as deleted
      membership = group.group_memberships.where(user_id: user).first
      membership.workflow_state = "deleted"
      membership.save!

      # delete the enrollment to trigger audit_groups_for_deleted_enrollments processing
      expect { enrollment.destroy }.not_to raise_error

      # she should still be removed from the group
      expect(group.users.size).to eq 0
      expect(group.users).not_to include(user)
    end
  end

  context "audit_groups_for_deleted_enrollments with differentiation tags" do
    before do
      Account.default.settings[:allow_assign_to_differentiation_tags] = { value: true }
      Account.default.save!
      Account.default.reload
      course_with_teacher(active_all: true)
    end

    it "removes the user from the differentiation tag when the enrollment is deleted" do
      student = user_model
      section1 = @course.course_sections.create!(name: "Section 1")
      section1.enroll_user(student, "StudentEnrollment")

      # Set up non-collaborative group (differentiation tag)
      non_collab_category = @course.group_categories.create!(name: "Non-Collaborative Category", non_collaborative: true)
      diff_tag = non_collab_category.groups.create!(context: @course)
      diff_tag.add_user(student)
      expect(diff_tag.users).to include(student)

      # Delete enrollment to trigger audit removal
      enrollment = student.enrollments.where(course_section_id: section1.id).first
      enrollment.destroy
      diff_tag.reload

      expect(diff_tag.users).not_to include(student)
    end

    it "removes the differentiation tag membership when the enrollment is rejected" do
      student = user_model
      section1 = @course.course_sections.create!(name: "Section 1")
      section1.enroll_user(student, "StudentEnrollment")

      non_collab_category = @course.group_categories.create!(name: "Non-Collaborative Category", non_collaborative: true)
      gm = non_collab_category.groups.create!(context: @course).add_user(student)

      enrollment = student.enrollments.where(course_section_id: section1.id).first
      enrollment.reject!

      expect(gm.reload).to be_deleted
    end

    it "does not remove the differentiation tag when a user's section is updated" do
      # Set up course with two users in one section
      student1 = user_model
      student2 = user_model
      section1 = @course.course_sections.create!(name: "Section 1")
      section1.enroll_user(student1, "StudentEnrollment")
      section1.enroll_user(student2, "StudentEnrollment")

      # Set up a non-collaborative group category with restricted self sign-up
      non_collab_category = @course.group_categories.create!(name: "Non-Collaborative Category", non_collaborative: true)

      diff_tag = non_collab_category.groups.create!(context: @course)
      diff_tag.add_user(student1)
      diff_tag.add_user(student2)
      # Move student2 to a new section
      section2 = @course.course_sections.create!(name: "Section 2")
      enrollment = student2.enrollments.where(course_section_id: section1.id).first
      enrollment.course_section = section2
      enrollment.save!
      diff_tag.reload
      non_collab_category.reload

      expect(diff_tag.users.size).to eq 2
      expect(diff_tag.users).to include(student2)
    end

    it "ignores previously deleted differentiation tag memberships" do
      student = user_model
      section1 = @course.course_sections.create!(name: "Section 1")
      section1.enroll_user(student, "StudentEnrollment")

      non_collab_category = @course.group_categories.create!(name: "Non-Collaborative Category", non_collaborative: true)
      diff_tag = non_collab_category.groups.create!(context: @course)
      diff_tag.add_user(student)

      # Mark the differentiation tag membership as deleted
      membership = diff_tag.group_memberships.where(user_id: student.id).first
      membership.update!(workflow_state: "deleted")

      enrollment = student.enrollments.where(course_section_id: section1.id).first
      expect { enrollment.destroy }.not_to raise_error
      diff_tag.reload

      expect(diff_tag.users).not_to include(student)
    end

    it "does not remove membership when the enrollment is destroyed if there are multiple section enrollments" do
      student = user_model
      section1 = @course.course_sections.create!(name: "Section 1")
      section2 = @course.course_sections.create!(name: "Section 2")
      @course.enroll_user(student, "StudentEnrollment", section: section1, enrollment_state: "active", allow_multiple_enrollments: true)
      @course.enroll_user(student, "StudentEnrollment", section: section2, enrollment_state: "active", allow_multiple_enrollments: true)

      non_collab_category = @course.group_categories.create!(name: "Non-Collaborative Category", non_collaborative: true)
      diff_tag = non_collab_category.groups.create!(context: @course)
      diff_tag.add_user(student)

      enrollment = student.enrollments.where(course_section_id: section1.id).first
      expect { enrollment.destroy }.not_to raise_error
      diff_tag.reload

      expect(diff_tag.users).to include(student)

      # After removing the last section enrollment the membership should also be removed
      enrollment = student.enrollments.where(course_section_id: section2.id).first
      expect { enrollment.destroy }.not_to raise_error
      diff_tag.reload

      expect(diff_tag.users).not_to include(student)
    end
  end

  describe "for_email" do
    before :once do
      course_factory(active_all: true)
    end

    it "returns candidate enrollments" do
      user_factory
      @user.update_attribute(:workflow_state, "creation_pending")
      communication_channel(@user, { username: "jt@instructure.com" })
      @course.enroll_user(@user)
      expect(Enrollment.invited.for_email("jt@instructure.com").count).to eq 1
    end

    it "does not return non-candidate enrollments" do
      # mismatched e-mail
      user_factory
      @user.update_attribute(:workflow_state, "creation_pending")
      communication_channel(@user, { username: "bob@instructure.com" })
      @course.enroll_user(@user)
      # registered user
      user_factory
      communication_channel(@user, { username: "jt@instructure.com" })
      @user.register!
      @course.enroll_user(@user)
      # active e-mail
      user_factory
      @user.update_attribute(:workflow_state, "creation_pending")
      communication_channel(@user, { username: "jt@instructure.com", active_cc: true })
      @course.enroll_user(@user)
      # accepted enrollment
      user_factory
      @user.update_attribute(:workflow_state, "creation_pending")
      communication_channel(@user, { username: "jt@instructure.com" })
      @course.enroll_user(@user).accept
      # rejected enrollment
      user_factory
      @user.update_attribute(:workflow_state, "creation_pending")
      communication_channel(@user, { username: "jt@instructure.com" })
      @course.enroll_user(@user).reject

      expect(Enrollment.invited.for_email("jt@instructure.com")).to eq []
    end
  end

  describe "cached_temporary_invitations" do
    it "uncaches temporary user invitations when state changes" do
      enable_cache do
        course_factory(active_all: true)
        user_factory
        @user.update_attribute(:workflow_state, "creation_pending")
        communication_channel(@user, { username: "jt@instructure.com" })
        @enrollment = @course.enroll_user(@user)
        expect(Enrollment.cached_temporary_invitations("jt@instructure.com").length).to eq 1
        @enrollment.accept
        expect(Enrollment.cached_temporary_invitations("jt@instructure.com")).to eq []
      end
    end

    it "uncaches user enrollments when rejected" do
      enable_cache do
        course_with_student(active_course: 1)
        User.where(id: @user).update_all(updated_at: 1.year.ago)
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq [@enrollment]
        @enrollment.reject!
        # have to get the new updated_at
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq []
      end
    end

    it "uncaches user enrollments when deleted" do
      enable_cache do
        course_with_student(active_course: 1)
        User.where(id: @user).update_all(updated_at: 1.year.ago)
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq [@enrollment]
        @enrollment.destroy
        # have to get the new updated_at
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq []
      end
    end

    context "sharding" do
      specs_require_sharding

      describe "limit_privileges_to_course_section!" do
        it "uses the right shard to find the enrollments" do
          @shard1.activate do
            account = Account.create!
            course_with_student(active_all: true, account:)
          end

          @shard2.activate do
            Enrollment.limit_privileges_to_course_section!(@course, @user, true)
          end

          expect(@enrollment.reload.limit_privileges_to_course_section).to be_truthy
        end
      end

      describe "cached_temporary_invitations" do
        before :once do
          course_factory(active_all: true)
          user_factory
          @user.update_attribute(:workflow_state, "creation_pending")
          communication_channel(@user, { username: "jt@instructure.com" })
          @enrollment1 = @course.enroll_user(@user)
          @shard1.activate do
            account = Account.create!
            course_factory(active_all: true, account:)
            user_factory
            @user.update_attribute(:workflow_state, "creation_pending")
            communication_channel(@user, { username: "jt@instructure.com" })
            @enrollment2 = @course.enroll_user(@user)
          end
        end

        before do
          allow(Enrollment).to receive(:cross_shard_invitations?).and_return(true)
          skip "working CommunicationChannel.associated_shards" unless CommunicationChannel.associated_shards("jt@instructure.com").length == 2
        end

        it "includes invitations from other shards" do
          expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
          @shard1.activate do
            expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
          end
          @shard2.activate do
            expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
          end
        end

        it "has a single cache for all shards" do
          enable_cache do
            @shard2.activate do
              expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
            end
            expect(Shard).not_to receive(:with_each_shard)
            @shard1.activate do
              expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
            end
            expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
          end
        end

        it "invalidates the cache from any shard" do
          enable_cache do
            @shard2.activate do
              expect(Enrollment.cached_temporary_invitations("jt@instructure.com").sort_by(&:global_id)).to eq [@enrollment1, @enrollment2].sort_by(&:global_id)
              @enrollment2.reject!
            end
            @shard1.activate do
              expect(Enrollment.cached_temporary_invitations("jt@instructure.com")).to eq [@enrollment1]
              @enrollment1.reject!
            end
            expect(Enrollment.cached_temporary_invitations("jt@instructure.com")).to eq []
          end
        end
      end
    end
  end

  describe "effective_start_at" do
    before :once do
      course_with_student(active_all: true)
      @term = @course.enrollment_term
      @section = @enrollment.course_section

      # 7 different possible times, make sure they're distinct
      @enrollment_date_start_at = 7.days.ago
      @enrollment.start_at = 6.days.ago
      @section.start_at = 5.days.ago
      @course.start_at = 4.days.ago
      @term.start_at = 3.days.ago
      @section.created_at = 2.days.ago
      @course.created_at = 1.day.ago
    end

    it "utilizes to enrollment_dates if it has a value" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[@enrollment_date_start_at, nil]])
      expect(@enrollment.effective_start_at).to eq @enrollment_date_start_at
    end

    it "uses earliest value from enrollment_dates if it has multiple" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[@enrollment.start_at, nil], [@enrollment_date_start_at, nil]])
      expect(@enrollment.effective_start_at).to eq @enrollment_date_start_at
    end

    it "follows chain of fallbacks in correct order if no enrollment_dates" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[nil, Time.zone.now]])

      # start peeling away things from most preferred to least preferred to
      # test fallback chain
      expect(@enrollment.effective_start_at).to eq @enrollment.start_at
      @enrollment.start_at = nil
      expect(@enrollment.effective_start_at).to eq @section.start_at
      @section.start_at = nil
      expect(@enrollment.effective_start_at).to eq @course.start_at
      @course.start_at = nil
      expect(@enrollment.effective_start_at).to eq @term.start_at
      @term.start_at = nil
      expect(@enrollment.effective_start_at).to eq @section.created_at
      @section.created_at = nil
      expect(@enrollment.effective_start_at).to eq @course.created_at
      @course.created_at = nil
      expect(@enrollment.effective_start_at).to be_nil
    end

    it "does not explode when missing section or term" do
      @enrollment.course_section = nil
      @course.enrollment_term = nil
      expect(@enrollment.effective_start_at).to eq @enrollment.start_at
      @enrollment.start_at = nil
      expect(@enrollment.effective_start_at).to eq @course.start_at
      @course.start_at = nil
      expect(@enrollment.effective_start_at).to eq @course.created_at
      @course.created_at = nil
      expect(@enrollment.effective_start_at).to be_nil
    end
  end

  describe "effective_end_at" do
    before :once do
      course_with_student(active_all: true)
      @term = @course.enrollment_term
      @section = @enrollment.course_section

      # 5 different possible times, make sure they're distinct
      @enrollment_date_end_at = 1.day.ago
      @enrollment.end_at = 2.days.ago
      @section.end_at = 3.days.ago
      @course.conclude_at = 4.days.ago
      @term.end_at = 5.days.ago
    end

    it "utilizes to enrollment_dates if it has a value" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[nil, @enrollment_date_end_at]])
      expect(@enrollment.effective_end_at).to eq @enrollment_date_end_at
    end

    it "uses earliest value from enrollment_dates if it has multiple" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[nil, @enrollment.end_at], [nil, @enrollment_date_end_at]])
      expect(@enrollment.effective_end_at).to eq @enrollment_date_end_at
    end

    it "follows chain of fallbacks in correct order if no enrollment_dates" do
      allow(@enrollment).to receive(:enrollment_dates).and_return([[nil, nil]])

      # start peeling away things from most preferred to least preferred to
      # test fallback chain
      expect(@enrollment.effective_end_at).to eq @enrollment.end_at
      @enrollment.end_at = nil
      expect(@enrollment.effective_end_at).to eq @section.end_at
      @section.end_at = nil
      expect(@enrollment.effective_end_at).to eq @course.conclude_at
      @course.conclude_at = nil
      expect(@enrollment.effective_end_at).to eq @term.end_at
      @term.end_at = nil
      expect(@enrollment.effective_end_at).to be_nil
    end

    it "does not explode when missing section or term" do
      @enrollment.course_section = nil
      @course.enrollment_term = nil

      expect(@enrollment.effective_end_at).to eq @enrollment.end_at
      @enrollment.end_at = nil
      expect(@enrollment.effective_end_at).to eq @course.conclude_at
      @course.conclude_at = nil
      expect(@enrollment.effective_end_at).to be_nil
    end
  end

  describe "conclude" do
    it "removes the enrollment from User#cached_currentish_enrollments" do
      enable_cache do
        course_with_student(active_all: 1)
        User.where(id: @user).update_all(updated_at: 1.day.ago)
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq [@enrollment]
        @enrollment.conclude
        @user.reload
        expect(@user.cached_currentish_enrollments).to eq []
      end
    end
  end

  describe "unconclude" do
    it "adds the enrollment to User#cached_currentish_enrollments" do
      enable_cache do
        course_with_student active_course: true, enrollment_state: "completed"
        User.where(id: @student).update_all(updated_at: 1.day.ago)
        @student.reload
        expect(@student.cached_currentish_enrollments).to eq []
        @enrollment.unconclude
        expect(@student.cached_currentish_enrollments).to eq [@enrollment]
      end
    end
  end

  describe "observing users" do
    before :once do
      @student = user_factory(active_all: true)
      @parent = user_with_pseudonym(active_all: true)
      add_linked_observer(@student, @parent)
    end

    it "gets new observer enrollments when an observed user gets a new enrollment" do
      se = course_with_student(active_all: true, user: @student)
      pe = @parent.observer_enrollments.first

      expect(pe).not_to be_nil
      expect(pe.course_id).to eql se.course_id
      expect(pe.course_section_id).to eql se.course_section_id
      expect(pe.workflow_state).to eql se.workflow_state
      expect(pe.associated_user_id).to eql se.user_id
    end

    it 'defaults observer enrollments to "active" state' do
      course_factory(active_all: true)
      @course.enroll_student(@student, enrollment_state: "invited")
      pe = @parent.observer_enrollments.where(course_id: @course).first
      expect(pe).not_to be_nil
      expect(pe.workflow_state).to eql "active"
    end

    it "updates their observer enrollments when an observed user's enrollment is updated" do
      se = course_with_student(user: @student)
      pe = @parent.observer_enrollments.first
      expect(pe).not_to be_nil

      se.invite
      se.accept
      expect(pe.reload).to be_active

      se.complete
      expect(pe.reload).to be_completed
    end

    it "does not undelete observer enrollments if the student enrollment wasn't already deleted" do
      se = course_with_student(user: @student)
      pe = @parent.observer_enrollments.first
      expect(pe).not_to be_nil
      pe.destroy

      se.invite
      expect(pe.reload).to be_deleted

      se.accept
      expect(pe.reload).to be_deleted
    end

    it "does not allow an observer to be observed by a user they are observing" do
      @observed = User.create!
      @observer = User.create!
      @course = Course.create!

      @enrollment = StudentEnrollment.create!(user: @observed, course: @course)
      @enrollment2 = StudentEnrollment.create!(user: @observer, course: @course)

      add_linked_observer(@observed, @observer)
      add_linked_observer(@observer, @observed)

      @enrollment.type = "ObserverEnrollment"
      @enrollment.associated_user_id = @enrollment2.user_id
      @enrollment.save!

      @enrollment2.type = "ObserverEnrollment"
      @enrollment2.associated_user_id = @enrollment.user_id

      expect(@enrollment).to be_valid
      expect { @enrollment2.save! }.to raise_error("Validation failed: Associated user Cannot observe observer observing self")
    end

    it "allows existing observer enrollment cycles to be deleted" do
      u1 = User.create!
      u2 = User.create!
      course = Course.create!

      e1 = ObserverEnrollment.create!(course:, user: u1, associated_user_id: u2.id)
      e2 = ObserverEnrollment.create!(course:, user: u2)
      ObserverEnrollment.where(id: e2).update_all(associated_user_id: u1.id) # bypass the validation

      expect(e1).not_to be_valid

      e1.destroy

      expect(e1).to be_valid
      expect(e1).to be_deleted
    end

    context "sharding" do
      specs_require_sharding

      it "allows enrolling a user that is observed from another shard" do
        se = @shard1.activate do
          @other_account = Account.create!
          expect_any_instance_of(User).to receive(:can_be_enrolled_in_course?).and_return(true)
          course_with_student(account: @other_account, active_all: true, user: @student)
        end
        add_linked_observer(@student, @parent, root_account: @other_account)
        pe = @parent.observer_enrollments.shard(@shard1).first

        expect(pe).not_to be_nil
        expect(pe.course_id).to eql se.course_id
        expect(pe.course_section_id).to eql se.course_section_id
        expect(pe.workflow_state).to eql se.workflow_state
        expect(pe.associated_user_id).to eql se.user_id
      end
    end
  end

  describe "temporary enrollments" do
    before(:once) do
      Account.default.enable_feature!(:temporary_enrollments)
      @source_user = user_factory(active_all: true)
      @temporary_enrollment_recipient = user_factory(active_all: true)
      temporary_enrollment_recipient2 = user_factory(active_all: true)
      @course1 = course_with_teacher(active_all: true, user: @source_user).course
      temporary_enrollment_pairing = TemporaryEnrollmentPairing.create!(root_account: Account.default, created_by: account_admin_user)
      @recipient_temp_enrollment = @course1.enroll_user(
        @temporary_enrollment_recipient,
        "TeacherEnrollment",
        {
          role: teacher_role,
          temporary_enrollment_source_user_id: @source_user.id,
          temporary_enrollment_pairing_id: temporary_enrollment_pairing.id
        }
      )
      course2 = course_with_teacher(active_all: true, user: @source_user).course
      @recipient2_temp_enrollment = course2.enroll_user(
        temporary_enrollment_recipient2,
        "TeacherEnrollment",
        {
          role: teacher_role,
          temporary_enrollment_source_user_id: @source_user.id,
          temporary_enrollment_pairing_id: temporary_enrollment_pairing.id
        }
      )
    end

    it "retrieves temporary enrollment recipients for provider" do
      expect(Enrollment.temporary_enrollment_recipients_for_provider(@source_user).sort)
        .to eq([@recipient_temp_enrollment, @recipient2_temp_enrollment].sort)
    end

    it "retrieves temporary enrollments for recipient" do
      expect(Enrollment.temporary_enrollments_for_recipient(@temporary_enrollment_recipient).take)
        .to eq(@recipient_temp_enrollment)
    end

    it "returns a boolean value if self is a temporary enrollment" do
      @recipient2_temp_enrollment.update!(temporary_enrollment_source_user_id: nil)
      expect(@recipient_temp_enrollment.temporary_enrollment?).to be_truthy
      expect(@recipient2_temp_enrollment.temporary_enrollment?).to be_falsey
    end
  end

  describe "#can_be_deleted_by" do
    describe "on a student enrollment" do
      let(:user) { double(id: 42) }
      let(:session) { double }

      before do
        course_with_student
        @enrollment.reload
      end

      it "is true for a user who has been granted :remove_student_from_course" do
        allow(@course).to receive(:grants_right?).with(user, session, :remove_student_from_course).and_return(true)
        allow(@course).to receive(:grants_right?).with(user, session, :allow_course_admin_actions).and_return(false)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_truthy
      end

      it "is false for a user without :remove_student_from_course" do
        allow(@course).to receive(:grants_right?).with(user, session, :remove_student_from_course).and_return(false)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end

      it "is false for someone with :allow_course_admin_actions in other context" do
        context = CourseSection.new(id: 10)
        allow(context).to receive(:grants_right?).with(user, session, :remove_student_from_course).and_return(true)
        allow(context).to receive(:grants_right?).with(user, session, :allow_course_admin_actions).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, context, session)).to be_falsey
      end

      it "is false if a user is trying to remove their own enrollment" do
        allow(@course).to receive(:grants_right?).with(user, session, :remove_student_from_course).and_return(true)
        allow(@course).to receive(:grants_right?).with(user, session, :allow_course_admin_actions).and_return(false)
        allow(@course).to receive_messages(account: @course)
        @enrollment.user_id = user.id
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end
    end

    describe "on an observer enrollment" do
      let(:user) { double(id: 42) }
      let(:session) { double }

      before do
        course_with_observer
        @enrollment.reload
      end

      it "is true with :remove_observer_from_course" do
        allow(@course).to receive(:grants_right?).and_return(false)
        allow(@course).to receive(:grants_right?).with(user, session, :remove_observer_from_course).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_truthy
      end

      it "is false with :allow_course_admin_actions but not the granular" do
        allow(@course).to receive(:grants_right?).and_return(false)
        allow(@course).to receive(:grants_right?).with(user, session, :allow_course_admin_actions).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end

      it "is false otherwise" do
        allow(@course).to receive(:grants_right?).and_return(false)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end
    end

    describe "on a teacher enrollment" do
      let(:user) { double(id: 42) }
      let(:session) { double }

      before do
        course_with_teacher
        @enrollment.reload
      end

      it "is false with :manage_students" do
        allow(@course).to receive(:grants_right?).and_return(false)
        allow(@course).to receive(:grants_right?).with(user, session, :manage_students).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end

      it "is true without :allow_course_admin_actions but with the :remove_teacher_from_account granular" do
        allow(@course).to receive(:grants_right?).and_return(false)
        allow(@course).to receive(:grants_right?).with(user, session, :remove_teacher_from_course).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_truthy
      end

      it "is false with only :allow_course_admin_actions" do
        allow(@course).to receive(:grants_right?).and_return(false)
        allow(@course).to receive(:grants_right?).with(user, session, :allow_course_admin_actions).and_return(true)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end

      it "is false otherwise" do
        allow(@course).to receive(:grants_right?).and_return(false)
        expect(@enrollment.can_be_deleted_by(user, @course, session)).to be_falsey
      end
    end
  end

  describe "updating cached due dates" do
    before :once do
      course_with_student
      @assignments = [
        assignment_model(course: @course),
        assignment_model(course: @course)
      ]
    end

    it "triggers a batch when enrollment is created" do
      added_user = user_factory
      expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(added_user.id, @course, nil, { update_grades: true })
      @course.enroll_student(added_user)
    end

    it "does not trigger a batch when enrollment is not student" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute_users_for_course)
      @course.enroll_teacher(user_factory)
    end

    it "triggers a batch when enrollment is deleted" do
      expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).with(@enrollment.user_id, @course, nil, { update_grades: false })
      @enrollment.destroy
    end

    it "does not trigger when nothing changed" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute_users_for_course)
      @enrollment.save
    end

    it "does not trigger when set_update_cached_due_dates callback is suspended" do
      expect(SubmissionLifecycleManager).not_to receive(:recompute_users_for_course)
      Enrollment.suspend_callbacks(:set_update_cached_due_dates) do
        @course.enroll_student(user_factory)
      end
    end

    it "triggers once for enrollment.destroy" do
      override = assignment_override_model(assignment: @assignments.first)
      override.assignment_override_students.create(user: @student)
      expect(SubmissionLifecycleManager).to receive(:recompute_users_for_course).once
      expect(SubmissionLifecycleManager).not_to receive(:recompute)
      @enrollment.destroy
    end

    context "with mastery paths" do
      before do
        allow(ConditionalRelease::Service).to receive(:enabled_in_context?).and_return(true)
        assignment_override_model(
          assignment: @assignments.first,
          set_type: AssignmentOverride::SET_TYPE_NOOP,
          set_id: AssignmentOverride::NOOP_MASTERY_PATHS
        )
        @override = assignment_override_model(assignment: @assignments.first)
        @student_override = @override.assignment_override_students.create(user: @student)
      end

      it "restores assignment override from mastery paths" do
        @enrollment.destroy
        expect(@override.reload.workflow_state).to eq "deleted"
        expect(@student_override.reload.workflow_state).to eq "deleted"
        @enrollment.enrollment_state.update(state: "invited")
        @enrollment.update(workflow_state: "invited")
        @enrollment.reload.accept!
        expect(@override.reload.workflow_state).to eq "active"
        expect(@student_override.reload.workflow_state).to eq "active"
      end

      it "restores student assignment override from mastery paths" do
        other_student = User.create!
        @course.enroll_student(other_student, enrollment_state: :active)
        @override.assignment_override_students.create!(user: other_student)
        @enrollment.destroy
        expect(@override.reload.workflow_state).to eq "active"
        expect(@student_override.reload.workflow_state).to eq "deleted"
        @enrollment.enrollment_state.update(state: "invited")
        @enrollment.update(workflow_state: "invited")
        @enrollment.reload.accept!
        expect(@override.reload.workflow_state).to eq "active"
        expect(@student_override.reload.workflow_state).to eq "active"
      end
    end
  end

  describe "#student_with_conditions?" do
    it "returns false if the enrollment is neither a student enrollment nor a fake student enrollment" do
      allow(@enrollment).to receive_messages(student?: false, fake_student?: false)
      expect(@enrollment.student_with_conditions?(include_future: true, include_fake_student: true)).to be(false)
    end

    context "the enrollment is a student enrollment" do
      before do
        allow(@enrollment).to receive_messages(student?: true, fake_student?: false)
      end

      it "returns true if include_future is true" do
        expect(@enrollment.student_with_conditions?(include_future: true, include_fake_student: false)).to be(true)
      end

      it "returns true if include_future is false and the enrollment is active" do
        allow(@enrollment).to receive(:participating?).and_return(true)
        expect(@enrollment.student_with_conditions?(include_future: false, include_fake_student: false)).to be(true)
      end

      it "returns false if include_future is false and the enrollment is inactive" do
        allow(@enrollment).to receive(:participating?).and_return(false)
        expect(@enrollment.student_with_conditions?(include_future: false, include_fake_student: false)).to be(false)
      end
    end

    context "the enrollment is a fake student enrollment" do
      before do
        allow(@enrollment).to receive_messages(student?: false, fake_student?: true)
      end

      it "returns false if include_fake_student is false" do
        expect(@enrollment.student_with_conditions?(include_future: true, include_fake_student: false)).to be(false)
      end

      context "include_fake_student is passed in as true" do
        it "returns true if include_future is true" do
          expect(@enrollment.student_with_conditions?(include_future: true, include_fake_student: true)).to be(true)
        end

        it "returns true if include_future is false and the enrollment is active" do
          allow(@enrollment).to receive(:participating?).and_return(true)
          expect(@enrollment.student_with_conditions?(include_future: false, include_fake_student: true)).to be(true)
        end

        it "returns false if include_future is false and the enrollment is inactive" do
          allow(@enrollment).to receive(:participating?).and_return(false)
          expect(@enrollment.student_with_conditions?(include_future: false, include_fake_student: true)).to be(false)
        end
      end
    end
  end

  describe "readable_state_based_on_date" do
    before :once do
      course_factory(active_all: true)
      @enrollment = @course.enroll_student(user_factory)
      @enrollment.accept!
    end

    it "returns pending for future enrollments (even if view restricted)" do
      @course.start_at = 1.month.from_now
      @course.restrict_student_future_view = true
      @course.restrict_enrollments_to_course_dates = true
      @course.save!

      @enrollment.reload
      expect(@enrollment.state_based_on_date).to eq :inactive
      expect(@enrollment.readable_state_based_on_date).to eq :pending

      @course.restrict_student_future_view = false
      @course.save!

      @enrollment = Enrollment.find(@enrollment.id)
      expect(@enrollment.state_based_on_date).to eq :accepted
      expect(@enrollment.readable_state_based_on_date).to eq :pending
    end

    it "returns completed for completed enrollments (even if view restricted)" do
      @course.start_at = 2.months.ago
      @course.conclude_at = 1.month.ago
      @course.restrict_student_past_view = true
      @course.restrict_enrollments_to_course_dates = true
      @course.save!

      @enrollment.reload
      expect(@enrollment.state_based_on_date).to eq :inactive
      expect(@enrollment.readable_state_based_on_date).to eq :completed

      @course.complete!

      @enrollment = Enrollment.find(@enrollment.id)
      expect(@enrollment.state_based_on_date).to eq :inactive
      expect(@enrollment.readable_state_based_on_date).to eq :completed

      @course.restrict_student_past_view = false
      @course.save!

      @enrollment = Enrollment.find(@enrollment.id)
      expect(@enrollment.state_based_on_date).to eq :completed
      expect(@enrollment.readable_state_based_on_date).to eq :completed
    end
  end

  describe "update user account associations if necessary" do
    it "creates a user_account_association when restoring a deleted enrollment" do
      sub_account = Account.default.sub_accounts.create!
      course = Course.create!(account: sub_account)
      @enrollment = course.enroll_student(user_factory)
      expect(@user.user_account_associations.where(account: sub_account).exists?).to be true

      @enrollment.destroy
      expect(@user.user_account_associations.where(account: sub_account).exists?).to be false

      @enrollment.restore
      expect(@user.user_account_associations.where(account: sub_account).exists?).to be true
    end
  end

  it "orders by state based on date correctly" do
    u = user_factory(active_all: true)
    c1 = course_factory(active_all: true)
    c1.start_at = 1.day.from_now
    c1.conclude_at = 2.days.from_now
    c1.restrict_enrollments_to_course_dates = true
    c1.restrict_student_future_view = true
    c1.save!
    restricted_enroll = c1.enroll_student(u)

    c2 = course_factory(active_all: true)
    c2.start_at = 1.day.from_now
    c2.conclude_at = 2.days.from_now
    c2.restrict_enrollments_to_course_dates = true
    c2.save!
    future_enroll = c2.enroll_student(u)

    c3 = course_factory(active_all: true)
    active_enroll = c3.enroll_student(u)

    [restricted_enroll, future_enroll, active_enroll].each do |e|
      e.workflow_state = "active"
      e.save!
    end

    enrolls = Enrollment.where(id: [restricted_enroll, future_enroll, active_enroll])
                        .joins(:enrollment_state).order(Enrollment.state_by_date_rank_sql).to_a
    expect(enrolls).to eq [active_enroll, future_enroll, restricted_enroll]
  end

  describe "restoring completed enrollments" do
    before(:once) do
      @student = @user
      @teacher = User.create!
      @course.enroll_teacher(@teacher, enrollment_state: :active)
      @enrollment = @course.enroll_student(@student, enrollment_state: :active)
      @assignment = @course.assignments.create!(submission_types: ["online_text_entry"], points_possible: 10)
    end

    it "restores deleted submissions for assignments that are still active" do
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        Submission.active.where(assignment_id: @assignment, user_id: @student.id).count
      }.from(0).to(1)
    end

    it "does not restore deleted submissions for assignments that are deleted" do
      @enrollment.destroy
      @assignment.destroy

      expect { @enrollment.update!(workflow_state: :completed) }.not_to(change do
        Submission.active.where(assignment_id: @assignment, user_id: @student.id).count
      end)
    end

    it "infers the appropriate workflow state for unsubmitted submissions when restoring them" do
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @assignment.all_submissions.find_by(user_id: @enrollment.user_id).workflow_state
      }.from("deleted").to("unsubmitted")
    end

    it "infers the appropriate workflow state for submitted, not-yet-graded submissions when restoring them" do
      @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a submission!")
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @assignment.all_submissions.find_by(user_id: @enrollment.user_id).workflow_state
      }.from("deleted").to("submitted")
    end

    it "infers the appropriate workflow state for graded submissions when restoring them" do
      @assignment.grade_student(@user, grade: 8, grader: @teacher)
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @assignment.all_submissions.find_by(user_id: @enrollment.user_id).workflow_state
      }.from("deleted").to("graded")
    end

    it "infers the appropriate workflow state for excused submissions when restoring them" do
      @assignment.grade_student(@user, excused: true, grader: @teacher)
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @assignment.all_submissions.find_by(user_id: @enrollment.user_id).workflow_state
      }.from("deleted").to("graded")
    end

    it "infers the appropriate workflow state for pending review submissions when restoring them" do
      quiz_with_graded_submission(
        [{ question_data: { :name => "Q1", :points_possible => 1, "question_type" => "essay_question" } }],
        user: @student,
        course: @course
      )
      submission = @quiz.assignment.all_submissions.find_by(user_id: @enrollment.user_id)
      submission.update!(score: nil)
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        submission.reload.workflow_state
      }.from("deleted").to("pending_review")
    end

    it "restores deleted course scores" do
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @enrollment.scores.where(course_score: true).count
      }.from(0).to(1)
    end

    it "restores scores for assignment groups that are still active" do
      @enrollment.destroy
      assignment_group = @course.assignment_groups.active.first
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @enrollment.scores.where(assignment_group_id: assignment_group).count
      }.from(0).to(1)
    end

    it "does not restore scores for assignment groups that are deleted" do
      @enrollment.destroy
      assignment_group = @course.assignment_groups.active.first
      assignment_group.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.not_to(change do
        @enrollment.scores.where(assignment_group_id: assignment_group).count
      end)
    end

    it "restores scores for grading periods that are still active" do
      grading_period_group = @course.root_account.grading_period_groups.create!
      grading_period_group.enrollment_terms << @course.enrollment_term
      grading_period = grading_period_group.grading_periods.create!(
        title: "Grading Period",
        start_date: 2.months.ago,
        end_date: 1.month.ago
      )
      @enrollment.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.to change {
        @enrollment.scores.where(grading_period_id: grading_period).count
      }.from(0).to(1)
    end

    it "does not restore scores for grading periods that are deleted" do
      grading_period_group = @course.root_account.grading_period_groups.create!
      grading_period_group.enrollment_terms << @course.enrollment_term
      grading_period = grading_period_group.grading_periods.create!(
        title: "Grading Period",
        start_date: 2.months.ago,
        end_date: 1.month.ago
      )
      @enrollment.destroy
      grading_period.destroy
      expect { @enrollment.update!(workflow_state: :completed) }.not_to(change do
        @enrollment.scores.where(grading_period_id: grading_period).count
      end)
    end

    it "does not restore scores for grading periods that are not associated with the course" do
      grading_period_group = @course.root_account.grading_period_groups.create!
      grading_period_group.enrollment_terms << @course.enrollment_term
      grading_period = grading_period_group.grading_periods.create!(
        title: "Grading Period",
        start_date: 2.months.ago,
        end_date: 1.month.ago
      )
      @enrollment.destroy
      @course.enrollment_term.update!(grading_period_group_id: nil)
      expect { @enrollment.update!(workflow_state: :completed) }.not_to(change do
        @enrollment.scores.where(grading_period_id: grading_period).count
      end)
    end
  end

  describe "#sync_microsoft_group" do
    let(:course) { course_factory }
    let(:enrollment_type) { "StudentEnrollment" }

    before do
      MicrosoftSync::Group.create!(course:)
    end

    # enroll user without running callbacks like update_user_account_associations,
    # so that the only jobs getting enqueued are the MSFT sync group type
    def enroll_user
      course.enroll_user(user_factory, enrollment_type, skip_touch_user: true)
    end

    context "when feature flag is off" do
      before do
        course.root_account.disable_feature!(:microsoft_group_enrollments_syncing)
      end

      it "does not enqueue a job" do
        expect { enroll_user }.to not_change { Delayed::Job.where(tag: "MicrosoftSync::StateMachineJob#run_later").count }
      end
    end

    context "when feature flag is on" do
      before do
        course.root_account.enable_feature!(:microsoft_group_enrollments_syncing)
      end

      context "when account has turned sync off" do
        before do
          course.root_account.settings[:microsoft_sync_enabled] = false
          course.root_account.save!
        end

        it "does not enqueue a job" do
          expect { enroll_user }.to not_change { Delayed::Job.where(tag: "MicrosoftSync::StateMachineJob#run_later").count }
        end
      end

      context "when account has turned sync on" do
        before do
          course.root_account.settings[:microsoft_sync_enabled] = true
          course.root_account.save!
        end

        it "enqueues a job" do
          expect { enroll_user }.to change { Delayed::Job.where(tag: "MicrosoftSync::StateMachineJob#run_later").count }.by 1
        end

        it "calls MicrosoftSync::Group#enqueue_future_partial_sync" do
          expect_any_instance_of(MicrosoftSync::Group).to receive(:enqueue_future_partial_sync)
          enroll_user
        end

        context "when the enrollment is a StudentViewEnrollment" do
          let(:enrollment_type) { "StudentViewEnrollment" }

          it "does not enqueue a job" do
            expect { enroll_user }.to not_change { Delayed::Job.where(tag: "MicrosoftSync::StateMachineJob#run_later").count }
          end
        end
      end
    end
  end

  describe ".all_student" do
    before(:once) do
      @student = @user
      @enrollment = @course.enroll_student(@student, enrollment_state: :active)
    end

    context "course workflow_state" do
      it "available" do
        @course.update_attribute(:workflow_state, "available")
        expect(Enrollment.all_student).to include(@enrollment)
      end

      it "completed" do
        @course.update_attribute(:workflow_state, "completed")
        expect(Enrollment.all_student).to include(@enrollment)
      end

      it "created" do
        @course.update_attribute(:workflow_state, "created")
        expect(Enrollment.all_student).to include(@enrollment)
      end

      it "claimed" do
        @course.update_attribute(:workflow_state, "claimed")
        expect(Enrollment.all_student).to include(@enrollment)
      end

      it "deleted" do
        @course.update_attribute(:workflow_state, "deleted")
        expect(Enrollment.all_student).not_to include(@enrollment)
      end
    end
  end

  describe "#delete_student_allocation_rules" do
    context "when the student enrollment is concluded" do
      before :once do
        @enrollment1 = course_with_student(active_all: 1)
        @course = @enrollment1.course
        @student1 = @enrollment1.user
        enrollment2 = course_with_student(active_all: 1, course: @course)
        @student2 = enrollment2.user
        @assignment = assignment_model(course: @course)
      end

      it "deletes allocation rules in the course associated to the concluded user" do
        assessor_rule = AllocationRule.create!(
          assessor_id: @student1.id,
          assessee_id: @student2.id,
          assignment: @assignment,
          course: @course
        )

        assessee_rule = AllocationRule.create!(
          assessor_id: @student2.id,
          assessee_id: @student1.id,
          assignment: @assignment,
          course: @course
        )

        expect { @enrollment1.conclude }
          .to change { AllocationRule.where(id: [assessor_rule.id, assessee_rule.id]).pluck(:workflow_state).uniq }
          .from(["active"]).to(["deleted"])
      end

      it "doesn't delete allocation rules associated to the concluded user from another course" do
        other_course = course_factory(active_all: true)
        other_course.enroll_student(@student1, enrollment_state: "active")
        other_course.enroll_student(@student2, enrollment_state: "active")
        other_assignment = assignment_model(course: other_course)
        other_course_rule = AllocationRule.create!(
          assessor_id: @student1.id,
          assessee_id: @student2.id,
          assignment: other_assignment,
          course: other_course
        )

        @enrollment1.conclude
        expect(other_course_rule.reload.workflow_state).to eq "active"
      end
    end

    context "when the student enrollment is deactivated" do
      before :once do
        @enrollment1 = course_with_student(active_all: 1)
        @course = @enrollment1.course
        @student1 = @enrollment1.user
        enrollment2 = course_with_student(active_all: 1, course: @course)
        @student2 = enrollment2.user
        @assignment = assignment_model(course: @course)
      end

      it "deletes allocation rules in the course associated to the deactivate user" do
        assessor_rule = AllocationRule.create!(
          assessor_id: @student1.id,
          assessee_id: @student2.id,
          assignment: @assignment,
          course: @course
        )

        assessee_rule = AllocationRule.create!(
          assessor_id: @student2.id,
          assessee_id: @student1.id,
          assignment: @assignment,
          course: @course
        )

        expect { @enrollment1.deactivate }
          .to change { AllocationRule.where(id: [assessor_rule.id, assessee_rule.id]).pluck(:workflow_state).uniq }
          .from(["active"]).to(["deleted"])
      end

      it "doesn't delete allocation rules associated to the deactivate user from another course" do
        other_course = course_factory(active_all: true)
        other_course.enroll_student(@student1, enrollment_state: "active")
        other_course.enroll_student(@student2, enrollment_state: "active")
        other_assignment = assignment_model(course: other_course, peer_reviews: true)
        other_course_rule = AllocationRule.create!(
          assessor_id: @student1.id,
          assessee_id: @student2.id,
          assignment: other_assignment,
          course: other_course
        )

        @enrollment1.deactivate
        expect(other_course_rule.reload.workflow_state).to eq "active"
      end
    end
  end
end
