# 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/>.
#

require_relative "../lib/validates_as_url"

describe Submission do
  subject(:submission) { Submission.new }

  before(:once) do
    course_with_teacher(active_all: true)
    course_with_student(active_all: true, course: @course)
    @context = @course
    @assignment = @context.assignments.create!(
      title: "some assignment",
      workflow_state: "published"
    )
    @valid_attributes = {
      assignment: @assignment,
      user: @user,
      grade: "1.5",
      grader: @teacher,
      url: "www.instructure.com",
      workflow_state: "submitted"
    }
  end

  describe "inferred values" do
    subject do
      submission.infer_values
      submission.state
    end

    let(:student) { @student }
    let(:assignment) { @assignment }

    describe "workflow_state" do
      context "when current state is unsubmitted and submitted_at is present" do
        before do
          submission.workflow_state = "unsubmitted"
          submission.submission_type = "online_text_entry"
          submission.submitted_at = Time.zone.now
        end

        it { is_expected.to be :submitted }
      end

      context "when current state is submitted and has_submission is false" do
        before do
          submission.workflow_state = "submitted"
        end

        it { is_expected.to be :unsubmitted }
      end

      context "when grade and score are present and grade matches current submission" do
        before do
          submission.submission_type = "online_text_entry"
          submission.submitted_at = Time.zone.now
          submission.grade = "5"
          submission.score = 5

          allow(submission).to receive(:grade_matches_current_submission).and_return(true)
        end

        it { is_expected.to be :graded }
      end

      context "when submission_type is online_quiz and latest submission is pending review" do
        before do
          submission.workflow_state = "pending_review"
          submission.submission_type = "online_quiz"

          allow(submission).to receive(:quiz_submission).and_return(double("QuizSubmission", "pending_review?" => true))
        end

        it { is_expected.to be :pending_review }
      end

      context "when workflow state is pending_review" do
        before do
          submission.workflow_state = "pending_review"
        end

        context "and the submission was graded by quizzes" do
          before do
            submission.grader_id = -1
            submission.cached_quiz_lti = true
          end

          it { is_expected.to be :pending_review }

          context "and the submission was manually given a late policy status of missing" do
            before do
              submission.grader_id = @teacher.id
              submission.cached_quiz_lti = true
              submission.late_policy_status = "missing"
            end

            it { is_expected.to be :pending_review }
          end

          context "and the submission was manually given a late policy status of late" do
            before do
              submission.grader_id = @teacher.id
              submission.cached_quiz_lti = true
              submission.late_policy_status = "late"
            end

            it { is_expected.to be :pending_review }
          end
        end
      end

      context "the submission's Lti::Result was marked as PendingManual by an external tool" do
        let(:tool) { external_tool_1_3_model }
        let(:result) { lti_result_model(result_overrides) }
        let(:submission) { result.submission }
        let(:result_overrides) do
          {
            assignment:,
            grading_progress: "PendingManual",
            result_score: assignment.points_possible,
            result_maximum: assignment.points_possible,
            tool:
          }
        end

        it "marks the submission as needing review" do
          submission.infer_values
          expect(submission).to be_pending_review
        end
      end
    end
  end

  describe ".json_serialization_full_parameters" do
    it "can be provided additional methods to include in the params" do
      params = Submission.json_serialization_full_parameters(methods: [:missing])
      expect(params[:methods]).to include :missing
    end

    it "can provide an additional method with singular form (no array)" do
      params = Submission.json_serialization_full_parameters(methods: :missing)
      expect(params[:methods]).to include :missing
    end
  end

  describe "#tool_default_query_params" do
    context "new quiz submissions" do
      before do
        @course.context_external_tools.create!(
          name: "Quizzes.Next",
          consumer_key: "test_key",
          shared_secret: "test_secret",
          tool_id: "Quizzes 2",
          url: "http://example.com/launch"
        )

        @assignment.quiz_lti!
        @assignment.save!
      end

      let(:submission) { @assignment.submissions.find_by!(user: @student) }

      it "returns grade_by_question_enabled: true when grade by question is enabled" do
        @teacher.update!(preferences: { enable_speedgrader_grade_by_question: true })
        query_params = submission.tool_default_query_params(@teacher)
        expect(query_params[:grade_by_question_enabled]).to be true
      end

      it "returns grade_by_question_enabled: false when grade by question is disabled" do
        query_params = submission.tool_default_query_params(@teacher)
        expect(query_params[:grade_by_question_enabled]).to be false
      end
    end

    it "returns an empty array for a non-new-quiz submission" do
      expect(submission.tool_default_query_params(@teacher)).to be_empty
    end
  end

  describe "#anonymous_id" do
    subject { submission.anonymous_id }

    let(:student) { @student }
    let(:assignment) { @assignment }

    it { is_expected.to be_blank }

    it "sets an anonymous_id on validation" do
      submission.validate
      expect(submission.anonymous_id).to be_present
    end

    it "does not change if already persisted" do
      submission = assignment.submissions.find_by!(user: student)
      expect { submission.save! }.not_to change { submission.anonymous_id }
    end
  end

  describe "#type_for_attempt" do
    before(:once) do
      @assignment.update!(submission_types: "online_text_entry,online_url")
      now = Time.zone.now
      Timecop.freeze(10.minutes.from_now(now)) do
        @assignment.submit_homework(@student, body: "hi", submission_type: "online_text_entry")
      end

      Timecop.freeze(20.minutes.from_now(now)) do
        @assignment.submit_homework(@student, url: "https://www.google.com", submission_type: "online_url")
      end
    end

    let(:submission) { @assignment.submissions.find_by(user: @student) }

    it "returns the correct submission type given the attempt number" do
      aggregate_failures do
        expect(submission.type_for_attempt(1)).to eq "online_text_entry"
        expect(submission.type_for_attempt(2)).to eq "online_url"
      end
    end

    it "returns nil if given a non-existent attempt number" do
      expect(submission.type_for_attempt(3)).to be_nil
    end
  end

  describe "#body_for_attempt" do
    before(:once) do
      @assignment.update!(submission_types: "online_text_entry,online_url")
      now = Time.zone.now
      Timecop.freeze(10.minutes.from_now(now)) do
        @assignment.submit_homework(@student, body: "body1", submission_type: "online_text_entry")
      end

      Timecop.freeze(20.minutes.from_now(now)) do
        @assignment.submit_homework(@student, body: "body2", submission_type: "online_text_entry")
      end
    end

    let(:submission) { @assignment.submissions.find_by(user: @student) }

    it "returns the correct body given the attempt number" do
      aggregate_failures do
        expect(submission.body_for_attempt(1)).to eq "body1"
        expect(submission.body_for_attempt(2)).to eq "body2"
      end
    end

    it "returns nil if given a non-existent attempt number" do
      expect(submission.body_for_attempt(3)).to be_nil
    end
  end

  describe ".anonymous_ids_for" do
    subject { Submission.anonymous_ids_for(@first_assignment) }

    before do
      student_with_anonymous_ids = @student
      student_without_anonymous_ids = student_in_course(course: @course, active_all: true).user
      @first_assignment = @course.assignments.create!
      @course.assignments.create! # second_assignment
      @first_assignment_submission = @first_assignment.submissions.find_by!(user: student_with_anonymous_ids)
      Submission.where(user: student_without_anonymous_ids).update_all(anonymous_id: nil)
    end

    it "only contains submissions with anonymous_ids" do
      expect(subject).to contain_exactly(@first_assignment_submission.anonymous_id)
    end
  end

  describe "with grading periods" do
    let(:in_closed_grading_period) { 9.days.ago }
    let(:in_open_grading_period) { 1.day.from_now }
    let(:outside_of_any_grading_period) { 10.days.from_now }

    before(:once) do
      @root_account = @context.root_account
      group = @root_account.grading_period_groups.create!
      @closed_period = group.grading_periods.create!(
        title: "Closed!",
        start_date: 2.weeks.ago,
        end_date: 1.week.ago,
        close_date: 3.days.ago
      )
      @open_period = group.grading_periods.create!(
        title: "Open!",
        start_date: 3.days.ago,
        end_date: 3.days.from_now,
        close_date: 5.days.from_now
      )
      group.enrollment_terms << @context.enrollment_term
    end

    describe "permissions" do
      before(:once) do
        @admin = user_factory(active_all: true)
        @root_account.account_users.create!(user: @admin)
        @teacher = user_factory(active_all: true)
        @context.enroll_teacher(@teacher)
      end

      describe "grade" do
        context "the submission is deleted" do
          before(:once) do
            @assignment.due_at = in_open_grading_period
            @assignment.save!
            submission_spec_model
            @submission.update(workflow_state: "deleted")
          end

          it "does not have grade permissions if the user is a root account admin" do
            expect(@submission.grants_right?(@admin, :grade)).to be(false)
          end

          it "does not have grade permissions if the user is non-root account admin with manage_grades permissions" do
            expect(@submission.grants_right?(@teacher, :grade)).to be(false)
          end

          it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
            @student = user_factory(active_all: true)
            @context.enroll_student(@student)
            expect(@submission.grants_right?(@student, :grade)).to be(false)
          end
        end

        context "the submission is due in an open grading period" do
          before(:once) do
            @assignment.due_at = in_open_grading_period
            @assignment.save!
            submission_spec_model
          end

          it "has grade permissions if the user is a root account admin" do
            expect(@submission.grants_right?(@admin, :grade)).to be(true)
          end

          it "has grade permissions if the user is non-root account admin with manage_grades permissions" do
            expect(@submission.grants_right?(@teacher, :grade)).to be(true)
          end

          it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
            @student = user_factory(active_all: true)
            @context.enroll_student(@student)
            expect(@submission.grants_right?(@student, :grade)).to be(false)
          end
        end

        context "the submission is due outside of any grading period" do
          before(:once) do
            @assignment.due_at = outside_of_any_grading_period
            @assignment.save!
            submission_spec_model
          end

          it "has grade permissions if the user is a root account admin" do
            expect(@submission.grants_right?(@admin, :grade)).to be(true)
          end

          it "has grade permissions if the user is non-root account admin with manage_grades permissions" do
            expect(@submission.grants_right?(@teacher, :grade)).to be(true)
          end

          it "doesn't have grade permissions if the user is non-root account admin without manage_grades permissions" do
            @student = user_factory(active_all: true)
            @context.enroll_student(@student)
            expect(@submission.grants_right?(@student, :grade)).to be(false)
          end
        end

        context "when part of a moderated assignment" do
          before(:once) do
            @assignment.update!(
              moderated_grading: true,
              grader_count: 2
            )
            submission_spec_model(assignment: @assignment)
          end

          it "may not be graded if grades are not published" do
            expect(@submission.grants_right?(@teacher, :grade)).to be false
          end

          it "sets an error message indicating moderation is in progress if grades are not published" do
            @submission.grants_right?(@teacher, :grade)
            expect(@submission.grading_error_message).to eq "This assignment is currently being moderated"
          end

          it "may be graded if grades are not published but grade_posting_in_progress is true" do
            @submission.grade_posting_in_progress = true
            expect(@submission.grants_right?(@teacher, :grade)).to be true
          end

          it "may be graded when grades for the assignment are published" do
            @submission.assignment.update!(grades_published_at: Time.zone.now)
            expect(@submission.grants_right?(@teacher, :grade)).to be true
          end
        end
      end

      describe "make_group_comment" do
        let_once(:course) { Course.create! }
        let_once(:student) { course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
        let_once(:student2) { course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
        let_once(:teacher) { course.enroll_user(User.create!, "TeacherEnrollment", enrollment_state: "active").user }

        before(:once) do
          all_groups = @course.group_categories.create!(name: "all groups")
          all_groups.groups.create!(context: @course, name: "group 1").add_user(student)
          all_groups.groups.create!(context: @course, name: "group 2").add_user(student2)

          @group_assignment = course.assignments.create!(
            grade_group_students_individually: false,
            group_category: all_groups,
            name: "group assignment"
          )
          @submission = @group_assignment.submissions.find_by(user: student)
        end

        it "allows a student to make a group comment for their own submission" do
          expect(@submission.grants_right?(student, :make_group_comment)).to be true
        end

        it "allows a teacher to make a group comment" do
          expect(@submission.grants_right?(teacher, :make_group_comment)).to be true
        end

        it "allows a peer reviewer to make a group comment for their assigned submission" do
          @group_assignment.update!(peer_reviews: true)
          peer_submission = @group_assignment.submissions.find_by(user: student2)
          AssessmentRequest.create!(assessor: student2, assessor_asset: peer_submission, asset: @submission, user: student)
          expect(@submission.grants_right?(student2, :make_group_comment)).to be true
        end

        it "does not allow a student not peer reviewing to make a group comment" do
          @group_assignment.update!(peer_reviews: true)
          expect(@submission.grants_right?(student2, :make_group_comment)).to be false
        end
      end
    end
  end

  describe "update_quiz_submission" do
    before do
      submission.workflow_state = "pending_review"
      submission.submission_type = "online_quiz"
    end

    it "does not set_final_score if kept_score equals score without deductions" do
      quiz_submission_mock = double("QuizSubmission", "kept_score" => 123)
      allow(submission).to receive(:quiz_submission).and_return(quiz_submission_mock)
      submission.update(score: 100, points_deducted: 23, quiz_submission_id: 1)
      expect(quiz_submission_mock).not_to receive(:set_final_score)
      submission.update_quiz_submission
    end

    it "does set_final_score if kept_score differs from score without deductions" do
      quiz_submission_mock = double("QuizSubmission", "kept_score" => 100)
      allow(submission).to receive(:quiz_submission).and_return(quiz_submission_mock)
      submission.update(score: 100, points_deducted: 23, quiz_submission_id: 1)
      expect(quiz_submission_mock).to receive(:set_final_score)
      submission.update_quiz_submission
    end
  end

  describe "entered_score" do
    let(:submission) { @assignment.submissions.find_by!(user_id: @student) }

    it "returns nil if score is not present" do
      expect(submission.entered_score).to be_nil
    end

    it "returns score if no points deducted" do
      submission.update(score: 123)
      expect(submission.entered_score).to eql(submission.score)
    end

    it "returns score without deduction" do
      submission.update(score: 100, points_deducted: 23)
      expect(submission.entered_score).to eq 123
    end

    it "returns the score without deduction when late policy is disabled" do
      late_policy_factory(course: @course, deduct: 2.35, every: :day)
      @assignment.update!(due_at: 1.hour.ago, points_possible: 10, submission_types: "online_text_entry")
      @assignment.submit_homework(@student, body: "late submission")
      @assignment.grade_student(@student, grade: 10, grader: @teacher)
      @course.late_policy.update!(late_submission_deduction_enabled: false)
      expect(submission.entered_score).to eq 10.0
    end
  end

  describe "entered_grade" do
    let(:submission) { @assignment.submissions.find_by!(user_id: @student) }

    it "returns grade without deduction" do
      @assignment.update(grading_type: "percent", points_possible: 100)
      submission.update(score: 25.5, points_deducted: 60)
      expect(submission.entered_grade).to eql("85.5%")
    end

    it "returns grade if grading_type is pass_fail" do
      @assignment.update(grading_type: "pass_fail")
      submission.update(grade: "complete")
      expect(submission.entered_grade).to eql("complete")
    end

    it "returns the grade for a letter grade assignment with no points possible" do
      @assignment.update(grading_type: "letter_grade", points_possible: 0)
      submission.update(grade: "B")
      expect(submission.entered_grade).to eql("B")
    end

    it "returns the grade for a GPA scale assignment with no points possible" do
      @assignment.update(grading_type: "gpa_scale", points_possible: nil)
      submission.update(grade: "B")
      expect(submission.entered_grade).to eql("B")
    end
  end

  describe "cached_due_date" do
    before(:once) do
      @now = Time.zone.local(2013, 10, 18)
    end

    let(:submission) { @assignment.submissions.find_by!(user_id: @student) }

    it "gets initialized during submission creation" do
      # create an invited user, so that the submission is not automatically
      # created by the SubmissionLifecycleManager
      student_in_course(active_all: true)
      @assignment.update_attribute(:due_at, 1.day.ago)

      override = @assignment.assignment_overrides.build
      override.title = "Some Title"
      override.set = @course.default_section
      override.override_due_at(1.day.from_now)
      override.save!

      submission = @assignment.submissions.find_by!(user: @user)
      expect(submission.cached_due_date).to eq override.reload.due_at.change(usec: 0)
    end

    it "does not truncate seconds off of cached due dates" do
      time = Time.zone.parse("2018-12-24 23:59:59")
      @assignment.update_attribute(:due_at, time)
      submission = @assignment.submissions.find_by!(user: @user)
      expect(submission.cached_due_date.to_i).to eq time.to_i
    end

    context "due date changes after student submits" do
      before(:once) do
        Timecop.freeze(@now) do
          @assignment.update!(due_at: 20.minutes.ago, submission_types: "online_text_entry")
          @assignment.submit_homework(@student, body: "a body")
        end
      end

      it "changes if the assignment due date changes" do
        expect { @assignment.update!(due_at: 15.minutes.ago(@now)) }.to change {
          submission.reload.cached_due_date
        }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
      end

      context "student overrides" do
        before(:once) do
          @override = @assignment.assignment_overrides.create!(due_at: 15.minutes.ago(@now), due_at_overridden: true)
        end

        it "changes if an override is added for the student" do
          expect { @override.assignment_override_students.create!(user: @student) }.to change {
            submission.reload.cached_due_date
          }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "changes if an individual override is added for the student, even when the due date is earlier " \
           "than an existing override that applies to the student for the assignment" do
          section = @course.course_sections.create!(name: "My Awesome Section")
          student_in_section(section, user: @student)
          @assignment.assignment_overrides.create!(
            due_at: 10.minutes.ago(@now),
            due_at_overridden: true,
            set: section
          )

          override = @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true
          )

          expect { override.assignment_override_students.create!(user: @student) }.to change {
            submission.reload.cached_due_date
          }.from(10.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "does not change if a non-individual-override is added for the student and the due date " \
           "is earlier than an existing override that applies to the student for the assignment" do
          category = @course.group_categories.create!(name: "New Group Category")
          group = @course.groups.create!(group_category: category)
          group.add_user(@student, "active")
          assignment = @course.assignments.create!(group_category: category)

          section = @course.course_sections.create!(name: "My Awesome Section")
          student_in_section(section, user: @student)
          assignment.assignment_overrides.create!(
            due_at: 10.minutes.ago(@now),
            due_at_overridden: true,
            set: section
          )

          expect do
            assignment.assignment_overrides.create!(
              due_at: 15.minutes.ago(@now),
              due_at_overridden: true,
              set: group
            )
          end.not_to change { submission.reload.cached_due_date }
        end

        it "changes if an override is removed for the student" do
          @override.assignment_override_students.create!(user: @student)
          expect { @override.assignment_override_students.find_by(user_id: @student).destroy }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
        end

        it "changes if the due date for the override is changed" do
          @override.assignment_override_students.create!(user: @student, workflow_state: "active")
          expect { @override.update!(due_at: 14.minutes.ago(@now)) }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
        end
      end

      context "section overrides" do
        before(:once) do
          @section = @course.course_sections.create!(name: "My Awesome Section")
        end

        it "changes if an override is added for the section the student is in" do
          student_in_section(@section, user: @student)
          expect do
            @assignment.assignment_overrides.create!(
              due_at: 15.minutes.ago(@now),
              due_at_overridden: true,
              set: @section
            )
          end.to change {
            submission.reload.cached_due_date
          }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "changes if a student is added to the section after the override is added for the section" do
          @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @section
          )

          expect { student_in_section(@section, user: @student) }.to change {
            submission.reload.cached_due_date
          }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "changes if a student is removed from a section with a due date for the assignment" do
          @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @section
          )

          @course.enroll_student(@student, section: @section, allow_multiple_enrollments: true).accept!
          expect { @student.enrollments.find_by(course_section_id: @section).destroy }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
        end

        it "changes if the due date for the override is changed" do
          student_in_section(@section, user: @student)
          override = @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @section
          )
          expect { override.update!(due_at: 14.minutes.ago(@now)) }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
        end
      end

      context "group overrides" do
        before(:once) do
          category = @course.group_categories.create!(name: "New Group Category")
          @group = @course.groups.create!(group_category: category)
          @assignment = @course.assignments.create!(group_category: category, due_at: 20.minutes.ago(@now))
          @assignment.submit_homework(@student, body: "a body")
        end

        it "changes if an override is added for the group the student is in" do
          @group.add_user(@student, "active")

          expect do
            @assignment.assignment_overrides.create!(
              due_at: 15.minutes.ago(@now),
              due_at_overridden: true,
              set: @group
            )
          end.to change {
            submission.reload.cached_due_date
          }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "changes if a student is added to the group after the override is added for the group" do
          @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @group
          )

          expect { @group.add_user(@student, "active") }.to change {
            submission.reload.cached_due_date
          }.from(20.minutes.ago(@now)).to(15.minutes.ago(@now))
        end

        it "changes if a student is removed from a group with a due date for the assignment" do
          @group.add_user(@student, "active")
          @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @group
          )

          expect { @group.group_memberships.find_by(user_id: @student).destroy }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(20.minutes.ago(@now))
        end

        it "changes if the due date for the override is changed" do
          @group.add_user(@student, "active")
          override = @assignment.assignment_overrides.create!(
            due_at: 15.minutes.ago(@now),
            due_at_overridden: true,
            set: @group
          )

          expect { override.update!(due_at: 14.minutes.ago(@now)) }.to change {
            submission.reload.cached_due_date
          }.from(15.minutes.ago(@now)).to(14.minutes.ago(@now))
        end
      end

      it "uses the individual override date, otherwise most lenient, if there are multiple overrides" do
        category = @course.group_categories.create!(name: "New Group Category")
        group = @course.groups.create!(group_category: category)
        assignment = @course.assignments.create!(group_category: category, due_at: 20.minutes.ago(@now))
        assignment.submit_homework(@student, body: "a body")

        section = @course.course_sections.create!(name: "My Awesome Section")
        @course.enroll_student(@student, section:, allow_multiple_enrollments: true).accept!
        assignment.assignment_overrides.create!(
          due_at: 6.minutes.ago(@now),
          due_at_overridden: true,
          set: section
        )

        group.add_user(@student, "active")
        assignment.assignment_overrides.create!(
          due_at: 21.minutes.ago(@now),
          due_at_overridden: true,
          set: group
        )

        student_override = assignment.assignment_overrides.create!(
          due_at: 14.minutes.ago(@now),
          due_at_overridden: true
        )
        override_student = student_override.assignment_override_students.create!(user: @student)

        submission = assignment.submissions.find_by!(user: @student)
        expect { override_student.destroy }.to change {
          submission.reload.cached_due_date
        }.from(14.minutes.ago(@now)).to(6.minutes.ago(@now))

        expect { @student.enrollments.find_by(course_section: section).destroy }.to change {
          submission.reload.cached_due_date
        }.from(6.minutes.ago(@now)).to(21.minutes.ago(@now))
      end

      it "uses override due dates instead of assignment due dates, even if the assignment due date is more lenient" do
        student_override = @assignment.assignment_overrides.create!(
          due_at: 21.minutes.ago(@now),
          due_at_overridden: true
        )

        student_override.assignment_override_students.create!(user: @student)
        expect(submission.cached_due_date).to eq(21.minutes.ago(@now))
      end

      it "falls back to use the assignment due date if all overrides are destroyed" do
        student_override = @assignment.assignment_overrides.create!(
          due_at: 21.minutes.ago(@now),
          due_at_overridden: true
        )

        override_student = student_override.assignment_override_students.create!(user: @student)
        expect { override_student.destroy }.to change {
          submission.reload.cached_due_date
        }.from(21.minutes.ago(@now)).to(20.minutes.ago(@now))
      end
    end
  end

  describe "#excused" do
    let(:submission) do
      submission = @assignment.submissions.find_by!(user: @student)
      submission.update!(excused: true)
      submission
    end

    let(:custom_grade_status) do
      admin = account_admin_user(account: @assignment.root_account)
      @assignment.root_account.custom_grade_statuses.create!(
        color: "#ABC",
        name: "yolo",
        created_by: admin
      )
    end

    it "sets excused to false if the late_policy_status is being changed to a not-nil value" do
      submission.update!(late_policy_status: "missing")
      expect(submission).not_to be_excused
    end

    it "does not set excused to false if the late_policy_status is being changed to a nil value" do
      # need to skip callbacks so excused does not get set to false
      submission.update_column(:late_policy_status, "missing")
      submission.update!(late_policy_status: nil)
      expect(submission).to be_excused
    end

    it "sets excused to false when a custom status is set" do
      expect { submission.update!(custom_grade_status:) }.to change {
        submission.excused?
      }.from(true).to(false)
    end

    it "does not set excused to false when a custom status is removed" do
      # need to skip callbacks so excused does not get set to false
      submission.update_column(:custom_grade_status_id, custom_grade_status.id)
      expect { submission.update!(custom_grade_status: nil) }.not_to change {
        submission.excused?
      }.from(true)
    end
  end

  describe "#status_tag" do
    before do
      @assignment.update!(submission_types: "online_text_entry")
      @submission = @assignment.submissions.find_by!(user: @student)
    end

    it "returns :custom when the submission has a custom grade status" do
      custom_grade_status = @submission.root_account.custom_grade_statuses.create!(
        name: "Potato",
        color: "#FFE8E5",
        created_by: @course.teachers.take
      )

      @submission.update!(custom_grade_status:)
      expect(@submission.status_tag).to eq :custom
    end

    it "returns :excused when the submission is excused" do
      @submission.update!(excused: true)
      expect(@submission.status_tag).to eq :excused
    end

    it "returns :late when the submission is marked late" do
      @submission.update!(late_policy_status: :late)
      expect(@submission.status_tag).to eq :late
    end

    it "returns :late when the submission is naturally late" do
      @assignment.update!(due_at: 1.day.ago)
      @assignment.submit_homework(@submission.user, body: "foo")
      @submission.reload
      expect(@submission.status_tag).to eq :late
    end

    it "returns :extended when the submission is extended" do
      @submission.update!(late_policy_status: :extended)
      expect(@submission.status_tag).to eq :extended
    end

    it "returns :missing when the submission is marked missing" do
      @submission.update!(late_policy_status: :missing)
      expect(@submission.status_tag).to eq :missing
    end

    it "returns :missing when the submission is naturally missing" do
      @assignment.update!(due_at: 1.day.ago)
      @submission.update!(score: nil, grader: nil)
      @submission.reload
      expect(@submission.status_tag).to eq :missing
    end

    it "returns :none when the submission is marked none" do
      @assignment.update!(due_at: 1.day.ago)
      @assignment.submit_homework(@submission.user, body: "foo")
      # the submission is naturally late, but marked as "none"
      @submission.update!(late_policy_status: :none)
      expect(@submission.status_tag).to eq :none
    end

    it "returns :none when the submission has no special status" do
      expect(@submission.status_tag).to eq :none
    end
  end

  describe "status" do
    before do
      @assignment.update!(submission_types: "online_text_entry")
      @submission = @assignment.submissions.find_by!(user: @student)
    end

    it "returns the custom status name when the submission has a custom grade status" do
      custom_grade_status = @submission.root_account.custom_grade_statuses.create!(
        name: "Potato",
        color: "#FFE8E5",
        created_by: @course.teachers.take
      )

      @submission.update!(custom_grade_status:)
      expect(@submission.status).to eq "Potato"
    end

    it "returns 'Excused' when the submission is excused" do
      @submission.update!(excused: true)
      expect(@submission.status).to eq "Excused"
    end

    it "returns 'Late' when the submission is late" do
      @submission.update!(late_policy_status: :late)
      expect(@submission.status).to eq "Late"
    end

    it "returns 'Extended' when the submission is extended" do
      @submission.update!(late_policy_status: :extended)
      expect(@submission.status).to eq "Extended"
    end

    it "returns 'Missing' when the submission is missing" do
      @submission.update!(late_policy_status: :missing)
      expect(@submission.status).to eq "Missing"
    end

    it "returns 'None' when the submission has no special status" do
      expect(@submission.status).to eq "None"
    end
  end

  describe "#late_policy_status" do
    let(:submission) do
      submission = @assignment.submissions.find_by!(user: @student)
      submission.update!(late_policy_status: "late", seconds_late_override: 60)
      submission
    end

    it "sets late_policy_status to nil if the submission is updated to be excused" do
      submission.update!(excused: true)
      expect(submission.late_policy_status).to be_nil
    end

    it "sets seconds_late_override to nil if the submission is updated to be excused" do
      submission.update!(excused: true)
      expect(submission.seconds_late_override).to be_nil
    end

    it "does not set late_policy_status to nil if the submission is updated to not be excused" do
      # need to skip callbacks so late_policy_status does not get set to nil
      submission.update_column(:excused, true)
      submission.update!(excused: false)
      expect(submission.late_policy_status).to eql "late"
    end

    it "does not set seconds_late_override to nil if the submission is updated to not be excused" do
      # need to skip callbacks so seconds_late_override does not get set to nil
      submission.update_column(:excused, true)
      submission.update!(excused: false)
      expect(submission.seconds_late_override).to be 60
    end

    context "custom statuses" do
      let(:custom_grade_status) do
        admin = account_admin_user(account: @assignment.root_account)
        @assignment.root_account.custom_grade_statuses.create!(
          color: "#ABC",
          name: "yolo",
          created_by: admin
        )
      end

      it "sets late_policy_status to nil if the custom_grade_status_id is being changed to a not-nil value" do
        submission.update!(custom_grade_status:)
        expect(submission.late_policy_status).to be_nil
      end

      it "sets seconds_late_override to nil if the submission is updated to have a custom status" do
        submission.update!(custom_grade_status:)
        expect(submission.seconds_late_override).to be_nil
      end

      it "does not set late_policy_status to nil if the custom_grade_status_id is being changed to a nil value" do
        # need to skip callbacks so excused does not get set to false
        submission.update_column(:custom_grade_status_id, custom_grade_status.id)
        submission.update!(custom_grade_status_id: nil)
        expect(submission.late_policy_status).to eql "late"
      end

      it "does not set seconds_late_override to nil if the submission is updated to not have a custom status" do
        # need to skip callbacks so seconds_late_override does not get set to nil
        submission.update_column(:custom_grade_status_id, custom_grade_status.id)
        submission.update!(custom_grade_status_id: nil)
        expect(submission.seconds_late_override).to be 60
      end
    end
  end

  describe "seconds_late_override" do
    let(:submission) { @assignment.submissions.find_by!(user: @student) }

    it "sets seconds_late_override to nil if the late_policy_status is set to anything other than 'late'" do
      submission.update!(late_policy_status: "late", seconds_late_override: 60)
      expect do
        submission.update!(late_policy_status: "missing")
      end.to change { submission.seconds_late_override }.from(60).to(nil)
    end

    it "does not set seconds_late_override if late_policy status is not 'late'" do
      submission.update!(seconds_late_override: 60)
      expect(submission.seconds_late_override).to be_nil
    end
  end

  describe "seconds_late" do
    before(:once) do
      @date = Time.zone.local(2017, 1, 15, 12)
      Timecop.travel(@date) do
        Auditors::ActiveRecord::Partitioner.process
      end
      @assignment.update!(due_at: 1.hour.ago(@date), submission_types: "online_text_entry")
    end

    let(:submission) { @assignment.submissions.find_by!(user_id: @student) }

    it "returns time between submitted_at and cached_due_date" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to eql 60.minutes.to_i
      end
    end

    it "is adjusted if the student resubmits" do
      Timecop.freeze(@date) { @assignment.submit_homework(@student, body: "a body") }
      Timecop.freeze(30.minutes.from_now(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to eql 90.minutes.to_i
      end
    end

    it "returns seconds_late_override if the submission has a late_policy_status of 'late' " \
       "and a seconds_late_override" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(late_policy_status: "late", seconds_late_override: 90.minutes)
        expect(submission.seconds_late).to eql 90.minutes.to_i
      end
    end

    it "is not adjusted if the student resubmits and the submission has a late_policy_status of 'late' " \
       "and a seconds_late_override" do
      Timecop.freeze(@date) { @assignment.submit_homework(@student, body: "a body") }
      submission.update!(late_policy_status: "late", seconds_late_override: 90.minutes)
      Timecop.freeze(40.minutes.from_now(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to eql 90.minutes.to_i
      end
    end

    it "returns 0 if the submission has a late_policy_status of 'late' but no seconds_late_override is present" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(late_policy_status: "late")
        expect(submission.seconds_late).to be 0
      end
    end

    it "is zero if it is not late" do
      Timecop.freeze(2.hours.ago(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to be 0
      end
    end

    it "is zero if it was turned in late but the teacher sets the late_policy_status to 'late' " \
       "and sets seconds_late_override to zero" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(late_policy_status: "late", seconds_late_override: 0)
        expect(submission.seconds_late).to be 0
      end
    end

    it "is zero if cached_due_date is nil" do
      Timecop.freeze(@date) do
        @assignment.update!(due_at: nil)
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to be 0
      end
    end

    it "subtracts 60 seconds from the time of submission when submission_type is 'online_quiz'" do
      Timecop.freeze(@date) do
        @assignment.update!(submission_types: "online_quiz")
        @assignment.submit_homework(@student, submission_type: "online_quiz", body: "a body")
        expect(submission.seconds_late).to eql 59.minutes.to_i
      end
    end

    context "when the submission is for a new quiz" do
      before do
        @course.context_external_tools.create!(
          name: "Quizzes.Next",
          consumer_key: "test_key",
          shared_secret: "test_secret",
          tool_id: "Quizzes 2",
          url: "http://example.com/launch"
        )

        @assignment.quiz_lti!
        @assignment.save!
      end

      it "subtracts 60 seconds from the submitted_at" do
        Timecop.freeze(@date) do
          submission = @assignment.submissions.find_by!(user: @student)
          submission.update!(submitted_at: Time.now.utc)
          expect(submission.seconds_late).to eql 59.minutes.to_i
        end
      end
    end

    it "includes seconds" do
      Timecop.freeze(30.seconds.from_now(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        expect(submission.seconds_late).to eql((60.minutes + 30.seconds).to_i)
      end
    end

    it "uses the current time if submitted_at is nil" do
      Timecop.freeze(1.day.from_now(@date)) do
        @assignment.grade_student(@student, score: 10, grader: @teacher)
        expect(submission.seconds_late).to eql 25.hours.to_i
      end
    end
  end

  describe "#apply_late_policy" do
    before(:once) do
      @date = Time.zone.local(2017, 1, 15, 12)
      Timecop.travel(@date) do
        Auditors::ActiveRecord::Partitioner.process
      end
      @assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "online_text_entry")
      @late_policy = late_policy_model(deduct: 10.0, every: :hour, missing: 80.0)
    end

    let(:submission) { @assignment.submissions.find_by(user_id: @student) }

    context "as a before_save" do
      before(:once) do
        @late_policy.update!(course_id: @course)
      end

      it "deducts a percentage per interval late if grade_matches_current_submission is true" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          submission.score = 700
          submission.grade_matches_current_submission = true
          submission.save!
          expect(submission.points_deducted).to eq 300.0
        end
      end

      it "deducts a percentage per interval late if grade_matches_current_submission is nil" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          submission.score = 700
          submission.grade_matches_current_submission = nil
          submission.save!
          expect(submission.points_deducted).to eq 300.0
        end
      end

      it "deducts nothing if grade_matches_current_submission is false" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          submission.score = 700
          submission.grade_matches_current_submission = false
          submission.save!
          expect(submission.points_deducted).to be_nil
        end
      end

      it "sets points_deducted to nil if a submission's status is changed to missing" do
        submission.update!(score: 5, points_deducted: 2)
        expect { submission.update!(late_policy_status: "missing") }.to change {
          submission.points_deducted
        }.from(2).to(nil)
      end

      it "sets score to raw_score if a submission has points_deducted and the status is changed to missing" do
        submission.update!(score: 5, points_deducted: 2)
        expect { submission.update!(late_policy_status: "missing") }.to change {
          submission.score
        }.from(5).to(7)
      end

      it "keeps the given score if a submission is set to missing and given a score" do
        submission.update!(score: 5, points_deducted: 2)
        expect { submission.update!(score: 3, late_policy_status: "missing") }.to change {
          submission.score
        }.from(5).to(3)
      end

      it "deducts nothing if the submission is for a checkpointed discussion" do
        @course.root_account.enable_feature!(:discussion_checkpoints)
        cd = DiscussionTopic.create_graded_topic!(course: @course, title: "checkpointed topic")

        Checkpoints::DiscussionCheckpointCreatorService.call(
          discussion_topic: cd,
          checkpoint_label: CheckpointLabels::REPLY_TO_TOPIC,
          dates: [{ type: "everyone", due_at: 3.hours.ago(@date) }],
          points_possible: 5
        )
        Checkpoints::DiscussionCheckpointCreatorService.call(
          discussion_topic: cd,
          checkpoint_label: CheckpointLabels::REPLY_TO_ENTRY,
          dates: [{ type: "everyone", due_at: 3.hours.ago(@date) }],
          points_possible: 5,
          replies_required: 1
        )
        cd_submission = cd.assignment.submissions.find_by!(user: @student)

        Timecop.freeze(@date) do
          cd.discussion_entries.create!(user: @student, message: "reply to topic")
          cd.discussion_entries.create!(user: @student, message: "reply to entry", parent_id: cd.discussion_entries.first.id)
          cd_submission.score = 10
          cd_submission.save!
          expect(cd_submission.points_deducted).to be_nil
        end
      end
    end

    it "deducts nothing if grading period is closed" do
      grading_period = double("grading_period", closed?: true)
      expect(submission).to receive(:grading_period).and_return(grading_period)
      @assignment.submit_homework(@student, body: "a body")
      submission.score = 700
      submission.apply_late_policy(@late_policy, @assignment)
      expect(submission.score).to eq 700
      expect(submission.points_deducted).to be_nil
    end

    it "deducts a percentage per interval late" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.score = 700
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 400
        expect(submission.points_deducted).to eq 300
      end
    end

    it "deducts nothing if there is no late policy" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.score = 700
        submission.apply_late_policy(nil, @assignment)
        expect(submission.score).to eq 700
        expect(submission.points_deducted).to eq 0
      end
    end

    it "deducts nothing if the submission is not late" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "gary, what have you done?")
        submission.score = 700
        submission.late_policy_status = "missing"
        submission.apply_late_policy(@late_policy, @assignment)

        expect(submission.score).to eq 700
        expect(submission.points_deducted).to be_nil
      end
    end

    it "does not round decimal places in the score" do
      Timecop.freeze(2.days.ago(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        original_score = 1.3800000000000001
        submission.score = original_score
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq original_score
      end
    end

    it "deducts only once even if called twice" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.score = 800
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 500
        expect(submission.points_deducted).to eq 300
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 500
        expect(submission.points_deducted).to eq 300
      end
    end

    it "sets the points_deducted to 0.0 if the score is set to nil and the submission is late" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(score: 400, points_deducted: 300)
        submission.score = nil
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.points_deducted).to eq 0.0
      end
    end

    it "sets the points_deducted to nil if the score is set to nil and the submission is not late" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(score: 400, points_deducted: 300)
        submission.score = nil
        submission.late_policy_status = "none"
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.points_deducted).to be_nil
      end
    end

    it "applies missing policy if submission is missing" do
      Timecop.freeze(1.day.from_now(@date)) do
        submission.score = nil
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 200
      end
    end

    context "past due date" do
      it "removes missing status when missing submission is graded" do
        Timecop.freeze(1.day.from_now(@date)) do
          expect(submission.missing?).to be true
          @assignment.grade_student(@student, score: 500, grader: @teacher)
          submission.reload
          expect(submission.missing?).to be false
        end
      end

      it "does not remove missing status when missing status was given manually" do
        Timecop.freeze(1.day.from_now(@date)) do
          expect(submission.missing?).to be true
          submission.update!(late_policy_status: "missing")
          expect(submission.missing?).to be true
          @assignment.grade_student(@student, score: 500, grader: @teacher)
          submission.reload
          expect(submission.missing?).to be true
        end
      end
    end

    it "does not remove missing status when missing status was given manually" do
      @assignment.update!(due_at: 3.hours.from_now(@date), points_possible: 1000, submission_types: "online_text_entry")
      submission.reload
      Timecop.freeze(@date) do
        expect(submission.missing?).to be false
        submission.update!(late_policy_status: "missing")
        expect(submission.missing?).to be true
        @assignment.grade_student(@student, score: 500, grader: @teacher)
        submission.reload
        expect(submission.missing?).to be true
      end
    end

    it "sets grade_matches_current_submission to true when missing policy is applied" do
      Timecop.freeze(1.day.from_now(@date)) do
        submission.score = nil
        submission.grade_matches_current_submission = false
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.grade_matches_current_submission).to be true
      end
    end

    it "sets the workflow state to 'graded' when submission is missing" do
      Timecop.freeze(1.day.from_now(@date)) do
        submission.score = nil
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.workflow_state).to eq "graded"
      end
    end

    describe "posting of missing submissions" do
      before(:once) do
        late_policy_factory(course: @course, missing: 50)
      end

      context "when the submission was not previously posted" do
        context "for an automatically-posted assignment" do
          it "posts a previously-unscored submission if deducting points for missing submissions" do
            submission.update!(late_policy_status: :missing)
            expect(submission.posted_at).not_to be_nil
          end

          it "does not post the submission if missing submission deduction is not enabled" do
            @course.late_policy.update!(missing_submission_deduction_enabled: false)
            expect do
              submission.update!(late_policy_status: :missing)
            end.not_to change { submission.reload.posted_at }
          end

          it "does not update the posted-at date of an already-posted submission" do
            @assignment.post_submissions

            expect do
              submission.update!(late_policy_status: :missing)
            end.not_to change { submission.reload.posted_at }
          end
        end

        it "does not post submissions if the assignment is manually posted" do
          @assignment.post_policy.update!(post_manually: true)

          expect do
            submission.update!(late_policy_status: :missing)
          end.not_to change { submission.reload.posted_at }
        end
      end
    end

    it "does not change the score of a missing submission if it already has one" do
      Timecop.freeze(1.day.from_now(@date)) do
        @assignment.grade_student(@student, grade: 1000, grader: @teacher)
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to be 1000.0
      end
    end

    context "with regraded" do
      it "does not apply the deduction multiple times if submission saved multiple times" do
        @late_policy.update!(course_id: @course)
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          # The submission is saved once in grade_student.  Using sub
          # here to avoid masking/using the submission in the let
          # above. I want to make sure I'm using the exact same object
          # as returned by grade_student.
          sub = @assignment.grade_student(@student, grade: 1000, grader: @teacher).first
          sub.save!
          expect(sub.score).to be 700.0
        end
      end
    end

    context "assignment on paper" do
      before(:once) do
        @date = Time.zone.local(2017, 1, 15, 12)
        Timecop.travel(@date) do
          Auditors::ActiveRecord::Partitioner.process
        end
        @assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "on_paper")
        @late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
      end

      it "does not deduct from late assignment" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 700, grader: @teacher)
          expect(submission.score).to eq 700
          expect(submission.points_deducted).to be_nil
        end
      end

      it "does not grade missing assignment" do
        Timecop.freeze(@date) do
          submission.apply_late_policy
          expect(submission.score).to be_nil
          expect(submission.points_deducted).to be_nil
        end
      end

      it "deducts a percentage per interval late if manually marked late" do
        @assignment.submit_homework(@student, body: "a body")
        submission.late_policy_status = "late"
        submission.seconds_late_override = 4.hours
        submission.score = 700
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to be 300.0
        expect(submission.points_deducted).to eq 400
      end

      context "when change late_policy_status from late to none" do
        before do
          @assignment.course.update!(late_policy: @late_policy)
          @assignment.submit_homework(@student, body: "a body")

          submission.update!(
            score: 700,
            late_policy_status: "late",
            seconds_late_override: 4.hours
          )
        end

        it "removes late penalty from score" do
          expect { submission.update!(late_policy_status: "none") }
            .to change { submission.score }.from(300).to(700)
        end

        it "sets points_deducted to nil" do
          expect { submission.update!(late_policy_status: "none") }
            .to change { submission.points_deducted }.from(400).to(nil)
        end
      end

      context "when change late_policy_status from late to extended" do
        before do
          @assignment.course.update!(late_policy: @late_policy)
          @assignment.submit_homework(@student, body: "a body")

          submission.update!(
            score: 700,
            late_policy_status: "late",
            seconds_late_override: 4.hours
          )
        end

        it "removes late penalty from score" do
          expect { submission.update!(late_policy_status: "extended") }
            .to change { submission.score }.from(300).to(700)
        end

        it "sets points_deducted to nil" do
          expect { submission.update!(late_policy_status: "extended") }
            .to change { submission.points_deducted }.from(400).to(nil)
        end
      end

      context "when changing late_policy_status from none to nil" do
        before do
          @assignment.update!(due_at: 1.hour.from_now)
          @assignment.course.update!(late_policy: @late_policy)
          @assignment.submit_homework(@student, body: "a body")
          submission.update!(score: 700, late_policy_status: "late", seconds_late_override: 4.hours)
        end

        it "applies the late policy to the score" do
          expect { submission.update!(late_policy_status: "none") }
            .to change { submission.score }.from(300).to(700)
        end

        it "applies the late policy to points_deducted" do
          expect { submission.update!(late_policy_status: "none") }
            .to change { submission.points_deducted }.from(400).to(nil)
        end
      end

      it "applies missing policy if submission is manually marked missing" do
        Timecop.freeze(1.day.from_now(@date)) do
          submission.score = nil
          submission.late_policy_status = "missing"
          submission.apply_late_policy(@late_policy, @assignment)
          expect(submission.score).to eq 200
        end
      end
    end

    context "assignment expecting no submission" do
      before(:once) do
        @date = Time.zone.local(2017, 1, 15, 12)
        Timecop.travel(@date) do
          Auditors::ActiveRecord::Partitioner.process
        end
        @assignment.update!(due_at: 3.hours.ago(@date), points_possible: 1000, submission_types: "none")
        @late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
      end

      it "does not deduct from late assignment" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 700, grader: @teacher)
          expect(submission.score).to eq 700
          expect(submission.points_deducted).to be_nil
        end
      end

      it "does not grade missing assignment" do
        Timecop.freeze(@date) do
          submission.apply_late_policy
          expect(submission.score).to be_nil
          expect(submission.points_deducted).to be_nil
        end
      end

      it "deducts a percentage per interval late if manually marked late" do
        @assignment.submit_homework(@student, body: "a body")
        submission.late_policy_status = "late"
        submission.seconds_late_override = 4.hours
        submission.score = 700
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 300
        expect(submission.points_deducted).to eq 400
      end

      it "applies missing policy if submission is manually marked missing" do
        Timecop.freeze(1.day.from_now(@date)) do
          submission.score = nil
          submission.late_policy_status = "missing"
          submission.apply_late_policy(@late_policy, @assignment)
          expect(submission.score).to eq 200
        end
      end
    end

    context "when applied late policy deducts 100%" do
      before(:once) do
        @date = Time.zone.local(2017, 1, 15, 12)
        Timecop.travel(@date) do
          Auditors::ActiveRecord::Partitioner.process
        end
        @assignment.update!(due_at: @date - 12.days, points_possible: 1, submission_types: "online_text_entry")
        @late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :day)
      end

      it "sets the score to 0 when grade has three decimal points and ending in 5" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 0.555, grader: @teacher)
          expect(submission.score).to eq 0.0
        end
      end
    end

    context "when submitting to an LTI assignment" do
      before(:once) do
        @date = Time.zone.local(2017, 1, 15, 12)
        Timecop.travel(@date) do
          Auditors::ActiveRecord::Partitioner.process
        end
        @assignment.update!(due_at: @date - 3.hours, points_possible: 1_000, submission_types: "external_tool")
        @late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
      end

      it "deducts a percentage per interval late if submitted late" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 700, grader: @teacher)
          expect(submission.points_deducted).to eq 300
        end
      end

      it "applies the deduction to the awarded score if submitted late" do
        Timecop.freeze(@date) do
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 700, grader: @teacher)
          expect(submission.score).to eq 400
        end
      end

      it "does not grade missing submissions" do
        Timecop.freeze(@date) do
          submission.apply_late_policy
          expect(submission.score).to be_nil
        end
      end

      it "deducts a percentage per interval late if the submission is manually marked late" do
        @assignment.submit_homework(@student, body: "a body")
        submission.late_policy_status = "late"
        submission.seconds_late_override = 4.hours
        submission.score = 700
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.points_deducted).to eq 400
      end

      it "applies the deduction to the awarded score if the submission is manually marked late" do
        @assignment.submit_homework(@student, body: "a body")
        submission.late_policy_status = "late"
        submission.seconds_late_override = 4.hours
        submission.score = 700
        submission.apply_late_policy(@late_policy, @assignment)
        expect(submission.score).to eq 300
      end

      it "applies the missing policy if the submission is manually marked missing" do
        Timecop.freeze(@date + 1.day) do
          submission.score = nil
          submission.late_policy_status = "missing"
          submission.apply_late_policy(@late_policy, @assignment)
          expect(submission.score).to eq 200
        end
      end
    end

    context "when submitting to a New Quiz LTI assignment" do
      before(:once) do
        @date = Time.zone.local(2017, 1, 15, 12)
        Timecop.travel(@date) do
          Auditors::ActiveRecord::Partitioner.process
        end
        @course.context_external_tools.create!(
          name: "Quizzes.Next",
          consumer_key: "test_key",
          shared_secret: "test_secret",
          tool_id: "Quizzes 2",
          url: "http://example.com/launch"
        )

        @assignment.quiz_lti!
        @assignment.save!
        @late_policy = late_policy_factory(course: @course, deduct: 10.0, every: :hour, missing: 80.0)
      end

      it "does grade missing new quiz submissions" do
        Timecop.freeze(@date) do
          submission.apply_late_policy
          expect(submission.score).to eq 200
        end
      end
    end
  end

  describe "#apply_late_policy_before_save" do
    before(:once) do
      @date = Time.zone.local(2017, 3, 25, 11)
      Timecop.travel(@date) do
        Auditors::ActiveRecord::Partitioner.process
      end
      @assignment.update!(due_at: 4.days.ago(@date), points_possible: 1000, submission_types: "online_text_entry")
      @late_policy = late_policy_factory(course: @course, deduct: 5.0, every: :day, missing: 80.0)
    end

    let(:submission) { @assignment.submissions.find_by(user_id: @student) }

    it "applies the missing policy to the score when changing from excused to missing" do
      @assignment.grade_student(@student, grader: @teacher, excused: true)
      expect { submission.update!(late_policy_status: "missing") }.to change {
        submission.score
      }.from(nil).to(200)
    end

    it "applies the missing policy to the grade when changing from excused to missing" do
      @assignment.grade_student(@student, grader: @teacher, excused: true)
      expect { submission.update!(late_policy_status: "missing") }.to change {
        submission.grade
      }.from(nil).to("200")
    end

    it "applies the late policy when score changes" do
      Timecop.freeze(2.days.ago(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        @assignment.grade_student(@student, grade: 600, grader: @teacher)
        expect(submission.score).to eq 500
        expect(submission.points_deducted).to eq 100
      end
    end

    it "applies the late policy when entered grade is equal to previous penalized grade" do
      Timecop.freeze(2.days.ago(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        @assignment.grade_student(@student, grade: 600, grader: @teacher)

        @assignment.grade_student(@student, grade: 500, grader: @teacher)
        expect(submission.score).to eq 400
      end
    end

    context "custom grade statuses" do
      let(:custom_grade_status) do
        admin = account_admin_user(account: @assignment.root_account)
        @assignment.root_account.custom_grade_statuses.create!(
          name: "Custom Status",
          color: "#ABC",
          created_by: admin
        )
      end

      it "does not apply the late policy when score changes if a custom status is applied" do
        Timecop.freeze(2.days.ago(@date)) do
          @assignment.submit_homework(@student, body: "a body")
          submission.update!(custom_grade_status:)
          expect { @assignment.grade_student(@student, grade: 600, grader: @teacher) }.not_to change {
            submission.reload.points_deducted
          }.from(nil)
        end
      end

      it "applies the late policy for a late submission when a custom status is removed" do
        Timecop.freeze(2.days.ago(@date)) do
          submission.update!(custom_grade_status:)
          @assignment.submit_homework(@student, body: "a body")
          @assignment.grade_student(@student, grade: 600, grader: @teacher)
          expect { submission.update!(custom_grade_status: nil) }.to change {
            submission.reload.points_deducted
          }.from(nil).to(100)
        end
      end

      it "does not apply the missing policy when a custom status is applied" do
        submission.update_columns(score: nil, grade: nil, posted_at: nil, workflow_state: "unsubmitted")
        expect { submission.update!(custom_grade_status:) }.not_to change {
          submission.reload.score
        }.from(nil)
      end
    end

    it "does not apply the late policy when what-if score changes" do
      Timecop.freeze(2.days.ago(@date)) do
        @assignment.submit_homework(@student, body: "a body")
        @assignment.grade_student(@student, grade: 600, grader: @teacher)
      end
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        submission.update!(student_entered_score: 900)
        expect(submission.score).to eq 500
        expect(submission.points_deducted).to eq 100
      end
    end

    it "does not apply the late policy more than once when working with decimals with a scale of more than 2" do
      Timecop.freeze(3.days.ago(@date)) do
        @course.late_policy.update!(late_submission_deduction: 2.35)
        @assignment.submit_homework(@student, body: "a body")
        @assignment.update!(points_possible: 10)
        @assignment.grade_student(@student, grade: 10, grader: @teacher)
        SubmissionLifecycleManager.recompute(@assignment, update_grades: true)
        expect(submission.score).to be 9.76
      end
    end

    it "does not change a previous grade when student submits ungraded work" do
      asg = @course.assignments.create!(points_possible: 1000, submission_types: "online_text_entry")
      Timecop.freeze(2.days.ago(@date)) do
        asg.update!(due_at: 4.days.ago(@date))
        ph = asg.submissions.last
        expect(ph.missing?).to be true
        expect(ph.score).to eq 200
        expect(ph.points_deducted).to be_nil
      end
      Timecop.freeze(@date) do
        hw = asg.submit_homework(@student, body: "a body", submission_type: "online_text_entry")
        hw.save!
        expect(hw.late?).to be true
        expect(hw.score).to eq 200
        expect(hw.points_deducted).to be_nil
      end
    end

    it "re-applies the late policy when seconds_late_override changes" do
      Timecop.freeze(@date) do
        @assignment.submit_homework(@student, body: "a body")
        @assignment.grade_student(@student, grade: 800, grader: @teacher)
      end
      submission.update!(seconds_late_override: 3.days, late_policy_status: "late")
      expect(submission.score).to eq 650
      expect(submission.points_deducted).to eq 150
    end
  end

  include_context "url validation tests"
  it "checks url validity" do
    test_url_validation(submission_spec_model)
  end

  it "adds http:// to the body for long urls, too" do
    s = submission_spec_model(submit_homework: true)
    expect(s.url).to eq "http://www.instructure.com"

    long_url = (("a" * 300) + ".com")
    s.url = long_url
    s.save!
    expect(s.url).to eq "http://#{long_url}"
    # make sure it adds the "http://" to the body for long urls, too
    expect(s.body).to eq "http://#{long_url}"
  end

  it "offers the context, if one is available" do
    @course = Course.new
    @assignment = Assignment.new(context: @course)
    expect(@assignment).to receive(:context).and_return(@course)

    @submission = Submission.new
    expect { @submission.context }.not_to raise_error
    expect(@submission.context).to be_nil
    @submission.assignment = @assignment
    expect(@submission.context).to eql(@course)
  end

  it "has an interesting state machine" do
    submission_spec_model(submit_homework: true)
    expect(@submission.state).to be(:submitted)
    @submission.grade_it
    expect(@submission.state).to be(:graded)
  end

  it "is versioned" do
    submission_spec_model
    expect(@submission).to respond_to(:versions)
  end

  it "does not save new versions by default" do
    submission_spec_model
    expect do
      @submission.save!
    end.not_to change(@submission.versions, :count)
  end

  it "does not create a new version if only the posted_at field is updated" do
    submission_spec_model
    expect do
      @submission.update!(posted_at: Time.zone.now)
    end.not_to change {
      @submission.reload.versions.count
    }
  end

  it "does not update the most recent version if only the posted_at field is updated" do
    submission_spec_model
    expect do
      @submission.update!(posted_at: Time.zone.now)
    end.not_to change {
      @submission.reload.versions.first.model.posted_at
    }
  end

  describe "version indexing" do
    it "creates a SubmissionVersion when a new submission is created" do
      expect do
        submission_spec_model
      end.to change(SubmissionVersion, :count)
    end

    it "creates a SubmissionVersion when a new version is saved" do
      submission_spec_model
      expect do
        @submission.with_versioning(explicit: true) { @submission.save }
      end.to change(SubmissionVersion, :count)
    end

    it "does not fail preload if versionable is nil" do
      submission_spec_model
      version = Version.find_by(versionable: @submission)
      version.update_attribute(:versionable_id, Submission.last.id + 1)
      expect do
        ActiveRecord::Associations.preload([version].map(&:model), :originality_reports)
      end.not_to raise_error
    end

    it "retries if there is a conflict with an existing version number" do
      submission_spec_model
      version = Version.find_by(versionable: @submission)

      allow(@submission.versions).to receive(:create).and_call_original
      called_times = 0
      expect(@submission.versions).to receive(:create) { |attributes|
        called_times += 1
        # This is a hacky way of mimicking a bug where two responses, received very quickly,
        # would create a RecordNotUnique error when the tried to increment the version
        # number at the same time.
        v = Version.create(
          **attributes,
          versionable_id: version.versionable_id,
          versionable_type: version.versionable_type
        )
        v.update_attribute(:number, version.number) if called_times == 1
        v
      }.twice

      submission_versions = @submission.versions.count
      @submission.with_versioning(explicit: true) do
        @submission.broadcast_group_submission
      end

      expect(@submission.versions.count).to eq(submission_versions + 1)
    end
  end

  it "ensures the media object exists" do
    assignment_model
    se = @course.enroll_student(user_factory)
    expect(MediaObject).to receive(:ensure_media_object).with("fake", { context: se.user, user: se.user })
    @submission = @assignment.submit_homework(se.user, media_comment_id: "fake", media_comment_type: "audio")
  end

  describe "#grade_change_audit" do
    before(:once) { Auditors::ActiveRecord::Partitioner.process }

    let_once(:submission) { @assignment.submissions.find_by(user: @student) }

    it "logs submissions with grade changes" do
      expect(Auditors::GradeChange).to receive(:record).once
      submission.update!(grader: @teacher, score: 5)
    end

    it "grade change event author can be set" do
      assistant = User.create!
      @course.enroll_ta(assistant, enrollment_state: "active")

      expect(Auditors::GradeChange).to receive(:record).once do |args|
        expect(args[:submission].grader_id).to eq assistant.id
      end

      submission.grade_change_event_author_id = assistant.id
      submission.update!(score: 5)
    end

    it "uses the existing grader_id as the author if grade_change_event_author_id is not set" do
      @assignment.grade_student(@student, grade: 10, grader: @teacher)

      expect(Auditors::GradeChange).to receive(:record).once do |args|
        expect(args[:submission].grader_id).to eq @teacher.id
      end

      submission.reload.update!(score: 5)
    end

    it "logs excused submissions" do
      expect(Auditors::GradeChange).to receive(:record).once
      submission.update!(excused: true, grader: @user)
    end

    it "logs just one submission affected by assignment update" do
      expect(Auditors::GradeChange).to receive(:record).twice
      # only graded submissions are updated by assignment
      submission.update!(score: 111, workflow_state: "graded")
      @assignment.update!(points_possible: 999)
    end

    it "does not log ungraded submission change when assignment muted" do
      expect(Auditors::GradeChange).not_to receive(:record)
      @assignment.mute!
      @assignment.unmute!
    end

    it "inserts a grade change audit record by default" do
      expect(Auditors::GradeChange).to receive(:record).once
      submission.grade_change_audit(force_audit: true)
    end

    it "does not insert a grade change audit record if grade not changed" do
      expect(Auditors::GradeChange::Stream).not_to receive(:insert)
      submission.grade_change_audit(force_audit: true)
    end

    it "inserts a grade change audit record if grade changed" do
      expect(Auditors::GradeChange::Stream).to receive(:insert)
      submission.score = 11
      submission.save!
    end

    it "emits a grade change live event when force_audit" do
      expect(Canvas::LiveEvents).to receive(:grade_changed).once
      submission.grade_change_audit(force_audit: true)
    end

    it "moves mastery path along on force audit if appropriate" do
      expect(ConditionalRelease::Rule).to receive(:is_trigger_assignment?).with(submission.assignment).once
      submission.update! score: 1, workflow_state: :graded, posted_at: Time.zone.now
      submission.grade_change_audit(force_audit: true)
    end

    context "with flag mastery_path_submission_trigger_reloaded_evaluation" do
      before(:once) do
        Account.site_admin.enable_feature!(:mastery_path_submission_trigger_reloaded_evaluation)
      end

      it "uses persisted values for mastery path evaluation even when attributes change within transaction" do
        allow(submission.assignment).to receive(:queue_conditional_release_grade_change_handler?).and_return(true)
        expect(submission).to receive(:queue_conditional_release_grade_change_handler).once

        ActiveRecord::Base.transaction do
          submission.update!(score: 11, workflow_state: :graded, posted_at: Time.zone.now)
          submission.posted_at = nil
        end
      end

      it "uses persisted values for mastery path evaluation even when attributes are updated with a different instance of the record" do
        allow(submission.assignment).to receive(:queue_conditional_release_grade_change_handler?).and_return(true)
        expect(submission).to receive(:queue_conditional_release_grade_change_handler).once

        ActiveRecord::Base.transaction do
          submission.update!(score: 11, workflow_state: :graded, posted_at: nil)
          submission2 = Submission.find(submission.id)
          submission2.update(posted_at: Time.zone.now)
        end
      end
    end
  end

  describe "#queue_conditional_release_grade_change_handler" do
    let_once(:submission) { @assignment.submissions.find_by(user: @student) }

    context "with conditional_release_tuning plugin priority set to 'high'" do
      it "queues override handler job with high priority" do
        allow(submission.assignment).to receive(:queue_conditional_release_grade_change_handler?).and_return(true)
        PluginSetting.create(name: "conditional_release_tuning", settings: { priority: "high" })
        submission.update!(score: 11, workflow_state: :graded, posted_at: Time.zone.now)

        job = Delayed::Job.find_by(tag: "ConditionalRelease::OverrideHandler.handle_grade_change")
        expect(job.priority).to eq(Delayed::HIGH_PRIORITY)
      end
    end

    context "with conditional_release_tuning plugin priority set to 'normal'" do
      it "queues override handler job with normal priority" do
        allow(submission.assignment).to receive(:queue_conditional_release_grade_change_handler?).and_return(true)
        PluginSetting.create(name: "conditional_release_tuning", settings: { priority: "normal" })
        submission.update!(score: 11, workflow_state: :graded, posted_at: Time.zone.now)

        job = Delayed::Job.find_by(tag: "ConditionalRelease::OverrideHandler.handle_grade_change")
        expect(job.priority).to eq(Delayed::NORMAL_PRIORITY)
      end
    end

    context "with conditional_release_tuning plugin priority unset" do
      it "queues override handler job with low priority" do
        allow(submission.assignment).to receive(:queue_conditional_release_grade_change_handler?).and_return(true)
        submission.update!(score: 11, workflow_state: :graded, posted_at: Time.zone.now)

        job = Delayed::Job.find_by(tag: "ConditionalRelease::OverrideHandler.handle_grade_change")
        expect(job.priority).to eq(Delayed::LOW_PRIORITY)
      end
    end
  end

  describe "#graded_anonymously" do
    it "saves when grade changed and set explicitly" do
      submission_spec_model
      expect(@submission.graded_anonymously).to be_falsey
      @submission.score = 42
      @submission.graded_anonymously = true
      @submission.save!
      expect(@submission.graded_anonymously).to be_truthy
      @submission.reload
      expect(@submission.graded_anonymously).to be_truthy
    end

    it "retains its value when grade does not change" do
      submission_spec_model(graded_anonymously: true, score: 3, grade: "3")
      @submission = Submission.find(@submission.id) # need new model object
      expect(@submission.graded_anonymously).to be_truthy
      @submission.body = "test body"
      @submission.save!
      @submission.reload
      expect(@submission.graded_anonymously).to be_truthy
    end

    it "resets when grade changed and not set explicitly" do
      submission_spec_model(graded_anonymously: true, score: 3, grade: "3")
      @submission = Submission.find(@submission.id) # need new model object
      expect(@submission.graded_anonymously).to be_truthy
      @submission.score = 42
      @submission.save!
      @submission.reload
      expect(@submission.graded_anonymously).to be_falsey
    end
  end

  context "Discussion Topic" do
    it "submitted_at does not change when a second discussion entry is created" do
      course_with_student(active_all: true)
      @topic = @course.discussion_topics.create(title: "some topic")
      @assignment = @course.assignments.create(title: "some discussion assignment")
      @assignment.submission_types = "discussion_topic"
      @assignment.save!
      @entry1 = @topic.discussion_entries.create(message: "first entry", user: @user)
      @topic.assignment_id = @assignment.id
      @topic.save!

      Timecop.freeze(30.minutes.from_now) do
        expect do
          @topic.discussion_entries.create(message: "second entry", user: @user)
        end.not_to(change { @assignment.submissions.find_by(user: @user).submitted_at })
      end
    end

    it "does not create multiple versions on submission for discussion topics" do
      course_with_student(active_all: true)
      @topic = @course.discussion_topics.create(title: "some topic")
      @assignment = @course.assignments.create(title: "some discussion assignment")
      @assignment.submission_types = "discussion_topic"
      @assignment.save!
      @topic.assignment_id = @assignment.id
      @topic.save!

      Timecop.freeze(1.second.ago) do
        @assignment.submit_homework(@student, submission_type: "discussion_topic")
      end
      @assignment.submit_homework(@student, submission_type: "discussion_topic")
      expect(@student.submissions.first.submission_history.count).to eq 1
    end
  end

  context "broadcast policy" do
    context "Submission Notifications" do
      before :once do
        Notification.create(name: "Assignment Submitted", category: "TestImmediately")
        Notification.create(name: "Assignment Resubmitted")
        Notification.create(name: "Assignment Submitted Late")
        Notification.create(name: "Group Assignment Submitted Late")

        course_with_teacher(course: @course, active_all: true)
      end

      it "sends the correct message when an assignment is turned in on-time" do
        @assignment.workflow_state = "published"
        @assignment.update(due_at: 15.minutes.from_now)

        submission_spec_model(user: @student, submit_homework: true)
        expect(@submission.messages_sent.keys).to eq ["Assignment Submitted"]
      end

      it "does not send a message to a TA without grading rights" do
        limited_role = custom_ta_role("limitedta", account: @course.account)
        [:view_all_grades, :manage_grades].each do |permission|
          @course.account.role_overrides.create!(permission:, enabled: false, role: limited_role)
        end

        limited_ta = user_factory(active_all: true, active_cc: true)
        @course.enroll_user(limited_ta, "TaEnrollment", role: limited_role, enrollment_state: "active")
        normal_ta = user_factory(active_all: true, active_cc: true)
        @course.enroll_user(normal_ta, "TaEnrollment", enrollment_state: "active")

        Notification.where(name: "Assignment Submitted").first

        @assignment.workflow_state = "published"
        @assignment.update(due_at: 15.minutes.from_now)

        submission_spec_model(user: @student, submit_homework: true)

        expect(@submission.messages_sent["Assignment Submitted"].map(&:user)).not_to include(limited_ta)
        expect(@submission.messages_sent["Assignment Submitted"].map(&:user)).to include(normal_ta)
      end

      it "sends the correct message when an assignment is turned in late" do
        @assignment.workflow_state = "published"
        @assignment.update(due_at: Time.zone.now - 1000)

        submission_spec_model(user: @student, submit_homework: true)
        expect(@submission.messages_sent.keys).to eq ["Assignment Submitted Late"]
      end

      it "sends the correct message when an assignment is resubmitted on-time" do
        @assignment.submission_types = ["online_text_entry"]
        @assignment.due_at = Time.zone.now + 1000
        @assignment.save!

        @assignment.submit_homework(@student, body: "lol")
        resubmission = @assignment.submit_homework(@student, body: "frd")
        expect(resubmission.messages_sent.keys).to eq ["Assignment Resubmitted"]
      end

      it "sends the correct message when an assignment is resubmitted late" do
        @assignment.submission_types = ["online_text_entry"]
        @assignment.due_at = Time.zone.now - 1000
        @assignment.save!

        @assignment.submit_homework(@student, body: "lol")
        resubmission = @assignment.submit_homework(@student, body: "frd")
        expect(resubmission.messages_sent.keys).to eq ["Assignment Submitted Late"]
      end

      it "sends the correct message when a group assignment is submitted late" do
        @a = assignment_model(course: @context, group_category: "Study Groups", due_at: Time.zone.now - 1000, submission_types: ["online_text_entry"])
        @group1 = @a.context.groups.create!(name: "Study Group 1", group_category: @a.group_category)
        @group1.add_user(@student)
        submission = @a.submit_homework @student, submission_type: "online_text_entry", body: "blah"

        expect(submission.messages_sent.keys).to eq ["Group Assignment Submitted Late"]
      end

      context "Submission Posted" do
        let(:submission) { @assignment.submissions.find_by!(user: @student) }
        let(:submission_posted_messages) do
          Message.where(
            communication_channel: @student.email_channel,
            notification: @submission_posted_notification
          )
        end

        before(:once) do
          @submission_posted_notification = Notification.find_or_create_by(
            category: "Grading",
            name: "Submission Posted"
          )
          @student.update!(email: "fakeemail@example.com")
          @student.email_channel.update!(workflow_state: :active)
        end

        it "does not send a notification when a submission is not being posted" do
          expect { submission.update!(body: "hello") }.not_to change { submission_posted_messages.count }
        end

        context "when grade_posting_in_progress is true" do
          before do
            submission.grade_posting_in_progress = true
          end

          it "sends a notification when a submission is posted and assignment posts manually" do
            @assignment.ensure_post_policy(post_manually: true)

            expect do
              submission.update!(posted_at: Time.zone.now)
            end.to change {
              submission_posted_messages.count
            }.by(1)
          end

          it "sends a notification when a submission is posted and assignment posts automatically" do
            expect do
              submission.update!(posted_at: Time.zone.now)
            end.to change {
              submission_posted_messages.count
            }.by(1)
          end
        end

        context "when grade_posting_in_progress is false" do
          before do
            submission.grade_posting_in_progress = false
          end

          it "does not send a notification when a submission is posted and assignment posts manually" do
            @assignment.ensure_post_policy(post_manually: true)

            expect do
              submission.update!(posted_at: Time.zone.now)
            end.not_to change {
              submission_posted_messages.count
            }
          end

          it "does not send a notification when a submission is posted and assignment posts automatically" do
            expect do
              submission.update!(posted_at: Time.zone.now)
            end.not_to change {
              submission_posted_messages.count
            }
          end
        end
      end
    end

    context "Submission Graded" do
      before :once do
        Auditors::ActiveRecord::Partitioner.process
        @assignment.ensure_post_policy(post_manually: false)
        Notification.create(name: "Submission Graded", category: "TestImmediately")
        submission_spec_model(submit_homework: true)
      end

      it "updates 'graded_at' on the submission when the late_policy_status is changed" do
        now = Time.zone.now
        Timecop.freeze(1.hour.ago(now)) do
          @submission.update!(late_policy_status: "late")
        end
        Timecop.freeze(now) do
          @submission.update!(late_policy_status: "missing")
        end
        expect(@submission.graded_at).to eq now
      end

      it "creates a message when the assignment has been graded and published" do
        communication_channel(@user, { username: "somewhere@test.com" })
        @submission.reload
        expect(@submission.assignment).to eql(@assignment)
        expect(@submission.assignment.state).to be(:published)
        @submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
        expect(@submission.messages_sent).to include("Submission Graded")
      end

      it "does not create a message for a soft-concluded student" do
        @course.start_at = 2.weeks.ago
        @course.conclude_at = 1.week.ago
        @course.restrict_enrollments_to_course_dates = true
        @course.save!

        communication_channel(@user, { username: "somewhere@test.com" })
        @submission.reload
        expect(@submission.assignment).to eql(@assignment)
        expect(@submission.assignment.state).to be(:published)
        @submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
        expect(@submission.messages_sent).to_not include("Submission Graded")
      end

      it "notifies observers" do
        course_with_observer(course: @course, associated_user_id: @user.id, active_all: true, active_cc: true)
        @assignment.grade_student(@student, grader: @teacher, score: 5)
        expect(@observer.email_channel.messages.length).to eq 1
      end

      it "does not create a message when a muted assignment has been graded and published" do
        communication_channel(@user, { username: "somewhere@test.com" })
        @assignment.ensure_post_policy(post_manually: true)
        @submission.reload
        expect(@submission.assignment).to eql(@assignment)
        expect(@submission.assignment.state).to be(:published)
        @submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
        expect(@submission.messages_sent).not_to include "Submission Graded"
      end

      it "does not create a message when this is a quiz submission" do
        communication_channel(@user, { username: "somewhere@test.com" })
        @quiz = Quizzes::Quiz.create!(context: @course)
        @submission.quiz_submission = @quiz.generate_submission(@user)
        @submission.save!
        @submission.reload
        expect(@submission.assignment).to eql(@assignment)
        expect(@submission.assignment.state).to be(:published)
        @submission = @assignment.grade_student(@student, grader: @teacher, score: 5).first
        expect(@submission.messages_sent).not_to include("Submission Graded")
      end

      it "creates a hidden stream_item_instance when muted, graded, and published" do
        communication_channel(@user, { username: "somewhere@test.com" })
        @assignment.ensure_post_policy(post_manually: true)
        expect do
          @assignment.grade_student(@user, grade: 10, grader: @teacher).first
        end.to change StreamItemInstance, :count
        expect(@user.stream_item_instances.last).to be_hidden
      end

      it "hides any existing stream_item_instances when grades are hidden" do
        communication_channel(@user, { username: "somewhere@test.com" })
        expect do
          @assignment.grade_student(@student, grader: @teacher, score: 5).first
        end.to change StreamItemInstance, :count
        expect(@user.stream_item_instances.last).not_to be_hidden
        @assignment.hide_submissions
        expect(@user.stream_item_instances.last).to be_hidden
      end

      it "shows hidden stream_item_instances when grades are posted" do
        communication_channel(@user, { username: "somewhere@test.com" })
        @assignment.ensure_post_policy(post_manually: true)
        expect do
          @assignment.update_submission(@student, author: @teacher, comment: "some comment")
        end.to change StreamItemInstance, :count
        expect(@submission.submission_comments.last).to be_hidden
        expect(@user.stream_item_instances.last).to be_hidden
        @assignment.post_submissions
        expect(@submission.submission_comments.last).to_not be_hidden
        expect(@submission.reload.submission_comments_count).to eq 1
        expect(@user.stream_item_instances.last).to_not be_hidden
      end

      it "does not create hidden stream_item_instances for instructors when muted, graded, and published" do
        communication_channel(@teacher, { username: "somewhere@test.com" })
        @assignment.mute!
        expect do
          @submission.add_comment(author: @student, comment: "some comment")
        end.to change StreamItemInstance, :count
        expect(@teacher.stream_item_instances.last).to_not be_hidden
      end

      it "does not hide any existing stream_item_instances for instructors when muted" do
        communication_channel(@teacher, { username: "somewhere@test.com" })
        expect do
          @submission.add_comment(author: @student, comment: "some comment")
        end.to change StreamItemInstance, :count
        expect(@teacher.stream_item_instances.last).to_not be_hidden
        @assignment.mute!
        @teacher.reload
        expect(@teacher.stream_item_instances.last).to_not be_hidden
      end

      it "does not create a message for admins and teachers with quiz submissions" do
        course_with_teacher(active_all: true)
        assignment = @course.assignments.create!(
          title: "assignment",
          points_possible: 10
        )
        quiz = @course.quizzes.build(
          assignment_id: assignment.id,
          title: "test quiz",
          points_possible: 10
        )
        quiz.workflow_state = "available"
        quiz.save!

        user = account_admin_user
        communication_channel(user, { username: "admin@example.com" })
        submission = quiz.generate_submission(user, false)
        Quizzes::SubmissionGrader.new(submission).grade_submission

        communication_channel(@teacher, { username: "chang@example.com" })
        submission2 = quiz.generate_submission(@teacher, false)
        Quizzes::SubmissionGrader.new(submission2).grade_submission

        expect(submission.submission.messages_sent).not_to include("Submission Graded")
        expect(submission2.submission.messages_sent).not_to include("Submission Graded")
      end
    end

    it "creates a stream_item_instance when graded and published" do
      Notification.create name: "Submission Graded"
      submission_spec_model
      communication_channel(@user, { username: "somewhere@test.com" })
      expect do
        @assignment.grade_student(@user, grade: 10, grader: @teacher)
      end.to change StreamItemInstance, :count
    end

    it "creates a stream_item_instance when graded, and then made it visible when unmuted" do
      Notification.create name: "Submission Graded"
      submission_spec_model
      communication_channel(@user, { username: "somewhere@test.com" })
      @assignment.mute!
      expect do
        @assignment.grade_student(@user, grade: 10, grader: @teacher)
      end.to change StreamItemInstance, :count

      @assignment.unmute!
      stream_item_ids       = StreamItem.where(asset_type: "Submission", asset_id: @assignment.submissions.all).pluck(:id)
      stream_item_instances = StreamItemInstance.where(stream_item_id: stream_item_ids)
      stream_item_instances.each { |sii| expect(sii).not_to be_hidden }
    end

    context "stream item comments" do
      before :once do
        course_with_student(active_all: true)
        @a1 = assignment_model(course: @course, group_category: "Study Groups", due_at: Time.zone.now - 1000, submission_types: ["online_text_entry"], suppress_assignment: true)
      end

      it "does not show stream item comments when suppress_assignment is true" do
        @a1.suppress_assignment = true
        @a1.save!
        @submission = @a1.submit_homework(@student, body: "some message")
        @submission.add_comment(author: @teacher, comment: "a")
        stream_item_ids       = StreamItem.where(asset_type: "Submission", asset_id: @a1.submissions.all).pluck(:id)
        stream_item_instances = StreamItemInstance.where(stream_item_id: stream_item_ids)
        expect(stream_item_instances).to all be_hidden
      end

      it "shows stream item comments when suppress_assignment is false" do
        @a1.suppress_assignment = false
        @a1.save!
        @submission = @a1.submit_homework(@student, body: "some message")
        @submission.add_comment(author: @teacher, comment: "a")
        stream_item_ids       = StreamItem.where(asset_type: "Submission", asset_id: @a1.submissions.all).pluck(:id)
        stream_item_instances = StreamItemInstance.where(stream_item_id: stream_item_ids)
        stream_item_instances.each { |sii| expect(sii).not_to be_hidden }
      end
    end

    context "Submission Grade Changed" do
      before :once do
        Auditors::ActiveRecord::Partitioner.process
        @assignment.ensure_post_policy(post_manually: false)
      end

      it "creates a message when the score is changed and the grades were already published" do
        Notification.create(name: "Submission Grade Changed")
        allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
        submission_spec_model

        communication_channel(@user, { username: "somewhere@test.com" })
        s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
        s.graded_at = Time.zone.parse("Jan 1 2000")
        s.save
        @submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
        expect(@submission).to eql(s)
        expect(@submission.messages_sent).to include("Submission Grade Changed")
      end

      it "does not create a grade changed message when theres a quiz attached" do
        Notification.create(name: "Submission Grade Changed")
        allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
        submission_spec_model

        @quiz = Quizzes::Quiz.create!(context: @course)
        @submission.quiz_submission = @quiz.generate_submission(@user)
        @submission.save!
        communication_channel(@user, { username: "somewhere@test.com" })
        s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
        s.graded_at = Time.zone.parse("Jan 1 2000")
        s.save
        @submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
        expect(@submission).to eql(s)
        expect(@submission.messages_sent).not_to include("Submission Grade Changed")
      end

      it "does not create a message when grades were already published for an assignment with hidden grades" do
        @assignment.ensure_post_policy(post_manually: true)
        Notification.create(name: "Submission Grade Changed")
        allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
        submission_spec_model

        communication_channel(@user, { username: "somewhere@test.com" })
        s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
        s.graded_at = Time.zone.parse("Jan 1 2000")
        s.save
        @submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
        expect(@submission).to eql(s)
        expect(@submission.messages_sent).not_to include("Submission Grade Changed")
      end

      it "does not create a message when the submission was recently graded" do
        Notification.create(name: "Submission Grade Changed")
        allow(@assignment).to receive_messages(score_to_grade: "10.0", due_at: Time.zone.now - 100)
        submission_spec_model

        communication_channel(@user, { username: "somewhere@test.com" })
        s = @assignment.grade_student(@user, grade: 10, grader: @teacher)[0] # @submission
        @submission = @assignment.grade_student(@user, grade: 9, grader: @teacher)[0]
        expect(@submission).to eql(s)
        expect(@submission.messages_sent).not_to include("Submission Grade Changed")
      end
    end
  end

  describe "permission policy" do
    describe "can :grade" do
      before do
        @submission = Submission.new
        @grader = User.new
      end

      it "delegates to can_grade?" do
        [true, false].each do |value|
          allow(@submission).to receive(:can_grade?).with(@grader).and_return(value)

          expect(@submission.grants_right?(@grader, :grade)).to eq(value)
        end
      end
    end

    describe "can :read_grade" do
      before(:once) do
        @course = Course.create!
        @student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
        @assignment = @course.assignments.create!
        @submission = @assignment.submissions.find_by(user: @student)
      end

      it "returns true when their submission is posted and assignment manually posts" do
        @assignment.ensure_post_policy(post_manually: true)
        @submission.update!(posted_at: Time.zone.now)
        expect(@submission.grants_right?(@student, :read_grade)).to be true
      end

      it "returns false when their submission is not posted and assignment manually posts" do
        @assignment.ensure_post_policy(post_manually: true)
        expect(@submission.grants_right?(@student, :read_grade)).to be false
      end

      it "returns true when their submission is posted and assignment automatically posts" do
        @assignment.ensure_post_policy(post_manually: false)
        @submission.update!(posted_at: Time.zone.now)
        expect(@submission.grants_right?(@student, :read_grade)).to be true
      end

      it "returns true when their submission is not posted and assignment automatically posts" do
        @assignment.ensure_post_policy(post_manually: false)
        expect(@submission.grants_right?(@student, :read_grade)).to be true
      end
    end

    describe "can :comment" do
      before(:once) do
        @course = Course.create!
        @student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
        @assignment = @course.assignments.create!
        @submission = @assignment.submissions.find_by(user: @student)
      end

      it "allows students to comment on own submission" do
        expect(@submission.grants_right?(@student, :comment)).to be true
      end

      it "does not allow students in limited access accounts to comment on submissions" do
        @course.root_account.enable_feature!(:allow_limited_access_for_students)
        @course.account.settings[:enable_limited_access_for_students] = true
        @course.account.save!
        expect(@submission.grants_right?(@student, :comment)).to be false
      end
    end
  end

  describe "computation of scores" do
    before(:once) do
      @assignment.ensure_post_policy(post_manually: false)
      @assignment.update!(points_possible: 10)
      submission_spec_model
    end

    let(:scores) do
      enrollment = Enrollment.where(user_id: @submission.user_id, course_id: @submission.context).first
      enrollment.scores.order(:grading_period_id)
    end

    let(:grading_period_scores) do
      scores.where.not(grading_period_id: nil)
    end

    let(:course_scores) do
      scores.where(course_score: true)
    end

    let(:course_and_grading_period_scores) do
      scores.where(course_score: true).or(scores.where.not(grading_period_id: nil).where(assignment_group_id: nil))
    end

    it "recomputes course scores when the submission score changes" do
      expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
        course_scores.pluck(:current_score)
      }.from([nil]).to([50.0])
    end

    context "with grading periods" do
      before(:once) do
        @now = Time.zone.now
        course = @submission.context
        assignment_outside_of_period = course.assignments.create!(
          due_at: 10.days.from_now(@now),
          points_possible: 10
        )
        assignment_outside_of_period.grade_student(@user, grade: 8, grader: @teacher)
        @assignment.update!(due_at: @now)
        @root_account = course.root_account
        group = @root_account.grading_period_groups.create!
        group.enrollment_terms << course.enrollment_term
        @grading_period = group.grading_periods.create!(
          title: "Current Grading Period",
          start_date: 5.days.ago(@now),
          end_date: 5.days.from_now(@now)
        )
      end

      it "updates the course score and grading period score if a submission " \
         "in a grading period is graded" do
        expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
          course_and_grading_period_scores.pluck(:current_score)
        }.from([nil, 80.0]).to([50.0, 65.0])
      end

      it "only updates the course score (not the grading period score) if a submission " \
         "not in a grading period is graded" do
        day_after_grading_period_ends = 1.day.from_now(@grading_period.end_date)
        @assignment.update!(due_at: day_after_grading_period_ends)
        expect { @assignment.grade_student(@student, grader: @teacher, score: 5) }.to change {
          course_and_grading_period_scores.pluck(:current_score)
        }.from([nil, 80.0]).to([nil, 65.0])
      end
    end
  end

  describe "#can_grade?" do
    before do
      @account = Account.new
      @course = Course.new(account: @account)
      @assignment = Assignment.new(course: @course)
      @submission = Submission.new(assignment: @assignment)

      @grader = User.new
      @grader.id = 10
      @student = User.new
      @student.id = 42

      allow(@course).to receive(:account_membership_allows).with(@grader).and_return(true)
      allow(@course).to receive(:grants_right?).with(@grader, nil, :manage_grades).and_return(true)

      @assignment.course = @course
      allow(@assignment).to receive(:published?).and_return(true)
      grading_period = double("grading_period", closed?: false)
      allow(@submission).to receive(:grading_period).and_return(grading_period)

      @submission.grader = @grader
      @submission.user = @student
    end

    it 'returns true for published assignments if the grader is a teacher who is allowed to
        manage grades' do
      expect(@submission.grants_right?(@grader, :grade)).to be_truthy
    end

    context "when assignment is unpublished" do
      before do
        allow(@assignment).to receive(:published?).and_return(false)

        @status = @submission.grants_right?(@grader, :grade)
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("unpublished")
      end
    end

    context "when the grader does not have the right to manage grades for the course" do
      before do
        allow(@course).to receive(:grants_right?).with(@grader, nil, :manage_grades).and_return(false)

        @status = @submission.grants_right?(@grader, :grade)
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("manage grades")
      end
    end

    context "when the grader is a teacher and the assignment is in a closed grading period" do
      before do
        allow(@course).to receive(:account_membership_allows).with(@grader).and_return(false)
        grading_period = double("grading_period", closed?: true)
        allow(@submission).to receive(:grading_period).and_return(grading_period)

        @status = @submission.grants_right?(@grader, :grade)
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("closed grading period")
      end
    end

    context "when grader_id is a teacher's id and the assignment is in a closed grading period" do
      before do
        allow(@course).to receive(:account_membership_allows).with(@grader).and_return(false)
        grading_period = double("grading_period", closed?: true)
        allow(@submission).to receive(:grading_period).and_return(grading_period)
        @submission.grader = nil
        @submission.grader_id = 10

        @status = @submission.grants_right?(@grader, :grade)
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("closed grading period")
      end
    end

    it 'returns true if the grader is an admin even if the assignment is in
        a closed grading period' do
      allow(@course).to receive(:account_membership_allows).with(@grader).and_return(true)
      grading_period = double("grading_period", closed?: false)
      allow(@submission).to receive(:grading_period).and_return(grading_period)

      expect(@submission.grants_right?(@grader, :grade)).to be_truthy
    end
  end

  describe "#can_autograde?" do
    before do
      @account = Account.new
      @course = Course.new(account: @account)
      @assignment = Assignment.new(course: @course)
      @submission = Submission.new(assignment: @assignment)

      @submission.grader_id = -1
      @submission.user_id = 10

      allow(@assignment).to receive(:published?).and_return(true)
      grading_period = double("grading_period", closed?: false)
      allow(@submission).to receive(:grading_period).and_return(grading_period)
    end

    it 'returns true for published assignments with an autograder and when the assignment is not
        in a closed grading period' do
      expect(@submission.can_autograde?).to be_truthy
    end

    context "when assignment is unpublished" do
      before do
        allow(@assignment).to receive(:published?).and_return(false)

        @status = @submission.can_autograde?
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("unpublished")
      end
    end

    context "when the grader is not an autograder" do
      before do
        @submission.grader_id = 1

        @status = @submission.can_autograde?
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("autograded")
      end
    end

    context "when the assignment is in a closed grading period for the student" do
      before do
        grading_period = double("grading_period", closed?: true)
        allow(@submission).to receive(:grading_period).and_return(grading_period)

        @status = @submission.can_autograde?
      end

      it "returns false" do
        expect(@status).to be_falsey
      end

      it "sets an appropriate error message" do
        expect(@submission.grading_error_message).to include("closed grading period")
      end
    end
  end

  describe "#can_read_submission_user_name?" do
    before(:once) do
      @course = Course.create!
      @student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
      @teacher = @course.enroll_teacher(User.create!, enrollment_state: "active").user
      @assignment = @course.assignments.create!
      @submission = @assignment.submissions.find_by(user: @student)
    end

    context "with anonymous grading" do
      before(:once) do
        @assignment.update!(anonymous_grading: true)
      end

      it "returns true when the user is the submission's owner" do
        expect(@submission.can_read_submission_user_name?(@student, nil)).to be true
      end

      it "returns false when the user is not the submission's owner" do
        expect(@submission.can_read_submission_user_name?(@teacher, nil)).to be false
      end
    end

    context "anonymous peer reviews" do
      before(:once) do
        @assignment.update!(anonymous_peer_reviews: true)
        @reviewer = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
      end

      it "returns true when the user is the submission's owner" do
        expect(@submission.can_read_submission_user_name?(@student, nil)).to be true
      end

      it "returns false for peer reviewers" do
        expect(@submission.can_read_submission_user_name?(@reviewer, nil)).to be false
      end

      it "returns true for users with view_all_grades permission" do
        admin = account_admin_user(account: @course.root_account)
        expect(@submission.can_read_submission_user_name?(admin, nil)).to be true
      end
    end

    context "with both anonymous grading and anonymous peer reviews enabled" do
      before(:once) do
        @assignment.update!(anonymous_grading: true, anonymous_peer_reviews: true)
        @reviewer = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
      end

      it "returns true when the user is the submission's owner" do
        expect(@submission.can_read_submission_user_name?(@student, nil)).to be true
      end

      it "returns false for teachers" do
        expect(@submission.can_read_submission_user_name?(@teacher, nil)).to be false
      end

      it "returns false for peer reviewers" do
        expect(@submission.can_read_submission_user_name?(@reviewer, nil)).to be false
      end
    end

    context "with session context" do
      before(:once) do
        @assignment.update!(anonymous_peer_reviews: true)
        @ta = @course.enroll_ta(User.create!, enrollment_state: "active").user
        @admin = account_admin_user(account: @course.root_account)
      end

      it "uses session context for permissions" do
        mock_session = { become_user_id: @admin.id }
        # Even as a TA, when in "become_user" mode as admin, you get admin permissions
        expect(@submission.can_read_submission_user_name?(@ta, mock_session)).to be true
      end
    end

    context "with different permission levels" do
      before(:once) do
        @assignment.update!(anonymous_peer_reviews: true)
        @ta_with_permissions = @course.enroll_ta(User.create!, enrollment_state: "active").user
        role = custom_account_role("Limited TA", account: @course.account)
        @ta_without_permissions = @course.enroll_user(
          User.create!,
          "TaEnrollment",
          enrollment_state: "active",
          role:
        ).user
        # Remove view_all_grades permission from the limited TA role
        @course.account.role_overrides.create!(
          permission: "view_all_grades",
          enabled: false,
          role:
        )
      end

      it "allows TAs with view_all_grades permission to see names" do
        expect(@submission.can_read_submission_user_name?(@ta_with_permissions, nil)).to be true
      end

      it "prevents TAs without view_all_grades permission from seeing names" do
        expect(@submission.can_read_submission_user_name?(@ta_without_permissions, nil)).to be false
      end
    end

    context "non-anonymous assignments" do
      it "returns true for any user" do
        student2 = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
        expect(@submission.can_read_submission_user_name?(student2, nil)).to be true
      end
    end
  end

  describe "#user_can_read_grade?" do
    before(:once) do
      @course = Course.create!
      @student = @course.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
      @assignment = @course.assignments.create!
      @submission = @assignment.submissions.find_by(user: @student)
    end

    it "returns true when their submission is posted and assignment manually posts" do
      @assignment.ensure_post_policy(post_manually: true)
      @submission.update!(posted_at: Time.zone.now)
      expect(@submission.user_can_read_grade?(@student)).to be true
    end

    it "returns false when their submission is not posted and assignment manually posts" do
      @assignment.ensure_post_policy(post_manually: true)
      expect(@submission.user_can_read_grade?(@student)).to be false
    end

    it "returns true when their submission is posted and assignment automatically posts" do
      @assignment.ensure_post_policy(post_manually: false)
      @submission.update!(posted_at: Time.zone.now)
      expect(@submission.user_can_read_grade?(@student)).to be true
    end

    it "returns true when their submission is not posted and assignment automatically posts" do
      @assignment.ensure_post_policy(post_manually: false)
      expect(@submission.user_can_read_grade?(@student)).to be true
    end
  end

  context "OriginalityReport" do
    let(:attachment) { attachment_model(context: group) }
    let(:course) { course_model }
    let(:submission) { submission_model }
    let(:group) { Group.create!(name: "test group", context: course) }

    let(:originality_report) do
      submission.update(attachment_ids: attachment.id.to_s)
      OriginalityReport.create!(attachment:, originality_score: "1", submission:)
    end

    describe "#originality_data" do
      it "generates the originality data" do
        originality_report.originality_report_url = "http://example.com"
        originality_report.save!
        expect(submission.originality_data).to eq(
          {
            attachment.asset_string => {
              similarity_score: originality_report.originality_score,
              state: originality_report.state,
              attachment_id: originality_report.attachment_id,
              report_url: originality_report.originality_report_url,
              status: originality_report.workflow_state,
              error_message: nil,
              created_at: originality_report.created_at,
              updated_at: originality_report.updated_at
            }
          }
        )
      end

      context "multiple originality reports for the same attachment" do
        let(:preferred_report) do
          OriginalityReport.create!(attachment:,
                                    submission:,
                                    workflow_state: preferred_state,
                                    originality_score: (preferred_state == "scored") ? 1 : nil)
        end
        let(:other_report) do
          OriginalityReport.create!(attachment:,
                                    submission:,
                                    workflow_state: other_state,
                                    originality_score: (other_state == "scored") ? 2 : nil)
        end

        before do
          submission.update(attachment_ids: attachment.id.to_s)
        end

        OriginalityReport::ORDERED_VALID_WORKFLOW_STATES.each do |state|
          context "and both reports have a workflow_state of #{state}" do
            let(:preferred_state) { state }
            let(:other_state) { state }

            it "chooses the more up-to-date report" do
              preferred_report.update!(updated_at: 1.minute.from_now)
              other_report.update!(updated_at: Time.zone.now)
              report_data = submission.originality_data[attachment.asset_string]
              expect(report_data[:similarity_score]).to eq preferred_report.originality_score
            end
          end
        end

        shared_examples_for "submission with duplicate reports with different states" do
          it "uses the preferred report" do
            preferred_report
            other_report
            report_data = submission.originality_data[attachment.asset_string]
            expect(report_data[:similarity_score]).to eq preferred_report.originality_score
            expect(report_data[:status]).to eq preferred_report.workflow_state
          end

          it "uses the preferred report even if the other report was updated later" do
            preferred_report.update(updated_at: Time.zone.now)
            other_report.update(updated_at: 1.minute.from_now)

            report_data = submission.originality_data[attachment.asset_string]
            expect(report_data[:similarity_score]).to eq preferred_report.originality_score
            expect(report_data[:status]).to eq preferred_report.workflow_state
          end
        end

        context "and the reports have differing workflow_states" do
          context "of scored and error" do
            let(:preferred_state) { "scored" }
            let(:other_state) { "error" }

            it_behaves_like "submission with duplicate reports with different states"
          end

          context "of scored and pending" do
            let(:preferred_state) { "scored" }
            let(:other_state) { "pending" }

            it_behaves_like "submission with duplicate reports with different states"
          end

          context "of error and pending" do
            let(:preferred_state) { "error" }
            let(:other_state) { "pending" }

            it_behaves_like "submission with duplicate reports with different states"
          end
        end
      end

      it "includes tii data" do
        tii_data = {
          similarity_score: 10,
          state: "acceptable",
          report_url: "http://example.com",
          status: "scored"
        }
        submission.turnitin_data[attachment.asset_string] = tii_data
        expect(submission.originality_data).to eq({
                                                    attachment.asset_string => tii_data
                                                  })
      end

      it "overwrites the tii data with the originality data" do
        originality_report.originality_report_url = "http://example.com"
        originality_report.save!
        tii_data = {
          similarity_score: 10,
          state: "acceptable",
          report_url: "http://example.com/tii",
          status: "pending"
        }
        submission.turnitin_data[attachment.asset_string] = tii_data
        expect(submission.originality_data).to eq(
          {
            attachment.asset_string => {
              similarity_score: originality_report.originality_score,
              attachment_id: attachment.id,
              state: originality_report.state,
              report_url: originality_report.originality_report_url,
              status: originality_report.workflow_state,
              error_message: nil,
              created_at: originality_report.created_at,
              updated_at: originality_report.updated_at
            }
          }
        )
      end

      it "does not cause error if originality score is nil" do
        originality_report.update(originality_score: nil)
        expect { submission.originality_data }.not_to raise_error
      end

      it "rounds the score to 2 decimal places" do
        originality_report.originality_score = 2.94997
        originality_report.save!
        expect(submission.originality_data[attachment.asset_string][:similarity_score]).to eq(2.95)
      end

      it "filters out :provider key and value" do
        originality_report.originality_report_url = "http://example.com"
        originality_report.save!
        tii_data = {
          provider: "vericite",
          similarity_score: 10,
          state: "acceptable",
          report_url: "http://example.com/tii",
          status: "pending"
        }
        submission.turnitin_data[attachment.asset_string] = tii_data
        expect(submission.originality_data).not_to include :vericite
      end

      it "finds originality data text entry submissions" do
        submission.update!(attachment_ids: attachment.id.to_s)
        originality_report.update!(attachment: nil)
        expect(submission.originality_data).to eq({
                                                    OriginalityReport.submission_asset_key(submission) => {
                                                      similarity_score: originality_report.originality_score,
                                                      attachment_id: nil,
                                                      state: originality_report.state,
                                                      report_url: originality_report.originality_report_url,
                                                      status: originality_report.workflow_state,
                                                      error_message: nil,
                                                      created_at: originality_report.created_at,
                                                      updated_at: originality_report.updated_at,
                                                    }
                                                  })
      end

      context "when originality report has an error message" do
        subject { submission.originality_data[attachment.asset_string] }

        let(:error_message) { "We can't process that file :(" }

        before { originality_report.update!(error_message:) }

        it "includes the error message" do
          expect(subject[:error_message]).to eq error_message
        end
      end
    end

    describe "#attachment_ids_for_version" do
      let(:attachments) do
        [
          attachment_model(filename: "submission-a.doc", context: @student),
          attachment_model(filename: "submission-b.doc", context: @student),
          attachment_model(filename: "submission-c.doc", context: @student)
        ]
      end
      let(:single_attachment) { attachment_model(filename: "single.doc", context: @student) }

      before { student_in_course(active_all: true) }

      it "includes attachment ids from 'attachment_id'" do
        submission = @assignment.submit_homework(@student, submission_type: "online_upload", attachments:)
        submission.update!(attachment_id: single_attachment)
        expect(submission.attachment_ids_for_version).to match_array attachments.map(&:id) + [single_attachment.id]
      end
    end

    describe "#has_originality_report?" do
      let(:test_course) do
        test_course = course_model
        test_course.enroll_teacher(test_teacher, enrollment_state: "active")
        test_course.enroll_student(test_student, enrollment_state: "active")
        test_course
      end
      let(:test_teacher) { User.create }
      let(:test_student) { User.create }
      let(:assignment) { Assignment.create!(title: "test assignment", context: test_course) }
      let(:attachment) { attachment_model(filename: "submission.doc", context: test_student) }
      let(:report_url) { "http://www.test-score.com" }

      it "returns true for standard reports" do
        submission = assignment.submit_homework(test_student, attachments: [attachment])
        OriginalityReport.create!(
          attachment:,
          submission:,
          originality_score: 0.5,
          originality_report_url: report_url
        )
        expect(submission.has_originality_report?).to be true
      end

      it "returns true for text entry reports" do
        submission = assignment.submit_homework(test_student, body: "hi")
        OriginalityReport.create!(
          submission:,
          originality_score: 0.5,
          originality_report_url: report_url
        )
        expect(submission.has_originality_report?).to be true
      end

      it "returns true for group reports" do
        user_two = test_student.dup
        user_two.update!(lti_context_id: SecureRandom.uuid, lti_id: SecureRandom.uuid, uuid: CanvasSlug.generate_securish_uuid)
        assignment.course.enroll_student(user_two)

        group = group_model(context: assignment.course)
        group.update!(users: [user_two, test_student])

        submission = assignment.submit_homework(test_student, submission_type: "online_upload", attachments: [attachment])
        submission_two = assignment.submit_homework(user_two, submission_type: "online_upload", attachments: [attachment])

        submission.update!(group:)
        submission_two.update!(group:)

        assignment.submissions.each do |s|
          s.update!(group:, turnitin_data: { blah: 1 })
        end

        report = OriginalityReport.create!(originality_score: "1", submission:, attachment:)
        report.copy_to_group_submissions!

        expect(assignment.submissions.map(&:has_originality_report?)).to match_array [true, true]
      end

      it "returns false when no reports are present" do
        submission = assignment.submit_homework(test_student, attachments: [attachment])
        expect(submission.has_originality_report?).to be false
      end
    end

    describe "#originality_report_url" do
      let_once(:test_course) do
        test_course = course_model
        test_course.enroll_teacher(test_teacher, enrollment_state: "active")
        test_course.enroll_student(test_student, enrollment_state: "active")
        test_course
      end

      let_once(:test_teacher) { User.create }
      let_once(:test_student) { User.create }
      let_once(:assignment) { Assignment.create!(title: "test assignment", context: test_course) }
      let_once(:attachment) { attachment_model(filename: "submission.doc", context: test_student) }
      let_once(:submission) { assignment.submit_homework(test_student, attachments: [attachment]) }
      let_once(:report_url) { "http://www.test-score.com" }
      let(:originality_report) do
        OriginalityReport.create!(attachment:,
                                  submission:,
                                  originality_score: 0.5,
                                  originality_report_url: report_url)
      end

      it "returns nil if no originality report exists for the submission" do
        originality_report.destroy
        expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to be_nil
      end

      it "returns nil if no report url is present in the report" do
        originality_report.update_attribute(:originality_report_url, nil)
        expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to be_nil
      end

      it "returns the originality_report_url if present" do
        originality_report
        expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq(report_url)
      end

      it "returns the report url for text entry submission reports" do
        originality_report.update!(attachment: nil)
        expect(submission.originality_report_url(submission.asset_string, test_teacher)).to eq report_url
      end

      context "when a report exists but for a different attempt" do
        it "finds the report url even when queried with a different attempt number" do
          first_submission = assignment.submit_homework(test_student, attachments: [attachment])
          OriginalityReport.create!(attachment:, submission: first_submission, originality_report_url: report_url)
          second_submission = assignment.submit_homework(test_student, body: "a newer submission")
          # We are querying the *second* submission (latest version)
          # but asking for the report for `attachment` while specifying attempt 2.
          # The report truly belongs to attempt 1, but the method should find it as a fallback.
          expect(second_submission.originality_report_url(attachment.asset_string, test_teacher, second_submission.attempt)).to eq(report_url)
        end

        it "finds the report when given no attempt number" do
          # Create initial submission with report
          originality_report

          # Create a new attempt
          Timecop.freeze(1.day.from_now) do
            assignment.submit_homework(test_student, attachments: [attachment])
          end

          # Should still find the original report
          expect(submission.reload.originality_report_url(attachment.asset_string, test_teacher))
            .to eq(report_url)
        end
      end

      context "with different workflow states" do
        let(:pending_report) do
          OriginalityReport.create!(attachment:,
                                    submission:,
                                    workflow_state: "pending",
                                    originality_report_url: "http://pending-report.com")
        end

        let(:error_report) do
          OriginalityReport.create!(attachment:,
                                    submission:,
                                    workflow_state: "error",
                                    originality_report_url: "http://error-report.com")
        end

        it "returns URL for pending reports" do
          pending_report
          expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq("http://pending-report.com")
        end

        it "returns URL for error reports" do
          error_report
          expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq("http://error-report.com")
        end

        it "prefers scored reports over pending reports" do
          originality_report # scored report
          pending_report
          expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq(report_url)
        end

        it "prefers error reports over pending reports when no scored report exists" do
          error_report
          pending_report
          expect(submission.originality_report_url(attachment.asset_string, test_teacher)).to eq("http://error-report.com")
        end
      end

      it "requires the :grade permission" do
        unauthorized_user = User.new
        expect(submission.originality_report_url(attachment.asset_string, unauthorized_user)).to be_nil
      end

      it "returns nil when given an invalid asset string" do
        originality_report
        expect(submission.originality_report_url("invalid_asset_string", test_teacher)).to be_nil
      end

      it "returns nil when given an empty asset string" do
        originality_report
        expect(submission.originality_report_url("", test_teacher)).to be_nil
      end

      context "when there are multiple originality reports" do
        context "for text entry submissions" do
          let(:other_submission) { assignment.submit_homework(test_student, body: "hello world") }
          let(:other_report_url) { "https://another-report.com" }
          let(:other_report) do
            OriginalityReport.create!(attachment: nil,
                                      submission: other_submission,
                                      originality_score: 0.4,
                                      originality_report_url: other_report_url)
          end

          it "can use attempt number to find the report url" do
            originality_report.update!(attachment: nil)
            other_report

            expect(other_submission.attempt).to be > submission.attempt
            expect(submission.originality_report_url(submission.asset_string,
                                                     test_teacher,
                                                     submission.attempt.to_s)).to eq report_url
            expect(submission.originality_report_url(submission.asset_string,
                                                     test_teacher,
                                                     other_submission.attempt.to_s)).to eq(other_report_url)
          end
        end

        context "for multiple attachments" do
          let(:other_attachment) { attachment_model(filename: "submission-b.doc", context: test_student) }
          let(:other_report_url) { "http://another-report.com" }
          let(:other_report) do
            OriginalityReport.create!(attachment: other_attachment,
                                      submission:,
                                      originality_score: 0.4,
                                      originality_report_url: other_report_url)
          end

          it "considers all attachments in submission history valid" do
            Timecop.freeze(2.days.ago) do
              assignment.submit_homework(test_student,
                                         submission_type: "online_upload",
                                         attachments: [attachment])
            end

            Timecop.freeze(1.day.ago) do
              assignment.submit_homework(test_student,
                                         submission_type: "online_upload",
                                         attachments: [other_attachment])
            end

            originality_report
            other_report
            expect(submission.originality_report_url(attachment.asset_string, test_teacher))
              .to eq(report_url)
            expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
              .to eq(other_report_url)
          end

          it "gives the correct url for each attachment" do
            assignment.submit_homework(test_student,
                                       submission_type: "online_upload",
                                       attachments: [attachment, other_attachment])
            originality_report
            other_report
            expect(submission.originality_report_url(attachment.asset_string, test_teacher))
              .to eq(report_url)
            expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
              .to eq(other_report_url)
          end

          # This combines having multiple attachments with some duplicate OriginalityReports.
          context "with some duplicate reports for an attachment" do
            let(:duplicate_url) { "http://duplicate.com" }
            let(:duplicate_report) do
              OriginalityReport.create!(attachment:,
                                        submission:,
                                        workflow_state: "pending",
                                        originality_report_url: duplicate_url)
            end

            before do
              assignment.submit_homework(test_student,
                                         submission_type: "online_upload",
                                         attachments: [attachment, other_attachment])
            end

            it "uses the scored report's URL" do
              originality_report
              other_report
              duplicate_report
              expect(submission.originality_report_url(attachment.asset_string, test_teacher))
                .to eq(report_url)
            end

            it "uses the scored report's URL even if the other report is newer" do
              originality_report.update(updated_at: 1.day.ago)
              other_report
              duplicate_report.update(updated_at: 1.day.from_now)

              expect(submission.originality_report_url(attachment.asset_string, test_teacher))
                .to eq(report_url)
            end

            it "can still get other attachment's URLs" do
              originality_report
              other_report
              duplicate_report

              expect(submission.originality_report_url(other_attachment.asset_string, test_teacher))
                .to eq(other_report_url)
            end
          end
        end

        # If we have multiple reports for the same attachment-submission combo, then
        # those reports are considered duplicates. However, they might have different states
        # so we have to be sure we're using the correct report.
        context "and the reports are for the same attachment" do
          let(:preferred_url) { "http://preferred-score.com" }
          let(:other_url) { "http://other-score.com" }
          let(:preferred_report) do
            OriginalityReport.create!(attachment:,
                                      submission:,
                                      originality_score: (preferred_state == "scored") ? 1 : nil,
                                      workflow_state: preferred_state,
                                      originality_report_url: preferred_url)
          end
          let(:other_report) do
            OriginalityReport.create!(attachment:,
                                      submission:,
                                      originality_score: (other_state == "scored") ? 2 : nil,
                                      workflow_state: other_state,
                                      originality_report_url: other_url)
          end

          before do
            submission.update(attachment_ids: attachment.id.to_s)
          end

          OriginalityReport::ORDERED_VALID_WORKFLOW_STATES.each do |state|
            context "and have the same workflow_state of #{state}" do
              let(:preferred_state) { state }
              let(:other_state) { state }

              it "chooses the more up-to-date report's URL" do
                preferred_report.update(updated_at: 1.minute.from_now)
                other_report.update(updated_at: Time.zone.now)
                expect(submission.originality_report_url(attachment.asset_string,
                                                         test_teacher)).to eq preferred_url
              end
            end
          end

          shared_examples_for "submission with duplicate reports with different states" do
            it "chooses the preferred report's URL" do
              preferred_report
              other_report
              expect(submission.originality_report_url(attachment.asset_string,
                                                       test_teacher)).to eq preferred_url
            end

            it "chooses the preferred report's URL even when the other report is newer" do
              preferred_report.update(updated_at: Time.zone.now)
              other_report.update(updated_at: 1.minute.from_now)
              expect(submission.originality_report_url(attachment.asset_string,
                                                       test_teacher)).to eq preferred_url
            end
          end

          context "and the reports have differing workflow_states" do
            context "of scored and error" do
              let(:preferred_state) { "scored" }
              let(:other_state) { "error" }

              it_behaves_like "submission with duplicate reports with different states"
            end

            context "of scored and pending" do
              let(:preferred_state) { "scored" }
              let(:other_state) { "pending" }

              it_behaves_like "submission with duplicate reports with different states"
            end

            context "of error and pending" do
              let(:preferred_state) { "error" }
              let(:other_state) { "pending" }

              it_behaves_like "submission with duplicate reports with different states"
            end
          end
        end
      end
    end
  end

  context "turnitin" do
    context "Turnitin LTI" do
      let(:lti_tii_data) do
        {
          "attachment_42" => {
            status: "error",
            outcome_response: {
              "outcomes_tool_placement_url" => "https://api.turnitin.com/api/lti/1p0/invalid?lang=en_us",
              "paperid" => "607954245",
              "lis_result_sourcedid" => "10-5-42-8-invalid"
            },
            public_error_message: "Turnitin has not returned a score after 11 attempts to retrieve one."
          }
        }
      end

      let(:submission) { Submission.new }

      describe "#turnitinable_by_lti?" do
        it "returns true if there is an associated lti tool and data stored" do
          submission.turnitin_data = lti_tii_data
          expect(submission.turnitinable_by_lti?).to be true
        end
      end

      describe "#resubmit_lti_tii" do
        let(:tool) do
          @course.context_external_tools.create(
            name: "a",
            consumer_key: "12345",
            shared_secret: "secret",
            url: "http://example.com/launch"
          )
        end

        it "resubmits errored tii attachments" do
          a = @course.assignments.create!(title: "test",
                                          submission_types: "external_tool",
                                          external_tool_tag_attributes: { url: tool.url })
          submission.assignment = a
          submission.turnitin_data = lti_tii_data
          submission.user = @user
          outcome_response_processor_mock = double("outcome_response_processor")
          expect(outcome_response_processor_mock).to receive(:resubmit).with(submission, "attachment_42")
          allow(Turnitin::OutcomeResponseProcessor).to receive(:new).and_return(outcome_response_processor_mock)
          submission.retrieve_lti_tii_score
        end

        it "resubmits errored tii attachments even if turnitin_data has non-hash values" do
          a = @course.assignments.create!(title: "test",
                                          submission_types: "external_tool",
                                          external_tool_tag_attributes: { url: tool.url })
          submission.assignment = a
          submission.turnitin_data = lti_tii_data.merge(last_processed_attempt: 1)
          submission.user = @user
          outcome_response_processor_mock = double("outcome_response_processor")
          expect(outcome_response_processor_mock).to receive(:resubmit).with(submission, "attachment_42")
          allow(Turnitin::OutcomeResponseProcessor).to receive(:new).and_return(outcome_response_processor_mock)
          submission.retrieve_lti_tii_score
        end
      end
    end

    context "submission" do
      def init_turnitin_api
        @turnitin_api = Turnitin::Client.new("test_account", "sekret")
        expect(@submission.context).to receive(:turnitin_settings).at_least(1).and_return([:placeholder])
        expect(Turnitin::Client).to receive(:new).at_least(1).with(:placeholder).and_return(@turnitin_api)
      end

      before(:once) do
        setup_account_for_turnitin(@assignment.context.account)
        @assignment.submission_types = "online_upload,online_text_entry"
        @assignment.turnitin_enabled = true
        @assignment.save!
        @submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
      end

      it "submits to turnitin after a delay" do
        job = Delayed::Job.list_jobs(:future, 100).find { |j| j.tag == "Submission#submit_to_turnitin" }
        expect(job).not_to be_nil
        expect(job.run_at).to be > Time.now.utc
      end

      it "initially sets turnitin submission to pending" do
        init_turnitin_api
        expect(@turnitin_api).to receive(:createOrUpdateAssignment).with(@assignment, @assignment.turnitin_settings).and_return({ assignment_id: "1234" })
        expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: true))
        expect(@turnitin_api).to receive(:submitPaper).and_return({
                                                                    @submission.asset_string => {
                                                                      object_id: "12345"
                                                                    }
                                                                  })
        @submission.submit_to_turnitin
        expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "pending"
      end

      it "schedules a retry if something fails initially" do
        init_turnitin_api
        expect(@turnitin_api).to receive(:createOrUpdateAssignment).with(@assignment, @assignment.turnitin_settings).and_return({ assignment_id: "1234" })
        expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false))
        @submission.submit_to_turnitin
        expect(Delayed::Job.list_jobs(:future, 100).count { |j| j.tag == "Submission#submit_to_turnitin" }).to eq 2
      end

      it "sets status as failed if something fails on a retry" do
        init_turnitin_api
        expect(@assignment).to receive(:create_in_turnitin).and_return(false)
        expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false, error?: true, error_hash: {}))
        expect(@turnitin_api).not_to receive(:submitPaper)
        @submission.submit_to_turnitin(Submission::TURNITIN_RETRY)
        expect(@submission.reload.turnitin_data[:status]).to eq "error"
      end

      it "sets status back to pending on retry" do
        init_turnitin_api
        # first a submission, to get us into failed state
        expect(@assignment).to receive(:create_in_turnitin).and_return(false)
        expect(@turnitin_api).to receive(:enrollStudent).with(@context, @user).and_return(double(success?: false, error?: true, error_hash: {}))
        expect(@turnitin_api).not_to receive(:submitPaper)
        @submission.submit_to_turnitin(Submission::TURNITIN_RETRY)
        expect(@submission.reload.turnitin_data[:status]).to eq "error"

        # resubmit
        @submission.resubmit_to_turnitin
        expect(@submission.reload.turnitin_data[:status]).to be_nil
        expect(@submission.turnitin_data[@submission.asset_string][:status]).to eq "pending"
      end

      it "sets status to scored on success" do
        init_turnitin_api
        @submission.turnitin_data ||= {}
        @submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
        expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({
                                                                                                                   similarity_score: 56,
                                                                                                                   web_overlap: 22,
                                                                                                                   publication_overlap: 0,
                                                                                                                   student_overlap: 33
                                                                                                                 })

        @submission.check_turnitin_status
        expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "scored"
      end

      it "sets status as failed if something fails after several attempts" do
        init_turnitin_api
        @submission.turnitin_data ||= {}
        @submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
        expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({})

        expects_job_with_tag("Submission#check_turnitin_status") do
          @submission.check_turnitin_status(Submission::TURNITIN_STATUS_RETRY - 1)
          expect(@submission.reload.turnitin_data[@submission.asset_string][:status]).to eq "pending"
        end

        @submission.check_turnitin_status(Submission::TURNITIN_STATUS_RETRY)
        @submission.reload
        updated_data = @submission.turnitin_data[@submission.asset_string]
        expect(updated_data[:status]).to eq "error"
      end

      it "checks status for all assets" do
        init_turnitin_api
        @submission.turnitin_data ||= {}
        @submission.turnitin_data[@submission.asset_string] = { object_id: "1234", status: "pending" }
        @submission.turnitin_data["other_asset"] = { object_id: "xxyy", status: "pending" }
        expect(@turnitin_api).to receive(:generateReport).with(@submission, @submission.asset_string).and_return({
                                                                                                                   similarity_score: 56, web_overlap: 22, publication_overlap: 0, student_overlap: 33
                                                                                                                 })
        expect(@turnitin_api).to receive(:generateReport).with(@submission, "other_asset").and_return({ similarity_score: 20 })

        @submission.check_turnitin_status
        @submission.reload
        expect(@submission.turnitin_data[@submission.asset_string][:status]).to eq "scored"
        expect(@submission.turnitin_data["other_asset"][:status]).to eq "scored"
      end

      it "does not blow up if submission_type has changed when job runs" do
        @submission.submission_type = "online_url"
        expect(@submission.context).not_to receive(:turnitin_settings)
        expect { @submission.submit_to_turnitin }.not_to raise_error
      end
    end

    describe "group" do
      before(:once) do
        @teacher = User.create(name: "some teacher")
        @student = User.create(name: "a student")
        @student1 = User.create(name: "student 1")
        @context.enroll_teacher(@teacher)
        @context.enroll_student(@student)
        @context.enroll_student(@student1)
        setup_account_for_turnitin(@context.account)

        @a = assignment_model(course: @context, group_category: "Study Groups")
        @a.submission_types = "online_upload,online_text_entry"
        @a.turnitin_enabled = true
        @a.save!

        @group1 = @a.context.groups.create!(name: "Study Group 1", group_category: @a.group_category)
        @group1.add_user(@student)
        @group1.add_user(@student1)
      end

      it "submits to turnitin for the original submitter" do
        submission = @a.submit_homework @student, submission_type: "online_text_entry", body: "blah"
        Submission.where(assignment_id: @a).each do |s|
          if s.id == submission.id
            expect(s.turnitin_data[:last_processed_attempt]).to be > 0
          else
            expect(s.turnitin_data).to eq({})
          end
        end
      end
    end

    context "report" do
      before :once do
        @assignment.submission_types = "online_upload,online_text_entry"
        @assignment.turnitin_enabled = true
        @assignment.turnitin_settings = @assignment.turnitin_settings # rubocop:disable Lint/SelfAssignment
        @assignment.save!
        @submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
        @submission.turnitin_data = {
          "submission_#{@submission.id}" => {
            web_overlap: 92,
            error: true,
            publication_overlap: 0,
            state: "failure",
            object_id: "123456789",
            student_overlap: 90,
            similarity_score: 92
          }
        }
        @submission.save!
      end

      before do
        api = Turnitin::Client.new("test_account", "sekret")
        expect(Turnitin::Client).to receive(:new).at_least(1).and_return(api)
        expect(api).to receive(:sendRequest).with(:generate_report, 1, include(oid: "123456789")).at_least(1).and_return("http://foo.bar")
      end

      it "lets teachers view the turnitin report" do
        @teacher = User.create
        @context.enroll_teacher(@teacher)
        expect(@submission).to be_grants_right(@teacher, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @teacher)).not_to be_nil
      end

      it "lets students view the turnitin report after grading" do
        @assignment.turnitin_settings[:originality_report_visibility] = "after_grading"
        @assignment.save!
        @submission.reload

        expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil

        @submission.score = 1
        @submission.grade_it!
        AdheresToPolicy::Cache.clear

        expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
      end

      it "lets students view the turnitin report immediately if the visibility setting allows it" do
        @assignment.turnitin_settings[:originality_report_visibility] = "after_grading"
        @assignment.save
        @submission.reload

        expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil

        @assignment.turnitin_settings[:originality_report_visibility] = "immediate"
        @assignment.save
        @submission.reload
        AdheresToPolicy::Cache.clear

        expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
      end

      it "lets students view the turnitin report after the due date if the visibility setting allows it" do
        @assignment.turnitin_settings[:originality_report_visibility] = "after_due_date"
        @assignment.due_at = 1.day.from_now
        @assignment.save
        @submission.reload

        expect(@submission).not_to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).to be_nil

        @assignment.due_at = 1.day.ago
        @assignment.save
        @submission.reload
        AdheresToPolicy::Cache.clear

        expect(@submission).to be_grants_right(@user, nil, :view_turnitin_report)
        expect(@submission.turnitin_report_url("submission_#{@submission.id}", @user)).not_to be_nil
      end
    end
  end

  describe "'view_turnitin_report' right" do
    subject { @submission }

    let(:teacher) { @teacher }
    let(:student) { @student }

    before :once do
      @assignment.update!(submission_types: "online_upload,online_text_entry")
      @submission = @assignment.submit_homework(student, { body: "hello there", submission_type: "online_text_entry" })
      @submission.update!(turnitin_data: {
                            "submission_#{@submission.id}" => {
                              web_overlap: 92,
                              error: true,
                              publication_overlap: 0,
                              state: "failure",
                              object_id: "123456789",
                              student_overlap: 90,
                              similarity_score: 92
                            }
                          })
    end

    it "is available when the plagiarism report is from turnitin" do
      expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
    end

    it "is available when the plagiarism report is blank (defaults to turnitin)" do
      @submission.turnitin_data.delete(:provider)
      expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
    end

    it "is not available when the plagiarism report is from vericite" do
      @submission.turnitin_data[:provider] = "vericite"
      expect(@submission).not_to be_grants_right(teacher, nil, :view_turnitin_report)
    end

    it "is available when the plagiarism data shows vericite and there is an LTI 2 assignment configuration for something else" do
      @submission.turnitin_data[:provider] = "vericite"
      AssignmentConfigurationToolLookup.create!(
        assignment: @assignment,
        tool_product_code: "turnitin-lti",
        tool_type: "Lti::MessageHandler",
        context_type: "Account",
        tool_resource_type_code: "code",
        tool_vendor_code: "turnitin.com"
      )
      expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
    end

    it "is not available when the plagiarism data shows vericite and there is an LTI 2 assignment configuration for vericite" do
      @submission.turnitin_data[:provider] = "vericite"
      AssignmentConfigurationToolLookup.create!(
        assignment: @assignment,
        tool_product_code: "vericite",
        tool_type: "Lti::MessageHandler",
        context_type: "Account",
        tool_resource_type_code: "code",
        tool_vendor_code: "vericite"
      )
      expect(@submission).not_to be_grants_right(teacher, nil, :view_turnitin_report)
    end

    it { expect(@submission).to be_grants_right(student, nil, :view_turnitin_report) }

    context "when originality report visibility is after_grading" do
      before do
        @assignment.update!(
          turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "after_grading")
        )
      end

      it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }

      context "when the submission is graded" do
        subject(:submission) { @assignment.grade_student(student, grade: 10, grader: teacher).first }

        it { is_expected.to be_grants_right(student, nil, :view_turnitin_report) }
      end
    end

    context "when originality report visibility is after_due_date" do
      before do
        @assignment.update!(
          turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "after_due_date")
        )
      end

      it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }

      context "when assignment.due_date is in the past" do
        before { @assignment.update!(due_at: 1.day.ago) }

        it { is_expected.to be_grants_right(student, nil, :view_turnitin_report) }
      end
    end

    context "when originality report visibility is never" do
      before do
        @assignment.update!(
          turnitin_settings: @assignment.turnitin_settings.merge(originality_report_visibility: "never")
        )
      end

      it { is_expected.not_to be_grants_right(student, nil, :view_turnitin_report) }

      context "when the teacher's enrollment is concluded" do
        before do
          @course.enrollments.where(user: teacher).each(&:conclude)
        end

        it "still allows the teacher (with view_all_grades) to see reports" do
          expect(@submission).to be_grants_right(teacher, nil, :view_turnitin_report)
        end
      end
    end
  end

  describe "'view_vericite_report' right" do
    let(:teacher) do
      user = User.create
      @context.enroll_teacher(user)
      user
    end

    before :once do
      Auditors::ActiveRecord::Partitioner.process
      @assignment.update!(submission_types: "online_upload,online_text_entry")

      @submission = @assignment.submit_homework(@user, { body: "hello there", submission_type: "online_text_entry" })
      @submission.turnitin_data = {
        "submission_#{@submission.id}" => {
          web_overlap: 92,
          error: true,
          publication_overlap: 0,
          state: "failure",
          object_id: "123456789",
          student_overlap: 90,
          similarity_score: 92
        },
        :provider => "vericite"
      }
      @submission.save!
    end

    it "is available when the plagiarism report is from vericite" do
      expect(@submission).to be_grants_right(teacher, nil, :view_vericite_report)
    end

    it "is not available when the plagiarism report is from turnitin" do
      @submission.turnitin_data[:provider] = "turnitin"
      expect(@submission).not_to be_grants_right(teacher, nil, :view_vericite_report)
    end

    it "is not available when the plagiarism report is blank (defaults to turnitin)" do
      @submission.turnitin_data.delete(:provider)
      expect(@submission).not_to be_grants_right(teacher, nil, :view_vericite_report)
    end
  end

  describe "#not_submitted?" do
    let(:submission) { @assignment.submissions.find_by(user: @student) }

    it "returns true if the workflow state is unsubmitted (same as unsubmitted?)" do
      expect(submission).to be_unsubmitted
      expect(submission).to be_not_submitted
    end

    it "returns true if the student has been graded but has not submitted (different from unsubmitted?)" do
      @assignment.grade_student(@student, grade: 3, grader: @teacher)
      expect(submission).not_to be_unsubmitted
      expect(submission).to be_not_submitted
    end

    it "returns false if the student has submitted (same as unsubmitted?)" do
      @assignment.submit_homework(@student, body: "hi")
      expect(submission).not_to be_unsubmitted
      expect(submission).not_to be_not_submitted
    end
  end

  describe "#external_tool_url" do
    let(:submission) { Submission.new }
    let(:lti_submission) { @assignment.submit_homework(@user, submission_type: "basic_lti_launch", url: "http://www.example.com") }

    context "submission_type of 'basic_lti_launch'" do
      it "returns a url containing the submitted url" do
        expect(lti_submission.external_tool_url).to eq(lti_submission.url)
      end

      it "accepts query params to be included in the URL" do
        url = lti_submission.external_tool_url(query_params: { foo: false })
        query_params = Rack::Utils.parse_query(URI(url).query)
        expect(query_params["foo"]).to eq("false")
      end
    end

    context 'submission_type of anything other than "basic_lti_launch"' do
      it "returns nothing" do
        expect(submission.external_tool_url).to be_nil
      end
    end
  end

  it "returns the correct quiz_submission_version" do
    # set up the data to have a submission with a quiz submission with multiple versions
    course_factory
    quiz = @course.quizzes.create!
    quiz_submission = quiz.generate_submission @user, false
    quiz_submission.save

    @assignment.submissions.find_by!(user: @user).update!(quiz_submission_id: quiz_submission.id)

    submission = @assignment.submit_homework @user, submission_type: "online_quiz"
    submission.quiz_submission_id = quiz_submission.id

    # set the microseconds of the submission.submitted_at to be less than the
    # quiz_submission.finished_at.

    # first set them to be exactly the same (with microseconds)
    time_to_i = submission.submitted_at.to_i
    usec = submission.submitted_at.usec
    timestamp = "#{time_to_i}.#{usec}".to_f

    quiz_submission.finished_at = Time.zone.at(timestamp)
    quiz_submission.save

    # get the data in a strange state where the quiz_submission.finished_at is
    # microseconds older than the submission (caused the bug in #6048)
    quiz_submission.finished_at = Time.zone.at(timestamp + 0.00001)
    quiz_submission.save

    # verify the data is weird, to_i says they are equal, but the usecs are off
    expect(quiz_submission.finished_at.to_i).to eq submission.submitted_at.to_i
    expect(quiz_submission.finished_at.usec).to be > submission.submitted_at.usec

    # create the versions that Submission#quiz_submission_version uses
    quiz_submission.with_versioning do
      quiz_submission.save
      quiz_submission.save
    end

    # the real test, quiz_submission_version shouldn't care about usecs
    expect(submission.reload.quiz_submission_version).to eq 2
  end

  it "returns only comments readable by the user" do
    course_with_teacher(active_all: true)
    @course.default_post_policy.update!(post_manually: false)
    @student1 = student_in_course(active_user: true).user
    @student2 = student_in_course(active_user: true).user

    @assignment = @course.assignments.new(title: "some assignment")
    @assignment.submission_types = "online_text_entry"
    @assignment.workflow_state = "published"
    @assignment.save

    @submission = @assignment.submit_homework(@student1, body: "some message")
    @submission.add_comment(author: @teacher, comment: "a")
    @submission.add_comment(author: @teacher, comment: "b", hidden: true)
    @submission.add_comment(author: @student1, comment: "c")
    @submission.add_comment(author: @student2, comment: "d")
    @submission.add_comment(author: @teacher, comment: "e", draft: true)
    @submission.reload

    @submission.limit_comments(@teacher)
    expect(@submission.submission_comments.count).to be 5
    expect(@submission.visible_submission_comments.count).to be 4

    @submission.limit_comments(@student1)
    expect(@submission.submission_comments.count).to be 4
    expect(@submission.visible_submission_comments.count).to be 4

    @submission.limit_comments(@student2)
    expect(@submission.submission_comments.count).to be 1
    expect(@submission.visible_submission_comments.count).to be 1
  end

  describe "read/unread state" do
    it "is read if a submission exists with no grade" do
      @submission = @assignment.submit_homework(@user)
      expect(@submission.read?(@user)).to be_truthy
    end

    it "is unread after assignment is graded" do
      @submission = @assignment.grade_student(@user, grade: 3, grader: @teacher).first
      expect(@submission.unread?(@user)).to be_truthy
    end

    it "is unread after submission is graded" do
      @assignment.submit_homework(@user)
      @submission = @assignment.grade_student(@user, grade: 3, grader: @teacher).first
      expect(@submission.unread?(@user)).to be_truthy
    end

    it "is unread after submission is commented on by teacher" do
      @student = @user
      course_with_teacher(course: @context, active_all: true)
      @submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first
      expect(@submission.unread?(@user)).to be_truthy
    end

    it "is read after submission is commented on by teacher and then teacher deletes comment" do
      student = @user
      submission = @assignment.submission_for_student(@student)

      submission.add_comment(author: @teacher, comment: "some comment")

      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1

      comment = submission.submission_comments.first

      expect do
        comment.updating_user = @current_user
        comment.destroy!
      end.to change { SubmissionComment.count }.from(1).to(0)

      expect(submission.read?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 0
    end

    it "is read after submission is commented on twice by teacher and then teacher deletes the first comment" do
      student = @user
      submission = @assignment.submission_for_student(student)

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1

      comment = submission.submission_comments.first

      expect do
        comment.updating_user = @current_user
        comment.destroy!
      end.to change { SubmissionComment.count }.from(2).to(1)

      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1
    end

    it "is read after submission is commented on by teacher, student views comment, teacher comments again, and then teacher deletes the not viewed comment" do
      student = @user
      submission = @assignment.submission_for_student(student)

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1

      submission.mark_submission_comments_read(student)
      submission.mark_item_read("comment")
      expect(submission.read?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 0

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      comment = submission.submission_comments[1]

      expect do
        comment.updating_user = @current_user
        comment.destroy!
      end.to change { SubmissionComment.count }.from(2).to(1)

      expect(submission.read?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 0
    end

    it "is unread after submission is commented on by teacher, student views comment, teacher comments again, and then teacher deletes the viewed comment" do
      student = @user
      submission = @assignment.submission_for_student(student)

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1

      submission.mark_submission_comments_read(student)
      submission.mark_item_read("comment")
      expect(submission.read?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: @student.id).first
      expect(content_participation_count.unread_count).to eq 0

      submission.add_comment(author: @teacher, comment: "some comment")
      expect(submission.unread?(student)).to be_truthy

      comment = submission.submission_comments.first

      expect do
        comment.updating_user = @current_user
        comment.destroy!
      end.to change { SubmissionComment.count }.from(2).to(1)

      expect(submission.unread?(student)).to be_truthy

      content_participation_count = ContentParticipationCount.where(user_id: student.id).first
      expect(content_participation_count.unread_count).to eq 1
    end

    it "is read if other submission fields change" do
      @submission = @assignment.submit_homework(@user)
      @submission.workflow_state = "graded"
      @submission.graded_at = Time.zone.now
      @submission.save!
      expect(@submission.read?(@user)).to be_truthy
    end

    it "mark read/unread" do
      @submission = @assignment.submit_homework(@user)
      @submission.workflow_state = "graded"
      @submission.graded_at = Time.zone.now
      @submission.save!
      expect(@submission.read?(@user)).to be_truthy
      @submission.mark_unread(@user)
      expect(@submission.read?(@user)).to be_falsey
      @submission.mark_read(@user)
      expect(@submission.read?(@user)).to be_truthy
    end

    it "is unread after submission is graded by teacher" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      expect(@submission.read?(@student)).to be_falsey
    end

    it "is unread after submission is graded and commented on by teacher" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first

      expect(@submission.read?(@student)).to be_falsey
    end

    it "is unread after grade is read and teacher posts a comment" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @submission.mark_item_read("grade")
      @submission = @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" }).first

      expect(@submission.reload.read?(@student)).to be_falsey
    end

    it "is read after grade is read and student posts a comment" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @submission.mark_item_read("grade")
      @submission = @assignment.update_submission(@student, { commenter: @student, comment: "good!" }).first

      expect(@submission.reload.read?(@student)).to be_truthy
    end

    it "is unread after student and teacher post a comment" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @assignment.update_submission(@student, { commenter: @student, comment: "good!" })
      @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })

      expect(@submission.read?(@student)).to be_falsey
    end

    it "is unread if there is any unread rubric" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      ContentParticipation.participate(content: @submission, user: @student, content_item: "rubric")

      expect(@submission.read?(@student)).to be_falsey
    end

    it "is read if grade and rubric are read" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      ContentParticipation.participate(content: @submission, user: @student, content_item: "rubric")

      @submission.mark_item_read("grade")
      @submission.mark_item_read("rubric")

      expect(@submission.read?(@student)).to be_truthy
    end

    it "changes the state from read to unread" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })

      @submission.mark_item_unread("comment")

      expect(@submission.unread?(@student)).to be_truthy
    end

    it "marks submission comments as read" do
      @student = @user
      @assignment.submit_homework(@student)
      @submission = @assignment.grade_student(@student, grade: 3, grader: @teacher).first
      @assignment.update_submission(@student, { commenter: @teacher, comment: "good!" })
      @submission.mark_submission_comments_read(@student)

      visible_comment = @submission.visible_submission_comments[0]
      viewed_comment = visible_comment.viewed_submission_comments[0]
      expect(viewed_comment.user).to eql @student
      expect(viewed_comment.submission_comment).to eql visible_comment
    end
  end

  describe "mute" do
    let(:submission) { Submission.new }

    before do
      submission.published_score = 100
      submission.published_grade = "A"
      submission.graded_at = Time.zone.now
      submission.grade = "B"
      submission.score = 90
      submission.mute
    end

    specify { expect(submission.published_score).to be_nil }
    specify { expect(submission.published_grade).to be_nil }
    specify { expect(submission.graded_at).to be_nil }
    specify { expect(submission.grade).to be_nil }
    specify { expect(submission.score).to be_nil }
  end

  describe "muted_assignment?" do
    it "returns true if assignment is muted" do
      assignment = double(muted?: true)
      @submission = Submission.new
      expect(@submission).to receive(:assignment).and_return(assignment)
      expect(@submission.muted_assignment?).to be true
    end

    it "returns false if assignment is not muted" do
      assignment = double(muted?: false)
      @submission = Submission.new
      expect(@submission).to receive(:assignment).and_return(assignment)
      expect(@submission.muted_assignment?).to be false
    end
  end

  describe "without_graded_submission?" do
    let(:submission) { Submission.new }

    it "returns false if submission does not has_submission?" do
      allow(submission).to receive_messages(has_submission?: false, graded?: true)
      expect(submission.without_graded_submission?).to be false
    end

    it "returns false if submission does is not graded" do
      allow(submission).to receive_messages(has_submission?: true, graded?: false)
      expect(submission.without_graded_submission?).to be false
    end

    it "returns true if submission is not graded and has no submission" do
      allow(submission).to receive_messages(has_submission?: false, graded?: false)
      expect(submission.without_graded_submission?).to be true
    end
  end

  describe "graded?" do
    it "is false before graded" do
      submission, _ = @assignment.find_or_create_submission(@user)
      expect(submission).to_not be_graded
    end

    it "is true for graded assignments" do
      submission = @assignment.grade_student(@user, grade: 1, grader: @teacher)[0]
      expect(submission).to be_graded
    end

    it "is also true for excused assignments" do
      submission, _ = @assignment.find_or_create_submission(@user)
      submission.excused = true
      expect(submission).to be_graded
    end
  end

  describe "autograded" do
    let(:submission) { Submission.new }

    it "returns false when its not autograded" do
      submission = Submission.new
      expect(submission).to_not be_autograded

      submission.grader_id = Shard.global_id_for(@user.id)
      expect(submission).to_not be_autograded
    end

    it "returns true when its autograded" do
      submission = Submission.new
      submission.grader_id = -1
      expect(submission).to be_autograded
    end
  end

  describe "past_due" do
    before :once do
      Auditors::ActiveRecord::Partitioner.process
      submission_spec_model
      @submission1 = @submission

      add_section("overridden section")
      u2 = student_in_section(@course_section, active_all: true)
      submission_spec_model(user: u2)
      @submission2 = @submission

      @assignment.update_attribute(:due_at, 1.day.ago)
      @submission1.reload
      @submission2.reload
    end

    it "updates when an assignment's due date is changed" do
      expect(@submission1).to be_past_due
      @assignment.reload.update_attribute(:due_at, 1.day.from_now)
      expect(@submission1.reload).not_to be_past_due
    end

    it "updates when an applicable override is changed" do
      expect(@submission1).to be_past_due
      expect(@submission2).to be_past_due

      assignment_override_model assignment: @assignment,
                                due_at: 1.day.from_now,
                                set: @course_section
      expect(@submission1.reload).to be_past_due
      expect(@submission2.reload).not_to be_past_due
    end

    it "gives a quiz submission 30 extra seconds before making it past due" do
      quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 1, "question_type" => "essay_question" } }]) do
        {
          "text_after_answers" => "",
          "question_#{@questions[0].id}" => "<p>Lorem ipsum answer.</p>",
          "context_id" => @course.id.to_s,
          "context_type" => "Course",
          "user_id" => @user.id.to_s,
          "quiz_id" => @quiz.id.to_s,
          "course_id" => @course.id.to_s,
          "question_text" => "Lorem ipsum question",
        }
      end
      @assignment.due_at = "20130101T23:59Z"
      @assignment.save!

      submission = @quiz_submission.submission.reload
      submission.submitted_at = @assignment.due_at + 3.days
      expect(submission).to be_past_due

      submission.submitted_at = @assignment.due_at + 30.seconds
      expect(submission).not_to be_past_due
    end
  end

  describe "late" do
    before :once do
      submission_spec_model(submit_homework: true)
    end

    it "is false if not past due" do
      @submission.submitted_at = 2.days.ago
      @submission.cached_due_date = 1.day.ago
      expect(@submission).not_to be_late
    end

    it "is false if not submitted, even if past due" do
      @submission.submission_type = nil
      @submission.cached_due_date = 1.day.ago # forces submitted_at to be nil
      expect(@submission).not_to be_late
    end

    it "is true if submitted and past due" do
      @submission.submitted_at = 1.day.ago
      @submission.cached_due_date = 2.days.ago
      expect(@submission).to be_late
    end
  end

  describe "scope: not_submitted_or_graded" do
    before do
      @assignment = @course.assignments.create!(submission_types: "online_text_entry")
      @submission = @assignment.submissions.find_by(user: @student)
    end

    it "includes submissions where the student has not submitted and has not been graded" do
      expect(Submission.not_submitted_or_graded).to include @submission
    end

    it "excludes submissions where the student has submitted" do
      @assignment.submit_homework(@student, body: "hi")
      expect(Submission.not_submitted_or_graded).not_to include @submission
    end

    it "excludes submissions where the student has been graded" do
      @assignment.grade_student(@student, grader: @teacher, grade: 10)
      expect(Submission.not_submitted_or_graded).not_to include @submission
    end

    it "excludes excused submissions" do
      @assignment.grade_student(@student, grader: @teacher, excused: true)
      expect(Submission.not_submitted_or_graded).not_to include @submission
    end
  end

  describe "scope: postable" do
    subject(:submissions) { assignment.submissions.postable }

    let(:assignment) { @course.assignments.create! }
    let(:submission) { assignment.submissions.find_by(user: @student) }

    before do
      assignment.ensure_post_policy(post_manually: true)
    end

    it "does not include submissions that neither have grades nor hidden comments" do
      submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
      expect(subject).not_to include(submission)
    end

    it "includes submissions with hidden comments" do
      submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
      expect(subject).to include(submission)
    end

    it "includes submissions with a grade" do
      assignment.grade_student(@student, grader: @teacher, grade: 10)
      expect(subject).to include(submission)
    end

    it "includes submissions that are excused" do
      assignment.grade_student(@student, grader: @teacher, excused: true)
      expect(subject).to include(submission)
    end
  end

  describe "scope: with_hidden_comments" do
    subject(:submissions) { assignment.submissions.with_hidden_comments }

    let(:assignment) { @course.assignments.create! }
    let(:submission) { assignment.submissions.find_by(user: @student) }

    before do
      assignment.grade_student(@student, grader: @teacher, score: 5)
    end

    it "does not include submissions without a hidden comment" do
      submission.add_comment(author: @teacher, comment: "good job!", hidden: false)
      expect(subject).not_to include(submission)
    end

    it "includes submissions with hidden comments" do
      submission.add_comment(author: @teacher, comment: "good job!", hidden: true)
      expect(subject).to include(submission)
    end
  end

  describe "scope: anonymized" do
    subject(:submissions) { assignment.all_submissions.anonymized }

    let(:assignment) { @course.assignments.create! }
    let(:first_student) { @student }
    let(:second_student) { student_in_course(course: @course, active_all: true).user }
    let(:submission_with_anonymous_id) { submission_model(assignment:, user: first_student) }
    let(:submission_without_anonymous_id) do
      submission_model(assignment:, user: second_student).tap do |submission|
        submission.update_attribute(:anonymous_id, nil)
      end
    end

    it "only contains submissions that have anonymous_ids" do
      expect(subject).to contain_exactly(submission_with_anonymous_id)
    end
  end

  describe "scope: due_in_past" do
    subject(:submissions) { student.submissions.due_in_past }

    let(:future_assignment) { @course.assignments.create!(due_at: 2.days.from_now) }
    let(:past_assignment) { @course.assignments.create!(due_at: 2.days.ago) }
    let(:whenever_assignment) { @course.assignments.create!(due_at: nil) }
    let(:student) { @student }
    let(:future_submission) { future_assignment.submission_for_student(student) }
    let(:past_submission) { past_assignment.submission_for_student(student) }
    let(:whenever_submission) { whenever_assignment.submission_for_student(student) }

    it "includes submissions with a due date in the past" do
      expect(subject).to include(past_submission)
    end

    it "excludes submissions with a due date in the future" do
      expect(subject).not_to include(future_submission)
    end

    it "excludes submissions without a due date" do
      expect(subject).not_to include(whenever_submission)
    end
  end

  describe "scope: excused" do
    before :once do
      submission_spec_model
    end

    it "includes submission when excused is true" do
      @submission.update(excused: true)
      expect(Submission.excused).to include @submission
    end

    it "does not include submission when excused is false" do
      @submission.update(excused: false)
      expect(Submission.excused).not_to include @submission
    end

    it "does not include the submission when excused is nil" do
      @submission.update(excused: nil)
      expect(Submission.excused).not_to include @submission
    end
  end

  describe "scope: unposted" do
    before :once do
      submission_spec_model
    end

    it "includes submission when posted_at is nil" do
      @submission.update(posted_at: nil, grade: 10, score: 10)
      expect(Submission.unposted).to include @submission
    end

    it "does not include submission when posted_at is not nil" do
      @submission.update(posted_at: Time.zone.now, grade: 10, score: 10)
      expect(Submission.unposted).not_to include @submission
    end
  end

  describe "scope: missing" do
    context "not submitted" do
      before :once do
        @now = Time.zone.now
        submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil)
        @submission.assignment.update!(submission_types: "online_upload")
      end

      it "excludes an otherwise missing submission that has been marked with a custom status" do
        @submission.update!(grader_id: nil)
        admin = account_admin_user(account: @course.root_account)
        custom_grade_status = @course.root_account.custom_grade_statuses.create!(
          name: "Custom Status",
          color: "#ABC",
          created_by: admin
        )

        expect { @submission.update!(custom_grade_status:) }.to change {
          Submission.missing.include?(@submission)
        }.from(true).to(false)
      end

      it "includes submission when due date has passed with no submission, late_policy_status is nil, excused is nil and grader is nil" do
        @submission.update(grader_id: nil)
        expect(Submission.missing).to include @submission
      end

      it 'includes submission when late_policy_status is "missing"' do
        @submission.update(late_policy_status: "missing")

        expect(Submission.missing).to include @submission
      end

      it "includes submission when late_policy_status is not nil, not missing" do
        @submission.update(late_policy_status: "none")

        expect(Submission.missing).to include @submission
      end

      it "excludes submission when past due and excused" do
        @submission.update(excused: true)

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when past due and extended" do
        @submission.update(late_policy_status: "extended")

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when past due and assignment does not expect a submission" do
        @submission.assignment.update(submission_types: "none")

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when it is excused and late_policy_status is missing" do
        @submission.update(excused: true, late_policy_status: "missing")

        expect(Submission.missing).to be_empty
      end

      it "includes submission when late_policy_status is missing and assignment does not expect a submission" do
        @submission.update(late_policy_status: "missing")
        @submission.assignment.update(submission_types: "none")

        expect(Submission.missing).to include @submission
      end

      it "excludes submission when due date has not passed" do
        @submission.update(cached_due_date: 1.day.from_now(@now))

        expect(Submission.missing).to be_empty
      end

      it "includes missing quiz_lti assignments" do
        @course.context_external_tools.create!(
          name: "Quizzes.Next",
          consumer_key: "test_key",
          shared_secret: "test_secret",
          tool_id: "Quizzes 2",
          url: "http://example.com/launch"
        )
        @assignment.quiz_lti!
        @assignment.due_at = 1.day.ago(@now)
        @assignment.save!

        @submission.update(grader_id: nil)
        expect(Submission.missing).to include @submission
      end
    end

    context "submitted" do
      before :once do
        @now = Time.zone.now
        submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil, submit_homework: true)
        @submission.assignment.update!(submission_types: "online_upload")
      end

      it "excludes submission when late_policy_status is nil" do
        expect(Submission.missing).to be_empty
      end

      it 'includes submission when late_policy_status is "missing"' do
        @submission.update(late_policy_status: "missing")

        expect(Submission.missing).not_to be_empty
      end

      it "excludes submission when late_policy_status is not nil, not missing" do
        @submission.update(late_policy_status: "foo")

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when late_policy_status is extended" do
        @submission.update(late_policy_status: "extended")

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when submitted before the due date" do
        @submission.update(submitted_at: 2.days.ago(@now))

        expect(Submission.missing).to be_empty
      end

      it "excludes submission when submitted after the due date" do
        @submission.update(submitted_at: @now)

        expect(Submission.missing).to be_empty
      end
    end
  end

  describe "scope: in_current_grading_period_for_courses" do
    before :once do
      @term = Account.default.enrollment_terms.create!(start_at: 10.years.ago)
      @course.enrollment_term_id = @term.id
      @course.save!

      period_group = Account.default.grading_period_groups.create!
      period_group.enrollment_terms << @course.enrollment_term
      now = Time.zone.now
      period_group.grading_periods.create!(
        title: "Closed Period",
        start_date: 5.months.ago(now),
        end_date: 2.months.ago(now),
        close_date: 2.months.ago(now)
      )
      period_group.grading_periods.create!(
        title: "Current Period",
        start_date: 2.months.ago(now),
        end_date: 2.months.from_now(now),
        close_date: 2.months.from_now(now)
      )

      @course.assignments.create!(
        name: "Assignment in closed period",
        workflow_state: "published",
        submission_types: "online_text_entry",
        due_at: 4.months.ago(now)
      )
      @course.assignments.create!(
        name: "Assignment in current period",
        workflow_state: "published",
        submission_types: "online_text_entry",
        due_at: 1.month.ago
      )
    end

    it "only returns submissions in the current grading period" do
      submissions = Submission.in_current_grading_period_for_courses([@course.id])
      expect(submissions.map { |s| s.assignment.name }.sort).to eq(["Assignment in current period", "some assignment"].sort)
    end

    it "includes assignments without a due date" do
      @course.assignments.create!(
        name: "Assignment without due date",
        workflow_state: "published",
        submission_types: "online_text_entry"
      )
      submissions = Submission.in_current_grading_period_for_courses([@course.id])
      expect(submissions.count).to be(3)
      expect(submissions.map { |s| s.assignment.name }).to include("Assignment without due date")
    end

    it "includes assignments from all specified courses" do
      course1 = @course
      course_factory(enrollment_term_id: @term.id, active_all: true)
      @course.enroll_student(@student, enrollment_state: :active)
      @course.assignments.create!(
        name: "Another in current period",
        workflow_state: "published",
        submission_types: "online_text_entry",
        due_at: 1.month.ago
      )
      submissions = Submission.in_current_grading_period_for_courses([course1.id, @course.id])
      expect(submissions.count).to be(3)
      expect(submissions.map { |s| s.assignment.name }).to include("Another in current period")
    end

    it "ignores courses not included in array" do
      course_factory(enrollment_term_id: @term.id, active_all: true)
      @course.enroll_student(@student, enrollment_state: :active)
      @course.assignments.create!(
        name: "Another in current period",
        workflow_state: "published",
        submission_types: "online_text_entry",
        due_at: 1.month.ago
      )
      submissions = Submission.in_current_grading_period_for_courses([@course.id])
      expect(submissions.count).to be(1)
      expect(submissions.first.assignment.name).to eq("Another in current period")
    end

    it "includes all submissions from courses without grading periods" do
      course1 = @course
      course_factory(active_all: true)
      @course.enroll_student(@student, enrollment_state: :active)
      @course.assignments.create!(
        name: "No GP 1",
        workflow_state: "published",
        submission_types: "online_text_entry",
        due_at: 1.month.ago
      )
      @course.assignments.create!(
        name: "No GP 2",
        workflow_state: "published",
        submission_types: "online_text_entry"
      )
      submissions = Submission.in_current_grading_period_for_courses([course1.id, @course.id])
      expect(submissions.map { |s| s.assignment.name }.sort).to eq(["Assignment in current period", "No GP 1", "No GP 2", "some assignment"].sort)
    end
  end

  describe "#late?" do
    before(:once) do
      @course = Course.create!
      student = User.create!
      @course.enroll_student(student, enrollment_state: "active")
      now = Time.zone.now
      assignment = @course.assignments.create!(submission_types: "online_text_entry", due_at: 10.days.ago(now))
      @submission = assignment.submit_homework(student, body: "Submitting late :(")
    end

    it "returns true if the submission is past due" do
      expect(@submission).to be_late
    end

    it "returns false if the submission is excused" do
      @submission.excused = true
      expect(@submission).not_to be_late
    end

    it "returns false if the submission is past due but has its late_policy_status set to something other than 'late'" do
      @submission.late_policy_status = "missing"
      expect(@submission).not_to be_late
    end

    it "returns false when an otherwise late submission has a custom status" do
      admin = account_admin_user(account: @course.root_account)
      custom_grade_status = @course.root_account.custom_grade_statuses.create!(
        name: "Custom Status",
        color: "#ABC",
        created_by: admin
      )
      expect { @submission.update!(custom_grade_status:) }.to change {
        @submission.late?
      }.from(true).to(false)
    end
  end

  describe "#extended?" do
    before(:once) do
      @course = Course.create!
      student = User.create!
      @course.enroll_student(student, enrollment_state: "active")
      assignment = @course.assignments.create!(submission_types: "online_text_entry")
      @submission = assignment.submissions.find_by(user: student)
    end

    it "returns false when a custom status has been applied" do
      @submission.update(late_policy_status: "extended")
      admin = account_admin_user(account: @course.root_account)
      custom_grade_status = @course.root_account.custom_grade_statuses.create!(
        name: "Custom Status",
        color: "#ABC",
        created_by: admin
      )
      expect { @submission.update!(custom_grade_status:) }.to change {
        @submission.extended?
      }.from(true).to(false)
    end
  end

  describe "#missing" do
    before :once do
      @now = Time.zone.now
      submission_spec_model(cached_due_date: 1.day.ago(@now), submission_type: nil, submit_homework: true)
      @submission.assignment.update!(submission_types: "on_paper")
      @another_assignment = assignment_model(course: @course, due_at: 1.day.ago)
      @another_submission = @another_assignment.submissions.last
    end

    submissions_that_cant_be_missing = %w[none on_paper external_tool]
    %w[none
       on_paper
       online_quiz
       discussion_topic
       external_tool
       online_upload
       online_text_entry
       online_url
       media_recording].each do |sub_type|
      should_not_be_missing = submissions_that_cant_be_missing.include?(sub_type)
      expected_status = should_not_be_missing ? "false" : "true"
      it "returns #{expected_status} when late_policy_status is nil and submission_type is #{sub_type}" do
        @another_assignment.update(submission_types: sub_type)

        if should_not_be_missing
          expect(@another_submission.reload).not_to be_missing
        else
          expect(@another_submission.reload).to be_missing
        end
      end
    end

    it "returns false when an otherwise missing submission has a custom status" do
      @another_assignment.update!(submission_types: "online_upload")
      admin = account_admin_user(account: @course.root_account)
      custom_grade_status = @course.root_account.custom_grade_statuses.create!(
        name: "Custom Status",
        color: "#ABC",
        created_by: admin
      )
      expect { @another_submission.update!(custom_grade_status:) }.to change {
        @another_submission.missing?
      }.from(true).to(false)
    end

    it "returns false when late_policy_status is nil standalone" do
      expect(@submission).not_to be_missing
    end

    it 'returns true when late_policy_status is "missing"' do
      @submission.update(late_policy_status: "missing")

      expect(@submission).to be_missing
    end

    it 'returns false when the submission is excused and late_policy_status is "missing"' do
      @submission.excused = true
      @submission.late_policy_status = "missing"
      expect(@submission).not_to be_missing
    end

    it "returns false when late_policy_status is not nil, not missing" do
      @submission.update(late_policy_status: "late")

      expect(@submission).not_to be_missing
    end

    it "returns false when not past due" do
      @submission.update(submitted_at: 2.days.ago(@now))

      expect(@submission).not_to be_missing
    end

    it "returns false when past due and submitted" do
      @submission.update(submitted_at: @now)

      expect(@submission).not_to be_missing
    end

    it "returns false when past due, not submitted, assignment does not expect a submission, is excused" do
      @submission.assignment.update(submission_types: "none")
      @submission.update(excused: true)
      @submission.update_columns(submission_type: nil)

      expect(@submission).not_to be_missing
    end

    it "returns false when past due, not submitted, assignment does not expect a submission, not excused, and no score" do
      @submission.assignment.update(submission_types: "none")
      @submission.update_columns(submission_type: nil)

      expect(@submission).not_to be_missing
    end

    it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is not "graded"' do
      @submission.update(score: 1)
      @submission.update_columns(submission_type: nil)

      expect(@submission).not_to be_missing
    end

    it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is "graded", and score is 0' do
      @submission.update(score: 0, workflow_state: "graded")
      @submission.update_columns(submission_type: nil)

      expect(@submission).not_to be_missing
    end

    it 'returns false when past due, not submitted, assignment does not expect a submission, not excused, has a score, workflow state is "graded", and score is greater than 0' do
      @submission.update(score: 1, workflow_state: "graded")
      @submission.update_columns(submission_type: nil)

      expect(@submission).not_to be_missing
    end

    it "returns true for missing quiz_lti submissions" do
      @course.context_external_tools.create!(
        name: "Quizzes.Next",
        consumer_key: "test_key",
        shared_secret: "test_secret",
        tool_id: "Quizzes 2",
        url: "http://example.com/launch"
      )

      @another_assignment.quiz_lti!
      @another_assignment.save!

      @another_submission.reload
      expect(@another_submission).to be_missing
    end

    it "returns true for missing quiz_lti submissions when cached_quiz_lti is false but assignment.quiz_lti is true" do
      @course.context_external_tools.create!(
        name: "Quizzes.Next",
        consumer_key: "test_key",
        shared_secret: "test_secret",
        tool_id: "Quizzes 2",
        url: "http://example.com/launch"
      )

      @another_assignment.quiz_lti!
      @another_assignment.save!

      @another_submission.reload
      @another_submission.update!(cached_quiz_lti: false)
      expect(@another_submission).to be_missing
    end

    context "checkpointed discussions" do
      before :once do
        @course.root_account.enable_feature!(:discussion_checkpoints)
        @checkpointed_assignment = assignment_model(
          course: @course,
          submission_types: "discussion_topic",
          due_at: 1.day.ago,
          has_sub_assignments: true
        )
        @discussion = discussion_topic_model(assignment: @checkpointed_assignment)

        # Create sub-assignments (checkpoints)
        @reply_to_topic_checkpoint = @checkpointed_assignment.sub_assignments.create!(
          context: @course,
          sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC,
          due_at: 1.day.ago,
          title: "Reply to Topic",
          submission_types: "discussion_topic"
        )
        @reply_to_entry_checkpoint = @checkpointed_assignment.sub_assignments.create!(
          context: @course,
          sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY,
          due_at: 1.day.ago,
          title: "Reply to Entry",
          submission_types: "discussion_topic"
        )

        @parent_submission = @checkpointed_assignment.submissions.find_by(user: @student)
        @topic_submission = @reply_to_topic_checkpoint.submissions.find_by(user: @student)
        @entry_submission = @reply_to_entry_checkpoint.submissions.find_by(user: @student)
      end

      context "parent assignment submissions" do
        it "returns true when any child submission is missing" do
          # Set one child as missing
          @topic_submission.update!(late_policy_status: "missing")
          @entry_submission.update!(submitted_at: 2.days.ago, workflow_state: "submitted", submission_type: "discussion_topic")

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).to be_missing
        end

        it "returns true when both child submissions are missing" do
          @topic_submission.update!(late_policy_status: "missing")
          @entry_submission.update!(late_policy_status: "missing")

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).to be_missing
        end

        it "returns true when user has submitted to neither checkpoint" do
          # Both submissions are unsubmitted and past due (default state)
          expect(@topic_submission.workflow_state).to eq("unsubmitted")
          expect(@entry_submission.workflow_state).to eq("unsubmitted")
          expect(@topic_submission.submitted_at).to be_nil
          expect(@entry_submission.submitted_at).to be_nil
          expect(@topic_submission.past_due?).to be true
          expect(@entry_submission.past_due?).to be true

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).to be_missing
        end

        it "returns false when no child submissions are missing" do
          @topic_submission.update!(submitted_at: 1.hour.ago, workflow_state: "submitted", submission_type: "discussion_topic")
          @entry_submission.update!(submitted_at: 1.hour.ago, workflow_state: "submitted", submission_type: "discussion_topic")

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).not_to be_missing
        end

        it "returns false when parent submission is excused" do
          @topic_submission.update!(late_policy_status: "missing")
          @entry_submission.update!(excused: true)

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).not_to be_missing
        end

        it "returns false when parent has custom grade status even with missing children" do
          admin = account_admin_user(account: @course.root_account)
          custom_grade_status = @course.root_account.custom_grade_statuses.create!(
            name: "Custom Status",
            color: "#ABC",
            created_by: admin
          )
          @topic_submission.update!(late_policy_status: "missing")
          @parent_submission.update!(custom_grade_status:)

          # Call the aggregator service to set the parent submission's late_policy_status
          Checkpoints::SubmissionAggregatorService.call(assignment: @checkpointed_assignment, student: @student)

          expect(@parent_submission.reload).not_to be_missing
        end
      end

      context "sub-assignment submissions" do
        it "returns true when sub-assignment submission is missing and past due" do
          @topic_submission.update!(late_policy_status: "missing")
          expect(@topic_submission.reload).to be_missing
        end

        it "returns false when sub-assignment submission is submitted" do
          @topic_submission.update!(submitted_at: 1.hour.ago, workflow_state: "submitted", submission_type: "discussion_topic")
          expect(@topic_submission.reload).not_to be_missing
        end

        it "returns false when sub-assignment submission is excused" do
          @topic_submission.update!(excused: true)
          expect(@topic_submission.reload).not_to be_missing
        end

        it "returns true when sub-assignment is past due and not submitted" do
          # Ensure submission expects submission and is past due
          expect(@reply_to_topic_checkpoint.expects_submission?).to be true
          expect(@topic_submission.past_due?).to be true
          expect(@topic_submission.submitted_at).to be_nil
          expect(@topic_submission.reload).to be_missing
        end
      end
    end
  end

  describe "update_attachment_associations" do
    before do
      course_with_student active_all: true
      @assignment = @course.assignments.create!
    end

    it "doesn't include random attachment ids" do
      f = Attachment.create! uploaded_data: StringIO.new("blah"),
                             context: @course,
                             filename: "blah.txt"
      sub = @assignment.submit_homework(@user, attachments: [f])
      expect(sub.attachments).to eq []
    end

    it "includes attachments in a user group that are not in a section group" do
      @group = @course.groups.create!
      @group.add_user(@user)
      f = Attachment.create! uploaded_data: StringIO.new("blah"),
                             context: @group,
                             filename: "blah.txt",
                             user: @user
      sub = @assignment.submit_homework(@user, attachments: [f])
      expect(sub.attachments).to eq [f]
    end

    it "creates associations for text entry submissions" do
      @course.enroll_student(@user, enrollment_state: :active)
      f = attachment_model(filename: "blah.txt", user: @user, context: @user)
      f.root_account.enable_feature!(:file_association_access)

      body = "<a href=/users/#{@user.id}/files/#{f.id}>blah.txt</a>"

      sub = @assignment.submit_homework(@user, submission_type: "online_text_entry", body:, saving_user: @user)
      expect(f.attachment_associations.pluck(:context_type)).to eq ["Submission"]
      expect(f.attachment_associations.pluck(:context_id)).to eq [sub.id]
    end

    it "creates associations for online_upload entry submissions with attachments" do
      @course.enroll_student(@user, enrollment_state: :active)
      f = attachment_model(filename: "blah.txt", user: @user, context: @user)
      sub = @assignment.submit_homework(@user, submission_type: "online_upload", attachments: [f])
      expect(f.attachment_associations.pluck(:context_type)).to eq ["Submission"]
      expect(f.attachment_associations.pluck(:context_id)).to eq [sub.id]
    end
  end

  describe "versioned_attachments" do
    it "includes user attachments" do
      student_in_course(active_all: true)
      att = attachment_model(filename: "submission.doc", context: @student)
      sub = @assignment.submit_homework(@student, attachments: [att])
      expect(sub.versioned_attachments).to eq [att]
    end

    it "does not include attachments with a context of Submission" do
      student_in_course(active_all: true)
      att = attachment_model(filename: "submission.doc", context: @student)
      sub = @assignment.submit_homework(@student, attachments: [att])
      sub.attachments.update_all(context_type: "Submission", context_id: sub.id)
      expect(sub.reload.versioned_attachments).to be_empty
    end

    it "includes attachments owned by other users in a group for a group submission" do
      student1, student2 = n_students_in_course(2, { course: @course })
      assignment = @course.assignments.create!(name: "A1", submission_types: "online_upload")

      group_category = @course.group_categories.create!(name: "Project Groups")
      group = group_category.groups.create!(name: "A Team", context: @course)
      group.add_user(student1)
      group.add_user(student2)
      assignment.update(group_category:)

      user_attachment = attachment_model(context: student1)
      assignment.submit_homework(student1, submission_type: "online_upload", attachments: [user_attachment])

      [student1, student2].each do |student|
        submission = assignment.submission_for_student(student)
        submission.versioned_attachments
        expect(submission.versioned_attachments).to include user_attachment
      end
    end

    it "includes attachments uploaded from group by user without matching section id" do
      @group = @course.groups.create!
      @group.add_user(@user)
      f = Attachment.create! uploaded_data: StringIO.new("blah"),
                             context: @group,
                             filename: "blah.txt",
                             user: @user
      sub = @assignment.submit_homework(@user, attachments: [f])
      expect(sub.versioned_attachments).to eq [f]
    end
  end

  describe "includes_attachment?" do
    it "includes current attachments" do
      spoiler = attachment_model(context: @student)
      attachment_model context: @student
      sub = @assignment.submit_homework @student, attachments: [@attachment]
      expect(sub.attachments).to eq([@attachment])
      expect(sub.includes_attachment?(spoiler)).to be false
      expect(sub.includes_attachment?(@attachment)).to be true
    end

    it "includes attachments to previous versions" do
      old_attachment_1 = attachment_model(context: @student)
      old_attachment_2 = attachment_model(context: @student)
      @assignment.submit_homework @student, attachments: [old_attachment_1, old_attachment_2]
      attachment_model context: @student
      sub = @assignment.submit_homework @student, attachments: [@attachment]
      expect(sub.attachments.to_a).to eq([@attachment])
      expect(sub.includes_attachment?(old_attachment_1)).to be true
      expect(sub.includes_attachment?(old_attachment_2)).to be true
    end
  end

  shared_examples_for "a method which finds versioned originality reports" do |get_reports_fn|
    def make_attachment(mod)
      attachment_model(filename: "attachment_#{mod}.doc", context: @student)
    end

    it "works correctly on originality reports without submission times with multiple text entry or same attachment ids" do
      reports = []
      submissions = (1..3).map do |i|
        sub = @assignment.submit_homework(@student, body: "body #{i}")
        report = OriginalityReport.create!(attachment: nil, originality_score: i, submission: sub)
        report.update_columns(submission_time: nil)
        reports << report
        sub
      end
      attachment = attachment_model(filename: "submission.doc", context: @student)
      submissions += (1..3).map do |i|
        sub = @assignment.submit_homework(@student, attachments: [attachment])
        report = OriginalityReport.create!(attachment:, originality_score: i, submission: sub)
        report.update_columns(submission_time: nil)
        reports << report
        sub
      end

      versioned_reports = get_reports_fn.call(submissions)

      # Submissions/originality reports without attachments.
      # Match based on `o.created_at > submitted_at`
      (0..2).each do |i|
        expect(versioned_reports[i]).to match_array reports[i..2]
      end

      # Submissions/originality report with attachment. Match all originality reports with matching attachment
      (3..5).each do |i|
        expect(versioned_reports[i]).to match_array reports[3..]
      end
    end

    it "works correctly on originality reports with different submission times and same attachment ids (ie. student resubmits a previous attachment from files)" do
      attachment1 = make_attachment(1)
      sub1 = @assignment.submit_homework(@student, attachments: [attachment1])
      report1 = OriginalityReport.create!(attachment: attachment1, originality_score: 1, submission: sub1)

      attachment2 = make_attachment(2)
      sub2 = @assignment.submit_homework(@student, attachments: [attachment2])
      report2 = OriginalityReport.create!(attachment: attachment2, originality_score: 2, submission: sub2)

      # the last submission has a resubmission of the first attachment created
      sub3 = @assignment.submit_homework(@student, attachments: [attachment1])
      # TII got back to us with a report but no change in score
      report1.touch

      versioned_reports = get_reports_fn.call([sub1, sub2, sub3])
      expect(versioned_reports[0]).to eq [report1]
      expect(versioned_reports[1]).to eq [report2]
      expect(versioned_reports[2]).to eq [report1]
    end
  end

  describe "#versioned_originality_reports" do
    it_behaves_like "a method which finds versioned originality reports", lambda { |submissions|
      submissions.map(&:versioned_originality_reports)
    }

    it "loads originality reports for the submission" do
      student_in_course(active_all: true)
      attachment = attachment_model(filename: "submission.doc", context: @student)
      submission = @assignment.submit_homework(@student, attachments: [attachment])
      report = OriginalityReport.create!(attachment:, originality_score: "1", submission:)

      expect(submission.versioned_originality_reports).to eq [report]
    end

    it "memoizes the loaded originality reports" do
      student_in_course(active_all: true)
      attachment = attachment_model(filename: "submission.doc", context: @student)
      submission = @assignment.submit_homework(@student, attachments: [attachment])
      OriginalityReport.create!(attachment:, originality_score: "1", submission:)

      submission.versioned_originality_reports
      expect(OriginalityReport).not_to receive(:where)
      submission.versioned_originality_reports
    end

    it "returns an empty array when there are no reports" do
      student_in_course(active_all: true)
      attachment = attachment_model(filename: "submission.doc", context: @student)
      submission = @assignment.submit_homework(@student, attachments: [attachment])

      expect(submission.versioned_originality_reports).to eq []
    end

    it "returns an empty array when there are no attachments" do
      student_in_course(active_all: true)
      submission = @assignment.submit_homework(@student, body: "Oh my!")

      expect(submission.versioned_originality_reports).to eq []
    end
  end

  describe "#bulk_load_versioned_originality_reports" do
    before :once do
      student_in_course(active_all: true)
    end

    it_behaves_like "a method which finds versioned originality reports", lambda { |submissions|
      Submission.bulk_load_versioned_originality_reports(submissions)
      submissions.map(&:versioned_originality_reports)
    }

    it "bulk loads originality reports for many submissions at once" do
      originality_reports = []
      submissions = Array.new(3) do |i|
        student_in_course(active_all: true)
        attachments = [
          attachment_model(filename: "submission#{i}-a.doc", context: @student),
          attachment_model(filename: "submission#{i}-b.doc", context: @student)
        ]

        sub = @assignment.submit_homework(@student, attachments:)
        originality_reports << attachments.map do |a|
          OriginalityReport.create!(attachment: a, originality_score: "1", submission: sub)
        end
        sub
      end

      Submission.bulk_load_versioned_originality_reports(submissions)
      submissions.each_with_index do |s, i|
        expect(s.versioned_originality_reports).to eq originality_reports[i]
      end
    end

    it "avoids N+1s in the bulk load" do
      attachment = attachment_model(filename: "submission.doc", context: @student)
      submission = @assignment.submit_homework(@student, attachments: [attachment])
      OriginalityReport.create!(attachment:, originality_score: "1", submission:)

      Submission.bulk_load_versioned_originality_reports([submission])
      expect(OriginalityReport).not_to receive(:where)
      submission.versioned_originality_reports
    end

    it "ignores invalid attachment ids" do
      s = @assignment.submit_homework(@student, submission_type: "online_url", url: "http://example.com")
      s.update_attribute(:attachment_ids, "99999999")
      Submission.bulk_load_versioned_originality_reports([s])
      expect(s.versioned_originality_reports).to eq []
    end

    it "loads only the originality reports that pertain to that version" do
      originality_reports = []
      attachment = attachment_model(filename: "submission-a.doc", context: @student)
      Timecop.freeze(10.seconds.ago) do
        sub = @assignment.submit_homework(@student, submission_type: "online_upload", attachments: [attachment])
        originality_reports <<
          OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
      end

      attachment = attachment_model(filename: "submission-b.doc", context: @student)
      Timecop.freeze(5.seconds.ago) do
        sub = @assignment.submit_homework(@student, attachments: [attachment])
        originality_reports <<
          OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
      end

      attachment = attachment_model(filename: "submission-c.doc", context: @student)
      Timecop.freeze(1.second.ago) do
        sub = @assignment.submit_homework(@student, attachments: [attachment])
        originality_reports <<
          OriginalityReport.create!(attachment:, originality_score: "1", submission: sub)
      end

      submission = @assignment.submission_for_student(@student)
      Submission.bulk_load_versioned_originality_reports(submission.submission_history)

      submission.submission_history.each_with_index do |s, index|
        expect(s.versioned_originality_reports.first).to eq originality_reports[index]
      end
    end

    it "works with unsubmitted submissions" do
      submissions = @assignment.submissions.where(user: @student)
      Submission.bulk_load_versioned_originality_reports(submissions)
      submissions.each do |s|
        expect(s.versioned_originality_reports).to eq []
      end
    end
  end

  context "bulk loading attachments" do
    def ensure_attachments_arent_queried
      expect(Attachment).not_to receive(:where)
    end

    def submission_for_some_user
      student_in_course active_all: true
      @assignment.submit_homework(@student,
                                  submission_type: "online_url",
                                  url: "http://example.com")
    end

    describe "#bulk_load_versioned_attachments" do
      it "loads attachments for many submissions at once" do
        attachments = []

        submissions = Array.new(3) do |i|
          student_in_course(active_all: true)
          attachments << [
            attachment_model(filename: "submission#{i}-a.doc", context: @student),
            attachment_model(filename: "submission#{i}-b.doc", context: @student)
          ]

          @assignment.submit_homework @student, attachments: attachments[i]
        end

        Submission.bulk_load_versioned_attachments(submissions)
        ensure_attachments_arent_queried
        submissions.each_with_index do |s, i|
          expect(s.versioned_attachments).to eq attachments[i]
        end
      end

      it "filters out deleted attachments" do
        student = student_in_course(active_all: true).user
        attachment = attachment_model(filename: "submission.doc", context: student)
        submission = @assignment.submit_homework(student, attachments: [attachment])
        attachment.destroy_permanently!
        submission_with_attachments = Submission.bulk_load_versioned_attachments([submission]).first
        expect(submission_with_attachments.versioned_attachments).to be_empty
      end

      it "includes url submission attachments" do
        s = submission_for_some_user
        s.attachment = attachment_model(filename: "screenshot.jpg",
                                        context: @student)

        Submission.bulk_load_versioned_attachments([s])
        ensure_attachments_arent_queried
        expect(s.versioned_attachments).to eq [s.attachment]
      end

      it "handles bad data" do
        s = submission_for_some_user
        s.update_attribute(:attachment_ids, "99999999")
        Submission.bulk_load_versioned_attachments([s])
        expect(s.versioned_attachments).to eq []
      end

      it "handles submission histories with different attachments" do
        student_in_course(active_all: true)
        attachments = [attachment_model(filename: "submission-a.doc", context: @student)]
        Timecop.freeze(10.seconds.ago) do
          @assignment.submit_homework(@student,
                                      submission_type: "online_upload",
                                      attachments: [attachments[0]])
        end

        attachments << attachment_model(filename: "submission-b.doc", context: @student)
        Timecop.freeze(5.seconds.ago) do
          @assignment.submit_homework @student, attachments: [attachments[1]]
        end

        attachments << attachment_model(filename: "submission-c.doc", context: @student)
        Timecop.freeze(1.second.ago) do
          @assignment.submit_homework @student, attachments: [attachments[2]]
        end

        submission = @assignment.submission_for_student(@student)
        Submission.bulk_load_versioned_attachments(submission.submission_history)

        submission.submission_history.each_with_index do |s, index|
          expect(s.attachment_ids.to_i).to eq attachments[index].id
        end
      end
    end

    describe "#bulk_load_attachments_for_submissions" do
      it "loads attachments for many submissions at once and returns a hash" do
        expected_attachments_for_submissions = {}

        submissions = Array.new(3) do |i|
          student_in_course(active_all: true)
          attachment = [attachment_model(filename: "submission#{i}.doc", context: @student)]
          sub = @assignment.submit_homework @student, attachments: attachment
          expected_attachments_for_submissions[sub] = attachment
          sub
        end

        result = Submission.bulk_load_attachments_for_submissions(submissions)
        ensure_attachments_arent_queried
        expect(result).to eq(expected_attachments_for_submissions)
      end

      it "handles bad data" do
        s = submission_for_some_user
        s.update_attribute(:attachment_ids, "99999999")
        expected_attachments_for_submissions = { s => [] }
        result = Submission.bulk_load_attachments_for_submissions(s)
        expect(result).to eq(expected_attachments_for_submissions)
      end

      it "filters out attachment associations that don't point to an attachment" do
        student = student_in_course(active_all: true).user
        attachment = attachment_model(filename: "submission.doc", context: student)
        submission = @assignment.submit_homework(student, attachments: [attachment])
        submission.attachment_associations.find_by(attachment_id: attachment.id).update!(attachment_id: nil)
        attachments = Submission.bulk_load_attachments_for_submissions([submission]).first.second
        expect(attachments).to be_empty
      end

      it "filters out attachment associations that point to deleted attachments" do
        student = student_in_course(active_all: true).user
        attachment = attachment_model(filename: "submission.doc", context: student)
        submission = @assignment.submit_homework(student, attachments: [attachment])
        attachment.destroy_permanently!
        attachments = Submission.bulk_load_attachments_for_submissions([submission]).first.second
        expect(attachments).to be_empty
      end

      it "includes valid attachments and filters out deleted attachments" do
        student = student_in_course(active_all: true).user
        attachment = attachment_model(filename: "submission.doc", context: student)
        submission = @assignment.submit_homework(student, attachments: [attachment])
        attachment.destroy_permanently!

        another_student = student_in_course(active_all: true).user
        another_attachment = attachment_model(filename: "submission.doc", context: another_student)
        another_submission = @assignment.submit_homework(another_student, attachments: [another_attachment])

        bulk_loaded_submissions = Submission.bulk_load_attachments_for_submissions([submission, another_submission])
        submission_attachments = bulk_loaded_submissions.find { |s| s.first.id == submission.id }.second
        expect(submission_attachments).to be_empty

        another_submission_attachments = bulk_loaded_submissions.find { |s| s.first.id == another_submission.id }.second
        expect(another_submission_attachments).not_to be_empty
      end
    end
  end

  describe "#assign_assessor" do
    def peer_review_assignment
      assignment = @course.assignments.build(title: "Peer review",
                                             due_at: 1.day.ago,
                                             points_possible: 5,
                                             submission_types: "online_text_entry")
      assignment.peer_reviews_assigned = true
      assignment.peer_reviews = true
      assignment.automatic_peer_reviews = true
      assignment.save!

      assignment
    end

    before do
      student_in_course(active_all: true)
      @student2 = user_factory
      @student2_enrollment = @course.enroll_student(@student2)
      @student2_enrollment.accept!
      @assignment = peer_review_assignment
      @student1_homework = @assignment.submit_homework(@student,  body: "Lorem ipsum dolor")
      @student2_homework = @assignment.submit_homework(@student2, body: "Sit amet consectetuer")
    end

    it "sends a reminder notification" do
      expect_any_instance_of(AssessmentRequest).to receive(:send_reminder!).once
      submission1, submission2 = @assignment.submissions
      submission1.assign_assessor(submission2)
    end

    it "does not allow read access when assignment's peer reviews are off" do
      @student1_homework.assign_assessor(@student2_homework)
      expect(@student1_homework.grants_right?(@student2, :read)).to be true
      @assignment.peer_reviews = false
      @assignment.save!
      @student1_homework.reload
      AdheresToPolicy::Cache.clear
      expect(@student1_homework.grants_right?(@student2, :read)).to be false
    end

    it "does not allow read access when other student's enrollment is not active" do
      @student2_homework.assign_assessor(@student1_homework)
      @student2_enrollment.conclude
      expect(@student2_homework.grants_right?(@student, :read)).to be false
    end
  end

  describe "#get_web_snapshot" do
    it "does not blow up if web snapshotting fails" do
      sub = submission_spec_model
      expect(CutyCapt).to receive(:enabled?).and_return(true)
      expect(CutyCapt).to receive(:snapshot_attachment_for_url).with(sub.url, context: sub).and_return(nil)
      sub.get_web_snapshot
    end
  end

  describe "capturing screenshots for online_url submissions" do
    let_once(:course) { Course.create! }
    let_once(:student) { course.enroll_student(User.create!, active_all: true).user }
    let(:assignment) { course.assignments.create!(submission_types: ["online_url"]) }
    let(:sub) { assignments.find_by(user: student) }
    let(:submitted_url) { "https://example.com" }
    let(:get_web_snapshot_jobs) { Delayed::Job.where(tag: "Submission#get_web_snapshot").order(:id) }

    before do
      allow(CutyCapt).to receive(:enabled?).and_return(true)
    end

    it "calls #get_web_snapshot when it's the first submission attempt" do
      expect do
        assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
      end.to change {
        get_web_snapshot_jobs.count
      }.by(1)
    end

    it "calls #get_web_snapshot when it's not the first submission attempt" do
      assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
      expect do
        assignment.submit_homework(student, submission_type: "online_url", url: "https://example.com/different")
      end.to change {
        get_web_snapshot_jobs.count
      }.by(1)
    end

    it "calls #get_web_snapshot when it's not the first submission attempt and the url hasn't changed" do
      assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
      expect do
        assignment.submit_homework(student, submission_type: "online_url", url: submitted_url)
      end.to change {
        get_web_snapshot_jobs.count
      }.by(1)
    end

    it "does not call #get_web_snapshot when a url is not included" do
      expect do
        assignment.submit_homework(student, submission_type: "online_url", url: nil)
      end.not_to change {
        get_web_snapshot_jobs.count
      }
    end
  end

  describe "#submit_attachments_to_canvadocs" do
    context "canvadocs_submissions records" do
      before(:once) do
        @student1, @student2 = n_students_in_course(2)
        @attachment = canvadocable_attachment_model(context: @student1)
        @assignment = @course.assignments.create! name: "A1",
                                                  submission_types: "online_upload"
      end

      before do
        allow(Canvadocs).to receive_messages(enabled?: true, annotations_supported?: true, config: nil)
      end

      it "ties submissions to canvadocs" do
        s = @assignment.submit_homework(@student1,
                                        submission_type: "online_upload",
                                        attachments: [@attachment])
        expect(@attachment.canvadoc.submissions).to eq [s]
      end

      context "preferred_plugins" do
        it "does not send o365 as preferred plugins by default" do
          @assignment.submit_homework(@student1,
                                      submission_type: "online_upload",
                                      attachments: [@attachment])

          job = Delayed::Job.where(strand: "canvadocs").last
          expect(job.payload_object.kwargs[:preferred_plugins]).to eq [
            Canvadocs::RENDER_PDFJS,
            Canvadocs::RENDER_BOX
          ]
        end

        it "sends o365 as a preferred plugin when the 'Prefer Office 365 file viewer' account setting is enabled" do
          @assignment.context.root_account.settings[:canvadocs_prefer_office_online] = true
          @assignment.context.root_account.save!
          @assignment.submit_homework(@student1,
                                      submission_type: "online_upload",
                                      attachments: [@attachment])

          job = Delayed::Job.where(strand: "canvadocs").last
          expect(job.payload_object.kwargs[:preferred_plugins]).to eq [
            Canvadocs::RENDER_O365,
            Canvadocs::RENDER_PDFJS,
            Canvadocs::RENDER_BOX
          ]
        end
      end

      it "create records for each group submission" do
        gc = @course.group_categories.create! name: "Project Groups"
        group = gc.groups.create! name: "A Team", context: @course
        group.add_user(@student1)
        group.add_user(@student2)

        @assignment.update_attribute :group_category, gc
        @assignment.submit_homework(@student1,
                                    submission_type: "online_upload",
                                    attachments: [@attachment])

        [@student1, @student2].each do |student|
          submission = @assignment.submission_for_student(student)
          expect(@attachment.canvadoc.submissions).to include submission
        end
      end
    end

    it "doesn't create jobs for non-previewable documents" do
      job_scope = Delayed::Job.where(strand: "canvadocs")
      orig_job_count = job_scope.count

      attachment = attachment_model(context: @user)
      @assignment.submit_homework(@user,
                                  submission_type: "online_upload",
                                  attachments: [attachment])
      expect(job_scope.count).to eq orig_job_count
    end
  end

  describe "#annotation_context" do
    before(:once) do
      @attachment = attachment_model(context: @user)
      @assignment.update!(annotatable_attachment_id: @attachment.id)
      @submission = @assignment.submissions.find_by(user: @user)
    end

    it "creates a canvadocs_annotation_context if draft is true" do
      new_student = @course.enroll_student(User.create!).user
      new_submission = @assignment.submissions.find_by(user: new_student)

      expect do
        new_submission.annotation_context(draft: true)
      end.to change {
        new_submission.canvadocs_annotation_contexts.where(attachment: @attachment, submission_attempt: nil).count
      }.by(1)
    end

    it "returns the already existing canvadocs_annotation_context when passed draft multiple times" do
      existing_context = @submission.annotation_context(draft: true)
      expect(@submission.annotation_context(draft: true)).to eq existing_context
    end

    it "returns nil if a canvadocs_annotation_context does not exist" do
      expect(@submission.annotation_context(attempt: 1)).to be_nil
    end

    it "returns the annotation_context if one exists for the attempt" do
      @submission.update!(attempt: 1)
      context = @submission.canvadocs_annotation_contexts.create!(
        attachment: @attachment,
        submission_attempt: @submission.attempt
      )
      expect(@submission.annotation_context(attempt: @submission.attempt)).to eq context
    end
  end

  describe ".process_bulk_update" do
    before(:once) do
      course_with_teacher active_all: true
      @u1, @u2 = n_students_in_course(2)
      @a1, @a2 = Array.new(2) do
        @course.assignments.create! points_possible: 10
      end
      @progress = Progress.create!(context: @course, tag: "submissions_update")
    end

    it "updates submissions on an assignment" do
      Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                       @a1.id.to_s => {
                                         @u1.id => { posted_grade: 5 },
                                         @u2.id => { posted_grade: 10 }
                                       }
                                     })

      expect(@a1.submission_for_student(@u1).grade).to eql "5"
      expect(@a1.submission_for_student(@u2).grade).to eql "10"
    end

    it "only recalculates scores for users with changed submissions" do
      data1 = { @a1.id.to_s => { @u1.id => { posted_grade: 5 }, @u2.id => { posted_grade: 10 } } }
      data2 = { @a1.id.to_s => { @u1.id => { posted_grade: 5 }, @u2.id => { posted_grade: 11 } } } # leave u1 the same
      Submission.process_bulk_update(@progress, @course, nil, @teacher, data1)

      expect_any_instantiation_of(@course).to receive(:recompute_student_scores).with([@u2.id])
      Submission.process_bulk_update(@progress, @course, nil, @teacher, data2)
    end

    it "updates submissions on multiple assignments" do
      Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                       @a1.id => {
                                         @u1.id => { posted_grade: 5 },
                                         @u2.id => { posted_grade: 10 }
                                       },
                                       @a2.id.to_s => {
                                         @u1.id => { posted_grade: 10 },
                                         @u2.id => { posted_grade: 5 }
                                       }
                                     })

      expect(@a1.submission_for_student(@u1).grade).to eql "5"
      expect(@a1.submission_for_student(@u2).grade).to eql "10"
      expect(@a2.submission_for_student(@u1).grade).to eql "10"
      expect(@a2.submission_for_student(@u2).grade).to eql "5"
    end

    it "maintains grade when only updating comments" do
      @a1.grade_student(@u1, grade: 3, grader: @teacher)
      Submission.process_bulk_update(@progress,
                                     @course,
                                     nil,
                                     @teacher,
                                     {
                                       @a1.id => {
                                         @u1.id => { text_comment: "comment" }
                                       }
                                     })

      expect(@a1.submission_for_student(@u1).grade).to eql "3"
    end

    it "nils grade when receiving empty posted_grade" do
      @a1.grade_student(@u1, grade: 3, grader: @teacher)
      Submission.process_bulk_update(@progress,
                                     @course,
                                     nil,
                                     @teacher,
                                     {
                                       @a1.id => {
                                         @u1.id => { posted_grade: nil }
                                       }
                                     })

      expect(@a1.submission_for_student(@u1).grade).to be_nil
    end

    it "does not explode if the assignment is deleted" do
      @a1.destroy
      expect do
        Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                         @a1.id.to_s => {
                                           @u1.id => { posted_grade: 5 },
                                           @u2.id => { posted_grade: 10 }
                                         }
                                       })
      end.to_not raise_error
      expect(@progress.reload.failed?).to be_truthy

      expect(@a1.submission_for_student(@u1).grade).to be_nil
      expect(@a1.submission_for_student(@u2).grade).to be_nil
    end

    it "does not update grader_id if submission is blank or missing with -" do
      Submission.process_bulk_update(@progress,
                                     @course,
                                     nil,
                                     @teacher,
                                     {
                                       @a1.id => {
                                         @u1.id => { posted_grade: nil }
                                       },
                                       @a1.id => {
                                         @u2.id => { posted_grade: "-" }
                                       }
                                     })

      submission1 = @a1.submission_for_student(@u1)
      submission2 = @a1.submission_for_student(@u2)

      expect(submission1.grade).to be_nil
      expect(submission2.grade).to be_nil
      expect(submission1.grader_id).to be_nil
      expect(submission2.grader_id).to be_nil
    end

    describe "submitting comments via bulk update" do
      let(:auto_assignment) { @a1 }
      let(:manual_assignment) do
        @a2.post_policy.update!(post_manually: true)
        @a2
      end

      it "sets the comment to visible if the assignment is automatically posted" do
        Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                         auto_assignment.id.to_s => {
                                           @u1.id => { text_comment: "hello there" }
                                         }
                                       })

        comment = auto_assignment.submission_for_student(@u1).submission_comments.last
        expect(comment).not_to be_hidden
      end

      it "sets the comment to visible if the relevant submission has already been posted" do
        auto_assignment.grade_student(@u1, grade: 0, grader: @teacher)
        Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                         auto_assignment.id.to_s => {
                                           @u1.id => { text_comment: "hello there" }
                                         }
                                       })

        comment = auto_assignment.submission_for_student(@u1).submission_comments.last
        expect(comment).not_to be_hidden
      end

      it "sets the comment to visible if a grade is also included in the update" do
        Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                         auto_assignment.id.to_s => {
                                           @u1.id => { posted_grade: 0, text_comment: "hello there" }
                                         }
                                       })

        comment = auto_assignment.submission_for_student(@u1).submission_comments.last
        expect(comment).not_to be_hidden
      end

      context "for a manually-posted assignment" do
        let(:submission) { manual_assignment.submission_for_student(@u1) }

        it "shows the comment if the associated submission is already posted" do
          manual_assignment.post_submissions(submission_ids: [submission.id])

          Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                           manual_assignment.id.to_s => {
                                             @u1.id => { text_comment: "hello there" }
                                           }
                                         })
          expect(submission.submission_comments.last).not_to be_hidden
        end

        it "leaves the comment hidden if the associated submission is not posted" do
          Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                           manual_assignment.id.to_s => {
                                             @u1.id => { text_comment: "clandestine comment" }
                                           }
                                         })
          expect(submission.submission_comments.last).to be_hidden
        end
      end
    end

    context "with the cache" do
      specs_require_cache(:redis_cache_store)

      it "updates submissions with an assignment override shortly after updating the override" do
        assignment = @course.assignments.create!(points_possible: 10, only_visible_to_overrides: true)
        # Force the course wide cache to be set. In the normal course of process_bulk_update, the front end would get
        # called to queue the delayed job update. In that flow, the cache gets set for the course, and assignment
        # without specific users. This next line is to mimic that behavior for testing.
        AssignmentVisibility::AssignmentVisibilityService.assignments_visible_to_students(course_ids: @course, assignment_ids: assignment)
        assignment.assignment_overrides.create!(set_type: "CourseSection", set: @course.course_sections.first)

        Submission.process_bulk_update(@progress, @course, nil, @teacher, {
                                         assignment.id.to_s => {
                                           @u1.id => { posted_grade: 5 },
                                           @u2.id => { posted_grade: 10 }
                                         }
                                       })

        expect(assignment.submission_for_student(@u1).grade).to eql "5"
        expect(assignment.submission_for_student(@u2).grade).to eql "10"
      end
    end
  end

  describe "find_or_create_provisional_grade!" do
    before(:once) do
      @assignment.grader_count = 1
      @assignment.moderated_grading = true
      @assignment.final_grader = @teacher
      @assignment.save!
      submission_spec_model

      @teacher2 = User.create(name: "some teacher 2")
      @context.enroll_teacher(@teacher2)
    end

    context "when force_save is true" do
      it do
        expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
          .to change { AnonymousOrModerationEvent.provisional_grade_created.count }.by(1)
      end

      it do
        expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
          .to_not change { AnonymousOrModerationEvent.provisional_grade_updated.count }
      end

      context "given an existing provisional grade" do
        before(:once) { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }

        it do
          expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
            .to change { AnonymousOrModerationEvent.provisional_grade_updated.count }.by(1)
        end

        it do
          expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
            .not_to change { AnonymousOrModerationEvent.provisional_grade_created.count }
        end
      end
    end

    it "properly creates a provisional grade with all default values but scorer" do
      @submission.find_or_create_provisional_grade!(@teacher)

      expect(@submission.provisional_grades.length).to be 1

      pg = @submission.provisional_grades.first

      expect(pg.scorer_id).to eql @teacher.id
      expect(pg.final).to be false
      expect(pg.graded_anonymously).to be_nil
      expect(pg.grade).to be_nil
      expect(pg.score).to be_nil
      expect(pg.source_provisional_grade).to be_nil
    end

    it "properly amends information to an existing provisional grade" do
      @submission.find_or_create_provisional_grade!(@teacher)
      @submission.find_or_create_provisional_grade!(
        @teacher,
        score: 15.0,
        grade: "20",
        graded_anonymously: true
      )

      expect(@submission.provisional_grades.length).to be 1

      pg = @submission.provisional_grades.first

      expect(pg.scorer_id).to eql @teacher.id
      expect(pg.final).to be false
      expect(pg.graded_anonymously).to be true
      expect(pg.grade).to eql "20"
      expect(pg.score).to be 15.0
      expect(pg.source_provisional_grade).to be_nil
    end

    it "computes provisional grade grade if not given" do
      @submission.find_or_create_provisional_grade!(@teacher)
      @submission.find_or_create_provisional_grade!(
        @teacher,
        score: 15.0
      )

      expect(@submission.provisional_grades.length).to be 1

      pg = @submission.provisional_grades.first

      expect(pg.grade).to eql "15"
    end

    it "does not update grade or score if not given" do
      @submission.find_or_create_provisional_grade!(@teacher, grade: "20", score: 12.0)

      expect(@submission.provisional_grades.first.grade).to eql "20"
      expect(@submission.provisional_grades.first.score).to be 12.0

      @submission.find_or_create_provisional_grade!(@teacher)

      expect(@submission.provisional_grades.first.grade).to eql "20"
      expect(@submission.provisional_grades.first.score).to be 12.0
    end

    it "does not update graded_anonymously if not given" do
      @submission.find_or_create_provisional_grade!(@teacher, graded_anonymously: true)

      expect(@submission.provisional_grades.first.graded_anonymously).to be true

      @submission.find_or_create_provisional_grade!(@teacher)

      expect(@submission.provisional_grades.first.graded_anonymously).to be true
    end

    it "raises an exception if final is true and user is not allowed to select final grade" do
      expect { @submission.find_or_create_provisional_grade!(@student, final: true) }
        .to raise_error(Assignment::GradeError, "User not authorized to give final provisional grades")
    end

    it "raises an exception if grade is not final and student does not need a provisional grade" do
      @assignment.grade_student(@student, grade: 2, grader: @teacher2, provisional: true)
      third_teacher = User.create!
      @course.enroll_teacher(third_teacher, enrollment_state: :active)

      expect { @submission.find_or_create_provisional_grade!(third_teacher, final: false) }
        .to raise_error(Assignment::GradeError, "Student already has the maximum number of provisional grades")
    end

    it "raises an exception if the grade is final and no non-final provisional grades exist" do
      expect { @submission.find_or_create_provisional_grade!(@teacher, final: true) }
        .to raise_error(Assignment::GradeError,
                        "Cannot give a final mark for a student with no other provisional grades")
    end

    it "raises an exception if the grade has been selected and is associated with a provisional grader" do
      pg = @submission.find_or_create_provisional_grade!(@teacher2, grade: "2", score: 2)
      selection = @assignment.moderated_grading_selections.where(student: @submission.user).first
      selection.provisional_grade = pg
      selection.save!

      expect do
        @submission.find_or_create_provisional_grade!(@teacher2, grade: "3", score: 3)
      end.to raise_error(Assignment::GradeError) do |error|
        expect(error.error_code).to eq Assignment::GradeError::PROVISIONAL_GRADE_MODIFY_SELECTED
      end
    end

    it "does not raise an exception if the grade has been selected and is associated with the final grader" do
      pg = @submission.find_or_create_provisional_grade!(@teacher, grade: "2", score: 2)
      selection = @assignment.moderated_grading_selections.where(student: @submission.user).first
      selection.provisional_grade = pg
      selection.save!

      expect do
        @submission.find_or_create_provisional_grade!(@teacher, grade: "3", score: 3)
      end.not_to raise_error
    end

    it "sets the source provisional grade if one is provided" do
      new_source = ModeratedGrading::ProvisionalGrade.new
      provisional_grade = @submission.find_or_create_provisional_grade!(@teacher, source_provisional_grade: new_source)
      expect(provisional_grade.source_provisional_grade).to be new_source
    end

    it "does not wipe out the existing source provisional grade, if a source_provisional_grade is not provided" do
      @submission.find_or_create_provisional_grade!(@teacher)
      expect { @submission.find_or_create_provisional_grade!(@teacher, force_save: true) }
        .not_to change { @submission.provisional_grades.last.source_provisional_grade }
    end
  end

  describe "moderated_grading_allow_list" do
    before(:once) do
      @student = @user
      @assignment.update!(
        final_grader: @teacher,
        grader_comments_visible_to_graders: false,
        grader_count: 3,
        moderated_grading: true,
        submission_types: :online_text_entry
      )
      @assignment.submit_homework(@student, body: "my submission", submission_type: :online_text_entry)
      @submission = @assignment.submissions.find_by(user: @student)
    end

    let(:user_ids_in_allow_list) { allow_list.map { |user| user.fetch(:global_id)&.to_i } }

    it "returns nil when the assignment is not moderated" do
      # Skipping validations here because they'd prevent turning off Moderated Grading
      # for an assignment with graded submissions.
      @assignment.update_column(:moderated_grading, false)
      expect(@submission.moderated_grading_allow_list).to be_nil
    end

    it "returns nil when the user is not present" do
      expect(@submission.moderated_grading_allow_list(nil)).to be_nil
    end

    it "returns a collection of moderated grading ids" do
      moderated_grading_ids = @student.moderated_grading_ids(false)
      expect(@submission.moderated_grading_allow_list.first).to eq moderated_grading_ids
    end

    it "calls moderation_allow_list_for_user to generate the allow_list" do
      expect(@submission).to receive(:moderation_allow_list_for_user).with(@student).once.and_call_original
      @submission.moderated_grading_allow_list
    end
  end

  describe "moderation_allow_list_for_user" do
    before(:once) do
      @student = @user
      @provisional_grader = User.create!
      @other_provisional_grader = User.create!
      @course.enroll_teacher(@provisional_grader, enrollment_state: :active)
      @course.enroll_teacher(@other_provisional_grader, enrollment_state: :active)
      @eligible_provisional_grader = User.create!
      @course.enroll_teacher(@eligible_provisional_grader, enrollment_state: :active)
      @admin = account_admin_user(account: @course.root_account)
      @assignment.update!(
        final_grader: @teacher,
        grader_comments_visible_to_graders: false,
        grader_count: 3,
        moderated_grading: true,
        submission_types: :online_text_entry
      )
      @assignment.submit_homework(@student, body: "my submission", submission_type: :online_text_entry)
      @submission = @assignment.submissions.find_by(user: @student)
      @assignment.grade_student(@student, grader: @teacher, provisional: true, score: 5)
      @assignment.grade_student(@student, grader: @provisional_grader, provisional: true, score: 1)
      @assignment.grade_student(@student, grader: @other_provisional_grader, provisional: true, score: 3)
    end

    let(:user_ids_in_allow_list) { allow_list.map { |user| user.fetch(:global_id)&.to_i } }

    it "returns an empty array when the assignment is not moderated" do
      # Skipping validations here because they'd prevent turning off Moderated Grading
      # for an assignment with graded submissions.
      @assignment.update_column(:moderated_grading, false)
      expect(@submission.moderation_allow_list_for_user(@teacher)).to be_empty
    end

    it "returns an empty array when the user is not present" do
      expect(@submission.moderation_allow_list_for_user(nil)).to be_empty
    end

    it "returns an empty array when the user is not permitted to view annotations for the submission" do
      other_student = User.create!
      @course.enroll_student(other_student, enrollment_state: :active)
      expect(@submission.moderation_allow_list_for_user(other_student)).to be_empty
    end

    it "always includes the submission owner when the assignment is of type Student Annotation" do
      ta = @course.enroll_ta(User.create!).user
      @assignment.update!(grader_count: 2, submission_types: "student_annotation")
      expect(@submission.moderation_allow_list_for_user(ta)).to eq [@student]
    end

    context "when the submission is not posted" do
      context "when the user is the final grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@teacher) }

        it "includes the current user" do
          expect(allow_list).to include @teacher
        end

        it "includes all provisional graders" do
          expect(allow_list).to include(*@assignment.moderation_grader_users)
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end
      end

      context "when the user is a provisional grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@provisional_grader) }

        context "when grader comments are visible to other graders" do
          before(:once) do
            @assignment.update!(grader_comments_visible_to_graders: true)
          end

          it "includes all provisional graders" do
            expect(allow_list).to include(*@assignment.moderation_grader_users)
          end

          it "includes the final grader" do
            expect(allow_list).to include @teacher
          end

          it "includes the student" do
            expect(allow_list).to include @student
          end

          it "does not include eligible provisional graders" do
            expect(allow_list).not_to include @eligible_provisional_grader
          end

          it "does not include duplicates" do
            expect(allow_list.uniq).to eq allow_list
          end

          it "does not include nil values" do
            expect(allow_list).not_to include nil
          end
        end

        context "when grader comments are not visible to other graders" do
          it "includes the current user" do
            expect(allow_list).to include @provisional_grader
          end

          it "does not include other provisional graders" do
            expect(allow_list).not_to include @other_provisional_grader
          end

          it "does not include the final grader" do
            expect(allow_list).not_to include @teacher
          end

          it "includes the student" do
            expect(allow_list).to include @student
          end

          it "does not include eligible provisional graders" do
            expect(allow_list).not_to include @eligible_provisional_grader
          end

          it "does not include duplicates" do
            expect(allow_list.uniq).to eq allow_list
          end

          it "does not include nil values" do
            expect(allow_list).not_to include nil
          end
        end
      end

      context "when the user is an eligible provisional grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@eligible_provisional_grader) }

        context "when grader comments are visible to other graders" do
          before(:once) do
            @assignment.update!(grader_comments_visible_to_graders: true)
          end

          it "includes the current user" do
            expect(allow_list).to include @eligible_provisional_grader
          end

          it "includes all provisional graders" do
            expect(allow_list).to include(*@assignment.moderation_grader_users)
          end

          it "includes the final grader" do
            expect(allow_list).to include @teacher
          end

          it "includes the student" do
            expect(allow_list).to include @student
          end

          it "does not include other eligible provisional graders" do
            other_eligible_provisional_grader = User.create!
            @course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
            expect(allow_list).not_to include other_eligible_provisional_grader
          end

          it "does not include duplicates" do
            expect(allow_list.uniq).to eq allow_list
          end

          it "does not include nil values" do
            expect(allow_list).not_to include nil
          end
        end

        context "when grader comments are not visible to other graders" do
          it "includes the current user" do
            expect(allow_list).to include @eligible_provisional_grader
          end

          it "does not include provisional graders" do
            expect(allow_list).not_to include(*@assignment.moderation_grader_users)
          end

          it "does not include the final grader" do
            expect(allow_list).not_to include @teacher
          end

          it "includes the student" do
            expect(allow_list).to include @student
          end

          it "does not include other eligible provisional graders" do
            other_eligible_provisional_grader = User.create!
            @course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
            expect(allow_list).not_to include other_eligible_provisional_grader
          end

          it "does not include duplicates" do
            expect(allow_list.uniq).to eq allow_list
          end

          it "does not include nil values" do
            expect(allow_list).not_to include nil
          end
        end
      end

      context "when the user is an admin" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@admin) }

        it "includes the current user" do
          expect(allow_list).to include @admin
        end

        it "includes all provisional graders" do
          expect(allow_list).to include(*@assignment.moderation_grader_users)
        end

        it "includes the final grader" do
          expect(allow_list).to include @teacher
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end
      end

      context "when the user is a student" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@student) }

        it "includes the current user" do
          expect(allow_list).to include @student
        end

        it "does not include the admin" do
          expect(allow_list).not_to include @admin
        end

        it "does not include provisional graders" do
          expect(allow_list).not_to include(*@assignment.moderation_grader_users)
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end
      end
    end

    context "when the submission is posted" do
      before(:once) do
        provisional_grade = @submission.find_or_create_provisional_grade!(@provisional_grader)
        selection = @assignment.moderated_grading_selections.find_by(student: @student)
        selection.update!(provisional_grade:)
        provisional_grade.publish!
        @assignment.update!(grades_published_at: 1.hour.ago)
        @assignment.post_submissions
        @submission.reload
      end

      context "when the user is the final grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@teacher) }

        it "includes the current user" do
          expect(allow_list).to include @teacher
        end

        it "includes the provisional grader whose grade was selected" do
          expect(allow_list).to include @provisional_grader
        end

        it "does not include the provisional grader whose grade was not selected" do
          expect(allow_list).not_to include @other_provisional_grader
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end

        it "does not raise an error when the submission has no grader" do
          @submission.update!(grader: nil, score: nil)
          expect { allow_list }.not_to raise_error
        end
      end

      context "when the user is a provisional grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@provisional_grader) }

        it "includes the current user" do
          expect(allow_list).to include @provisional_grader
        end

        it "does not include other provisional graders whose grades were not selected" do
          expect(allow_list).not_to include @other_provisional_grader
        end

        it "does not include the final grader if their grade was not selected" do
          expect(allow_list).not_to include @teacher
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end

        it "does not raise an error when the submission has no grader" do
          @submission.update!(grader: nil, score: nil)
          expect { allow_list }.not_to raise_error
        end
      end

      context "when the user is an eligible provisional grader" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@eligible_provisional_grader) }

        it "includes the current user" do
          expect(allow_list).to include @eligible_provisional_grader
        end

        it "includes the provisional grader whose grade was selected" do
          expect(allow_list).to include @provisional_grader
        end

        it "does not include the provisional grader whose grade was not selected" do
          expect(allow_list).not_to include @other_provisional_grader
        end

        it "does not include the final grader if their grade was not selected" do
          expect(allow_list).not_to include @teacher
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include other eligible provisional graders" do
          other_eligible_provisional_grader = User.create!
          @course.enroll_teacher(other_eligible_provisional_grader, enrollment_state: :active)
          expect(allow_list).not_to include other_eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end

        it "does not raise an error when the submission has no grader" do
          @submission.update!(grader: nil, score: nil)
          expect { allow_list }.not_to raise_error
        end
      end

      context "when the user is an admin" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@admin) }

        it "includes the current user" do
          expect(allow_list).to include @admin
        end

        it "includes the provisional grader whose grade was selected" do
          expect(allow_list).to include @provisional_grader
        end

        it "does not include the provisional grader whose grade was not selected" do
          expect(allow_list).not_to include @other_provisional_grader
        end

        it "does not include the final grader if their grade was not selected" do
          expect(allow_list).not_to include @teacher
        end

        it "includes the student" do
          expect(allow_list).to include @student
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end

        it "does not raise an error when the submission has no grader" do
          @submission.update!(grader: nil, score: nil)
          expect { allow_list }.not_to raise_error
        end
      end

      context "when the user is a student" do
        let(:allow_list) { @submission.moderation_allow_list_for_user(@student) }

        it "includes the current user" do
          expect(allow_list).to include @student
        end

        it "includes the provisional grader whose grade was selected" do
          expect(allow_list).to include @provisional_grader
        end

        it "does not include the provisional grader whose grade was not selected" do
          expect(allow_list).not_to include @other_provisional_grader
        end

        it "does not include the final grader if their grade was not selected" do
          expect(allow_list).not_to include @teacher
        end

        it "does not include eligible provisional graders" do
          expect(allow_list).not_to include @eligible_provisional_grader
        end

        it "does not include duplicates" do
          expect(allow_list.uniq).to eq allow_list
        end

        it "does not include nil values" do
          expect(allow_list).not_to include nil
        end

        it "does not raise an error when the submission has no grader" do
          @submission.update!(grader: nil, score: nil)
          expect { allow_list }.not_to raise_error
        end
      end
    end
  end

  describe "anonymous_identities" do
    let(:submission) { @assignment.submissions.first }

    it "includes the student in the list" do
      expect(submission.anonymous_identities).to have_key @student.id
    end

    it "includes the anonymous name" do
      expect(submission.anonymous_identities.dig(@student.id, :name)).to eq "Student"
    end

    it "includes the anonymous id" do
      expect(submission.anonymous_identities.dig(@student.id, :id)).to eq submission.anonymous_id
    end
  end

  describe "#visible_rubric_assessments_for" do
    subject { @submission.visible_rubric_assessments_for(@viewing_user) }

    before :once do
      submission_model assignment: @assignment, user: @student
      @viewing_user = @teacher
      @assessed_user = @student
      rubric_association_model association_object: @assignment, purpose: "grading"
      [@teacher, @student].each do |user|
        @rubric_association.rubric_assessments.create!({
                                                         artifact: @submission,
                                                         assessment_type: "grading",
                                                         assessor: user,
                                                         rubric: @rubric,
                                                         user: @assessed_user
                                                       })
      end
      @teacher_assessment = @submission.rubric_assessments.where(assessor_id: @teacher).first
      @student_assessment = @submission.rubric_assessments.where(assessor_id: @student).first
    end

    context "when the submission is unposted and the viewing user cannot :read_grade" do
      before(:once) do
        @assignment.post_policy.update!(post_manually: true)
        @viewing_user = @student
      end

      it "excludes assessments by other users" do
        expect(subject).not_to include(@teacher_assessment)
      end

      it "includes assessments authored by the viewing user" do
        course = Course.create!
        assessed_student = course.enroll_student(User.create!, workflow_state: "active").user
        assessing_student = course.enroll_student(User.create!, workflow_state: "active").user

        assignment = course.assignments.create!(peer_reviews: true)
        rubric_association = rubric_association_model(context: course, association_object: assignment, purpose: "grading")

        submission = assignment.submission_for_student(assessed_student)
        submission.assessment_requests.create!(
          user: assessed_student,
          assessor: assessing_student,
          assessor_asset: assignment.submission_for_student(assessing_student)
        )
        peer_review_assessment = rubric_association.rubric_assessments.create!({
                                                                                 artifact: submission,
                                                                                 assessment_type: "grading",
                                                                                 assessor: assessing_student,
                                                                                 rubric: rubric_association.rubric,
                                                                                 user: assessed_student
                                                                               })

        expect(submission.visible_rubric_assessments_for(assessing_student)).to include(peer_review_assessment)
      end
    end

    it "returns the rubric assessments if user can :read_grade" do
      expect(subject).to contain_exactly(@teacher_assessment, @student_assessment)
    end

    it "returns the rubric assessments if the submission is posted" do
      @submission.update!(posted_at: Time.zone.now)
      expect(subject).to contain_exactly(@teacher_assessment, @student_assessment)
    end

    it "does not return rubric assessments if assignment has no rubric" do
      @assignment.rubric_association.destroy!

      expect(subject).not_to include(@teacher_assessment)
    end

    it "only returns rubric assessments from associated rubrics" do
      other = @rubric_association.dup
      other.save!
      other_assessment = other.rubric_assessments.create!({
                                                            artifact: @submission,
                                                            assessment_type: "grading",
                                                            assessor: @teacher,
                                                            rubric: @rubric,
                                                            user: @assessed_user
                                                          })

      expect(subject).to eq([other_assessment])
    end

    context "attempt argument" do
      before(:once) do
        @submission2 = @assignment.submit_homework(@student, body: "bar", submitted_at: 1.hour.since)
      end

      it "returns an empty list if no rubric assessments exist for the desired attempt" do
        expect(
          @submission2.visible_rubric_assessments_for(@viewing_user, attempt: @submission2.attempt)
        ).to be_empty
      end

      it "can find historic rubric assessments of older attempts" do
        expect(
          @submission2.visible_rubric_assessments_for(@viewing_user, attempt: @submission.attempt)
        ).to contain_exactly(@teacher_assessment, @student_assessment)
      end

      it "returns assessments for every attempt if attempt is nil" do
        @teacher_assessment.update!(artifact_attempt: 0)
        @student_assessment.update!(artifact_attempt: 1)
        expect(
          @submission2.visible_rubric_assessments_for(@viewing_user, attempt: nil)
        ).to contain_exactly(@teacher_assessment, @student_assessment)
      end

      it "specifically returns assessments with a nil artifact_attempt if an attempt of 0 is specified" do
        assignment = @course.assignments.create!(submission_types: "online_text_entry")
        rubric_association = rubric_association_model(association_object: assignment, purpose: "grading")
        submission = assignment.submission_for_student(@student)

        assessment_before_submitting = rubric_association.rubric_assessments.create!({
                                                                                       artifact: submission,
                                                                                       assessment_type: "grading",
                                                                                       assessor: @student,
                                                                                       rubric: rubric_association.rubric,
                                                                                       user: @student
                                                                                     })

        submission = assignment.submit_homework(@student, body: "hi")

        rubric_association.rubric_assessments.create!({
                                                        artifact: submission,
                                                        assessment_type: "grading",
                                                        assessor: @teacher,
                                                        rubric: rubric_association.rubric,
                                                        user: @student
                                                      })

        expect(submission.visible_rubric_assessments_for(@student, attempt: 0))
          .to contain_exactly(assessment_before_submitting)
      end
    end

    context "with provisional grades" do
      before(:once) do
        @final_grader = @teacher
        @moderated_assignment = @course.assignments.create!(
          due_at: 2.years.from_now,
          final_grader: @final_grader,
          grader_count: 2,
          moderated_grading: true,
          points_possible: 10,
          submission_types: :online_text_entry,
          title: "Moderated Assignment"
        )
        @moderated_rubric_association = rubric_association_model(
          context: @course,
          rubric: @rubric,
          association_object: @moderated_assignment,
          purpose: "grading"
        )
        @moderated_submission = @moderated_assignment.submit_homework(@student, body: "foo", submitted_at: 2.hours.ago)

        moderator_provisional_grade = @moderated_submission.find_or_create_provisional_grade!(@final_grader)
        @moderator_assessment = @moderated_rubric_association.assess({
                                                                       user: @student,
                                                                       assessor: @final_grader,
                                                                       artifact: moderator_provisional_grade,
                                                                       assessment: { assessment_type: "grading", criterion_crit1: { points: 5 } }
                                                                     })

        @provisional_grader = user_factory(active_all: true)
        @course.enroll_ta(@provisional_grader, enrollment_state: "active")
        provisional_grade = @moderated_submission.find_or_create_provisional_grade!(@provisional_grader)
        @provisional_assessment = @moderated_rubric_association.assess({
                                                                         user: @student,
                                                                         assessor: @provisional_grader,
                                                                         artifact: provisional_grade,
                                                                         assessment: { assessment_type: "grading", criterion_crit1: { points: 5 } }
                                                                       })
        @other_grader = user_factory(active_all: true)
        @course.enroll_ta(@other_grader, enrollment_state: "active")
        other_provisional_grade = @moderated_submission.find_or_create_provisional_grade!(@other_grader)
        @other_assessment = @moderated_rubric_association.assess({
                                                                   user: @student,
                                                                   assessor: @other_grader,
                                                                   artifact: other_provisional_grade,
                                                                   assessment: { assessment_type: "grading", criterion_crit1: { points: 5 } }
                                                                 })
        @all_provisional_assessments = [@moderator_assessment, @provisional_assessment, @other_assessment]
      end

      it "excludes provisional assessments by default" do
        assessments = @moderated_submission.visible_rubric_assessments_for(@provisional_grader)
        expect(assessments).to be_empty
      end

      it "includes provisional assessments when include_provisional is true" do
        assessments = @moderated_submission.visible_rubric_assessments_for(@provisional_grader, include_provisional: true, provisional_assessments: @all_provisional_assessments)
        expect(assessments).to contain_exactly(@provisional_assessment)
      end

      it "allows moderators to see all provisional assessments" do
        assessments = @moderated_submission.visible_rubric_assessments_for(@final_grader, include_provisional: true, provisional_assessments: @all_provisional_assessments)
        expect(assessments).to contain_exactly(@moderator_assessment, @provisional_assessment, @other_assessment)
      end

      it "allows provisional graders to see only their own provisional assessments" do
        assessments = @moderated_submission.visible_rubric_assessments_for(@other_grader, include_provisional: true, provisional_assessments: @all_provisional_assessments)
        expect(assessments).to contain_exactly(@other_assessment)
      end
    end

    context "anonymous peer reviews" do
      before(:once) do
        course = Course.create!
        @reviewed_student = course.enroll_student(User.create!, workflow_state: "active").user
        @reviewing_student = course.enroll_student(User.create!, workflow_state: "active").user
        @grading_teacher = course.enroll_teacher(User.create!, workflow_state: "active").user

        assignment = course.assignments.create!(peer_reviews: true, anonymous_peer_reviews: true)
        rubric_association = rubric_association_model(context: course, association_object: assignment, purpose: "grading")

        @submission = assignment.submission_for_student(@reviewed_student)
        @submission.assessment_requests.create!(
          user: @reviewed_student,
          assessor: @reviewing_student,
          assessor_asset: assignment.submission_for_student(@reviewing_student)
        )
        rubric_association.rubric_assessments.create!({
                                                        artifact: @submission,
                                                        assessment_type: "peer_review",
                                                        assessor: @reviewing_student,
                                                        rubric: rubric_association.rubric,
                                                        user: @reviewed_student
                                                      })

        rubric_association.rubric_assessments.create!({
                                                        artifact: @submission,
                                                        assessment_type: "grading",
                                                        assessor: @grading_teacher,
                                                        rubric: rubric_association.rubric,
                                                        user: @reviewed_student
                                                      })
      end

      it "viewed by reviewed_student include rubric assessments from teachers with identity attached" do
        expect(@submission.visible_rubric_assessments_for(@reviewed_student)[0].assessor).to eql(@grading_teacher)
      end

      it "viewed by reviewed_student does not include peer reviewer's identity when viewed by the reviewee" do
        expect(@submission.visible_rubric_assessments_for(@reviewed_student)[1].assessor).to be_nil
      end

      it "includes peer reviewer's identity when viewed by the reviewer" do
        expect(@submission.visible_rubric_assessments_for(@reviewing_student)[0].assessor).to eql(@reviewing_student)
      end
    end

    context "self assessments" do
      before(:once) do
        course = Course.create!
        @student = course.enroll_student(User.create!, workflow_state: "active").user

        assignment = course.assignments.create!(peer_reviews: true, anonymous_peer_reviews: true)
        rubric_association = rubric_association_model(context: course, association_object: assignment, purpose: "grading")

        @submission = assignment.submission_for_student(@student)
        rubric_association.rubric_assessments.create!({
                                                        artifact: @submission,
                                                        assessment_type: "self_assessment",
                                                        assessor: @student,
                                                        rubric: rubric_association.rubric,
                                                        user: @student
                                                      })
      end

      it "includes self assessments" do
        assessments = @submission.visible_rubric_assessments_for(@student)
        expect(assessments.count).to eq 1
        expect(assessments.first.assessment_type).to eq "self_assessment"
      end

      it "includes self assessments no matter the attempt" do
        @submission.update!(attempt: 2)
        assessments = @submission.visible_rubric_assessments_for(@student, attempt: 2)
        expect(assessments.count).to eq 1
        expect(assessments.first.assessment_type).to eq "self_assessment"
      end
    end
  end

  describe "#rubric_assessment" do
    let(:submission) { @assignment.submission_for_student(@student) }

    it "excludes non-grading assessments" do
      grading_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
      grading_assessment = grading_rubric_association.rubric_assessments.create!(
        artifact: submission,
        assessment_type: "grading",
        assessor: @teacher,
        rubric: grading_rubric_association.rubric,
        user: @student
      )

      non_grading_rubric_association = rubric_association_model(association_object: @assignment, purpose: "pleasurable event")
      non_grading_rubric_association.rubric_assessments.create!(
        artifact: submission,
        assessment_type: "pleasurable event",
        assessor: @teacher,
        rubric: non_grading_rubric_association.rubric,
        user: @student
      )

      expect(submission.rubric_assessment).to eq grading_assessment
    end

    it "prioritizes assessments with a non-nil rubric_association when multiple grading assessments exist" do
      old_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
      old_assessment = old_rubric_association.rubric_assessments.create!(
        artifact: submission,
        assessment_type: "grading",
        assessor: @teacher,
        rubric: old_rubric_association.rubric,
        user: @student
      )
      old_rubric_association.destroy

      new_rubric_association = rubric_association_model(association_object: @assignment, purpose: "grading")
      new_assessment = new_rubric_association.rubric_assessments.create!(
        artifact: submission,
        assessment_type: "grading",
        assessor: @teacher,
        rubric: new_rubric_association.rubric,
        user: @student
      )

      aggregate_failures do
        expect(submission.rubric_assessments).to contain_exactly(old_assessment, new_assessment)
        expect(submission.rubric_assessment).to eq new_assessment
      end
    end
  end

  describe "#add_comment" do
    before(:once) do
      submission_spec_model
    end

    it "creates a draft comment when passed true in the draft_comment option" do
      comment = @submission.add_comment(author: @teacher, comment: "42", draft_comment: true)

      expect(comment).to be_draft
    end

    it "creates a final comment when not passed in a draft_comment option" do
      comment = @submission.add_comment(author: @teacher, comment: "42")

      expect(comment).not_to be_draft
    end

    it "creates a final comment when passed false in the draft_comment option" do
      comment = @submission.add_comment(author: @teacher, comment: "42", draft_comment: false)

      expect(comment).not_to be_draft
    end

    it "creates a comment without an author when skip_author option is true" do
      comment = @submission.add_comment(comment: "42", skip_author: true)

      expect(comment.author).to be_nil
    end

    it "allows you to specify submission attempt for the comment" do
      @submission.update!(attempt: 4)
      comment = @submission.add_comment(author: @teacher, comment: "42", attempt: 3)
      expect(comment.attempt).to eq 3
    end

    it "sets the attempt to latest submission attempt when an attempt option is not specified" do
      @submission.update!(attempt: 5, workflow_state: "graded")
      comment = @submission.add_comment(author: @teacher, comment: "42")
      expect(comment.attempt).to eq 5
    end

    it "sets comment hidden to false if comment causes posting" do
      @assignment.ensure_post_policy(post_manually: false)
      @assignment.grade_student(@student, grader: @teacher, score: 5)
      @submission.update!(posted_at: nil)
      comment = @submission.add_comment(author: @teacher, comment: "a comment!", hidden: true)
      expect(comment).not_to be_hidden
    end

    it "does not set comment hidden to false if comment does not cause posting" do
      @assignment.ensure_post_policy(post_manually: true)
      @assignment.grade_student(@student, grader: @teacher, score: 5)
      @submission.update!(posted_at: nil)
      comment = @submission.add_comment(author: @teacher, comment: "a comment!", hidden: true)
      expect(comment).to be_hidden
    end

    describe "audit event logging" do
      let(:course) { Course.create! }
      let(:assignment) { course.assignments.create!(title: "ok", anonymous_grading: true) }
      let(:student) { course.enroll_student(User.create!, enrollment_state: "active").user }
      let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: "active").user }
      let(:submission) { assignment.submissions.find_by!(user: student) }
      let(:comment_params) { { comment: "my great submission", author: student } }
      let(:last_event) { AnonymousOrModerationEvent.where(assignment:, submission:).last }

      context "for an auditable assignment" do
        it "creates an event when a non-draft comment is published" do
          expect { submission.add_comment(comment_params) }.to change {
            AnonymousOrModerationEvent.where(assignment:, submission:).count
          }.by(1)
        end

        it 'sets "submission_comment_created" as the event type' do
          submission.add_comment(comment_params)
          expect(last_event.event_type).to eq "submission_comment_created"
        end

        it "sets the user ID to the author of the comment" do
          submission.add_comment(comment_params)
          expect(last_event.user_id).to eq student.id
        end

        it "does not create events for draft comments" do
          draft_params = comment_params.merge(draft_comment: true)
          expect { submission.add_comment(draft_params) }.not_to change {
            AnonymousOrModerationEvent.where(assignment:, submission:).count
          }
        end

        describe "auditable attributes" do
          it 'captures the value of the "comment" attribute' do
            submission.add_comment(comment_params)
            expect(last_event.payload["comment"]).to eq "my great submission"
          end

          it 'captures the value of the "author_id" attribute' do
            submission.add_comment(comment_params)
            expect(last_event.payload["author_id"]).to eq student.id
          end

          it 'captures the value of the "media_comment_id" attribute' do
            submission.add_comment(comment_params.merge(media_comment_id: 12))
            expect(last_event.payload["media_comment_id"]).to eq "12"
          end

          it 'captures the value of the "media_comment_type" attribute' do
            submission.add_comment(comment_params.merge(media_comment_type: "audio"))
            expect(last_event.payload["media_comment_type"]).to eq "audio"
          end

          it 'captures the value of the "group_comment_id" attribute' do
            submission.add_comment(comment_params.merge(group_comment_id: 12))
            expect(last_event.payload["group_comment_id"]).to eq "12"
          end

          it 'captures the value of the "assessment_request" attribute' do
            assessment_request = submission.assessment_requests.create!(
              user: student,
              assessor: student,
              assessor_asset: submission
            )
            submission.add_comment(comment_params.merge(assessment_request:))
            expect(last_event.payload["assessment_request_id"]).to eq assessment_request.id
          end

          it 'captures the value of the "attachments" attribute' do
            attachment = Attachment.create!(
              filename: "my_great_file.txt",
              uploaded_data: StringIO.new("hello!"),
              context: course
            )
            submission.add_comment(comment_params.merge(attachments: [attachment]))
            expect(last_event.payload["attachment_ids"]).to eq attachment.id.to_s
          end

          it 'captures the value of the "anonymous" attribute' do
            assignment.update!(anonymous_peer_reviews: true)
            submission.add_comment(comment_params)
            expect(last_event.payload["anonymous"]).to be true
          end

          it 'captures the value of the "provisional_grade_id" attribute' do
            assignment.update!(moderated_grading: true, final_grader: teacher, grader_count: 1)
            provisional_grade = submission.find_or_create_provisional_grade!(teacher)

            provisional_comment_params = comment_params.merge(provisional: true, author: teacher)
            submission.add_comment(provisional_comment_params)
            expect(last_event.payload["provisional_grade_id"]).to eq provisional_grade.id
          end
        end

        describe "external tool autograding" do
          let(:external_tool) do
            Account.default.context_external_tools.create!(
              name: "Undertow",
              url: "http://www.example.com",
              consumer_key: "12345",
              shared_secret: "secret"
            )
          end

          it "creates an event when graded by an external tool" do
            expect { assignment.grade_student(student, grader_id: -external_tool.id, score: 80) }.to change {
              AnonymousOrModerationEvent.where(assignment:, submission:).count
            }.by(1)
          end
        end

        describe "quiz autograding" do
          let(:quiz) do
            quiz = course.quizzes.create!
            quiz.workflow_state = "available"
            quiz.quiz_questions.create!({ question_data: test_quiz_data.first })
            quiz.save!
            quiz.assignment.updating_user = teacher
            quiz.assignment.update_attribute(:anonymous_grading, true)
            quiz
          end
          let(:quiz_assignment) { quiz.assignment }
          let(:quiz_submission) do
            qsub = Quizzes::SubmissionManager.new(quiz).find_or_create_submission(student)
            qsub.quiz_data = test_quiz_data
            qsub.started_at = 1.minute.ago
            qsub.attempt = 1
            qsub.submission_data = [{ points: 0, text: "7051", question_id: 128, correct: false, answer_id: 7051 }]
            qsub.score = 0
            qsub.save!
            qsub.finished_at = Time.now.utc
            qsub.workflow_state = "complete"
            qsub.submission = quiz.assignment.find_or_create_submission(student)
            qsub
          end

          it "creates an event when graded by a quiz" do
            real_submission = quiz_submission.submission
            real_submission.audit_grade_changes = true
            expect { quiz_submission.with_versioning(true) { quiz_submission.save! } }.to change {
              AnonymousOrModerationEvent.where(assignment: quiz_assignment, submission: real_submission).count
            }.by(1)
          end
        end
      end

      it "does not create audit events when the assignment is not auditable" do
        assignment1 = course.assignments.create!(title: "ok", anonymous_grading: false)
        submission1 = assignment1.submission_for_student(student)
        expect { submission1.add_comment(comment_params) }.not_to change {
          AnonymousOrModerationEvent.where(assignment:, submission:).count
        }
      end
    end

    describe "submission posting" do
      let(:course) { Course.create! }
      let(:assignment) { course.assignments.create!(title: "ok") }
      let(:student) { course.enroll_student(User.create!, enrollment_state: "active").user }
      let(:teacher) { course.enroll_teacher(User.create!, enrollment_state: "active").user }
      let(:submission) { assignment.submissions.find_by!(user: student) }
      let(:comment_params) { { comment: "oh no", author: teacher } }

      context "when the submission is unposted" do
        it "posts the submission if the comment is from an instructor in the course" do
          submission.add_comment(comment_params)
          expect(submission).to be_posted
        end

        it "posts the submission if the comment is from an admin" do
          admin = User.create!
          course.root_account.account_users.create!(user: admin)
          submission.add_comment(comment_params.merge({ author: admin }))
          expect(submission).to be_posted
        end

        it "does not post the submission if the comment is not from an instructor or admin" do
          submission.add_comment(comment_params.merge({ author: student }))
          expect(submission).not_to be_posted
        end

        it "does not post the submission if the comment is a draft" do
          submission.add_comment(comment_params.merge({ draft_comment: true }))
          expect(submission).not_to be_posted
        end

        it "does not post the submission if the comment has no author" do
          comment_params.delete(:author)
          submission.add_comment(comment_params)
          expect(submission).not_to be_posted
        end

        it "does not post the submission if the comment is provisional" do
          moderated_assignment = course.assignments.create!(
            title: "aa",
            moderated_grading: true,
            final_grader: teacher,
            grader_count: 2
          )

          moderated_submission = moderated_assignment.submission_for_student(student)
          moderated_submission.add_comment(comment_params.merge({ provisional: true }))
          expect(moderated_submission).not_to be_posted
        end

        it "does not post the submission if the assignment is manually-posted" do
          assignment.ensure_post_policy(post_manually: true)
          submission.add_comment(comment_params)
          expect(submission).not_to be_posted
        end

        it "does not post the submission if post policies are not enabled and the assignment is muted" do
          assignment.mute!
          expect(submission).not_to be_posted
        end
      end

      it "does not update the posted_at date if a submission is already posted" do
        submission.update!(posted_at: 1.day.ago)

        expect do
          submission.add_comment(comment_params)
        end.not_to change {
          assignment.submission_for_student(student).posted_at
        }
      end
    end
  end

  describe "#last_teacher_comment" do
    before(:once) do
      submission_spec_model
    end

    it "returns the last published comment made by the teacher" do
      @submission.add_comment(author: @teacher, comment: "a comment")
      expect(@submission.last_teacher_comment).to be_present
    end

    it "does not include draft comments" do
      @submission.add_comment(author: @teacher, comment: "a comment", draft_comment: true)
      expect(@submission.last_teacher_comment).to be_nil
    end
  end

  describe "#ensure_grader_can_grade" do
    before do
      @submission = Submission.new
    end

    context "when #grader_can_grade? returns true" do
      before do
        expect(@submission).to receive(:grader_can_grade?).and_return(true)
      end

      it "returns true" do
        expect(@submission.ensure_grader_can_grade).to be_truthy
      end

      it "does not add any errors to @submission" do
        @submission.ensure_grader_can_grade

        expect(@submission.errors.full_messages).to be_empty
      end
    end

    context "when #grader_can_grade? returns false" do
      before do
        expect(@submission).to receive(:grader_can_grade?).and_return(false)
      end

      it "returns false" do
        expect(@submission.ensure_grader_can_grade).to be_falsey
      end

      it "adds an error to the :grade field" do
        @submission.ensure_grader_can_grade

        expect(@submission.errors[:grade]).not_to be_empty
      end

      describe "skip_grader_check" do
        it "does not add an error to the :grade field if skip_grader_check is true" do
          @submission.skip_grader_check = true
          @submission.ensure_grader_can_grade
          expect(@submission.errors[:grade]).to be_empty
        end

        it "adds an error to the :grade field if skip_grader_check is false" do
          @submission.skip_grader_check = false
          @submission.ensure_grader_can_grade
          expect(@submission.errors[:grade]).not_to be_empty
        end

        it "adds an error to the :grade field if skip_grader_check is not set" do
          @submission.ensure_grader_can_grade
          expect(@submission.errors[:grade]).not_to be_empty
        end
      end
    end
  end

  describe "#grader_can_grade?" do
    before do
      @submission = Submission.new
    end

    it "returns true if grade hasn't been changed" do
      expect(@submission).to receive(:grade_changed?).and_return(false)

      expect(@submission.grader_can_grade?).to be_truthy
    end

    it "returns true if the submission is autograded and the submission can be autograded" do
      expect(@submission).to receive(:grade_changed?).and_return(true)

      expect(@submission).to receive(:autograded?).and_return(true)
      expect(@submission).to receive(:can_autograde?).and_return(true)

      expect(@submission.grader_can_grade?).to be_truthy
    end

    it "returns true if the submission isn't autograded but can still be graded" do
      expect(@submission).to receive(:grade_changed?).and_return(true)
      expect(@submission).to receive(:autograded?).and_return(false)

      @submission.grader = @grader = User.new

      expect(@submission).to receive(:grants_right?).with(@grader, :grade).and_return(true)

      expect(@submission.grader_can_grade?).to be_truthy
    end

    it "returns false if the grade changed but the submission can't be graded at all" do
      @submission.grader = @grader = User.new

      expect(@submission).to receive(:grade_changed?).and_return(true)
      expect(@submission).to receive(:autograded?).and_return(false)
      expect(@submission).to receive(:grants_right?).with(@grader, :grade).and_return(false)

      expect(@submission.grader_can_grade?).to be_falsey
    end
  end

  describe "#submission_history" do
    let!(:student) { student_in_course(active_all: true).user }
    let(:attachment) { attachment_model(filename: "submission-a.doc", context: student) }
    let(:submission) { @assignment.submit_homework(student, submission_type: "online_upload", attachments: [attachment]) }

    it "includes originality data" do
      OriginalityReport.create!(submission:, attachment:, originality_score: 1.0, workflow_state: "pending")
      submission.originality_reports.load_target
      expect(submission.submission_history.first.turnitin_data[attachment.asset_string][:similarity_score]).to eq 1.0
    end

    it "doesn't include the originality_data if originality_report isn't pre loaded" do
      OriginalityReport.create!(submission:, attachment:, originality_score: 1.0, workflow_state: "pending")
      expect(submission.submission_history.first.turnitin_data[attachment.asset_string]).to be_nil
    end

    it "returns self as complete history when no history record is present" do
      student.submissions.destroy_all

      create_sql = "INSERT INTO #{Submission.quoted_table_name}
                     (assignment_id, user_id, workflow_state, created_at, updated_at, course_id)
                     values
                     (#{@assignment.id}, #{student.id}, 'unsubmitted', now(), now(), #{@assignment.context_id})"

      sub = Submission.find(Submission.connection.create(create_sql))
      expect(sub.submission_history).to eq([sub])
    end

    it "optionally returns the version along with the model" do
      aggregate_failures do
        history = submission.submission_history(include_version: true)
        expect(history.size).to be 1
        entry = history.first
        expect(entry[:version].id).to eql submission.versions.first.id
        expect(entry[:model].id).to eql submission.id
      end
    end

    it "considers the include_version arg in memoization" do
      aggregate_failures do
        submission.submission_history(include_version: true)
        history = submission.submission_history(include_version: false)
        expect(history.first).to be_a Submission
      end
    end
  end

  context "draft comments" do
    before do
      @teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
      ta = course_with_user("TaEnrollment", course: @course, name: "First Ta", active_all: true).user
      @student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
      @assignment = @course.assignments.create!(name: "plain assignment")
      @assignment.ensure_post_policy(post_manually: true)
      @submission = @assignment.submissions.find_by(user: @student)
      @student_comment = @submission.add_comment(author: @student, comment: "Student comment")
      @teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher comment", draft_comment: true)
      @ta_comment = @submission.add_comment(author: ta, comment: "Ta comment")
    end

    describe "#comments_excluding_drafts_for" do
      it "doesn't blow up when the submission has no course" do
        @submission.course = nil
        expect { @submission.comments_excluding_drafts_for(@teacher) }.not_to raise_error
      end

      it "returns non-draft comments, filtering out draft comments" do
        comments = @submission.comments_excluding_drafts_for(@teacher)
        expect(comments).to include @student_comment, @ta_comment
        expect(comments).not_to include @teacher_comment
      end

      it "does not return hidden comments if the user is a student and the comment has not been posted" do
        hidden_comment = @submission.add_comment(author: @teacher, comment: "Hidden comment", hidden: true)
        @assignment.ensure_post_policy(post_manually: false)
        comments = @submission.comments_excluding_drafts_for(@student)
        expect(comments).not_to include hidden_comment
      end

      context "when comments are preloaded" do
        it "returns non-draft comments, filtering out draft comments" do
          preloaded_submission = Submission.where(id: @submission.id).preload(:submission_comments).first
          comments = preloaded_submission.comments_excluding_drafts_for(@teacher)
          expect(comments).to include @student_comment, @ta_comment
          expect(comments).not_to include @teacher_comment
        end
      end
    end

    describe "#comments_including_drafts_for" do
      it "returns draft comments, filtering out draft comments" do
        comments = @submission.comments_including_drafts_for(@teacher)
        expect(comments).to include @student_comment, @ta_comment
        expect(comments).to include @teacher_comment
      end

      context "when comments are preloaded" do
        it "returns non-draft comments, filtering out draft comments" do
          preloaded_submission = Submission.where(id: @submission.id).preload(:submission_comments).first
          comments = preloaded_submission.comments_including_drafts_for(@teacher)
          expect(comments).to include @student_comment, @ta_comment
          expect(comments).to include @teacher_comment
        end
      end
    end
  end

  describe "#feedback_for_current_attempt?" do
    before(:once) do
      @teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
      @student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
      @peer = course_with_user("StudentEnrollment", course: @course, name: "Peer", active_all: true).user
      @assignment = @course.assignments.create!(name: "HasFeedback Assignment")
      @submission = @assignment.submissions.find_by(user: @student)
    end

    it "is true when a teacher leaves a comment" do
      @submission.attempt = 1
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 1)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is true when a peer leaves a comment" do
      @submission.attempt = 1
      @submission.add_comment(author: @peer, comment: "Peer comment", attempt: 1)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is true when a teacher leaves a comment on the latest attempt" do
      @submission.attempt = 3
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 3)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is true when a teacher has left a comment prior to the first attempt (nil)" do
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is true when a teacher has left a comment prior to the first attempt (zero)" do
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 0)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is true when a teacher leaves a comment prior to the first attempt and it has been submitted" do
      @submission.attempt = 1
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
      expect(@submission).to be_feedback_for_current_attempt
    end

    it "is false when no comments exist" do
      expect(@submission).not_to be_feedback_for_current_attempt
    end

    it "is false when a teacher leaves comment on prior attempt" do
      @submission.attempt = 2
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: 1)
      expect(@submission).not_to be_feedback_for_current_attempt
    end

    it "is false when a teacher leaves a comment prior to first attempt and a second is started" do
      @submission.attempt = 2
      @submission.add_comment(author: @teacher, comment: "Teacher comment", attempt: nil)
      expect(@submission).not_to be_feedback_for_current_attempt
    end

    it "is false when a peer leaves comment on prior attempt" do
      @submission.attempt = 2
      @submission.add_comment(author: @peer, comment: "Peer comment", attempt: 1)
      expect(@submission).not_to be_feedback_for_current_attempt
    end

    it "is false when only submitter has commented on the current attempt" do
      @submission.attempt = 1
      @submission.add_comment(author: @student, comment: "Student comment", attempt: 1)
      expect(@submission).not_to be_feedback_for_current_attempt
    end
  end

  describe "#visible_submission_comments_for" do
    before(:once) do
      @teacher = course_with_user("TeacherEnrollment", course: @course, name: "Teacher", active_all: true).user
      @first_ta = course_with_user("TaEnrollment", course: @course, name: "First Ta", active_all: true).user
      @second_ta = course_with_user("TaEnrollment", course: @course, name: "Second Ta", active_all: true).user
      @third_ta = course_with_user("TaEnrollment", course: @course, name: "Third Ta", active_all: true).user
      @student = course_with_user("StudentEnrollment", course: @course, name: "Student", active_all: true).user
      @admin = account_admin_user(account: @course.account)

      @assignment = @course.assignments.create!(name: "plain assignment")
      @assignment.ensure_post_policy(post_manually: true)

      @submission = @assignment.submissions.find_by(user: @student)
      @student_comment = @submission.add_comment(author: @student, comment: "Student comment")
      @teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher comment")
      @first_ta_comment = @submission.add_comment(author: @first_ta, comment: "First Ta comment")
    end

    it "shows teacher all comments" do
      comments = @submission.reload.visible_submission_comments_for(@teacher)
      expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
    end

    it "shows ta all comments" do
      comments = @submission.reload.visible_submission_comments_for(@first_ta)
      expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
    end

    it "shows student all comments, when submission is posted" do
      @submission.update!(posted_at: Time.zone.now)
      comments = @submission.reload.visible_submission_comments_for(@student)
      expect(comments).to match_array([@student_comment, @teacher_comment, @first_ta_comment])
    end

    it "shows student only their own comment, when submission is unposted" do
      comments = @submission.reload.visible_submission_comments_for(@student)
      expect(comments).to match_array([@student_comment])
    end

    context "for an assignment with peer reviews" do
      let_once(:assignment) do
        @course.assignments.create!(name: "peer review assignment", peer_reviews: true, muted: true)
      end

      before(:once) do
        assignment.ensure_post_policy(post_manually: true)
        @submission = assignment.submissions.find_by(user: @student)
        @student2 = course_with_user("StudentEnrollment", course: @course, name: "Student2", active_all: true).user
        student2_sub = assignment.submissions.find_by(user: @student2)
        student2_request = AssessmentRequest.create!(assessor: @student2, assessor_asset: student2_sub, asset: @submission, user: @student)
        @teacher_comment = @submission.add_comment(author: @teacher, comment: "A teacher comment")
        @peer_review_comment = @submission.add_comment(author: @student2, comment: "A peer reviewer's comment", assessment_request: student2_request)
        @student_comment = @submission.add_comment(author: @student, comment: "A comment by the submitter")
      end

      context "when grades are hidden" do
        before(:once) do
          other_assessor = @course.enroll_student(User.create!(name: "Student3")).user
          other_request = AssessmentRequest.create!(
            assessor: other_assessor,
            assessor_asset: @assignment.submission_for_student(other_assessor),
            asset: @submission,
            user: @student
          )
          @alternate_assessment_comment = @submission.add_comment(author: other_assessor, comment: "Other assessment", assessment_request: other_request)
        end

        it "shows the submitting student their own comments and any peer review comments" do
          comments = @submission.reload.visible_submission_comments_for(@student)
          expect(comments).to match_array([@peer_review_comment, @student_comment, @alternate_assessment_comment])
        end

        it "shows a peer-reviewing student only their own comments" do
          comments = @submission.reload.visible_submission_comments_for(@student2)
          expect(comments).to match_array([@peer_review_comment])
        end
      end

      context "when grades have been posted" do
        before(:once) do
          assignment.post_submissions
        end

        it "shows the submitting student comments from all users" do
          comments = @submission.reload.visible_submission_comments_for(@student)
          expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
        end

        it "shows a peer-reviewing student only their own comments" do
          comments = @submission.reload.visible_submission_comments_for(@student2)
          expect(comments).to match_array([@peer_review_comment])
        end
      end
    end

    context "when assignment is graded as a group" do
      let_once(:all_groups) { @course.group_categories.create!(name: "all groups") }

      before(:once) do
        student2 = course_with_user("StudentEnrollment", course: @course, name: "Student2", active_all: true).user
        group1 = all_groups.groups.create!(context: @course)
        group1.add_user(@student)
        group1.add_user(student2)
        assignment = @course.assignments.create!(
          grade_group_students_individually: false,
          group_category: all_groups,
          name: "group assignment"
        )
        @submission = assignment.submissions.find_by(user: @student)
        @student_comment = @submission.add_comment(author: @student, comment: "Student comment", group_comment_id: group1.id)
        @student2_comment = @submission.add_comment(author: student2, comment: "Student2 comment", group_comment_id: group1.id)
      end

      it "returns comments scoped to that group" do
        comments = @submission.reload.visible_submission_comments_for(@teacher)
        expect(comments).to match_array([@student_comment, @student2_comment])
      end

      context "when peer reviews are enabled" do
        before(:once) do
          @student = @course.enroll_student(User.create!, enrollment_state: "active").user
          @student2 = @course.enroll_student(User.create!, enrollment_state: "active").user
          all_groups.groups.create!(context: @course).add_user(@student)
          all_groups.groups.create!(context: @course).add_user(@student2)
          assignment = @course.assignments.create!(
            grade_group_students_individually: false,
            group_category: all_groups,
            name: "group assignment",
            peer_reviews: true
          )
          @submission = assignment.submissions.find_by(user: @student)
          student2_sub = assignment.submissions.find_by(user: @student2)
          AssessmentRequest.create!(
            assessor: @student2,
            assessor_asset: student2_sub,
            asset: @submission,
            user: @student
          )
          @peer_review_comment = @submission.add_comment(author: @student2, comment: "Student2", group_comment_id: "ab")
          @student_comment = @submission.add_comment(author: @student, comment: "Student", group_comment_id: "ac")
          @teacher_comment = @submission.add_comment(author: @teacher, comment: "Teacher", group_comment_id: "ad")
        end

        it "shows a peer reviewer only their own comments" do
          comments = @submission.reload.visible_submission_comments_for(@student2)
          expect(comments).to match_array([@peer_review_comment])
        end

        it "shows all comments to the submitting student" do
          comments = @submission.reload.visible_submission_comments_for(@student)
          expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
        end

        it "shows all comments to a teacher" do
          comments = @submission.reload.visible_submission_comments_for(@teacher)
          expect(comments).to match_array([@peer_review_comment, @student_comment, @teacher_comment])
        end
      end
    end

    context "when the assignment is a group peer-reviewed assignment" do
      let_once(:student1) { @course.enroll_student(User.create!, active_all: true).user }
      let_once(:student2) { @course.enroll_student(User.create!, active_all: true).user }
      let_once(:student3) { @course.enroll_student(User.create!, active_all: true).user }
      let_once(:student4) { @course.enroll_student(User.create!, active_all: true).user }

      let_once(:group_category) do
        group_category = @course.group_categories.create!(name: "a group")
        group_category.create_groups(3)

        group_category.groups.first.add_user(student1)
        group_category.groups.second.add_user(student2)
        group_category.groups.second.add_user(student3)
        group_category.groups.third.add_user(student4)
        group_category
      end

      let_once(:assignment) do
        @course.assignments.create!(group_category:, peer_reviews: true)
      end

      before(:once) do
        assignment.submit_homework(student1, body: "I am student 1")
        assignment.submit_homework(student2, body: "I am student 2")
        assignment.submit_homework(student3, body: "I am student 3")
        assignment.submit_homework(student4, body: "I am student 4")

        assignment.assign_peer_review(student1, student2)
        assignment.assign_peer_review(student1, student4)
      end

      context "when the assignment is manually posted" do
        before(:once) do
          assignment.post_policy.update!(post_manually: true)

          # Call update_submission to post the comment (rather than add_comment)
          # so that it gets propagated to other group members
          student2_submission_params = {
            assessment_request: AssessmentRequest.find_by(assessor: student1, user: student2),
            author: student1,
            comment: "good job",
            group_comment: true
          }
          assignment.update_submission(student2, student2_submission_params)

          student4_submission_params = {
            assessment_request: AssessmentRequest.find_by(assessor: student1, user: student4),
            author: student1,
            comment: "bad job",
            group_comment: true
          }
          assignment.update_submission(student4, student4_submission_params)
        end

        it "allows the specific recipient of the comment to view it" do
          comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student2), author: student1)

          expect(comment).to be_grants_right(student2, :read)
        end

        it "allows other students in the recipient's group to view their respective comment" do
          comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student3), author: student1)

          expect(comment).to be_grants_right(student3, :read)
        end

        it "does not allow assessed students in a different group to view the comment" do
          comment = SubmissionComment.find_by(submission: assignment.submission_for_student(student2), author: student1)

          expect(comment).not_to be_grants_right(student4, :read)
        end
      end
    end

    context "for a moderated assignment" do
      before(:once) do
        @assignment = @course.assignments.create!(
          name: "moderated assignment",
          moderated_grading: true,
          grader_count: 10,
          final_grader: @teacher
        )
        @assignment.grade_student(@student, grade: 1, grader: @first_ta, provisional: true)
        @assignment.grade_student(@student, grade: 1, grader: @second_ta, provisional: true)
        @assignment.grade_student(@student, grade: 1, grader: @teacher, provisional: true)
        @submission = @assignment.submissions.find_by(user: @student)
        @student_comment = @submission.add_comment(author: @student, comment: "Student comment")
        @first_ta_comment = @submission.add_comment(author: @first_ta, comment: "First Ta comment", provisional: true)
        @second_ta_comment = @submission.add_comment(author: @second_ta, comment: "Second Ta comment", provisional: true)
        @third_ta_comment = @submission.add_comment(author: @third_ta, comment: "Third Ta comment", provisional: true)
        @final_grader_comment = @submission.add_comment(author: @teacher, comment: "Final Grader comment", provisional: true)
      end

      context "when graders can view other graders' comments" do
        context "when grades are unpublished" do
          it "shows final grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@teacher)
            expect(comments).to match_array([
                                              @student_comment,
                                              @first_ta_comment,
                                              @second_ta_comment,
                                              @third_ta_comment,
                                              @final_grader_comment
                                            ])
          end

          it "shows provisional grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@first_ta)
            expect(comments).to match_array([
                                              @student_comment,
                                              @first_ta_comment,
                                              @second_ta_comment,
                                              @third_ta_comment,
                                              @final_grader_comment
                                            ])
          end

          it "shows student only their own comments" do
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments).to match_array([@student_comment])
          end

          it "shows admins all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@admin)
            expect(comments).to match_array([
                                              @student_comment,
                                              @first_ta_comment,
                                              @second_ta_comment,
                                              @third_ta_comment,
                                              @final_grader_comment
                                            ])
          end
        end

        context "when grades are published" do
          before(:once) do
            ModeratedGrading::ProvisionalGrade.find_by(submission: @submission, scorer: @first_ta).publish!
            @assignment.update!(grades_published_at: Time.zone.now)
            @submission.reload
          end

          it "shows final grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@teacher)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Third Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows provisional grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@first_ta)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Third Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows student only their own comments" do
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments).to match_array([@student_comment])
          end

          it "when grades are posted, shows student their own, chosen grader's, and final grader's comments" do
            @assignment.post_submissions
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows admins all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@admin)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Third Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end
        end
      end

      context "when graders cannot view other graders' comments" do
        before(:once) do
          @assignment.update!(grader_comments_visible_to_graders: false)
        end

        context "when grades are unpublished" do
          it "shows final grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@teacher)
            expect(comments).to match_array([
                                              @student_comment,
                                              @first_ta_comment,
                                              @second_ta_comment,
                                              @third_ta_comment,
                                              @final_grader_comment
                                            ])
          end

          it "shows provisional grader their own and student's" do
            comments = @submission.reload.visible_submission_comments_for(@second_ta)
            expect(comments.pluck(:comment)).to match_array(["Student comment", "Second Ta comment"])
          end

          it "shows student only their own comments" do
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments).to match_array([@student_comment])
          end

          it "shows admins all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@admin)
            expect(comments).to match_array([
                                              @student_comment,
                                              @first_ta_comment,
                                              @second_ta_comment,
                                              @third_ta_comment,
                                              @final_grader_comment
                                            ])
          end
        end

        context "when grades are published" do
          before(:once) do
            ModeratedGrading::ProvisionalGrade.find_by(submission: @submission, scorer: @first_ta).publish!
            @assignment.update!(grades_published_at: Time.zone.now)
            @submission.reload
          end

          it "shows final grader all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@teacher)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Third Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows provisional grader their own, student's, chosen grader's, and final grader's comments" do
            comments = @submission.reload.visible_submission_comments_for(@second_ta)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows student only their own comments" do
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments).to match_array([@student_comment])
          end

          it "when grades are posted, shows student own, chosen grader's, and final grader's comments" do
            @assignment.post_submissions
            comments = @submission.reload.visible_submission_comments_for(@student)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end

          it "shows admins all submission comments" do
            comments = @submission.reload.visible_submission_comments_for(@admin)
            expect(comments.pluck(:comment)).to match_array([
                                                              "Student comment",
                                                              "First Ta comment",
                                                              "Second Ta comment",
                                                              "Third Ta comment",
                                                              "Final Grader comment"
                                                            ])
          end
        end
      end
    end
  end

  describe ".needs_grading" do
    before :once do
      @submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
    end

    it "includes submission that has not been graded" do
      expect(Submission.needs_grading.count).to eq(1)
    end

    it "includes submission by enrolled student" do
      @student.enrollments.take!.complete
      expect(Submission.needs_grading.count).to eq(0)
      @course.enroll_student(@student).accept
      expect(Submission.needs_grading.count).to eq(1)
    end

    it "includes submission by user with multiple enrollments in the course only once" do
      another_section = @course.course_sections.create(name: "two")
      @course.enroll_student(@student, section: another_section, allow_multiple_enrollments: true).accept
      expect(Submission.needs_grading.count).to eq(1)
    end

    it "does not include submission that has been graded" do
      @assignment.grade_student(@student, grade: "100", grader: @teacher)
      expect(Submission.needs_grading.count).to eq(0)
    end

    it "does include submissions that have been graded but the score was reset to nil" do
      @assignment.grade_student(@student, grade: "100", grader: @teacher)
      @assignment.grade_student(@student, grade: nil, grader: @teacher)
      expect(Submission.needs_grading.count).to eq(1)
    end

    it "does not include submission by non-student user" do
      @student.enrollments.take!.complete
      @course.enroll_user(@student, "TaEnrollment").accept
      expect(Submission.needs_grading.count).to eq(0)
    end

    it "does not include excused submissions" do
      @assignment.grade_student(@student, excused: true, grader: @teacher)
      expect(Submission.needs_grading.count).to eq(0)
    end

    it "does not include submissions for inactive/concluded students who have other active enrollments somewhere" do
      @course.enroll_student(@student).update_attribute(:workflow_state, "inactive")
      course_with_student(user: @student, active_all: true)
      expect(Submission.needs_grading).not_to include @assignment.submissions.first
    end

    context "sharding" do
      specs_require_sharding

      it "serializes relative to current scope's shard" do
        @shard1.activate do
          expect(Submission.shard(Shard.default).needs_grading.count).to eq(1)
        end
      end

      it "works with cross shard attachments" do
        @shard1.activate do
          @student = user_factory(active_user: true)
          @attachment = Attachment.create! uploaded_data: StringIO.new("blah"), context: @student, filename: "blah.txt"
        end
        course_factory(active_all: true)
        @course.enroll_user(@student, "StudentEnrollment").accept!
        @assignment = @course.assignments.create!

        sub = @assignment.submit_homework(@user, attachments: [@attachment])
        expect(sub.attachments).to eq [@attachment]
      end

      it "bulk_load_versioned_attachments works with attachments in a different shard" do
        course_factory(active_all: true)
        student = user_factory(active_user: true)
        attachment = attachment_model(filename: "submission.doc", context: student)

        @course.enroll_user(student, "StudentEnrollment").accept!
        assignment = @course.assignments.create!
        submission = assignment.submit_homework(student, attachments: [attachment])
        submission.update_attribute(:attachment_ids, attachment.id.to_s)

        @shard1.activate do
          submission_with_attachments = Submission.bulk_load_versioned_attachments([submission]).first
          expect(submission_with_attachments.versioned_attachments).to eq [attachment]
        end
      end
    end
  end

  describe "#can_view_details?" do
    before do
      @assignment.update!(anonymous_grading: true)
      @submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
    end

    context "for observers" do
      let(:observer) do
        course_with_observer(
          course: @assignment.course,
          associated_user_id: @submission.user_id,
          active_all: true
        ).user
      end

      it "allows observers of the submission's owner to view details" do
        expect(@submission).to be_can_view_details(observer)
      end

      it "does not allow observers to view details if they're not observing the submission's owner" do
        new_student = User.create!
        @context.enroll_student(new_student, enrollment_state: "active")
        new_student_submission = @assignment.submissions.find_by(user: new_student)
        expect(new_student_submission).not_to be_can_view_details(observer)
      end
    end

    context "for peer reviewers" do
      let(:reviewer) { @context.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user }
      let(:reviewer_sub) { @assignment.submissions.find_by!(user: reviewer) }

      before do
        @assignment.update!(submission_types: "online_text_entry", peer_reviews: true)
      end

      it "returns false for peer reviewer of student under view that has not submitted" do
        AssessmentRequest.create!(assessor: reviewer, assessor_asset: reviewer_sub, asset: @submission, user: @student)
        expect(@submission.can_view_details?(reviewer)).to be false
      end

      it "returns true for peer reviewer of student under view that has submitted" do
        AssessmentRequest.create!(assessor: reviewer, assessor_asset: reviewer_sub, asset: @submission, user: @student)
        @assignment.submit_homework(reviewer, body: "hi")
        expect(@submission.can_view_details?(reviewer)).to be true
      end

      it "returns false for peer reviewer of student not under view" do
        new_student = @context.enroll_user(User.create!, "StudentEnrollment", enrollment_state: "active").user
        new_student_sub = @assignment.submissions.find_by!(user: new_student)
        expect(new_student_sub.can_view_details?(reviewer)).to be false
      end
    end

    context "when the assignment is muted" do
      it "returns false if user isn't present" do
        expect(@submission).not_to be_can_view_details(nil)
      end

      it "returns true for submitting student if assignment anonymous grading" do
        expect(@submission.can_view_details?(@student)).to be true
      end

      it "returns false for non-submitting student if assignment anonymous grading" do
        new_student = User.create!
        @context.enroll_student(new_student, enrollment_state: "active")
        expect(@submission.can_view_details?(@new_student)).to be false
      end

      it "returns false for teacher if assignment anonymous grading" do
        expect(@submission.can_view_details?(@teacher)).to be false
      end

      it "returns false for admin if assignment anonymous grading" do
        expect(@submission.can_view_details?(account_admin_user)).to be false
      end

      it "returns true for site admin if assignment anonymous grading" do
        expect(@submission.can_view_details?(site_admin_user)).to be true
      end
    end

    context "when the assignment is unmuted" do
      before do
        @assignment.unmute!
      end

      it "returns false if user isn't present" do
        expect(@submission).not_to be_can_view_details(nil)
      end

      it "returns true for submitting student if assignment anonymous grading" do
        expect(@submission.can_view_details?(@student)).to be true
      end

      it "returns false for non-submitting student if assignment anonymous grading" do
        new_student = User.create!
        @context.enroll_student(new_student, enrollment_state: "active")
        expect(@submission.can_view_details?(@new_student)).to be false
      end

      it "returns true for teacher if assignment anonymous grading" do
        expect(@submission.can_view_details?(@teacher)).to be true
      end

      it "returns true for admin if assignment anonymous grading" do
        expect(@submission.can_view_details?(account_admin_user)).to be true
      end

      it "returns true for site admin if assignment anonymous grading" do
        expect(@submission.can_view_details?(site_admin_user)).to be true
      end
    end
  end

  describe "#needs_grading?" do
    before :once do
      @submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "a body")
    end

    it "returns true for submission that has not been graded" do
      expect(@submission.needs_grading?).to be true
    end

    it "returns false for submission that has been graded" do
      @assignment.grade_student(@student, grade: "100", grader: @teacher)
      @submission.reload
      expect(@submission.needs_grading?).to be false
    end

    it "returns true for submission that has been graded but the score was reset to nil" do
      @assignment.grade_student(@student, grade: "100", grader: @teacher)
      @assignment.grade_student(@student, grade: nil, grader: @teacher)
      @submission.reload
      expect(@submission.needs_grading?).to be true
    end

    it "returns true for submission that is pending review" do
      @submission.workflow_state = "pending_review"
      expect(@submission.needs_grading?).to be true
    end

    it "returns false for submission with nil submission_type" do
      @submission.submission_type = nil
      expect(@submission.needs_grading?).to be false
    end
  end

  describe "#checkpoints_needs_grading?" do
    before(:once) do
      @course = course_model
      @course.root_account.enable_feature!(:discussion_checkpoints)
      @student = student_in_course(course: @course, active_all: true).user
      @topic = DiscussionTopic.create_graded_topic!(course: @course, title: "graded topic")
      checkpoint = @topic.create_checkpoints(reply_to_topic_points: 4, reply_to_entry_points: 8)
      @parent_assignment = checkpoint.parent_assignment
      @checkpoints = checkpoint.parent_assignment.sub_assignments
      @checkpoints.first.submit_homework(@student, submission_type: "discussion_topic")
      @checkpoints.first.update(workflow_state: "deleted")
      @checkpoints.last.update(workflow_state: "deleted")
      @checkpoints.reload
      @topic.create_checkpoints(reply_to_topic_points: 3, reply_to_entry_points: 7)
    end

    it "returns true if one sub_assigment(REPLY_TO_ENTRY) needs grading" do
      @topic.reply_to_topic_checkpoint.submit_homework(@student, submission_type: "discussion_topic")
      @topic.assignment.grade_student(@student, grader: @teacher, score: 7, sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY)

      expect(Submission.where(user_id: @student.id, assignment_id: @topic.assignment_id).first.checkpoints_needs_grading?).to be true
    end

    it "returns true if one sub_assigment(REPLY_TO_TOPIC) needs grading" do
      @topic.assignment.grade_student(@student, grader: @teacher, score: 3, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC)
      @topic.reply_to_entry_checkpoint.submit_homework(@student, submission_type: "discussion_topic")

      expect(Submission.where(user_id: @student.id, assignment_id: @topic.assignment_id).first.checkpoints_needs_grading?).to be true
    end

    it "returns false if one sub_assigments has been graded(other is unsubmitted)" do
      @topic.assignment.grade_student(@student, grader: @teacher, score: 3, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC)
      expect(Submission.where(user_id: @student.id, assignment_id: @topic.assignment_id).first.checkpoints_needs_grading?).to be false
    end

    it "returns false if both sub_assigments have been graded" do
      @topic.assignment.grade_student(@student, grader: @teacher, score: 3, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC)
      @topic.assignment.grade_student(@student, grader: @teacher, score: 7, sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY)
      expect(Submission.where(user_id: @student.id, assignment_id: @topic.assignment_id).first.checkpoints_needs_grading?).to be false
    end
  end

  describe "#plagiarism_service_to_use" do
    it "returns nil when no service is configured" do
      submission = @assignment.submit_homework(@student,
                                               submission_type: "online_text_entry",
                                               body: "whee")

      expect(submission.plagiarism_service_to_use).to be_nil
    end

    it "returns :turnitin when only turnitin is configured" do
      setup_account_for_turnitin(@context.account)
      submission = @assignment.submit_homework(@student,
                                               submission_type: "online_text_entry",
                                               body: "whee")

      expect(submission.plagiarism_service_to_use).to eq(:turnitin)
    end

    it "returns :vericite when only vericite is configured" do
      plugin = Canvas::Plugin.find(:vericite)
      PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)

      submission = @assignment.submit_homework(@student,
                                               submission_type: "online_text_entry",
                                               body: "whee")

      expect(submission.plagiarism_service_to_use).to eq(:vericite)
    end

    it "returns :vericite when both vericite and turnitin are enabled" do
      setup_account_for_turnitin(@context.account)
      plugin = Canvas::Plugin.find(:vericite)
      PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)

      submission = @assignment.submit_homework(@student,
                                               submission_type: "online_text_entry",
                                               body: "whee")

      expect(submission.plagiarism_service_to_use).to eq(:vericite)
    end
  end

  describe "#resubmit_to_vericite" do
    it "calls resubmit_to_plagiarism_later" do
      plugin = Canvas::Plugin.find(:vericite)
      PluginSetting.create!(name: plugin.id, settings: plugin.default_settings, disabled: false)

      submission = @assignment.submit_homework(@student,
                                               submission_type: "online_text_entry",
                                               body: "whee")

      expect(submission).to receive(:submit_to_plagiarism_later).once
      submission.resubmit_to_vericite
    end
  end

  describe "scope: late" do
    before :once do
      @now = Time.zone.now

      ### Quizzes
      @quiz = generate_quiz(@course)
      @quiz_assignment = @quiz.assignment

      @unsubmitted_quiz_submission = @assignment.submissions.create(user: User.create, submission_type: "online_quiz")
      Submission.where(id: @unsubmitted_quiz_submission.id).update_all(submitted_at: nil, cached_due_date: nil)

      @ongoing_unsubmitted_quiz = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_unsubmitted_quiz_submission = @ongoing_unsubmitted_quiz.submission
      @ongoing_unsubmitted_quiz_submission.save!
      Submission.where(id: @ongoing_unsubmitted_quiz_submission.id).update_all(submitted_at: nil)

      @timely_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz1_submission = @timely_quiz1.submission
      Submission.where(id: @timely_quiz1_submission.id).update_all(submitted_at: @now, cached_due_date: nil)

      @timely_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz2_submission = @timely_quiz2.submission
      Submission.where(id: @timely_quiz2_submission.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)

      @timely_quiz3 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz3_submission = @timely_quiz3.submission
      Submission.where(id: @timely_quiz3_submission.id)
                .update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)

      @late_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz1_submission = @late_quiz1.submission
      Submission.where(id: @late_quiz1_submission).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @late_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz2_submission = @late_quiz2.submission
      Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz_extended_submission = @late_quiz_extended.submission
      Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")

      @timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
      Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
      Submission.where(id: @timely_quiz_marked_late_submission).update_all(late_policy_status: "late")

      @ongoing_late_quiz1 = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_late_quiz1_submission = @ongoing_late_quiz1.submission
      @ongoing_late_quiz1_submission.save!
      Submission.where(id: @ongoing_late_quiz1_submission)
                .update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @ongoing_late_quiz2 = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_late_quiz2_submission = @ongoing_late_quiz2.submission
      @ongoing_late_quiz2_submission.save!
      Submission.where(id: @ongoing_late_quiz2_submission)
                .update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @ongoing_timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_timely_quiz_marked_late_submission = @ongoing_timely_quiz_marked_late.submission
      @ongoing_timely_quiz_marked_late_submission.save!
      Submission.where(id: @ongoing_timely_quiz_marked_late_submission)
                .update_all(submitted_at: @now, cached_due_date: nil, late_policy_status: "late")

      ### Homeworks
      @unsubmitted_hw = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @unsubmitted_hw.id).update_all(submitted_at: nil, cached_due_date: nil)

      @timely_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw1.id).update_all(submitted_at: @now, cached_due_date: nil)

      @timely_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw2.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)

      @late_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw1.id).update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)

      @late_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw2.id).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @late_hw3 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw3.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @late_hw_excused = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw_excused.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, excused: true)

      @timely_hw_marked_late = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw_marked_late.id).update_all(submitted_at: @now, cached_due_date: nil)
      Submission.where(id: @timely_hw_marked_late.id).update_all(late_policy_status: "late")
      @late_submission_ids = Submission.late.map(&:id)
    end

    ### Quizzes
    it "excludes unsubmitted quizzes" do
      expect(@late_submission_ids).not_to include(@unsubmitted_quiz_submission.id)
    end

    it "excludes ongoing quizzes that have never been submitted before" do
      expect(@late_submission_ids).not_to include(@ongoing_unsubmitted_quiz_submission.id)
    end

    it "excludes quizzes submitted with no due date" do
      expect(@late_submission_ids).not_to include(@timely_quiz1_submission.id)
    end

    it "excludes quizzes submitted before the due date" do
      expect(@late_submission_ids).not_to include(@timely_quiz2_submission.id)
    end

    it "excludes quizzes submitted less than 60 seconds after the due date" do
      expect(@late_submission_ids).not_to include(@timely_quiz3_submission.id)
    end

    it "includes quizzes submitted more than 60 seconds after the due date" do
      expect(@late_submission_ids).to include(@late_quiz1_submission.id)
    end

    it "excludes quizzes that were last submitted more than 60 seconds after the due date but are being retaken" do
      expect(@late_submission_ids).not_to include(@ongoing_late_quiz1_submission.id)
    end

    it "includes quizzes submitted after the due date" do
      expect(@late_submission_ids).to include(@late_quiz2_submission.id)
    end

    it "excludes quizzes that were last submitted after the due date but are being retaken" do
      expect(@late_submission_ids).not_to include(@ongoing_late_quiz2_submission.id)
    end

    it "includes quizzes that have been manually marked as late" do
      expect(@late_submission_ids).to include(@timely_quiz_marked_late_submission.id)
    end

    it "includes quizzes that have been manually marked as late but are being retaken" do
      expect(@late_submission_ids).to include(@ongoing_timely_quiz_marked_late_submission.id)
    end

    it "excludes quizzes that are late but have been marked as extended" do
      expect(@late_submission_ids).not_to include(@late_quiz_extended_submission.id)
    end

    ### Homeworks
    it "excludes an otherwise late submission that has been marked with a custom status" do
      admin = account_admin_user(account: @course.root_account)
      custom_grade_status = @course.root_account.custom_grade_statuses.create!(
        name: "Custom Status",
        color: "#ABC",
        created_by: admin
      )
      expect { @late_hw1.update!(custom_grade_status:) }.to change {
        Submission.late.include?(@late_hw1)
      }.from(true).to(false)
    end

    it "excludes unsubmitted homeworks" do
      expect(@late_submission_ids).not_to include(@unsubmitted_hw.id)
    end

    it "excludes homeworks submitted with no due date" do
      expect(@late_submission_ids).not_to include(@timely_hw1.id)
    end

    it "excludes homeworks submitted before the due date" do
      expect(@late_submission_ids).not_to include(@timely_hw2.id)
    end

    it "includes homeworks submitted less than 60 seconds after the due date" do
      expect(@late_submission_ids).to include(@late_hw1.id)
    end

    it "includes homeworks submitted more than 60 seconds after the due date" do
      expect(@late_submission_ids).to include(@late_hw2.id)
    end

    it "includes homeworks submitted after the due date" do
      expect(@late_submission_ids).to include(@late_hw3.id)
    end

    it "excludes excused homework submitted after the due date" do
      expect(@late_submission_ids).not_to include(@late_hw_excused.id)
    end

    it "includes homeworks that have been manually marked as late" do
      expect(@late_submission_ids).to include(@timely_hw_marked_late.id)
    end
  end

  describe "scope: not_late" do
    before :once do
      @now = Time.zone.now

      ### Quizzes
      @quiz = generate_quiz(@course)
      @quiz_assignment = @quiz.assignment

      @unsubmitted_quiz_submission = @assignment.submissions.create(user: User.create, submission_type: "online_quiz")
      Submission.where(id: @unsubmitted_quiz_submission.id).update_all(submitted_at: nil, cached_due_date: nil)

      @ongoing_unsubmitted_quiz = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_unsubmitted_quiz_submission = @ongoing_unsubmitted_quiz.submission
      @ongoing_unsubmitted_quiz_submission.save!
      Submission.where(id: @ongoing_unsubmitted_quiz_submission.id).update_all(submitted_at: nil)

      @timely_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz1_submission = @timely_quiz1.submission
      Submission.where(id: @timely_quiz1_submission.id).update_all(submitted_at: @now, cached_due_date: nil)

      @timely_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz2_submission = @timely_quiz2.submission
      Submission.where(id: @timely_quiz2_submission.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)

      @timely_quiz3 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz3_submission = @timely_quiz3.submission
      Submission.where(id: @timely_quiz3_submission.id)
                .update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)

      @late_quiz1 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz1_submission = @late_quiz1.submission
      Submission.where(id: @late_quiz1_submission).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @late_quiz2 = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz2_submission = @late_quiz2.submission
      Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @late_quiz_extended_submission = @late_quiz_extended.submission
      Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")

      @timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
      @timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
      Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
      Submission.where(id: @timely_quiz_marked_late_submission).update_all(late_policy_status: "late")

      @ongoing_late_quiz1 = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_late_quiz1_submission = @ongoing_late_quiz1.submission
      @ongoing_late_quiz1_submission.save!
      Submission.where(id: @ongoing_late_quiz1_submission)
                .update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @ongoing_late_quiz2 = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_late_quiz2_submission = @ongoing_late_quiz2.submission
      @ongoing_late_quiz2_submission.save!
      Submission.where(id: @ongoing_late_quiz2_submission)
                .update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @ongoing_timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create)
      @ongoing_timely_quiz_marked_late_submission = @ongoing_timely_quiz_marked_late.submission
      @ongoing_timely_quiz_marked_late_submission.save!
      Submission.where(id: @ongoing_timely_quiz_marked_late_submission)
                .update_all(submitted_at: @now, cached_due_date: nil, late_policy_status: "late")

      ### Homeworks
      @unsubmitted_hw = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @unsubmitted_hw.id).update_all(submitted_at: nil, cached_due_date: nil)

      @timely_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw1.id).update_all(submitted_at: @now, cached_due_date: nil)

      @timely_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw2.id).update_all(submitted_at: @now, cached_due_date: @now + 1.hour)

      @late_hw1 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw1.id).update_all(submitted_at: @now, cached_due_date: @now - 45.seconds)

      @late_hw2 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw2.id).update_all(submitted_at: @now, cached_due_date: @now - 61.seconds)

      @late_hw3 = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw3.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)

      @late_hw_excused = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @late_hw_excused.id).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, excused: true)

      @timely_hw_marked_late = @assignment.submissions.create(user: User.create, submission_type: "online_text_entry")
      Submission.where(id: @timely_hw_marked_late.id).update_all(submitted_at: @now, cached_due_date: nil)
      Submission.where(id: @timely_hw_marked_late.id).update_all(late_policy_status: "late")
      @not_late_submission_ids = Submission.not_late.map(&:id)
    end

    ### Quizzes
    it "includes unsubmitted quizzes" do
      expect(@not_late_submission_ids).to include(@unsubmitted_quiz_submission.id)
    end

    it "includes ongoing quizzes that have never been submitted before" do
      expect(@not_late_submission_ids).to include(@ongoing_unsubmitted_quiz_submission.id)
    end

    it "includes quizzes submitted with no due date" do
      expect(@not_late_submission_ids).to include(@timely_quiz1_submission.id)
    end

    it "includes quizzes submitted before the due date" do
      expect(@not_late_submission_ids).to include(@timely_quiz2_submission.id)
    end

    it "includes quizzes submitted less than 60 seconds after the due date" do
      expect(@not_late_submission_ids).to include(@timely_quiz3_submission.id)
    end

    it "excludes quizzes submitted more than 60 seconds after the due date" do
      expect(@not_late_submission_ids).not_to include(@late_quiz1_submission.id)
    end

    it "includes quizzes that were last submitted more than 60 seconds after the due date but are being retaken" do
      expect(@not_late_submission_ids).to include(@ongoing_late_quiz1_submission.id)
    end

    it "excludes quizzes submitted after the due date" do
      expect(@not_late_submission_ids).not_to include(@late_quiz2_submission.id)
    end

    it "includes quizzes that were last submitted after the due date but are being retaken" do
      expect(@not_late_submission_ids).to include(@ongoing_late_quiz2_submission.id)
    end

    it "excludes quizzes that have been manually marked as late" do
      expect(@not_late_submission_ids).not_to include(@timely_quiz_marked_late_submission.id)
    end

    it "excludes quizzes that have been manually marked as late but are being retaken" do
      expect(@not_late_submission_ids).not_to include(@ongoing_timely_quiz_marked_late_submission.id)
    end

    it "includes quizzes that are late but have been marked as extended" do
      expect(@not_late_submission_ids).to include(@late_quiz_extended_submission.id)
    end

    ### Homeworks
    it "includes an otherwise late submission that has been marked with a custom status" do
      admin = account_admin_user(account: @course.root_account)
      custom_grade_status = @course.root_account.custom_grade_statuses.create!(
        name: "Custom Status",
        color: "#ABC",
        created_by: admin
      )
      expect { @late_hw1.update!(custom_grade_status:) }.to change {
        Submission.not_late.include?(@late_hw1)
      }.from(false).to(true)
    end

    it "includes unsubmitted homeworks" do
      expect(@not_late_submission_ids).to include(@unsubmitted_hw.id)
    end

    it "includes homeworks submitted with no due date" do
      expect(@not_late_submission_ids).to include(@timely_hw1.id)
    end

    it "includes homeworks submitted before the due date" do
      expect(@not_late_submission_ids).to include(@timely_hw2.id)
    end

    it "excludes homeworks submitted less than 60 seconds after the due date" do
      expect(@not_late_submission_ids).not_to include(@late_hw1.id)
    end

    it "excludes homeworks submitted more than 60 seconds after the due date" do
      expect(@not_late_submission_ids).not_to include(@late_hw2.id)
    end

    it "excludes homeworks submitted after the due date" do
      expect(@not_late_submission_ids).not_to include(@late_hw3.id)
    end

    it "includes excused homework submitted after the due date" do
      expect(@not_late_submission_ids).to include(@late_hw_excused.id)
    end

    it "excludes homeworks that have been manually marked as late" do
      expect(@not_late_submission_ids).not_to include(@timely_hw_marked_late.id)
    end
  end

  describe "scope: with_assignment" do
    it "excludes submissions to deleted assignments" do
      expect { @assignment.destroy }.to change { @student.submissions.with_assignment.count }.by(-1)
    end
  end

  describe "scope: for_assignment" do
    it "includes all submissions for a given assignment" do
      first_assignment = @assignment
      @course.assignments.create!

      submissions = Submission.for_assignment(@assignment)
      expect(submissions).to match_array(first_assignment.submissions)
    end
  end

  describe "#filter_attributes_for_user" do
    let(:user) { instance_double(User, id: 1) }
    let(:session) { {} }
    let(:submission) { @assignment.submissions.build(user_id: 2) }

    context "assignment is set to manually post grades" do
      before do
        @assignment.ensure_post_policy(post_manually: true)
        @assignment.grade_student(@student, grader: @teacher, score: 5)
      end

      it "filters score" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "score" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("score")
      end

      it "filters grade" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "grade" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("grade")
      end

      it "filters published_score" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "published_score" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("published_score")
      end

      it "filters published_grade" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "published_grade" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("published_grade")
      end

      it "filters entered_score" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "entered_score" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("entered_score")
      end

      it "filters entered_grade" do
        expect(submission.assignment).to receive(:user_can_read_grades?).and_return(false)
        hash = { "entered_grade" => 10 }
        expect(submission.filter_attributes_for_user(hash, user, session)).not_to have_key("entered_grade")
      end
    end
  end

  describe "#provisional_grade" do
    before(:once) do
      @assignment.update!(moderated_grading: true, grader_count: 2, final_grader: @teacher)
      @assignment.grade_student(@student, score: 10, grader: @teacher, provisional: true)
      @assignment.grade_student(@student, score: 50, grader: @teacher, provisional: true, final: true)
    end

    let(:submission) { @assignment.submissions.first }

    it "returns the provisional grade matching the passed-in scorer if provided" do
      expect(submission.provisional_grade(@teacher).score).to eq 10
    end

    it "returns the final provisional grade if final is true" do
      expect(submission.provisional_grade(@teacher, final: true).score).to eq 50
    end

    context "when no matching grade is found" do
      let(:non_scorer) { User.new }

      it "returns a null provisional grade by default" do
        provisional_grade = submission.provisional_grade(non_scorer)
        expect(provisional_grade).to be_a(ModeratedGrading::NullProvisionalGrade)
      end

      it "returns nil if default_to_null_grade is false" do
        provisional_grade = submission.provisional_grade(non_scorer, default_to_null_grade: false)
        expect(provisional_grade).to be_nil
      end
    end
  end

  describe "#update_line_item_result" do
    let(:submission) { submission_model(assignment: @assignment) }

    context "when lti_result does not exist" do
      it "does nothing when there is no line item" do
        expect do
          submission.update!(score: 1.3)
        end.to_not change { submission.lti_result }.from(nil)
      end

      context "when there is a line item" do
        before { line_item_model(assignment: @assignment) }

        it "does nothing if score has not changed" do
          expect do
            submission.update!(body: "hello abc")
          end.to_not change { submission.lti_result }.from(nil)
        end

        it "creates an the lti_result with the correct score_given if the score has changed" do
          expect do
            submission.update!(score: 1.3)
          end.to change { submission.lti_result&.reload&.result_score }.from(nil).to(1.3)
        end

        it "does nothing if the lti_result was updated by a tool" do
          expect do
            submission.update!(score: 1.3, grader_id: -123)
          end.to_not change { submission.lti_result }.from(nil)
        end
      end
    end

    context "with lti_result" do
      let(:lti_result) { lti_result_model(assignment: @assignment) }
      let(:submission) { lti_result.submission }

      it "does nothing if score has not changed" do
        expect do
          submission.save!
        end.to_not change { lti_result.result_score }
      end

      it "updates the lti_result score_given if the score has changed" do
        expect do
          submission.update!(score: 1.3)
        end.to change { lti_result.reload.result_score }.from(nil).to(1.3)
      end

      it "does nothing if the lti_result was updated by a tool" do
        expect do
          submission.update!(score: 1.3, grader_id: -123)
        end.to_not change { lti_result.reload.result_score }
      end
    end
  end

  describe "#delete_ignores" do
    context "for submission ignores" do
      before :once do
        @submission = @assignment.submissions.find_by!(user_id: @student)
        @ignore = Ignore.create!(asset: @assignment, user: @student, purpose: "submitting")
      end

      it "deletes submission ignores when asset is submitted" do
        @assignment.submit_homework(@student, { submission_type: "online_text_entry", body: "Hi" })
        expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
      end

      it "does not delete submission ignores when asset is not submitted" do
        @submission.student_entered_score = 5
        @submission.save!
        expect(@ignore.reload).to eq @ignore
      end

      it "deletes submission ignores when asset is excused" do
        @submission.excused = true
        @submission.save!
        expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
      end
    end

    context "for grading ignores" do
      before :once do
        @student1 = @student
        @student2 = student_in_course(course: @course, active_all: true).user
        @sub1 = @assignment.submit_homework(@student1, { submission_type: "online_text_entry", body: "Hi" })
        @sub2 = @assignment.submit_homework(@student2, { submission_type: "online_text_entry", body: "Hi" })
        @ignore = Ignore.create!(asset: @assignment, user: @teacher, purpose: "grading")
      end

      it "deletes grading ignores if every submission is graded or excused" do
        @sub1.score = 5
        @sub1.save!
        @sub2.excused = true
        @sub2.save!
        expect { @ignore.reload }.to raise_error ActiveRecord::RecordNotFound
      end

      it "does not delete grading ignores if some submissions are ungraded" do
        @sub1.score = 5
        @sub1.save!
        expect(@ignore.reload).to eq @ignore
      end
    end
  end

  def submission_spec_model(opts = {})
    submit_homework = opts.delete(:submit_homework)
    opts = submit_homework ? @valid_attributes.merge(opts) : @valid_attributes.except(:workflow_state, :url).merge(opts)
    assignment = opts.delete(:assignment) || Assignment.find(opts.delete(:assignment_id))
    user = opts.delete(:user) || User.find(opts.delete(:user_id))

    @submission = if submit_homework
                    assignment.submit_homework(user)
                  else
                    assignment.submissions.find_by!(user:)
                  end
    unless assignment.grades_published? || @submission.grade_posting_in_progress || assignment.permits_moderation?(user)
      opts.delete(:grade)
    end
    @submission.update!(opts)
    @submission
  end

  def setup_account_for_turnitin(account)
    account.update(turnitin_account_id: "test_account",
                   turnitin_shared_secret: "skeret",
                   settings: account.settings.merge(enable_turnitin: true))
  end

  context "generated observer alerts" do
    before :once do
      course_with_teacher
      @threshold = observer_alert_threshold_model(alert_type: "assignment_grade_high", threshold: "80", course: @course)
      @assignment = assignment_model(context: @course, points_possible: 10)
    end

    it "doesn't create an alert if the observer has been deleted" do
      # This sets up the environment for an error we've seen in the wild
      observer_user_ids = @course.observer_enrollments.pluck(:user_id).uniq
      User.where(id: observer_user_ids).destroy_all

      expect do
        @assignment.grade_student(@threshold.student, score: 10, grader: @teacher)
      end.not_to change {
        ObserverAlert.where(context: @assignment, alert_type: :assignment_grade_high).count
      }
    end

    it "logs if it can't create an observer alert" do
      allow(ObserverAlert).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new)
      submission_id = @assignment.submissions.find_by(user: @threshold.student).id

      expect(Rails.logger).to receive(:error)
        .with("Couldn't create ObserverAlert for submission #{submission_id} observer #{@threshold.observer_id}")

      @assignment.grade_student(@threshold.student, score: 10, grader: @teacher)
    end
  end

  describe "#grade_posting_in_progress" do
    subject { submission.grade_posting_in_progress }

    it { is_expected.to be_nil }

    it "reports its value" do
      submission.grade_posting_in_progress = true
      expect(submission.grade_posting_in_progress).to be true
    end
  end

  describe "#grade_posting_in_progress=" do
    it "can set a value" do
      expect { submission.grade_posting_in_progress = true }.to change {
        submission.grade_posting_in_progress
      }.from(nil).to(true)
    end
  end

  describe "sticker validations" do
    it "allows a nil sticker" do
      submission = @assignment.submissions.first
      submission.sticker = nil
      expect(submission).to be_valid
    end

    it "does not allow a sticker that is not in the approved list" do
      submission = @assignment.submissions.first
      submission.sticker = "my_custom_sticker"
      expect(submission).not_to be_valid
    end

    it "allows a sticker that is in the approved list" do
      submission = @assignment.submissions.first
      submission.sticker = "basketball"
      expect(submission).to be_valid
    end
  end

  describe "extra_attempts validations" do
    describe "#extra_attempts_can_only_be_set_on_online_uploads" do
      it "does not allow extra_attempts to be set for non online upload submission types" do
        submission = @assignment.submissions.first

        %w[online_upload online_url online_text_entry].each do |submission_type|
          submission.assignment.submission_types = submission_type
          submission.assignment.save!
          submission.extra_attempts = 10
          expect(submission).to be_valid
        end

        %w[discussion_entry online_quiz].each do |submission_type|
          submission.assignment.submission_types = submission_type
          submission.assignment.save!
          submission.extra_attempts = 10
          expect(submission).to_not be_valid
        end
      end
    end
  end

  describe "#ensure_attempts_are_in_range" do
    let(:submission) { @assignment.submissions.first }

    context "the assignment is of a type that is restricted by attempts" do
      before do
        @assignment.allowed_attempts = 10
        @assignment.submission_types = "online_upload"
        @assignment.save!
      end

      context "attempts_left <= 0" do
        before do
          submission.attempt = 10
          submission.save!
        end

        context "the submitted_at changed" do
          it "is invalid" do
            submission.submitted_at = Time.zone.now
            expect(submission).to_not be_valid
          end
        end

        context "the submitted_at did not change" do
          it "is valid" do
            expect(submission).to be_valid
          end
        end
      end
    end

    context "the assignment is of a type that is not restricted by attempts" do
      before do
        @assignment.allowed_attempts = 10
        @assignment.submission_types = "online_discussion"
        @assignment.save!
        submission.attempt = 10
        submission.save!
      end

      it "is valid" do
        expect(submission).to be_valid
      end
    end
  end

  describe "#attempts_left" do
    let(:submission) { @assignment.submissions.first }

    context "allowed_attempts is set to a number > 0 on the assignment" do
      before do
        @assignment.allowed_attempts = 10
        @assignment.submission_types = "online_upload"
        @assignment.save!
      end

      context "the submission has extra_attempts set to a value > 0" do
        it "returns assignment.allowed_attempts + submission.extra_attempts - submission.attempt" do
          submission.extra_attempts = 12
          submission.attempt = 6
          submission.save!
          expect(submission.attempts_left).to eq(10 + 12 - 6)
        end

        it "correctly recalculates when allowed_attempts and extra_attempts change" do
          submission.extra_attempts = 12
          submission.attempt = 22
          submission.save!
          expect(submission.attempts_left).to eq(0)
          @assignment.allowed_attempts = 11
          @assignment.save!
          expect(submission.attempts_left).to eq(1)
          submission.extra_attempts = 13
          submission.save!
          expect(submission.attempts_left).to eq(2)
        end

        it "will never return negative values" do
          submission.attempt = 1000
          submission.save!
          expect(submission.attempts_left).to eq(0)
        end
      end

      context "the submission has extra_attempts set to nil" do
        it "returns allowed_attempts from the assignment" do
          submission.extra_attempts = nil
          submission.attempt = 6
          submission.save!
          expect(submission.attempts_left).to eq(10 - 6)
        end
      end
    end

    context "allowed_attempts is set to nil or -1 on the assignment" do
      it "returns nil" do
        @assignment.allowed_attempts = nil
        @assignment.save!
        expect(submission.attempts_left).to be_nil
        @assignment.allowed_attempts = -1
        @assignment.save!
        expect(submission.attempts_left).to be_nil
      end
    end
  end

  describe "#attempt" do
    it "is nil when homework has not been submitted" do
      submission = Submission.find_by(user: @student)
      expect(submission.attempt).to be_nil
    end

    it "is 1 when homework is submitted" do
      submission = @assignment.submit_homework(
        @student,
        submission_type: "online_text_entry",
        body: "body"
      )
      expect(submission.attempt).to eq 1
    end

    it "is incremented when homework is resubmitted" do
      submission = @assignment.submit_homework(
        @student,
        submission_type: "online_text_entry",
        body: "body",
        submitted_at: 1.hour.ago
      )

      # Due to unit tests being ran in a transaction and not actually committed
      # to the database, we can't call submit_homework multiple times. We are
      # instead just updating the submitted_at time, which triggers the before_save
      # callback.
      submission.update!(submitted_at: 2.hours.ago)
      submission.update!(submitted_at: 1.hour.ago)
      expect(submission.attempt).to eq 3
    end
  end

  describe "sticker removal" do
    before(:once) do
      @submission = Submission.find_by(user: @student)
    end

    it "removes the sticker when a new attempt is submitted" do
      @submission.update!(sticker: "basketball")
      @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
      expect(@submission.reload.sticker).to be_nil
    end

    it "does not remove the sticker when the submission is updated but there's not a new attempt" do
      @submission.update!(sticker: "basketball")
      @assignment.grade_student(@student, score: 5, grader: @teacher)
      expect(@submission.reload.sticker).to eq "basketball"
    end

    it "preserves previously awarded stickers in submission history" do
      submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
      submission.update!(sticker: "basketball")
      Timecop.freeze(10.minutes.from_now) do
        submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "bar")
        submission.update!(sticker: "paintbrush")
      end

      sticker = submission.submission_history.find { |sub| sub.attempt == 1 }.sticker
      expect(sticker).to eq "basketball"
    end
  end

  describe "#submission_drafts" do
    before(:once) do
      @submission = Submission.find_by(user: @student)
    end

    it "is empty by default" do
      expect(@submission.submission_drafts).to eq []
    end

    describe "with drafts for multiple attempts" do
      before(:once) do
        @submission = @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
        @draft1 = SubmissionDraft.new(submission: @submission, submission_attempt: 0)
        @draft2 = SubmissionDraft.new(submission: @submission, submission_attempt: 1)
        @submission.submission_drafts << @draft1
        @submission.submission_drafts << @draft2
      end

      it "can have drafts for different submission attempts" do
        expect(@submission.submission_drafts.sort).to eq [@draft1, @draft2]
      end

      it "deletes all drafts for all submission attempts when homework is submitted" do
        @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
        @submission.reload
        expect(@submission.submission_drafts).to eq []
        expect(SubmissionDraft.count).to be 0
      end
    end

    describe "with attachments" do
      before(:once) do
        @attachment1 = attachment_model
        @attachment2 = attachment_model
        @submission_draft = SubmissionDraft.create!(
          submission: @submission,
          submission_attempt: 0
        )
        @submission_draft.attachments = [@attachment1, @attachment2]
      end

      it "can access the attachments" do
        expect(@submission.submission_drafts.first.attachments.sort).to eq [@attachment1, @attachment2]
      end

      it "will cascade deletes to SubmissionDraftAttachments when homework is submitted" do
        @assignment.submit_homework(@student, submission_type: "online_text_entry", body: "foo")
        @submission.reload
        expect(@submission.submission_drafts).to eq []
        expect(SubmissionDraft.count).to be 0
        expect(SubmissionDraftAttachment.count).to be 0
      end
    end
  end

  describe "#hide_grade_from_student?" do
    subject(:submission) { assignment.submissions.find_by!(user: student) }

    let(:course) { @course }
    let(:assignment) { @assignment }
    let(:teacher) { @teacher }
    let(:student) { @student }

    before do
      course.enroll_student(student)
      course.enroll_teacher(teacher)
    end

    it { is_expected.not_to be_hide_grade_from_student }

    context "when assignment posts manually" do
      before { assignment.ensure_post_policy(post_manually: true) }

      it { is_expected.to be_hide_grade_from_student }
      it { is_expected.not_to be_hide_grade_from_student(for_plagiarism: true) }

      context "when a submission is posted" do
        before { submission.update!(posted_at: Time.zone.now) }

        it { is_expected.not_to be_hide_grade_from_student }
      end
    end

    context "when assignment posts automatically" do
      before { assignment.ensure_post_policy(post_manually: false) }

      it { is_expected.not_to be_hide_grade_from_student }

      context "when a submission is posted" do
        before { submission.update!(posted_at: Time.zone.now) }

        it { is_expected.not_to be_hide_grade_from_student }
      end

      context "when a submission is graded but not posted" do
        before do
          assignment.grade_student(student, score: 5, grader: teacher)
          assignment.hide_submissions
        end

        it { is_expected.to be_hide_grade_from_student }
      end

      context "when homework has been submitted, but the submission is not graded or posted" do
        before do
          assignment.update!(submission_types: "online_text_entry")
          assignment.submit_homework(student, submission_type: "online_text_entry", body: "hi")
        end

        it { is_expected.not_to be_hide_grade_from_student }
      end

      context "when a student re-submits to a previously graded and subsequently hidden submission" do
        before do
          assignment.update!(submission_types: "online_text_entry")
          assignment.submit_homework(student, submission_type: "online_text_entry", body: "hi")
          assignment.grade_student(student, score: 0, grader: teacher)
          assignment.hide_submissions
          assignment.submit_homework(student, submission_type: "online_text_entry", body: "I will never give up")
        end

        it { is_expected.to be_hide_grade_from_student }
      end
    end
  end

  describe "posting and unposting" do
    subject(:submission) { @assignment.submissions.first }

    describe "#posted?" do
      it { is_expected.not_to be_posted }

      it "returns true if the submission's posted_at date is not nil" do
        submission.update!(posted_at: Time.zone.now)
        expect(submission).to be_posted
      end
    end

    describe "#handle_posted_at_changed" do
      describe "when an student that is also admin posts an submission" do
        it "unmutes the assignment if all submissions are now posted" do
          admin = account_admin_user(account: @account, name: "default admin")
          @course.enroll_student(admin)
          assignment = @course.assignments.create!(
            title: "some assignment",
            workflow_state: "published"
          )
          submission_model(user: admin, assignment:, body: "first student submission text")
          expect { assignment.reload }.not_to raise_error
        end
      end

      context "when posting an individual submission" do
        context "when post policies are enabled" do
          it "unmutes the assignment if all submissions are now posted" do
            submission.update!(posted_at: Time.zone.now)
            expect(@assignment.reload).not_to be_muted
          end

          it "does not unmute the assignment if some submissions remain unposted" do
            @course.enroll_student(User.create!, enrollment_state: "active")
            submission.update!(posted_at: Time.zone.now)
            expect(@assignment.reload).to be_muted
          end
        end
      end

      context "when unposting an individual submission" do
        before { submission.update!(posted_at: 1.day.ago) }

        context "when post policies are enabled" do
          it "mutes an unmuted assignment when a submission is hidden" do
            @assignment.post_submissions

            submission.update!(posted_at: nil)
            expect(@assignment.reload).to be_muted
          end
        end
      end
    end
  end

  context "caching" do
    specs_require_cache(:redis_cache_store)

    def check_cache_clear
      key = @student.cache_key(:submissions)
      yield
      expect(@student.cache_key(:submissions)).to_not eq key
    end

    it "clears key when submission is deleted" do
      check_cache_clear do
        sub = @student.submissions.first
        @student.enrollments.first.destroy
        expect(sub.reload).to be_deleted
      end
    end

    it "clears key when a submission comment is made" do
      check_cache_clear do
        @student.submissions.first.add_comment(author: @teacher, comment: "some comment")
      end
    end

    it "clears key when assignment is unmuted" do
      @assignment.mute!
      check_cache_clear do
        @assignment.unmute!
      end
    end
  end

  describe "postable scope" do
    specs_require_sharding

    it "works cross-shard" do
      @shard1.activate do
        expect(@assignment.submissions.postable.to_sql).to_not include(@shard1.name)
      end
    end
  end

  describe "root account ID" do
    let_once(:root_account) { Account.create! }
    let_once(:subaccount) { Account.create!(root_account:) }
    let_once(:course) { Course.create!(account: subaccount) }
    let_once(:student) { course.enroll_student(User.create!, workflow_state: "active").user }

    it "is set to the root account ID of the owning course" do
      assignment = course.assignments.create!
      expect(assignment.submission_for_student(student).root_account_id).to eq root_account.id
    end
  end

  describe "redo request" do
    subject(:submission) { @assignment.submissions.new user: User.create, workflow_state: "submitted", redo_request: true, attempt: 1 }

    it "redo request is reset on an updated submission" do
      submission.update!(attempt: 2)
      expect(submission.redo_request).to be false
    end
  end

  describe "word_count" do
    before do
      @submission = @assignment.submissions.find_by(user: @student)
    end

    it "sums word counts of attachments if there are any" do
      student_in_course(active_all: true)
      submission_text = "Text based submission with some words"
      attachment1 = attachment_model(uploaded_data: stub_file_data("submission.txt", submission_text, "text/plain"), context: @student)
      attachment2 = attachment_model(uploaded_data: stub_file_data("submission.txt", submission_text, "text/plain"), context: @student)
      sub = @assignment.submit_homework(@student, attachments: [attachment1, attachment2])
      run_jobs
      expect(sub.reload.word_count).to eq 12
    end

    it "uses the attachment content — and not the body — to calculate word count when it's a file upload submission" do
      student_in_course(active_all: true)
      submission_text = "@channel MY LEGSSSS!!!!! THEY'RE STUCK UNDER MY DESK!!!!!!!"
      attachment = attachment_model(uploaded_data: stub_file_data("submission.txt", submission_text, "text/plain"), context: @student)
      sub = @assignment.submit_homework(@student, attachments: [attachment])
      sub.update!(body: "")
      run_jobs
      expect(sub.reload.word_count).to eq 8
    end

    context "when calculating word count on-the-fly" do
      before { Account.site_admin.disable_feature!(:use_body_word_count) }

      it "returns the word count" do
        @submission.update(body: "test submission")
        expect(@submission.word_count).to eq 2
      end

      it "returns the word count if body is split up by <br> tags" do
        @submission.update(body: "test<br>submission")
        expect(@submission.word_count).to eq 2
      end

      it "returns nil if there is no body" do
        expect(@submission.body).to be_nil
        expect(@submission.word_count).to be_nil
      end

      it "returns nil if it's a quiz submission" do
        @submission.update(submission_type: "online_quiz", body: "test submission")
        expect(@submission.submission_type).to eq "online_quiz"
        expect(@submission.body).not_to be_nil
        expect(@submission.word_count).to be_nil
      end

      it "returns 0 if the body is empty" do
        @submission.update(body: "")
        expect(@submission.word_count).to eq 0
      end

      it "ignores HTML tags" do
        @submission.update(body: "<span>test <div></div>submission</span> <p></p>")
        expect(@submission.word_count).to eq 2
        @submission.instance_variable_set :@word_count, nil
        @submission.update(body: '<p>This is my submission, which has&nbsp;<strong>some bold&nbsp;<em>italic text</em> in</strong> it.</p>
          <p>A couple paragraphs, and maybe super<sup>script</sup>.&nbsp;</p>')
        expect(@submission.reload.word_count).to eq 18
      end
    end

    context "when the use_body_word_count feature is enabled" do
      let(:word_count) do
        run_jobs
        @submission.reload.word_count
      end

      it "falls back to calculating on-the-fly if body_word_count is nil" do
        @submission.update_columns(body: "two words")
        run_jobs
        @submission.reload
        expect(@submission.body_word_count).to be_nil
        expect(@submission.word_count).to eq 2
      end

      it "sets the word_count to 0 if an exception is thrown when trying to calculate the count" do
        # stubbing this way (instead of expect(submission)...) because the method is called on an object in a delayed job.
        expect_any_instance_of(Submission).to receive(:calc_body_word_count).and_raise("Kaboom")
        @submission.update(body: "submission body that can't be parsed for some reason (usually because it's too big)")
        expect(word_count).to eq 0
      end

      it "returns the word count" do
        @submission.update(body: "test submission")
        expect(word_count).to eq 2
      end

      it "returns the word count if body is split up by <br> tags" do
        @submission.update(body: "test<br>submission")
        expect(word_count).to eq 2
      end

      it "returns nil if there is no body" do
        expect(@submission.body).to be_nil
        expect(word_count).to be_nil
      end

      it "returns nil if it's a quiz submission" do
        @submission.update(submission_type: "online_quiz", body: "test submission")
        expect(@submission.submission_type).to eq "online_quiz"
        expect(@submission.body).not_to be_nil
        expect(word_count).to be_nil
      end

      it "returns 0 if the body is empty" do
        @submission.update(body: "")
        expect(word_count).to eq 0
      end

      it "ignores HTML tags" do
        @submission.update(body: "<span>test <div></div>submission</span> <p></p>")
        expect(word_count).to eq 2
        @submission.instance_variable_set :@word_count, nil
        @submission.update(body: '<p>This is my submission, which has&nbsp;<strong>some bold&nbsp;<em>italic text</em> in</strong> it.</p>
          <p>A couple paragraphs, and maybe super<sup>script</sup>.&nbsp;</p>')
        run_jobs
        expect(@submission.reload.word_count).to eq 18
      end
    end
  end

  context "Assignment Cache" do
    specs_require_cache(:redis_cache_store)

    describe "creating a new submission" do
      subject(:submission) { @assignment.submissions.new user: User.create, workflow_state: "submitted" }

      it "invalidates submitted count cache if submitted" do
        Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
        subject.run_callbacks :create
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(false)
      end

      it "does not invalidate submitted count cache if unsubmitted" do
        Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
        subject.workflow_state = "unsubmitted"
        subject.run_callbacks :create
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
      end

      it "invalidates graded count cache if graded" do
        Rails.cache.write(["graded_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
        subject.score = 10
        subject.workflow_state = "graded"
        subject.run_callbacks :create
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(false)
      end

      it "does not invalidate graded count cache if unsubmitted" do
        Rails.cache.write(["graded_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
        subject.run_callbacks :create
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
      end
    end

    describe "updating a submission" do
      subject(:submission) { @assignment.submissions.first }

      it "invalidates submitted count cache if submitted" do
        Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
        subject.workflow_state = "submitted"
        subject.run_callbacks :update
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(false)
      end

      it "does not invalidate submitted count cache if unsubmitted" do
        Rails.cache.write(["submitted_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
        subject.workflow_state = "unsubmitted"
        subject.run_callbacks :update
        expect(Rails.cache.exist?(["submitted_count", @assignment].cache_key)).to be(true)
      end

      it "invalidates graded count cache if graded" do
        Rails.cache.write(["graded_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
        subject.score = 10
        subject.workflow_state = "graded"
        subject.run_callbacks :update
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(false)
      end

      it "does not invalidate graded count cache if unsubmitted" do
        Rails.cache.write(["graded_count", @assignment].cache_key, "test")
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
        subject.workflow_state = "submitted"
        subject.run_callbacks :create
        expect(Rails.cache.exist?(["graded_count", @assignment].cache_key)).to be(true)
      end
    end
  end

  describe "#observer?" do
    before do
      @student = user_factory
      course_with_observer(
        course: @course,
        associated_user_id: @student.id,
        active_all: true,
        active_cc: true
      )
      @submission = @assignment.submission_for_student(@student)
    end

    it "is true for observer" do
      expect(@submission.observer?(@observer)).to be true
    end

    it "is false for student" do
      expect(@submission.observer?(@student)).to be false
    end

    it "is false for teacher" do
      expect(@submission.observer?(@teacher)).to be false
    end

    it "is false for others" do
      expect(@submission.observer?(user_factory)).to be false
    end
  end

  describe "#peer_reviewer?" do
    before do
      student_in_course(active_all: true)
      @peer_reviewer = user_factory
      @course.enroll_student(@peer_reviewer).accept!
      @assignment = @course.assignments.build(
        title: "Peer Reviews",
        submission_types: "online_text_entry",
        peer_reviews: true
      )
      @assignment.save!
      @submission = @assignment.submission_for_student(@student)
      @submission.assessment_requests.create!(
        user: @student,
        assessor: @peer_reviewer,
        assessor_asset: @submission
      )
    end

    it "is true for reviewer" do
      expect(@submission.peer_reviewer?(@peer_reviewer)).to be true
    end

    it "is false for student" do
      expect(@submission.peer_reviewer?(@student)).to be false
    end

    it "is false for teacher" do
      expect(@submission.peer_reviewer?(@teacher)).to be false
    end

    it "is false for others" do
      expect(@submission.peer_reviewer?(user_factory)).to be false
    end
  end

  describe "send_timing_data_if_needed" do
    it "calls Statsd when a classic quiz is manually graded" do
      expect(InstStatsd::Statsd).to receive(:gauge).once.with("submission.manually_graded.grading_time", 600.0, 1.0, tags: { quiz_type: "classic_quiz" })

      now = Time.zone.now
      Timecop.freeze(now) do
        quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
      end

      Timecop.freeze(10.minutes.from_now(now)) do
        @quiz_submission.set_final_score(7)
        @quiz_submission.save!
      end
    end

    it "calls Statsd when a new quiz is manually graded" do
      expect(InstStatsd::Statsd).to receive(:gauge).once.with("submission.manually_graded.grading_time", 300.0, 1.0, tags: { quiz_type: "new_quiz" })

      now = Time.zone.now
      Timecop.freeze(now) do
        quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
      end

      allow(@quiz_submission.submission).to receive_messages(submission_type: "basic_lti_launch", url: "https://quiz-lti-iad-prod.instructure.com/lti/launch")
      Timecop.freeze(5.minutes.from_now(now)) do
        @quiz_submission.set_final_score(7)
        @quiz_submission.save!
      end
    end

    it "does not call Statsd when a quiz is automatically graded" do
      expect(InstStatsd::Statsd).not_to receive(:gauge)

      quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "multiple_choice_question" } }])
    end

    it "does not call Statsd when a submission is updated" do
      expect(InstStatsd::Statsd).not_to receive(:gauge)

      now = Time.zone.now
      Timecop.freeze(now) do
        quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
      end

      Timecop.freeze(10.minutes.from_now(now)) do
        submission = @quiz.submissions.first
        submission.excused = false
        submission.save!
      end
    end

    it "does not call Statsd when the time between submission and grading is less than 30 seconds" do
      expect(InstStatsd::Statsd).not_to receive(:gauge)

      now = Time.zone.now
      Timecop.freeze(now) do
        quiz_with_graded_submission([{ question_data: { :name => "question 1", :points_possible => 10, "question_type" => "essay_question" } }])
      end

      Timecop.freeze(29.seconds.from_now(now)) do
        @quiz_submission.set_final_score(7)
        @quiz_submission.save!
      end
    end
  end

  describe "checkpoint submissions" do
    before(:once) do
      course = course_model
      student = student_in_course(course:, active_all: true).user
      course.root_account.enable_feature!(:discussion_checkpoints)
      topic = DiscussionTopic.create_graded_topic!(course:, title: "graded topic")
      topic.create_checkpoints(reply_to_topic_points: 3, reply_to_entry_points: 7)
      @checkpoint_submission = topic.reply_to_topic_checkpoint.submissions.find_by(user: student)
      @parent_submission = topic.assignment.submissions.find_by(user: student)
    end

    it "updates the parent submission when tracked attrs change on a checkpoint submission" do
      expect { @checkpoint_submission.update!(score: 3) }.to change { @parent_submission.reload.score }.from(nil).to(3)
    end

    it "does not update the parent submission when attrs that changed are not tracked" do
      expect { @checkpoint_submission.update!(lti_user_id: "some-id") }.not_to change { @parent_submission.reload.updated_at }
    end

    it "does not update the parent submission when the checkpoints flag is disabled" do
      @checkpoint_submission.root_account.disable_feature!(:discussion_checkpoints)
      expect { @checkpoint_submission.update!(score: 3) }.not_to change { @parent_submission.reload.score }
    end
  end

  describe "lti_attempt_id is set automatically" do
    before do
      course_with_teacher
      @assignment = @course.assignments.create!(title: "some assignment")
      @submission = @assignment.submissions.create!(user: @teacher)
    end

    it "for new submissions" do
      expect(@submission.lti_attempt_id.length).to be > 30
    end

    it "for existing submissions on save" do
      @submission.update_column(:lti_id, nil)
      @submission.save!
      expect(@submission.lti_attempt_id.length).to be > 30
    end

    it "does not change already existing lti_id, but lit_attempt_id is different" do
      old_lti_attempt_id = @submission.lti_attempt_id
      @submission.update(attempt: 2)
      expect(@submission.reload.lti_attempt_id.split(":").first).to eq old_lti_attempt_id.split(":").first
      expect(@submission.lti_attempt_id).not_to eq old_lti_attempt_id
    end

    it "cannot change already existing lti_id" do
      @submission.lti_id = "new_id"
      expect(@submission.save).to be_falsey
      expect(@submission.errors[:lti_id]).to include("Cannot change lti_id!")
    end
  end

  describe "#visible_provisional_comments" do
    context "with moderated grading" do
      before(:once) do
        @course = Course.create!
        @teacher = course_with_teacher(course: @course, active_all: true).user
        @first_ta = course_with_ta(course: @course, active_all: true).user
        @second_ta = course_with_ta(course: @course, active_all: true).user
        @student = course_with_student(course: @course, active_all: true).user

        @assignment = @course.assignments.create!(
          moderated_grading: true,
          grader_count: 2,
          final_grader: @teacher
        )
        @submission = @assignment.submit_homework(@student, body: "hello")

        first_ta_pg = @submission.find_or_create_provisional_grade!(@first_ta)
        second_ta_pg = @submission.find_or_create_provisional_grade!(@second_ta)
        final_grader_pg = @submission.find_or_create_provisional_grade!(@teacher)

        @first_ta_comment = @submission.add_comment(author: @first_ta, comment: "First TA comment", provisional: true)
        @first_ta_comment.update!(provisional_grade_id: first_ta_pg.id)

        @second_ta_comment = @submission.add_comment(author: @second_ta, comment: "Second TA comment", provisional: true)
        @second_ta_comment.update!(provisional_grade_id: second_ta_pg.id)

        @final_grader_comment = @submission.add_comment(author: @teacher, comment: "Final Grader comment", provisional: true)
        @final_grader_comment.update!(provisional_grade_id: final_grader_pg.id)
      end

      context "when grades are not published" do
        context "when grader comments are visible to graders" do
          before(:once) do
            @assignment.update!(grader_comments_visible_to_graders: true)
          end

          it "returns all provisional comments for the moderator" do
            comments = @submission.visible_provisional_comments(@teacher)
            expect(comments).to match_array([@first_ta_comment, @second_ta_comment, @final_grader_comment])
          end

          it "returns all provisional comments for a grader" do
            comments = @submission.visible_provisional_comments(@first_ta)
            expect(comments).to match_array([@first_ta_comment, @second_ta_comment, @final_grader_comment])
          end

          it "returns no comments for a student" do
            comments = @submission.visible_provisional_comments(@student)
            expect(comments).to be_empty
          end
        end

        context "when grader comments are not visible to graders" do
          before(:once) do
            @assignment.update!(grader_comments_visible_to_graders: false)
          end

          it "returns all provisional comments for the moderator" do
            comments = @submission.visible_provisional_comments(@teacher)
            expect(comments).to match_array([@first_ta_comment, @second_ta_comment, @final_grader_comment])
          end

          it "returns only their own comments for a grader" do
            comments = @submission.visible_provisional_comments(@first_ta)
            expect(comments).to match_array([@first_ta_comment])
          end

          it "returns no comments for a student" do
            comments = @submission.visible_provisional_comments(@student)
            expect(comments).to be_empty
          end
        end
      end

      context "when grades are published" do
        before(:once) do
          # Select the first TA's provisional grade and publish grades
          first_ta_pg = @submission.find_or_create_provisional_grade!(@first_ta)
          first_ta_pg.publish!
          @assignment.update!(grades_published_at: Time.zone.now)

          # When grades are published, the selected grader's comments are duplicated as non-provisional
          @submission.add_comment(author: @first_ta, comment: "First TA comment")
        end

        it "filters out the selected grader's provisional comments" do
          comments = @submission.visible_provisional_comments(@teacher)
          expect(comments).to match_array([@second_ta_comment, @final_grader_comment])
          expect(comments).not_to include(@first_ta_comment)
        end

        it "still shows other graders' provisional comments" do
          comments = @submission.visible_provisional_comments(@teacher)
          expect(comments).to include(@second_ta_comment)
        end
      end
    end
  end

  describe "#auto_grade_result_present" do
    before(:once) do
      @course = Course.create!
      @student = course_with_student(course: @course, active_all: true).user
      @assignment = @course.assignments.create!(title: "Test Assignment")
      @submission = @assignment.submit_homework(@student, body: "test submission")
    end

    it "returns false when no AutoGradeResult exists" do
      expect(@submission.auto_grade_result_present).to be false
    end

    it "returns true when AutoGradeResult exists for current attempt" do
      AutoGradeResult.create!(
        submission: @submission,
        attempt: @submission.attempt || 1,
        grade_data: { "score" => 85 },
        grading_attempts: 1,
        root_account_id: @course.root_account_id
      )
      expect(@submission.auto_grade_result_present).to be true
    end

    it "returns false when AutoGradeResult exists for different attempt" do
      AutoGradeResult.create!(
        submission: @submission,
        attempt: (@submission.attempt || 1) + 1,
        grade_data: { "score" => 85 },
        grading_attempts: 1,
        root_account_id: @course.root_account_id
      )
      expect(@submission.auto_grade_result_present).to be false
    end

    it "handles nil attempt correctly" do
      @submission.update!(attempt: nil)
      AutoGradeResult.create!(
        submission: @submission,
        attempt: 1,
        grade_data: { "score" => 85 },
        grading_attempts: 1,
        root_account_id: @course.root_account_id
      )
      expect(@submission.auto_grade_result_present).to be true
    end
  end

  describe "#need_to_extract_text? (private method)" do
    before(:once) do
      @course = Course.create!
      @student = course_with_student(course: @course, active_all: true).user
      @assignment = @course.assignments.create!(title: "Test Assignment")
      @submission = @assignment.submit_homework(@student, body: "test submission")
      @submission.reload # Clear any saved changes from the creation
    end

    it "returns false when attempt has not changed" do
      expect(@submission.send(:need_to_extract_text?)).to be false
    end

    context "when attempt has changed" do
      before do
        @submission.update!(attempt: 2)
      end

      context "for upload submissions" do
        before do
          allow(@submission).to receive(:extract_text_from_upload?).and_return(true)
        end

        it "returns true when attachment_ids changed and attachments are present" do
          allow(@submission).to receive_messages(saved_change_to_attachment_ids?: true, attachment_ids: [1, 2])

          expect(@submission.send(:need_to_extract_text?)).to be true
        end

        it "returns false when attachment_ids changed but no attachments" do
          allow(@submission).to receive_messages(saved_change_to_attachment_ids?: true, attachment_ids: [])

          expect(@submission.send(:need_to_extract_text?)).to be false
        end

        it "returns false when attachment_ids did not change" do
          allow(@submission).to receive(:saved_change_to_attachment_ids?).and_return(false)

          expect(@submission.send(:need_to_extract_text?)).to be false
        end
      end

      context "for non-upload submissions" do
        before do
          allow(@submission).to receive(:extract_text_from_upload?).and_return(false)
        end

        it "returns true when body changed and body is present" do
          allow(@submission).to receive_messages(saved_change_to_body?: true, body: "some text")

          expect(@submission.send(:need_to_extract_text?)).to be true
        end

        it "returns false when body changed but body is blank" do
          allow(@submission).to receive_messages(saved_change_to_body?: true, body: "")

          expect(@submission.send(:need_to_extract_text?)).to be false
        end

        it "returns false when body did not change" do
          allow(@submission).to receive(:saved_change_to_body?).and_return(false)

          expect(@submission.send(:need_to_extract_text?)).to be false
        end
      end
    end
  end

  describe "#extract_text (private method)" do
    before(:once) do
      @course = Course.create!
      @student = course_with_student(course: @course, active_all: true).user
      @assignment = @course.assignments.create!(title: "Test Assignment")
    end

    context "when extracting from uploads" do
      before do
        @attachment1 = Attachment.create!(
          context: @assignment,
          uploaded_data: StringIO.new("file content"),
          filename: "test1.pdf",
          display_name: "test1.pdf"
        )
        @attachment2 = Attachment.create!(
          context: @assignment,
          uploaded_data: StringIO.new("file content"),
          filename: "test2.pdf",
          display_name: "test2.pdf"
        )
        @submission = @assignment.submit_homework(@student, submission_type: "online_upload", attachments: [@attachment1, @attachment2])
        allow(@submission).to receive_messages(extract_text_from_upload?: true, root_account: @course.root_account)
      end

      it "processes all attachments and creates SubmissionText records when text is present" do
        service_result1 = FileTextExtractionService::Result.new("extracted text 1", true)
        service_result2 = FileTextExtractionService::Result.new("extracted text 2", false)

        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment1).and_return(double(call: service_result1))
        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment2).and_return(double(call: service_result2))

        expect(SubmissionText).to receive(:upsert_all) do |rows, options|
          expect(rows.length).to eq(2)
          expect(rows[0][:text]).to eq("extracted text 1")
          expect(rows[0][:contains_images]).to be true
          expect(rows[0][:attachment_id]).to eq(@attachment1.id)
          expect(rows[1][:text]).to eq("extracted text 2")
          expect(rows[1][:contains_images]).to be false
          expect(rows[1][:attachment_id]).to eq(@attachment2.id)
          expect(options[:unique_by]).to eq(:index_on_sub_attempt_attach)
        end

        @submission.send(:extract_text)
      end

      it "skips attachments when FileTextExtractionService returns nil" do
        service_result1 = nil
        service_result2 = FileTextExtractionService::Result.new("extracted text 2", false)

        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment1).and_return(double(call: service_result1))
        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment2).and_return(double(call: service_result2))

        expect(SubmissionText).to receive(:upsert_all) do |rows, _options|
          valid_rows = rows.compact
          expect(valid_rows.length).to eq(1)
          expect(valid_rows[0][:text]).to eq("extracted text 2")
          expect(valid_rows[0][:attachment_id]).to eq(@attachment2.id)
        end

        @submission.send(:extract_text)
      end

      it "skips attachments when FileTextExtractionService returns empty text" do
        service_result1 = FileTextExtractionService::Result.new("", false)
        service_result2 = FileTextExtractionService::Result.new("extracted text 2", false)

        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment1).and_return(double(call: service_result1))
        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment2).and_return(double(call: service_result2))

        expect(SubmissionText).to receive(:upsert_all) do |rows, _options|
          valid_rows = rows.compact
          expect(valid_rows.length).to eq(1)
          expect(valid_rows[0][:text]).to eq("extracted text 2")
        end

        @submission.send(:extract_text)
      end

      it "skips attachments when FileTextExtractionService returns blank text" do
        service_result1 = FileTextExtractionService::Result.new("   ", false)
        service_result2 = FileTextExtractionService::Result.new("extracted text 2", false)

        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment1).and_return(double(call: service_result1))
        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment2).and_return(double(call: service_result2))

        expect(SubmissionText).to receive(:upsert_all) do |rows, _options|
          valid_rows = rows.compact
          expect(valid_rows.length).to eq(1)
          expect(valid_rows[0][:text]).to eq("extracted text 2")
        end

        @submission.send(:extract_text)
      end

      it "does not call upsert_all when no valid text is extracted" do
        service_result1 = nil
        service_result2 = FileTextExtractionService::Result.new("", false)

        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment1).and_return(double(call: service_result1))
        allow(FileTextExtractionService).to receive(:new).with(attachment: @attachment2).and_return(double(call: service_result2))

        expect(SubmissionText).not_to receive(:upsert_all)

        @submission.send(:extract_text)
      end
    end

    context "when extracting from submission body" do
      before do
        @submission = @assignment.submit_homework(@student, body: "<p>Submission text with <img src='file.jpg'> image</p>")
        allow(@submission).to receive_messages(extract_text_from_upload?: false, root_account: @course.root_account)
      end

      it "creates SubmissionText record from body when body is present" do
        allow(@submission).to receive(:contains_rce_file_link?).with(@submission.body).and_return(true)

        expect(SubmissionText).to receive(:upsert_all) do |rows, options|
          expect(rows.length).to eq(1)
          expect(rows[0][:text]).to eq(@submission.body)
          expect(rows[0][:contains_images]).to be true
          expect(rows[0][:attachment_id]).to be_nil
          expect(options[:unique_by]).to eq(:index_on_sub_attempt)
        end

        @submission.send(:extract_text)
      end

      it "sets contains_images to false when body has no file links" do
        allow(@submission).to receive(:contains_rce_file_link?).with(@submission.body).and_return(false)

        expect(SubmissionText).to receive(:upsert_all) do |rows, _options|
          expect(rows[0][:contains_images]).to be false
        end

        @submission.send(:extract_text)
      end

      it "does not create SubmissionText record when body is blank" do
        @submission.update!(body: "")

        expect(SubmissionText).not_to receive(:upsert_all)

        @submission.send(:extract_text)
      end

      it "does not create SubmissionText record when body is nil" do
        @submission.update!(body: nil)

        expect(SubmissionText).not_to receive(:upsert_all)

        @submission.send(:extract_text)
      end
    end

    context "when neither upload nor body conditions are met" do
      before do
        @submission = @assignment.submit_homework(@student, body: "")
        allow(@submission).to receive(:extract_text_from_upload?).and_return(false)
      end

      it "does not create any SubmissionText records" do
        expect(SubmissionText).not_to receive(:upsert_all)

        @submission.send(:extract_text)
      end
    end
  end

  describe "#skip_broadcasts" do
    let(:submission) { @assignment.submissions.find_by(user: @student) }

    context "when super method would return true" do
      before do
        @assignment.update!(
          submission_types: "online_upload",
          title: "Regular Assignment"
        )

        submission.skip_broadcasts = true
      end

      it "returns true regardless of rollcall assignment status" do
        expect(submission.skip_broadcasts).to be true
      end
    end

    context "when super method would return false" do
      context "and assignment is rollcall" do
        before do
          @assignment.update!(
            submission_types: "external_tool",
            title: "Roll Call Attendance"
          )

          submission.skip_broadcasts = false
        end

        it "returns true because of rollcall assignment" do
          expect(submission.skip_broadcasts).to be true
        end
      end

      context "and assignment is not rollcall" do
        before do
          @assignment.update!(
            submission_types: "online_upload",
            title: "Regular Assignment"
          )

          submission.skip_broadcasts = false
        end

        it "returns false when both conditions are false" do
          expect(submission.skip_broadcasts).to be false
        end
      end
    end
  end
end
