# frozen_string_literal: true

#
# Copyright (C) 2014 - 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/>.
#

require_relative "../spec_helper"

describe GradingPeriod do
  subject(:grading_period) { grading_period_group.grading_periods.create!(params) }

  let(:group_helper) { Factories::GradingPeriodGroupHelper.new }
  let(:account) { Account.create! }
  let(:course) { account.courses.create! }
  let(:grading_period_group) do
    group = account.grading_period_groups.create!(title: "A Group")
    term = course.enrollment_term
    group.enrollment_terms << term
    group
  end
  let(:now) { Time.zone.now }
  let(:params) do
    {
      title: "A Grading Period",
      start_date: now,
      end_date: 1.day.from_now(now),
      close_date: 5.days.from_now(now)
    }
  end

  it { is_expected.to be_valid }

  it "requires a start_date" do
    grading_period = GradingPeriod.new(params.except(:start_date))
    expect(grading_period).not_to be_valid
  end

  it "requires an end_date" do
    grading_period = GradingPeriod.new(params.except(:end_date))
    expect(grading_period).not_to be_valid
  end

  it "requires start_date to be before end_date" do
    subject.assign_attributes(start_date: now.change(sec: 59), end_date: now.change(sec: 0))
    expect(subject).not_to be_valid
  end

  it "requires a title" do
    grading_period = GradingPeriod.new(params.except(:title))
    expect(grading_period).not_to be_valid
  end

  describe ".in_closed_grading_period?" do
    let(:in_closed_grading_period) { closed_period.start_date + 1.day }
    let(:in_not_closed_grading_period) { not_closed_period.start_date + 1.day }
    let(:outside_of_any_grading_period) { not_closed_period.end_date + 1.week }
    let!(:closed_period) do
      grading_period_group.grading_periods.create!(
        title: "closed",
        start_date: 2.weeks.ago(now),
        end_date: 1.week.ago(now),
        close_date: 3.days.ago(now)
      )
    end
    let!(:another_closed_period) do
      grading_period_group.grading_periods.create!(
        title: "another closed period",
        start_date: 4.weeks.ago(now),
        end_date: 3.weeks.ago(now),
        close_date: 2.weeks.ago(now)
      )
    end
    let!(:not_closed_period) do
      grading_period_group.grading_periods.create!(
        title: "a period",
        start_date: 3.days.ago(now),
        end_date: 3.days.from_now(now),
        close_date: 5.days.from_now(now)
      )
    end

    it "returns true if the submission is due in a closed grading period" do
      result = GradingPeriod.date_in_closed_grading_period?(
        course:,
        date: in_closed_grading_period
      )
      expect(result).to be true
    end

    it "returns false if the submission is due in a not closed grading period" do
      result = GradingPeriod.date_in_closed_grading_period?(
        course:,
        date: in_not_closed_grading_period
      )
      expect(result).to be false
    end

    it "returns false if the submission is due outside of any grading period" do
      result = GradingPeriod.date_in_closed_grading_period?(
        course:,
        date: outside_of_any_grading_period
      )
      expect(result).to be false
    end

    it "returns true if the due date is null and the last grading period is closed" do
      not_closed_period.destroy
      result = GradingPeriod.date_in_closed_grading_period?(
        course:,
        date: nil
      )
      expect(result).to be true
    end

    it "returns false if the due date is null and the last grading period is not closed" do
      result = GradingPeriod.date_in_closed_grading_period?(
        course:,
        date: nil
      )
      expect(result).to be false
    end
  end

  describe ".current" do
    subject { grading_period_group.grading_periods.current }

    context "no periods" do
      it "finds no current periods" do
        expect(subject).to be_empty
      end
    end

    context "one current period" do
      let!(:period) do
        grading_period_group.grading_periods.create!(
          title: "a period",
          start_date: 1.day.ago(now),
          end_date: 1.day.from_now(now),
          close_date: 2.days.from_now(now)
        )
      end

      it "finds one current period" do
        expect(subject).to eq [period]
      end

      it "includes the period if the current time is the same as the grading period end date" do
        Timecop.freeze(now) do
          period.update!(end_date: now)
          expect(subject).to be_present
        end
      end

      it "includes the period if the current time is past the end date but the minute has not passed" do
        Timecop.freeze(now.change(sec: 59)) do
          period.update!(end_date: now.change(sec: 0))
          expect(subject).to be_present
        end
      end

      it "does not include the period if the current time is past the end date and the minute has passed" do
        Timecop.freeze(1.minute.from_now(now)) do
          period.update!(end_date: now)
          expect(subject).to be_empty
        end
      end

      it "does not include the period if start_date equals Time.now" do
        Timecop.freeze(now) do
          period.update!(start_date: now)
          expect(subject).to be_empty
        end
      end

      it "does not include period if the current time is past the start date and the minute has not passed" do
        Timecop.freeze(now.change(sec: 59)) do
          period.update!(start_date: now.change(sec: 0))
          expect(subject).to be_empty
        end
      end

      it "includes the period if the current time is past the start date and the minute has passed" do
        Timecop.freeze(1.minute.from_now(now)) do
          period.update!(start_date: now)
          expect(subject).to be_present
        end
      end
    end
  end

  describe "#as_json_with_user_permissions" do
    it "includes the close_date in the returned object" do
      json = grading_period.as_json_with_user_permissions(User.new)
      expect(json).to have_key("close_date")
    end

    it "includes the weight in the returned object" do
      json = grading_period.as_json_with_user_permissions(User.new)
      expect(json).to have_key("weight")
    end
  end

  describe "close_date" do
    context "grading period group belonging to an account" do
      it "allows setting a close_date that is after the end_date" do
        grading_period = grading_period_group.grading_periods.create!(params)
        expect(grading_period.close_date).not_to eq(grading_period.end_date)
      end

      it "sets the close_date to the end_date if no close_date is provided" do
        grading_period = grading_period_group.grading_periods.create!(params.except(:close_date))
        expect(grading_period.close_date).to eq(grading_period.end_date)
      end

      it "is invalid if the close date is before the end date" do
        period_params = params.merge(close_date: 1.day.ago(params[:end_date]))
        grading_period = grading_period_group.grading_periods.build(period_params)
        expect(grading_period).not_to be_valid
      end

      it "considers the grading period valid if the close date is equal to the end date" do
        period_params = params.merge(close_date: params.fetch(:end_date))
        grading_period = grading_period_group.grading_periods.build(period_params)
        expect(grading_period).to be_valid
      end
    end

    context "grading period group belonging to a course" do
      let(:course_grading_period_group) { group_helper.legacy_create_for_course(course) }

      it "does not allow setting a close_date that is different from the end_date" do
        grading_period = course_grading_period_group.grading_periods.create!(params)
        expect(grading_period.close_date).to eq(params[:end_date])
      end

      it "sets the close_date to the end_date if no close_date is provided" do
        grading_period = course_grading_period_group.grading_periods.create!(params.except(:close_date))
        expect(grading_period.close_date).to eq(grading_period.end_date)
      end

      it "sets the close_date to the end_date when the grading period is updated" do
        grading_period = course_grading_period_group.grading_periods.create!(params.except(:close_date))
        new_end_date = 5.weeks.from_now(now)
        grading_period.end_date = new_end_date
        grading_period.save!
        expect(grading_period.close_date).to eq(new_end_date)
      end
    end
  end

  describe "scope: closed" do
    around { |example| Timecop.freeze(now, &example) }

    it "includes grading period if the current date is past the close date" do
      period = grading_period_group.grading_periods.create(
        title: "Closed Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 3.days.ago(now)
      )
      expect(GradingPeriod.closed).to include period
    end

    it "excludes grading period if the current date is before the close date" do
      period = grading_period_group.grading_periods.create(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 2.days.from_now(now)
      )
      expect(GradingPeriod.closed).not_to include period
    end

    it "excludes grading period if the current date matches the close date" do
      period = grading_period_group.grading_periods.create(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: now
      )
      expect(GradingPeriod.closed).not_to include period
    end
  end

  describe "scope: open" do
    around { |example| Timecop.freeze(now, &example) }

    it "excludes grading period if the current date is past the close date" do
      period = grading_period_group.grading_periods.create(
        title: "Closed Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 3.days.ago(now)
      )
      expect(GradingPeriod.open).not_to include period
    end

    it "includes grading period if the current date is before the close date" do
      period = grading_period_group.grading_periods.create(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 2.days.from_now(now)
      )
      expect(GradingPeriod.open).to include period
    end

    it "includes grading period if the current date matches the close date" do
      period = grading_period_group.grading_periods.create(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: now
      )
      expect(GradingPeriod.open).to include period
    end
  end

  describe "#closed?" do
    around { |example| Timecop.freeze(now, &example) }

    it "returns true if the current date is past the close date" do
      period = grading_period_group.grading_periods.build(
        title: "Closed Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 3.days.ago(now)
      )
      expect(period).to be_closed
    end

    it "returns false if the current date is before the close date" do
      period = grading_period_group.grading_periods.build(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: 2.days.from_now(now)
      )
      expect(period).not_to be_closed
    end

    it "returns false if the current date matches the close date" do
      period = grading_period_group.grading_periods.build(
        title: "A Period",
        start_date: 10.days.ago(now),
        end_date: 5.days.ago(now),
        close_date: now
      )
      expect(period).not_to be_closed
    end
  end

  describe "#destroy" do
    it_behaves_like "soft deletion" do
      subject { grading_period_group.grading_periods }

      let(:creation_arguments) { params }
    end

    it "destroys associated scores" do
      course = Course.create!
      enrollment = student_in_course(course:)
      score = enrollment.scores.create!(grading_period:)
      grading_period.destroy
      expect(score.reload).to be_deleted
    end

    it "recalculates course scores if the grading period group is weighted" do
      course = Course.create!
      grading_period_group.enrollment_terms << course.enrollment_term
      enrollment = student_in_course(course:)
      enrollment.scores.create!(grading_period:)
      grading_period_group.update_column(:weighted, true)
      expect(GradeCalculator).to receive(:recompute_final_score)
      grading_period.destroy
    end

    it "runs SubmissionLifecycleManager for courses from the same enrollment term when the grading period is deleted" do
      course2 = account.courses.create!
      course2.enrollment_term = account.enrollment_terms.create!
      course2.save!
      student_in_course(course: course2)
      a = course2.assignments.create!
      a.submissions.find_by(user_id: @student).update(grading_period_id: grading_period.id)
      expect(SubmissionLifecycleManager).to receive(:recompute_course).with(course, any_args)
      expect(SubmissionLifecycleManager).not_to receive(:recompute_course).with(course2, any_args)
      grading_period.destroy
    end

    it "runs SubmissionLifecycleManager for courses from the same enrollment term when the grading period set is deleted" do
      course2 = account.courses.create!
      course2.enrollment_term = account.enrollment_terms.create!
      course2.save!
      student_in_course(course: course2)
      a = course2.assignments.create!
      a.submissions.find_by(user_id: @student).update(grading_period_id: grading_period.id)
      expect(SubmissionLifecycleManager).to receive(:recompute_course).with(course, any_args)
      expect(SubmissionLifecycleManager).not_to receive(:recompute_course).with(course2, any_args)
      grading_period_group.destroy
    end

    it "does not destroy the set when the last grading period is destroyed (account grading periods)" do
      grading_period.save!
      grading_period.destroy
      expect(grading_period_group).not_to be_deleted
    end

    it "updates the grading_period_id to nil on submissions that were in the deleted grading period" do
      student = User.create!
      course.enroll_student(student, enrollment_state: :active)
      grading_period.save!
      assignment = course.assignments.create!(due_at: 2.hours.from_now(grading_period.start_date))
      submission = assignment.submissions.find_by(user_id: student)
      expect { grading_period.destroy }.to change {
        submission.reload.grading_period_id
      }.from(grading_period.id).to(nil)
    end

    it 'places submissions without due dates in the new "last" period if the "last" period was deleted' do
      student = User.create!
      course.enroll_student(student, enrollment_state: :active)
      grading_period.save!
      other_period = grading_period_group.grading_periods.create!(
        title: "I will be the last period when the other one is deleted",
        start_date: 2.days.ago(grading_period.start_date),
        end_date: 1.day.ago(grading_period.start_date)
      )
      assignment = course.assignments.create!
      submission = assignment.submissions.find_by(user_id: student)
      expect { grading_period.destroy }.to change {
        submission.reload.grading_period_id
      }.from(grading_period.id).to(other_period.id)
    end

    context "course grading periods (legacy support)" do
      before(:once) do
        @grading_period_set = course.grading_period_groups.create!
        @period = @grading_period_set.grading_periods.create!(
          title: "Grading Period",
          start_date: 5.days.ago,
          end_date: 2.days.ago
        )
      end

      it "destroys the set when the last grading period is destroyed" do
        @period.destroy
        expect(@grading_period_set).to be_deleted
      end

      it "does not destroy the set when a grading period is destroyed and it is not the last period" do
        @grading_period_set.grading_periods.create!(
          title: "A New Grading Period",
          start_date: 2.days.from_now,
          end_date: 5.days.from_now
        )
        @period.destroy
        expect(@grading_period_set).not_to be_deleted
      end
    end
  end

  describe ".for" do
    let(:group_helper)  { Factories::GradingPeriodGroupHelper.new }
    let(:period_helper) { Factories::GradingPeriodHelper.new }
    let(:term) do
      enrollment_term = @root_account.enrollment_terms.create!
      @course.enrollment_term = enrollment_term
      @course.save!
      enrollment_term
    end

    context "when context is a course" do
      before(:once) do
        @root_account = account_model
        @sub_account = @root_account.sub_accounts.create!
        @course = Course.create!(account: @sub_account)
      end

      it "finds all grading periods on a course" do
        group_1 = group_helper.legacy_create_for_course(@course)
        group_2 = group_helper.legacy_create_for_course(@course)
        period_1 = period_helper.create_with_weeks_for_group(group_1, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group_2, 3, 1)
        expect(GradingPeriod.for(@course)).to match_array([period_1, period_2])
      end

      it "ignores grading periods associated with unrelated courses" do
        other_course = Course.create!(account: @sub_account)
        group_1 = group_helper.legacy_create_for_course(@course)
        group_2 = group_helper.legacy_create_for_course(other_course)
        period_1 = period_helper.create_with_weeks_for_group(group_1, 5, 3)
        period_helper.create_with_weeks_for_group(group_2, 3, 1)
        expect(GradingPeriod.for(@course)).to match_array([period_1])
      end

      it "returns grading periods for the course enrollment term when the course has no grading period groups" do
        group = group_helper.create_for_account(@root_account)
        term.update_attribute(:grading_period_group_id, group)
        period = period_helper.create_with_weeks_for_group(group, 5, 3)
        expect(GradingPeriod.for(@course)).to match_array([period])
      end

      it "returns grading periods for the course enrollment term when the course has no grading periods" do
        group_helper.legacy_create_for_course(@course)
        group_2 = group_helper.create_for_account(@root_account)
        term.update_attribute(:grading_period_group_id, group_2)
        period = period_helper.create_with_weeks_for_group(group_2, 5, 3)
        expect(GradingPeriod.for(@course)).to match_array([period])
      end

      it "returns an empty array when the course has no grading periods groups" do
        expect(GradingPeriod.for(@course)).to be_empty
      end

      it "returns an empty array when the course has no grading periods" do
        group_helper.legacy_create_for_course(@course)
        expect(GradingPeriod.for(@course)).to be_empty
      end

      it "includes only 'active' grading periods from the course grading period group" do
        group_1 = group_helper.legacy_create_for_course(@course)
        group_2 = group_helper.legacy_create_for_course(@course)
        period_1 = period_helper.create_with_weeks_for_group(group_1, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group_2, 3, 1)
        period_2.workflow_state = :deleted
        period_2.save
        expect(GradingPeriod.for(@course)).to match_array([period_1])
      end

      it "includes only 'active' grading periods from the course enrollment term group" do
        group = group_helper.create_for_account(@root_account)
        term.update_attribute(:grading_period_group_id, group)
        period_1 = period_helper.create_with_weeks_for_group(group, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group, 3, 1)
        period_2.workflow_state = :deleted
        period_2.save
        expect(GradingPeriod.for(@course)).to match_array([period_1])
      end

      it "does not include grading periods from the course enrollment term group if inherit is false" do
        group = group_helper.create_for_account(@root_account)
        term.update_attribute(:grading_period_group_id, group)
        period_helper.create_with_weeks_for_group(group, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group, 3, 1)
        period_2.workflow_state = :deleted
        period_2.save
        expect(GradingPeriod.for(@course, inherit: false)).to be_empty
      end
    end

    context "when context is an account" do
      before(:once) do
        @root_account = account_model
        @sub_account = @root_account.sub_accounts.create!
        @course = Course.create!(account: @sub_account)
      end

      it "finds all grading periods on an account" do
        group_1 = group_helper.create_for_account(@root_account)
        group_2 = group_helper.create_for_account(@root_account)
        period_1 = period_helper.create_with_weeks_for_group(group_1, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group_2, 3, 1)
        expect(GradingPeriod.for(@root_account)).to match_array([period_1, period_2])
      end

      it "returns an empty array when the account has no grading period groups" do
        expect(GradingPeriod.for(@root_account)).to be_empty
      end

      it "returns an empty array when the account has no grading periods" do
        group_helper.create_for_account(@root_account)
        expect(GradingPeriod.for(@root_account)).to be_empty
      end

      it "does not return grading periods on the course directly" do
        group = group_helper.legacy_create_for_course(@course)
        period_helper.create_with_weeks_for_group(group, 5, 3)
        period_helper.create_with_weeks_for_group(group, 3, 1)
        expect(GradingPeriod.for(@root_account)).to be_empty
      end

      it "includes only 'active' grading periods from the account grading period group" do
        group_1 = group_helper.create_for_account(@root_account)
        group_2 = group_helper.create_for_account(@root_account)
        period_1 = period_helper.create_with_weeks_for_group(group_1, 5, 3)
        period_2 = period_helper.create_with_weeks_for_group(group_2, 3, 1)
        period_2.workflow_state = :deleted
        period_2.save
        expect(GradingPeriod.for(@root_account)).to match_array([period_1])
      end
    end
  end

  describe ".current_period_for" do
    let(:account) { Account.new }
    let(:not_current_grading_period) { double }
    let(:current_grading_period) { double }

    it "returns the current grading period given a context" do
      expect(GradingPeriod).to receive(:for).with(account).and_return([not_current_grading_period, current_grading_period])
      expect(not_current_grading_period).to receive(:current?).and_return(false)
      expect(current_grading_period).to receive(:current?).and_return(true)
      expect(GradingPeriod.current_period_for(account)).to eq(current_grading_period)
    end

    it "returns nil if grading periods exist for the given context, but none are current" do
      expect(GradingPeriod).to receive(:for).with(account).and_return([not_current_grading_period])
      expect(not_current_grading_period).to receive(:current?).and_return(false)
      expect(GradingPeriod.current_period_for(account)).to be_nil
    end

    it "returns nil if no grading periods exist for the given context" do
      expect(GradingPeriod).to receive(:for).with(account).and_return([])
      expect(GradingPeriod.current_period_for(account)).to be_nil
    end
  end

  describe "#assignments" do
    before(:once) do
      account = Account.create!
      @course = account.courses.create!
      student = User.create!
      @course.enroll_student(student, enrollment_state: :active)
      @grading_period_group = group_helper.legacy_create_for_course(@course)
      @first_grading_period = @grading_period_group.grading_periods.create!(
        title: "1st period",
        start_date: 2.months.from_now(now),
        end_date: 3.months.from_now(now)
      )
      @second_grading_period = @grading_period_group.grading_periods.create!(
        title: "2nd period",
        start_date: 3.months.from_now(now),
        end_date: 4.months.from_now(now)
      )
      @first_assignment = @course.assignments.create!(due_at: @first_grading_period.start_date + 1.minute)
      @second_assignment = @course.assignments.create!(due_at: @second_grading_period.start_date + 1.minute)
      @third_assignment = @course.assignments.create!(due_at: nil)
    end

    it "filters the first grading period" do
      assignments = @first_grading_period.assignments(@course, @course.assignments)
      expect(assignments).to eq [@first_assignment]
    end

    it "filters assignments without a due_at into the last grading period" do
      assignments = @second_grading_period.assignments(@course, @course.assignments)
      expect(assignments).to eq [@second_assignment, @third_assignment]
    end

    describe "when due at is near the edge of a period" do
      let!(:fourth_assignment) do
        @course.assignments.create!(
          due_at: third_grading_period.end_date - 0.995.seconds
        )
      end

      let!(:fifth_assignment) do
        @course.assignments.create!(
          due_at: fourth_grading_period.start_date - 0.005.seconds
        )
      end

      let(:third_grading_period) do
        @grading_period_group.grading_periods.create!(
          title: "3rd period",
          start_date: 5.months.from_now(now),
          end_date: 6.months.from_now(now)
        )
      end

      let(:fourth_grading_period) do
        @grading_period_group.grading_periods.create!(
          title: "4th period",
          start_date: 7.months.from_now(now),
          end_date: 8.months.from_now(now)
        )
      end

      it "includes assignments if they are on the future edge of end date" do
        assignments = third_grading_period.assignments(@course, @course.assignments)
        expect(assignments).to include fourth_assignment
      end

      it "does NOT include assignments if they are on the past edge of start date" do
        assignments = fourth_grading_period.assignments(@course, @course.assignments)
        expect(assignments).not_to include fifth_assignment
      end
    end
  end

  describe "#current?" do
    subject(:grading_period) { GradingPeriod.new }

    it "returns false for a grading period in the past" do
      grading_period.assign_attributes(
        start_date: 2.months.ago(now),
        end_date: 1.month.ago(now)
      )
      expect(grading_period).to_not be_current
    end

    it "returns true if the current time falls between the start date and end date (inclusive)",
       priority: "2" do
      grading_period.assign_attributes(
        start_date: 1.month.ago(now),
        end_date: 1.month.from_now(now)
      )
      expect(grading_period).to be_current
    end

    it "returns false for a grading period in the future" do
      grading_period.assign_attributes(
        start_date: 1.month.from_now(now),
        end_date: 2.months.from_now(now)
      )
      expect(grading_period).to_not be_current
    end
  end

  context "given an existing grading_period" do
    let(:course) { Course.create! }
    let(:grading_period_group) { group_helper.legacy_create_for_course(course) }

    describe "#overlapping?" do
      before(:once) do
        @existing_grading_period = grading_period_group.grading_periods.create!(
          title: "a title",
          start_date: now.change(sec: 0),
          end_date: 2.days.from_now(now).change(sec: 59)
        )
        @grading_period = grading_period_group.grading_periods.build
      end

      it "is overlapping if the start date and end date match an existing period" do
        @grading_period.start_date = @existing_grading_period.start_date
        @grading_period.end_date = @existing_grading_period.end_date
        expect(@grading_period).to be_overlapping
      end

      it "is not overlapping if the start date is the end date of an existing period" do
        @grading_period.start_date = @existing_grading_period.end_date
        @grading_period.end_date = 1.month.from_now(@existing_grading_period.end_date)
        expect(@grading_period).not_to be_overlapping
      end

      it "is not overlapping if the start date is before the end date of an existing period " \
         "but they are in the same minute" do
        @grading_period.start_date = @existing_grading_period.end_date.change(sec: 0)
        @grading_period.end_date = 1.month.from_now(@existing_grading_period.end_date)
        expect(@grading_period).not_to be_overlapping
      end

      it "is overlapping if the start date is before the end date of an existing period and " \
         "they are not in the same minute" do
        @grading_period.start_date = 1.minute.ago(@existing_grading_period.end_date)
        @grading_period.end_date = 1.month.from_now(@existing_grading_period.end_date)
        expect(@grading_period).to be_overlapping
      end

      it "is not overlapping if the end date is the start date of an existing period" do
        @grading_period.start_date = 1.month.from_now(@existing_grading_period.start_date)
        @grading_period.end_date = @existing_grading_period.start_date
        expect(@grading_period).not_to be_overlapping
      end

      it "is not overlapping if the end date is past the start date of an existing period, " \
         "but there are in the same minute" do
        @grading_period.start_date = 1.month.ago(@existing_grading_period.start_date)
        @grading_period.end_date = @existing_grading_period.start_date.change(sec: 59)
        expect(@grading_period).not_to be_overlapping
      end

      it "is overlapping if the end date is past the start date of an existing period and " \
         "they are not in the same minute" do
        @grading_period.start_date = 1.month.ago(@existing_grading_period.start_date)
        @grading_period.end_date = 1.minute.from_now(@existing_grading_period.start_date)
        expect(@grading_period).to be_overlapping
      end

      it "after a grading period is persisted it continues to not overlap" do
        expect(@existing_grading_period).not_to be_overlapping
      end
    end
  end

  describe "Soft deletion" do
    subject { grading_period_group.grading_periods }

    let(:creation_arguments) { [period_one, period_two] }
    let(:period_one) { { title: "an title", start_date: 1.week.ago(now), end_date: 2.weeks.from_now(now) } }
    let(:period_two) { { title: "an title", start_date: 2.weeks.from_now(now), end_date: 5.weeks.from_now(now) } }

    it_behaves_like "soft deletion"
  end

  describe ".in_date_range?" do
    subject(:period) do
      grading_period_group.grading_periods.build(
        title: "a period",
        start_date: 1.week.ago(now).change(sec: 0),
        end_date: 2.weeks.from_now(now).change(sec: 0)
      )
    end

    it "is in date range for a date that equals end_date" do
      expect(subject).to be_in_date_range(period.end_date)
    end

    it "is in date range for a date that is past the end date but the minute has not yet passed" do
      expect(subject).to be_in_date_range(period.end_date.change(sec: 59))
    end

    it "is not in date range for a date that is past the end date and the minute has passed" do
      expect(subject).not_to be_in_date_range(1.minute.from_now(period.end_date))
    end

    it "is not in date range for a date before the period" do
      expect(subject).not_to be_in_date_range(2.weeks.ago(now))
    end

    it "is not in date range for or a date after the period" do
      expect(subject).not_to be_in_date_range(3.weeks.from_now(now))
    end

    it "is not in date range for a date that equals start_date" do
      expect(subject).not_to be_in_date_range(period.start_date)
    end

    it "is not in date range for a date that is past the start_date but the minute has not yet passed" do
      expect(subject).not_to be_in_date_range(period.start_date.change(sec: 59))
    end

    it "is in date range for a date that is past the start_date and the minute has passed" do
      expect(subject).to be_in_date_range(1.minute.from_now(period.start_date))
    end
  end

  describe ".json_for" do
    context "when given a course" do
      it "returns a list sorted by date with is_last" do
        group = group_helper.legacy_create_for_course(course)
        group.grading_periods.create!(
          start_date: 1.week.ago(now),
          end_date: 2.weeks.from_now(now),
          title: "C"
        )
        group.grading_periods.create!(
          start_date: 4.weeks.ago(now),
          end_date: 3.weeks.ago(now),
          title: "A"
        )
        group.grading_periods.create!(
          start_date: 3.weeks.ago(now),
          end_date: 2.weeks.ago(now),
          title: "B"
        )
        json = GradingPeriod.json_for(course, nil)
        expect(json.pluck("title")).to eq %w[A B C]
        expect(json.pluck("is_last")).to eq [false, false, true]
      end
    end
  end

  describe "#account_group?" do
    context "given an account grading period group" do
      it { is_expected.to be_account_group }
    end

    context "given a course grading period group" do
      subject(:course_period) { grading_period_group.grading_periods.create!(params) }

      let(:grading_period_group) { group_helper.legacy_create_for_course(course) }

      it { is_expected.not_to be_account_group }
    end
  end

  describe "#course_group?" do
    context "given a course grading period group" do
      subject(:course_period) { grading_period_group.grading_periods.create!(params) }

      let(:grading_period_group) { group_helper.legacy_create_for_course(course) }

      it { is_expected.to be_course_group }
    end

    context "given an account grading period group" do
      it { is_expected.not_to be_course_group }
    end
  end

  describe "#weight" do
    it "can persist double precision values" do
      subject.update!(weight: 1.5)
      expect(subject.reload.weight).to be 1.5
    end
  end

  describe "root_account_id" do
    context "on create" do
      it "sets root_account_id to the grading period group's root_account_id if root_account_id is nil" do
        period = grading_period_group.grading_periods.create!(params)
        expect(period.root_account_id).to eq grading_period_group.root_account_id
      end

      it "does not modify root_account_id if it is already set" do
        second_account = account_model
        period = grading_period_group.grading_periods.create!(
          params.merge(root_account_id: second_account.id)
        )
        expect(period.root_account_id).to eq second_account.id
      end
    end

    context "on update" do
      it "sets root_account_id to the grading period group's root_account_id if root_account_id is nil" do
        grading_period.update_column(:root_account_id, nil)
        grading_period.update!(title: "A New Title")
        expect(grading_period.root_account_id).to eq grading_period_group.root_account_id
      end

      it "does not modify root_account_id if it is already set" do
        second_account = account_model
        period = grading_period_group.grading_periods.create!(
          params.merge(root_account_id: second_account.id)
        )

        period.update!(title: "A New Title")
        expect(period.root_account_id).to eq second_account.id
      end
    end
  end

  describe "grading period scores" do
    before do
      student_in_course(course:, active_all: true)
      teacher_in_course(course:, active_all: true)
      @assignment = course.assignments.create!(due_at: 10.days.from_now(now), points_possible: 10)
      @assignment.grade_student(@student, grade: 8, grader: @teacher)
    end

    it "creates scores for the grading period upon its creation" do
      expect { grading_period.save! }.to change { Score.count }.by(1)
    end

    it "updates grading period scores when the grading period end date is changed" do
      grading_period.save!
      expect do
        day_after_assignment_is_due = 1.day.from_now(@assignment.due_at)
        grading_period.update!(
          end_date: day_after_assignment_is_due,
          close_date: day_after_assignment_is_due
        )
      end.to change {
        Score.where(grading_period_id: grading_period).first.current_score
      }.from(nil).to(80.0)
    end

    it "updates grading period scores when the grading period start date is changed" do
      day_before_grading_period_starts = 1.day.ago(grading_period.start_date)
      @assignment.update!(due_at: day_before_grading_period_starts)
      grading_period.save!
      expect { grading_period.update!(start_date: 1.day.ago(@assignment.due_at)) }.to change {
        Score.where(grading_period_id: grading_period).first.current_score
      }.from(nil).to(80.0)
    end

    it 'updates grading period ids on submissions without due dates if the "last" period changes' do
      student = User.create!
      course.enroll_student(student, enrollment_state: :active)
      grading_period.save!
      other_period = grading_period_group.grading_periods.create!(
        title: "I will be the last period when the other one changes dates",
        start_date: 2.days.ago(grading_period.start_date),
        end_date: 1.day.ago(grading_period.start_date)
      )
      assignment = course.assignments.create!
      submission = assignment.submissions.find_by(user_id: student)
      expect do
        grading_period.update!(
          start_date: 2.days.ago(other_period.start_date),
          end_date: 1.day.ago(other_period.start_date)
        )
      end.to change {
        submission.reload.grading_period_id
      }.from(grading_period.id).to(other_period.id)
    end

    it "updates course score when the grading period weight is changed" do
      grading_period.save!
      # Changing the assinment due date, so there are scores to update in the course
      @assignment.update!(due_at: 2.hours.from_now(now))
      grading_period_group.update!(weighted: true)
      expect { grading_period.update!(weight: 50) }.to change {
        Score.where(grading_period_id: nil).first.updated_at
      }
    end

    it "does not update grading period score when the grading period weight is changed" do
      grading_period.save!
      grading_period_group.update!(weighted: true)
      expect { grading_period.update!(weight: 20) }.not_to change {
        Score.where(grading_period_id: grading_period).first.updated_at
      }
    end

    it "does not update course score when weight is changed but weighted grading periods are disabled" do
      grading_period.save!
      grading_period_group.update!(weighted: false)
      expect { grading_period.update!(weight: 50) }.not_to change {
        Score.where(grading_period_id: nil).first.updated_at
      }
      expect { grading_period.update!(weight: 20) }.not_to change {
        Score.where(grading_period_id: grading_period).first.updated_at
      }
    end

    it "does not update grading period score when weight is changed but weighted grading periods are disabled" do
      grading_period.save!
      grading_period_group.update!(weighted: false)
      expect { grading_period.update!(weight: 20) }.not_to change {
        Score.where(grading_period_id: grading_period).first.updated_at
      }
    end
  end
end
