describe AssignmentsController do
  include AutomatedTestsHelper

  # TODO: add 'role is from a different course' shared tests to each route test below

  let(:annotation_category) { create(:annotation_category) }

  let(:example_form_params) do
    {
      course_id: course.id,
      is_group_assignment: true,
      is_hidden: 0,
      assignment: {
        assignment_properties_attributes: {
          scanned_exam: false, section_due_dates_type: 0, allow_web_submits: 0, vcs_submit: 0,
          display_median_to_students: 0, display_grader_names_to_students: 0,
          student_form_groups: 0, group_min: 1, group_max: 1, group_name_autogenerated: 1, allow_remarks: 0
        },
        submission_rule_attributes: {
          type: 'PenaltyDecayPeriodSubmissionRule',
          periods_attributes: { 999 => { deduction: 10.0, interval: 1.0, hours: 10.0, _destroy: 0, id: nil } }
        },
        description: 'Test',
        short_identifier: 'Test123',
        message: '',
        due_date: Time.current.to_s
      }
    }
  end

  describe '#start_timed_assignment' do
    let(:course) { assignment.course }
    let(:assignment) { create(:timed_assignment) }

    context 'as a student' do
      let(:role) { create(:student) }

      context 'when a grouping exists' do
        let!(:grouping) { create(:grouping_with_inviter, assignment: assignment, start_time: nil, inviter: role) }

        it 'should respond with 302' do
          put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
          expect(response).to have_http_status :redirect
        end

        it 'should redirect to show' do
          put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
          expect(response).to redirect_to(action: :show)
        end

        it 'should update the start_time' do
          put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
          expect(grouping.reload.start_time).to be_within(5.seconds).of(Time.current)
        end

        context 'a validation fails' do
          it 'should flash an error message' do
            allow_any_instance_of(Grouping).to receive(:update).and_return false
            put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
            expect(flash[:error]).not_to be_nil
          end
        end
      end

      context 'when a grouping does not exist' do
        before { put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id } }

        xcontext 'when the group_max is 1' do
          let(:assignment) { create(:timed_assignment, assignment_properties_attributes: { group_max: 1 }) }

          it 'should create a grouping' do
            expect(role.student_memberships.size).to eq 1
            expect(role.groupings.first.assessment_id).to eq assignment.id
          end
        end

        context 'when the group_max is > 1' do
          let(:assignment) { create(:timed_assignment, assignment_properties_attributes: { group_max: 2 }) }

          it 'should respond with 403' do
            expect(response).to have_http_status :forbidden
          end
        end
      end
    end

    context 'as an instructor' do
      let(:role) { create(:instructor) }

      it 'should respond with 403' do
        put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
        expect(response).to have_http_status :forbidden
      end
    end

    context 'as an grader' do
      let(:role) { create(:ta) }

      it 'should respond with 403' do
        put_as role, :start_timed_assignment, params: { course_id: course.id, id: assignment.id }
        expect(response).to have_http_status :forbidden
      end
    end
  end

  context 'download the most recent test results as JSON' do
    let(:user) { create(:instructor) }
    let(:assignment) { create(:assignment_with_criteria_and_test_results) }

    it 'responds with the appropriate status' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      expect(response).to have_http_status :success
    end

    it 'responds with the appropriate header' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      expect(response.header['Content-Type']).to eq('application/json')
    end

    it 'sets disposition as attachment' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      d = response.header['Content-Disposition'].split.first
      expect(d).to eq 'attachment;'
    end

    it 'responds with the appropriate filename' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      filename = response.header['Content-Disposition'].split[1].split('"').second
      expect(filename).to eq("#{assignment.short_identifier}_test_results.json")
    end

    it 'returns application/json type' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      expect(response.media_type).to eq 'application/json'
    end

    it 'returns the most recent test results' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'json'
      body = response.parsed_body

      # We want to ensure that the test result's group name, test name and status exists
      body.map do |group_name, group|
        group.map do |test_group_name, test_group|
          test_group.each do |test_result|
            expect(test_result.fetch('name')).to eq test_group_name
            expect(test_result.fetch('group_name')).to eq group_name
            expect(test_result.key?('status')).to be true
          end
        end
      end
    end
  end

  context 'download the most recent test results as CSV' do
    let(:user) { create(:instructor) }
    let(:assignment) { create(:assignment_with_criteria_and_test_results) }

    it 'responds with the appropriate status' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      expect(response).to have_http_status :success
    end

    it 'sets disposition as attachment' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      d = response.header['Content-Disposition'].split.first
      expect(d).to eq 'attachment;'
    end

    it 'responds with the appropriate filename' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      filename = response.header['Content-Disposition'].split[1].split('"').second
      expect(filename).to eq("#{assignment.short_identifier}_test_results.csv")
    end

    it 'returns text/csv type' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      expect(response.media_type).to eq 'text/csv'
    end

    it 'returns the most recent test results of the correct size' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'

      test_results = CSV.parse(response.body, headers: true)

      expect(test_results.to_a.size).to eq 4
      expect(test_results.headers.length).to eq 4
    end

    it 'returns the correct csv headers' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      test_results = CSV.parse(response.body, headers: true)

      assignment_results = assignment.summary_test_results

      headers = Set.new(test_results.headers.drop(1)).sort
      assignment_results.each do |result|
        expect(headers.include?("#{result['name']}:#{result['test_result_name']}")).to be true
      end
    end

    it 'returns the correct csv headers in the correct order' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      test_results = CSV.parse(response.body, headers: true)

      headers = test_results.headers.drop(1)
      sorted_headers = Set.new(headers).sort
      sorted_headers.each_with_index do |header, i|
        expect(header).to eq headers[i]
      end
    end

    it 'returns the correct amount of passed tests per group' do
      get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      test_results = CSV.parse(response.body, headers: true).to_a.drop(1)
      test_results.to_a.each do |row|
        count = 0
        row.each do |cell|
          if cell == 'pass'
            count += 1
          end
        end
        expect(count).to eq 1
      end
    end

    context 'most recent test results with static names' do
      let(:assignment) { create(:static_assignment_with_criteria_and_test_results) }
      let(:expected_csv_file) { fixture_file_upload('assignments/most_recent_test_results.csv', 'text/csv') }
      let(:expected_csv_results) { CSV.parse(expected_csv_file, headers: true) }
      let(:csv_results) { CSV.parse(response.body, headers: true) }

      before do
        get_as user, :download_test_results, params: { course_id: user.course.id, id: assignment.id }, format: 'csv'
      end

      it 'returns the correct headers' do
        expect(expected_csv_results.headers).to match_array(csv_results.headers)
      end

      it 'returns the correct results' do
        expect(expected_csv_results.map(&:to_h)).to match_array(csv_results.map(&:to_h))
      end
    end
  end

  describe '#index' do
    let(:course) { role.course }

    context 'an instructor' do
      let(:role) { create(:instructor) }

      context 'when there are no assessments' do
        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end

      context 'where there are some assessments' do
        before do
          create_list(:assignment_with_criteria_and_results, 3)
          create_list(:grade_entry_form_with_data, 2)
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end
    end

    context 'a TA' do
      let(:role) { create(:ta) }

      context 'when there are no assessments' do
        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end

      context 'where there are some assessments' do
        before do
          create_list(:assignment_with_criteria_and_results, 3)
          create_list(:grade_entry_form_with_data, 2)
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end
    end

    context 'a student' do
      let(:role) { create(:student) }

      context 'when there are no assessments' do
        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end

      context 'where there are some assessments' do
        before do
          3.times do
            assignment = create(:assignment_with_criteria_and_results)
            create(:accepted_student_membership, role: role, grouping: assignment.groupings.first)
          end
          create_list(:grade_entry_form_with_data, 2)
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end

      context 'where there are some assessments, including hidden assessments' do
        before do
          3.times do
            assignment = create(:assignment_with_criteria_and_results)
            create(:accepted_student_membership, role: role, grouping: assignment.groupings.first)
          end
          create_list(:grade_entry_form_with_data, 2)
          Assignment.first.update(is_hidden: true)
          GradeEntryForm.first.update(is_hidden: true)
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          expect(response).to have_http_status(:success)
        end
      end
    end
  end

  describe '#set_boolean_graders_options' do
    let!(:assignment) { create(:assignment) }

    context 'an instructor' do
      let(:role) { create(:instructor) }
      let(:value) { !assignment.assignment_properties[attribute] }

      before do
        post_as role, :set_boolean_graders_options,
                params: { course_id: assignment.course.id, id: assignment.id,
                          attribute: { assignment_properties_attributes: { attribute => value } } }
        assignment.reload
      end

      shared_examples 'successful tests' do
        it 'should respond with 200' do
          expect(response).to have_http_status(:ok)
        end

        it 'should update the attribute' do
          expect(assignment.assignment_properties[attribute]).to eq(value)
        end
      end

      context 'with a valid attribute field' do
        context 'is assign_graders_to_criteria' do
          let(:attribute) { :assign_graders_to_criteria }

          it_behaves_like 'successful tests'
        end

        context 'is anonymize_groups' do
          let(:attribute) { :anonymize_groups }

          it_behaves_like 'successful tests'
        end

        context 'is hide_unassigned_criteria' do
          let(:attribute) { :hide_unassigned_criteria }

          it_behaves_like 'successful tests'
        end
      end

      context 'with an invalid attribute field' do
        let(:attribute) { :this_is_not_a_real_attribute }
        let(:value) { true }

        it 'should respond with 400' do
          expect(response).to have_http_status(:bad_request)
        end
      end
    end
  end

  describe '#show' do
    let(:course) { assignment.course }
    let!(:assignment) { create(:assignment) }
    let!(:role) { create(:student) }

    context 'when the assignment is an individual assignment' do
      before do
        assignment.update!(group_min: 1, group_max: 1)
        post_as role, :show, params: { course_id: course.id, id: assignment.id }
      end

      xcontext 'a regular assignment' do
        it 'should create a new grouping' do
          expect(role.student_memberships.size).to eq 1
          expect(role.groupings.first.assessment_id).to eq assignment.id
        end

        it('should respond with success') { expect(response).to have_http_status(:success) }
      end

      context 'a timed assessment' do
        context 'before the due date' do
          let(:assignment) { create(:timed_assignment, due_date: 1.hour.from_now) }

          it('should respond with success') { expect(response).to have_http_status(:success) }

          it 'should not create a grouping' do
            expect(role.student_memberships.size).to eq 0
          end
        end

        xcontext 'after the due date' do
          let(:assignment) { create(:timed_assignment, due_date: 1.hour.ago) }

          it('should respond with success') { expect(response).to have_http_status(:success) }

          it 'should create a grouping' do
            expect(role.student_memberships.size).to eq 1
            expect(role.groupings.first.assessment_id).to eq assignment.id
          end
        end
      end
    end

    xcontext 'when the assignment is a group assignment and the student belongs to a group' do
      it 'responds with a success' do
        assignment.update!(group_min: 1, group_max: 3)
        grouping = create(:grouping_with_inviter, assignment: assignment)
        post_as grouping.inviter, :show, params: { course_id: course.id, id: assignment.id }
        expect(response).to have_http_status(:success)
      end
    end

    context 'when the assignment is a group assignment and the student does not belong to a group' do
      it 'responds with a success and does not create a grouping' do
        assignment.update!(group_min: 1, group_max: 3)
        post_as role, :show, params: { course_id: course.id, id: assignment.id }
        expect(response).to have_http_status(:success)

        expect(role.groupings.size).to eq 0
      end
    end

    context 'when the assignment is hidden' do
      it 'responds with a not_found status' do
        assignment.update!(is_hidden: true)
        post_as role, :show, params: { course_id: course.id, id: assignment.id }
        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when an error is raised' do
      before do
        allow_any_instance_of(Student).to receive(:create_group_for_working_alone_student).and_raise(RuntimeError)
        post_as role, :show, params: { course_id: course.id, id: assignment.id }
      end

      it { expect(response).to have_http_status(:redirect) }

      it 'is expected to flash an error message' do
        expect(flash[:error]).not_to be_empty
      end
    end
  end

  describe '#summary' do
    let(:course) { role.course }

    shared_examples 'An authorized role viewing assignment summary' do
      context 'requests an HTML response' do
        it 'responds with a success' do
          post_as role, :summary, params: { course_id: course.id, id: assignment.id }, format: 'html'
          expect(response).to have_http_status(:success)
        end
      end

      context 'requests a JSON response' do
        before do
          post_as role, :summary, params: { course_id: course.id, id: assignment.id }, format: 'json'
        end

        it 'responds with a success' do
          expect(response).to have_http_status(:success)
        end

        it 'responds with the correct keys' do
          expect(response.parsed_body.keys.to_set).to eq Set[
            'data', 'criteriaColumns', 'numAssigned', 'numMarked', 'enableTest', 'ltiDeployments'
          ]
        end
      end

      context 'requests a CSV response' do
        before do
          post_as role, :summary, params: { course_id: course.id, id: assignment.id }, format: 'csv'
        end

        it 'responds with a success' do
          expect(response).to have_http_status(:success)
        end
      end
    end

    describe 'When the role is instructor' do
      let(:role) { create(:instructor) }
      let(:assignment) { create(:assignment) }

      it_behaves_like 'An authorized role viewing assignment summary'
    end

    describe 'When the role is grader' do
      let(:role) { create(:ta) }
      let(:assignment) { create(:assignment) }

      it_behaves_like 'An authorized role viewing assignment summary'
    end
  end

  shared_examples 'An authorized role updating the assignment' do
    let(:assignment) { create(:assignment) }
    let(:submission_rule) { create(:penalty_decay_period_submission_rule, assignment: build(:assignment)) }
    let(:params) do
      example_form_params[:id] = assignment.id
      example_form_params[:course_id] = assignment.course.id
      example_form_params[:assignment][:assignment_properties_attributes][:id] = assignment.id
      example_form_params[:assignment][:short_identifier] = assignment.short_identifier
      example_form_params[:assignment][:submission_rule_attributes][:periods_attributes] = submission_rule.id
      example_form_params
    end
    it 'should update an assignment without errors' do
      expect { patch_as role, :update, params: params }.not_to raise_error
    end

    shared_examples 'update assignment_properties' do |property, before, after|
      it "should update #{property}" do
        assignment.update!(property => before)
        params[:assignment][:assignment_properties_attributes][property] = after
        patch_as role, :update, params: params
        expect(assignment.reload.assignment_properties[property]).to eq after
      end
    end

    shared_examples 'update assignment' do |property, before, after|
      it "should update #{property}" do
        assignment.update!(property => before)
        params[:assignment][property] = after
        patch_as role, :update, params: params
        expect(assignment.reload[property]).to eq after
      end
    end

    it_behaves_like 'update assignment_properties', :section_due_dates_type, false, true
    it_behaves_like 'update assignment_properties', :allow_web_submits, false, true
    it_behaves_like 'update assignment_properties', :vcs_submit, false, true
    it_behaves_like 'update assignment_properties', :display_median_to_students, false, true
    it_behaves_like 'update assignment_properties', :display_grader_names_to_students, false, true
    it_behaves_like 'update assignment_properties', :has_peer_review, false, true
    it_behaves_like 'update assignment_properties', :student_form_groups, false, true
    it_behaves_like 'update assignment_properties', :group_name_autogenerated, false, true
    it_behaves_like 'update assignment_properties', :allow_remarks, false, true
    it_behaves_like 'update assignment', :description, 'AAA', 'BBB'
    it_behaves_like 'update assignment', :message, 'AAA', 'BBB'
    it_behaves_like 'update assignment', :due_date, Time.current.to_s, 1.hour.ago.to_s
    it 'should update group_min and group_max when is_group_assignment is true' do
      assignment.update!(group_min: 1, group_max: 1)
      params[:assignment][:assignment_properties_attributes][:group_min] = 2
      params[:assignment][:assignment_properties_attributes][:group_max] = 3
      params[:is_group_assignment] = true
      patch_as role, :update, params: params
      assignment.reload
      expect(assignment.assignment_properties[:group_min]).to eq 2
      expect(assignment.assignment_properties[:group_max]).to eq 3
    end

    it 'should not update group_min and group_max when is_group_assignment is false' do
      assignment.update!(group_min: 1, group_max: 1)
      params[:assignment][:assignment_properties_attributes][:group_min] = 2
      params[:assignment][:assignment_properties_attributes][:group_max] = 3
      params[:is_group_assignment] = false
      patch_as role, :update, params: params
      assignment.reload
      expect(assignment.assignment_properties[:group_min]).to eq 1
      expect(assignment.assignment_properties[:group_max]).to eq 1
    end

    it 'should update duration when this is a timed assignment' do
      assignment.update!(is_timed: true, start_time: 10.hours.ago, duration: 10.minutes)
      params[:assignment][:assignment_properties_attributes][:duration] = { hours: 2, minutes: 20 }
      patch_as role, :update, params: params
      expect(assignment.reload.duration).to eq(2.hours + 20.minutes)
    end

    it 'should not update duration when this is a not a timed assignment' do
      assignment.update!(is_timed: false)
      params[:assignment][:assignment_properties_attributes][:duration] = { hours: 2, minutes: 20 }
      patch_as role, :update, params: params
      expect(assignment.reload.duration).to be_nil
    end
  end

  shared_examples 'An authorized role managing assignments' do
    describe '#new' do
      shared_examples 'assignment_new_success' do
        it 'responds with a success' do
          expect(response).to have_http_status :success
        end

        it 'renders the new template' do
          expect(response).to render_template(:new)
        end
      end

      context 'when the assignment is a scanned assignment' do
        before do
          get_as role, :new, params: { course_id: course.id, scanned: true }
        end

        it_behaves_like 'assignment_new_success'
        it 'assigns @assignment as a scanned assignment' do
          expect(assigns(:assignment).scanned_exam).to be true
        end

        it 'does not assign @assignment as a timed assignment' do
          expect(assigns(:assignment).is_timed).to be false
        end
      end

      context 'when the assignment is a timed assignment' do
        before do
          get_as role, :new, params: { course_id: course.id, timed: true }
        end

        it_behaves_like 'assignment_new_success'
        it 'assigns @assignment as a timed assignment' do
          expect(assigns(:assignment).is_timed).to be true
        end

        it 'does not assign @assignment as a scanned assignment' do
          expect(assigns(:assignment).scanned_exam).to be false
        end
      end

      context 'when the assignment is a regular assignment' do
        before do
          get_as role, :new, params: { course_id: course.id }
        end

        it_behaves_like 'assignment_new_success'
        it 'does not assign @assignment as a timed assignment' do
          expect(assigns(:assignment).is_timed).to be false
        end

        it 'does not assign @assignment as a scanned assignment' do
          expect(assigns(:assignment).scanned_exam).to be false
        end
      end
    end

    describe '#create' do
      let(:params) { example_form_params }

      it 'should create an assignment without errors' do
        expect { post_as role, :create, params: params }.not_to raise_error
      end

      it 'should respond with 302' do
        post_as role, :create, params: params
        expect(response).to have_http_status :found
      end

      shared_examples 'create assignment_properties' do |property, after|
        it "should create #{property}" do
          params[:assignment][:assignment_properties_attributes][property] = after
          post_as role, :create, params: params
          expect(assigns(:assignment).assignment_properties[property]).to eq after
        end
      end

      shared_examples 'create assignment' do |property, after|
        it "should create #{property}" do
          params[:assignment][property] = after
          post_as role, :create, params: params
          expect(assigns(:assignment)[property]).to eq after
        end
      end

      it_behaves_like 'create assignment_properties', :section_due_dates_type, true
      it_behaves_like 'create assignment_properties', :allow_web_submits, true
      it_behaves_like 'create assignment_properties', :vcs_submit, true
      it_behaves_like 'create assignment_properties', :display_median_to_students, true
      it_behaves_like 'create assignment_properties', :display_grader_names_to_students, true
      it_behaves_like 'create assignment_properties', :has_peer_review, true
      it_behaves_like 'create assignment_properties', :student_form_groups, true
      it_behaves_like 'create assignment_properties', :group_name_autogenerated, true
      it_behaves_like 'create assignment_properties', :allow_remarks, true
      it_behaves_like 'create assignment_properties', :group_min, 2
      it_behaves_like 'create assignment_properties', :group_min, 3
      it_behaves_like 'create assignment_properties', :release_with_urls, true
      it_behaves_like 'create assignment', :description, 'BBB'
      it_behaves_like 'create assignment', :message, 'BBB'
      it_behaves_like 'create assignment', :due_date, 1.hour.ago.to_s
      it 'should set duration when this is a timed assignment' do
        params[:assignment][:assignment_properties_attributes][:duration] = { hours: 2, minutes: 20 }
        params[:assignment][:assignment_properties_attributes][:start_time] = 10.hours.ago
        params[:assignment][:assignment_properties_attributes][:is_timed] = true
        post_as role, :create, params: params
        expect(assigns(:assignment).duration).to eq(2.hours + 20.minutes)
      end

      it 'should not set duration when this is a not a timed assignment' do
        params[:assignment][:assignment_properties_attributes][:duration] = { hours: 2, minutes: 20 }
        params[:assignment][:assignment_properties_attributes][:start_time] = 10.hours.ago
        params[:assignment][:assignment_properties_attributes][:is_timed] = false
        post_as role, :create, params: params
        expect(assigns(:assignment).duration).to be_nil
      end

      it 'should not require submission rule when assignment is scanned' do
        params[:assignment][:assignment_properties_attributes][:scanned_exam] = true
        params[:assignment].delete :submission_rule_attributes
        post_as role, :create, params: params
        expect(assigns(:assignment).reload).to be_valid
      end
    end

    describe '#edit' do
      let(:assignment) { create(:assignment) }

      before { post_as role, :edit, params: { course_id: course.id, id: assignment.id } }

      it('should respond with 200') { expect(response).to have_http_status :ok }
    end

    describe '#update' do
      let(:params) do
        {
          id: assignment.id,
          course_id: course.id,
          assignment: {
            short_identifier: assignment.short_identifier,
            assignment_properties_attributes: {
              scanned_exam: false, section_due_dates_type: 0, allow_web_submits: 0, vcs_submit: 0,
              display_median_to_students: 0, display_grader_names_to_students: 0,
              student_form_groups: 0, group_min: 1, group_max: 1, group_name_autogenerated: 1, allow_remarks: 0
            },
            submission_rule_attributes: {
              type: 'PenaltyPeriodSubmissionRule',
              periods_attributes: { 999 => { deduction: 10.0, interval: 1.0, hours: 10.0, _destroy: 0, id: nil } }
            }
          }
        }
      end
      let(:submission_rule) { create(:penalty_period_submission_rule, assignment: build(:assignment)) }
      let(:assignment) { submission_rule.assignment }
      let!(:period) { create(:period, submission_rule: submission_rule) }

      it 'should delete other penalty periods' do
        put_as role, :update, params: params
        expect { period.reload }.to raise_exception(ActiveRecord::RecordNotFound)
      end

      it 'should create new penalty periods' do
        put_as role, :update, params: params
        expect(assignment.reload.submission_rule.periods).not_to be_empty
      end

      context 'the submission rule is a different type' do
        before do
          params[:assignment][:submission_rule_attributes][:type] = 'PenaltyDecayPeriodSubmissionRule'
          put_as role, :update, params: params
        end

        it 'should delete other penalty periods' do
          expect { period.reload }.to raise_exception(ActiveRecord::RecordNotFound)
        end

        it 'should delete the old submission rule' do
          expect { submission_rule.reload }.to raise_exception(ActiveRecord::RecordNotFound)
        end

        it 'should create new penalty periods' do
          expect(assignment.reload.submission_rule.periods).not_to be_empty
        end

        it 'should create a new submission_rule' do
          expect(assignment.reload.submission_rule).to be_truthy
        end
      end
    end
  end

  shared_examples 'An authorized role managing tests' do
    let(:assignment) { create(:assignment_for_tests) }
    let(:grouping) { create(:grouping, assignment: assignment) }
    let(:test_run) { create(:test_run, grouping: grouping) }
    describe '#batch_runs' do
      before { get_as role, :batch_runs, params: { course_id: course.id, id: assignment.id } }

      it('should respond with 200') { expect(response).to have_http_status :ok }
    end

    describe '#stop_test' do
      before { get_as role, :stop_test, params: { course_id: course.id, id: assignment.id, test_run_id: test_run.id } }

      it('should respond with 302') { expect(response).to have_http_status :found }
    end
  end

  describe 'When the role is instructor' do
    let(:role) { create(:instructor) }
    let(:course) { role.course }

    it_behaves_like 'An authorized role updating the assignment'
    it_behaves_like 'An authorized role managing assignments'
    it_behaves_like 'An authorized role managing tests'
  end

  describe 'When the role is grader' do
    let(:course) { role.course }

    context 'When the grader is allowed to manage assignments' do
      let(:role) { create(:ta, manage_assessments: true) }

      it_behaves_like 'An authorized role updating the assignment'
      it_behaves_like 'An authorized role managing assignments'
    end

    context 'When the grader is not allowed to manage assignments' do
      # By default all the grader permissions are set to false
      let(:role) { create(:ta) }

      describe '#new' do
        before { get_as role, :new, params: { course_id: course.id } }

        it('should respond with 403') { expect(response).to have_http_status :forbidden }
      end

      describe '#create' do
        let(:params) do
          { course_id: course.id, short_identifier: 'A0', description: 'Ruby on rails', due_date: Time.current }
        end

        before { post_as role, :create, params: params }

        it('should respond with 403') { expect(response).to have_http_status :forbidden }
      end

      describe '#edit' do
        let(:assignment) { create(:assignment) }

        before { post_as role, :edit, params: { course_id: course.id, id: assignment.id } }

        it('should respond with 403') { expect(response).to have_http_status :forbidden }
      end
    end

    context 'When the grader is allowed to run tests' do
      let(:role) { create(:ta, manage_assessments: true) }

      it_behaves_like 'An authorized role managing tests'
    end

    context 'When the grader is not allowed to run tests' do
      let(:assignment) { create(:assignment_for_tests) }
      let(:grouping) { create(:grouping, assignment: assignment) }
      let(:test_run) { create(:test_run, grouping: grouping) }
      # By default all the grader permissions are set to false
      let(:role) { create(:ta) }

      describe '#batch_runs' do
        before { get_as role, :batch_runs, params: { course_id: course.id, id: assignment.id } }

        it('should respond with 403') { expect(response).to have_http_status :forbidden }
      end

      describe '#stop_test' do
        before do
          get_as role, :stop_test, params: { course_id: course.id, id: assignment.id, test_run_id: test_run.id }
        end

        it('should respond with 403') { expect(response).to have_http_status(:forbidden) }
      end
    end
  end

  describe 'when the role is student' do
    let(:course) { create(:course) }
    let(:role) { create(:student, course: course) }
    let(:assignment) { create(:assignment_for_student_tests, course: course) }
    let(:grouping) { create(:grouping_with_inviter, inviter: role, assignment: assignment) }
    let(:test_run) { create(:student_test_run, grouping: grouping, status: :in_progress) }

    describe '#stop_test' do
      context 'when student tests are allowed to cancel tests' do
        it 'should respond with 302' do
          assignment.assignment_properties.update!(unlimited_tokens: true)
          allow_any_instance_of(AutotestCancelJob).to receive(:perform_now).and_return []
          get_as role, :stop_test, params: { course_id: course.id,
                                             id: assignment.id,
                                             test_run_id: test_run.id }
          expect(response).to have_http_status(:found)
        end
      end

      context 'when students are not allowed to cancel tests' do
        it 'should respond with 403' do
          get_as role, :stop_test, params: { course_id: course.id,
                                             id: assignment.id,
                                             test_run_id: test_run.id }
          expect(response).to have_http_status(:forbidden)
        end
      end
    end
  end

  describe '#starter_file' do
    before { get_as role, :starter_file, params: params }

    let(:assignment) { create(:assignment) }
    let(:params) { { course_id: assignment.course.id, id: assignment.id } }

    shared_examples 'a user with permission to view the starter file page' do
      it 'should return a 200 status code' do
        expect(subject).to respond_with(:ok)
      end

      context 'the assignment exists' do
        it 'should render the starter_file view' do
          expect(response).to render_template(:starter_file)
        end

        it 'should render the assignment_content layout' do
          expect(response).to render_template(:assignment_content)
        end

        it 'should respond with success' do
          expect(subject).to respond_with(:success)
        end
      end

      context 'the assignment does not exist' do
        let(:params) { { course_id: assignment.course.id, id: -1 } }

        it 'should return a 404 error' do
          expect(subject).to respond_with(:not_found)
        end
      end
    end

    context 'an instructor' do
      let(:role) { create(:instructor) }

      it_behaves_like 'a user with permission to view the starter file page'
    end

    context 'a grader' do
      let(:role) { create(:ta) }

      it_behaves_like 'a user with permission to view the starter file page'
    end

    context 'a student' do
      let(:role) { create(:student) }

      it 'should return a 403 error' do
        expect(subject).to respond_with(:forbidden)
      end
    end
  end

  describe '#populate_starter_file_manager' do
    before { get_as role, :populate_starter_file_manager, params: params }

    let(:assignment) { create(:assignment) }
    let(:params) { { course_id: assignment.course.id, id: assignment.id } }

    shared_examples 'a user with permission to view starter files' do
      it 'should return a 200 status code' do
        expect(subject).to respond_with(:ok)
      end

      it 'should contain the right values' do
        expected = { available_after_due: true,
                     starterfileType: assignment.starter_file_type,
                     defaultStarterFileGroup: '',
                     files: [],
                     sections: [] }
        expect(response.parsed_body).to eq(expected.transform_keys(&:to_s))
      end

      context 'the file data' do
        let(:starter_file_group) { create(:starter_file_group_with_entries, assignment: assignment) }
        let(:params) { { course_id: assignment.course.id, id: starter_file_group.assignment.id } }

        it 'should contain the right keys' do
          file_data = response.parsed_body['files'].first.keys
          expect(file_data).to contain_exactly('id', 'name', 'entry_rename', 'use_rename', 'files')
        end

        it 'should contain the right values' do
          file_data = response.parsed_body['files'].first.slice('id', 'name', 'entry_rename', 'use_rename')
          grp = starter_file_group
          expected = { id: grp.id, name: grp.name, entry_rename: grp.entry_rename, use_rename: grp.use_rename }
          expect(file_data).to eq(expected.transform_keys(&:to_s))
        end

        it 'should contain the right file data' do
          file_names = response.parsed_body['files']
                               .first['files']
                               .map { |h| h['key'].split(File::Separator).first }
          expected_entries = starter_file_group.starter_file_entries.pluck('path')
          expect(file_names.to_set).to match_array(expected_entries)
        end
      end

      context 'the section data' do
        let(:starter_file_group) { create(:starter_file_group, assignment: assignment) }
        let(:section) { create(:section) }
        let(:ssfg) { create(:section_starter_file_group, starter_file_group: starter_file_group, section: section) }
        let(:params) { { course_id: assignment.course.id, id: ssfg.starter_file_group.assignment.id } }

        it 'should contain the right values' do
          expect(response.parsed_body['sections'].size).to eq 1
          file_data = response.parsed_body['sections'].first
          expected = { section_id: section.id,
                       section_name: section.name,
                       group_id: starter_file_group.id,
                       group_name: starter_file_group.name }
          expect(file_data).to eq(expected.transform_keys(&:to_s))
        end

        context 'when there is another assignment with sections' do
          let(:a2) { create(:assignment) }
          let(:other_starter_file_group) { create(:starter_file_group, assignment: a2) }
          let(:ssfg2) do
            create(:section_starter_file_group, starter_file_group: other_starter_file_group, section: section)
          end
          let(:params) do
            # Ensure creation of both assignments and starter file groups
            ssfg
            ssfg2
            { course_id: assignment.course.id, id: ssfg.starter_file_group.assignment.id }
          end

          it 'should only contain the section data for the requested assignment' do
            expect(response.parsed_body['sections'].size).to eq 1
            file_data = response.parsed_body['sections'].first
            expected = { section_id: section.id,
                         section_name: section.name,
                         group_id: starter_file_group.id,
                         group_name: starter_file_group.name }
            expect(file_data).to eq(expected.transform_keys(&:to_s))
          end
        end
      end
    end

    context 'an instructor' do
      let(:role) { create(:instructor) }

      it_behaves_like 'a user with permission to view starter files'
    end

    context 'a grader' do
      let(:role) { create(:ta) }

      it_behaves_like 'a user with permission to view starter files'
    end

    context 'a student' do
      let(:role) { create(:student) }

      it 'should return a 403 error' do
        expect(subject).to respond_with(:forbidden)
      end
    end
  end

  describe '#update_starter_file' do
    subject { post_as role, :update_starter_file, params: params }

    let!(:assignment) { create(:assignment) }
    let!(:grouping) { create(:grouping, assignment: assignment) }
    let!(:starter_file_group1) do
      create(:starter_file_group, assignment: assignment, name: 'name', entry_rename: 'name', use_rename: false)
    end
    let!(:starter_file_group2) { create(:starter_file_group, assignment: assignment) }
    let!(:starter_file_group3) { create(:starter_file_group, assignment: assignment) }
    let(:section) { create(:section) }
    let(:base_params) do
      { id: assignment.id,
        course_id: role.course.id,
        assignment: { starter_file_type: :shuffle,
                      default_starter_file_group_id: starter_file_group2.id },
        sections: [{ section_id: section.id, group_id: starter_file_group3.id }],
        starter_file_groups: [{ id: starter_file_group1.id,
                                name: 'changed_name',
                                entry_rename: 'changed_rename',
                                use_rename: true }] }
    end
    let(:params) { base_params }

    context 'an instructor' do
      let(:role) { create(:instructor) }

      it 'should update starter_file_type' do
        expect { subject }.to change { assignment.reload.starter_file_type }.from('simple').to('shuffle')
      end

      it 'should update default_starter_file_group_id' do
        expect { subject }.to(
          change { assignment.reload.default_starter_file_group_id }.from(nil).to(starter_file_group2.id)
        )
      end

      it 'should update starter_file_updated_at' do
        expect { subject }.to(
          change { assignment.assignment_properties.reload.starter_file_updated_at }
        )
      end

      context 'when a grouping for the assignment exists' do
        it 'should update the grouping starter_file_changed attribute' do
          grouping
          expect { subject }.to(
            change { grouping.reload.starter_file_changed }.from(false).to(true)
          )
        end
      end

      context 'when a section exists' do
        it 'should update section starter file mappings' do
          expect { subject }.to(
            change { section.starter_file_group_for(assignment) }.from(starter_file_group1).to(starter_file_group3)
          )
        end
      end

      context 'when a section does not exist' do
        let(:params) do
          base_params[:sections].first[:section_id] = section.id + 1
          base_params
        end

        it 'should not update section starter file mappings' do
          subject
          expect(section.starter_file_group_for(assignment)).to eq starter_file_group1
        end
      end

      context 'when updating starter file attributes' do
        it 'should update name' do
          expect { subject }.to change { starter_file_group1.reload.name }.to('changed_name')
        end

        it 'should update entry_rename' do
          expect { subject }.to change { starter_file_group1.reload.entry_rename }.to('changed_rename')
        end

        it 'should update use_rename' do
          expect { subject }.to change { starter_file_group1.reload.use_rename }.to(true)
        end

        context 'when the starter file group does not exist' do
          let(:params) do
            base_params[:starter_file_groups].first[:id] = starter_file_group1.id + 1
            base_params
          end

          it 'should not update anything' do
            grp = starter_file_group1
            expect { subject }.not_to(change { [grp.reload.name, grp.entry_rename, grp.use_rename] })
          end
        end
      end

      context 'when only updating assignment starter_files_after_due attribute' do
        let(:params) do
          { id: assignment.id,
            course_id: role.course.id,
            assignment: { starter_files_after_due: false },
            sections: [],
            starter_file_groups: [{ id: starter_file_group1.id,
                                    name: starter_file_group1.name,
                                    entry_rename: starter_file_group1.entry_rename,
                                    use_rename: starter_file_group1.use_rename }] }
        end

        it 'should update starter_files_after_due' do
          expect { subject }.to(
            change { assignment.assignment_properties.reload.starter_files_after_due }.from(true).to(false)
          )
        end

        it 'should not update starter_file_updated_at' do
          expect { subject }.not_to(
            change { assignment.assignment_properties.reload.starter_file_updated_at }
          )
        end

        context 'when a grouping for the assignment exists' do
          it 'should not update the grouping starter_file_changed attribute' do
            grouping
            expect { subject }.not_to(
              change { grouping.reload.starter_file_changed }
            )
          end
        end
      end
    end

    context 'a grader' do
      let(:role) { create(:ta) }

      it 'should return a 403 error' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'a student' do
      let(:role) { create(:student) }

      it 'should return a 403 error' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end
  end

  describe '#download_starter_file_mappings' do
    subject { get_as role, :download_starter_file_mappings, params: params }

    let(:assignment) { create(:assignment) }
    let(:params) { { course_id: assignment.course.id, id: assignment.id } }

    shared_examples 'a user with permission to download starter file mappings' do
      let!(:starter_file_group) { create(:starter_file_group_with_entries, assignment: assignment) }
      let!(:grouping) { create(:grouping_with_inviter, assignment: assignment) }
      let(:params) { { course_id: assignment.course.id, id: starter_file_group.assignment.id } }
      it 'should return a 200 status code' do
        subject
        expect(response).to have_http_status(:ok)
      end

      it 'should contain mappings' do
        expect(@controller).to receive(:send_data) do |file_content|
          mappings = file_content.split("\n")[1..-1].map { |m| m.split(',') }
          expected = StarterFileEntry.pluck(:path).map { |p| [grouping.group.group_name, starter_file_group.name, p] }
          expect(mappings).to match_array expected
        end
        subject
      end
    end

    context 'an instructor' do
      let(:role) { create(:instructor) }

      it_behaves_like 'a user with permission to download starter file mappings'
    end

    context 'a grader' do
      let(:role) { create(:ta) }

      it_behaves_like 'a user with permission to download starter file mappings'
    end

    context 'a student' do
      let(:role) { create(:student) }

      it 'should return a 403 error' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end
  end

  describe '#grade_distribution' do
    let(:assignment) { create(:assignment_with_criteria_and_results_and_tas) }
    let(:role) { create(:instructor) }
    let(:params) { { course_id: assignment.course.id, id: assignment.id } }

    before { get_as role, :grade_distribution, params: params }

    context 'response' do
      it 'should respond with 200' do
        expect(response).to have_http_status :ok
      end
    end

    context 'summary' do
      it 'should contain the right keys' do
        keys = response.parsed_body['summary'].keys
        expect(keys).to contain_exactly('name',
                                        'average_annotations',
                                        'average',
                                        'median',
                                        'max_mark',
                                        'standard_deviation',
                                        'num_submissions_collected',
                                        'num_submissions_graded',
                                        'num_fails',
                                        'num_zeros',
                                        'groupings_size',
                                        'num_students_in_group',
                                        'num_active_students',
                                        'remark_requests_enabled',
                                        'num_remark_requests',
                                        'num_remark_requests_completed')
      end

      it 'should contain the right values' do
        summary = response.parsed_body['summary']
        assignment_remark_requests = assignment.groupings.joins(current_submission_used: :submitted_remark)
        expected = { name: "#{assignment.short_identifier}: #{assignment.description}",
                     average_annotations: assignment.average_annotations,
                     average: assignment.results_average(points: true) || 0,
                     median: assignment.results_median(points: true) || 0,
                     max_mark: assignment.max_mark || 0,
                     standard_deviation: assignment.results_standard_deviation || 0,
                     num_submissions_collected: assignment.current_submissions_used.size,
                     num_submissions_graded: assignment.current_submissions_used.size -
                       assignment.ungraded_submission_results.size,
                     num_fails: assignment.results_fails,
                     num_zeros: assignment.results_zeros,
                     groupings_size: assignment.groupings.size,
                     num_students_in_group: assignment.groupings.joins(:accepted_students).size,
                     num_active_students: assignment.course.students.active.size,
                     remark_requests_enabled: assignment.allow_remarks,
                     num_remark_requests: assignment_remark_requests.size,
                     num_remark_requests_completed: assignment_remark_requests.where('results.marking_state': :complete)
                                                                              .size }
        expect(summary).to eq expected.as_json
      end
    end

    context 'grade_distribution' do
      context 'data' do
        it 'should contain the right keys' do
          data = response.parsed_body['grade_distribution']
          expect(data.keys).to contain_exactly('labels', 'datasets')
        end
      end

      context 'labels' do
        it 'should contain the right values' do
          labels = response.parsed_body['grade_distribution']['labels']
          expected = (0..19).map { |i| "#{5 * i}-#{5 * i + 5}" }
          expect(labels).to eq(expected)
        end
      end

      context 'datasets' do
        it 'should contain the right data' do
          data = response.parsed_body['grade_distribution']['datasets'].first['data']
          expected = assignment.grade_distribution_array
          expect(data).to match_array(expected)
        end
      end
    end

    context 'ta_data' do
      it 'should contain the right data' do
        response.parsed_body['ta_data']['datasets'].each_with_index do |data_response, index|
          data = data_response['data']
          ta = assignment.tas[index]
          expect(ta.grade_distribution_array(assignment, 20)).to eq(data)
        end
      end

      context 'with multiple TAs and memberships' do
        let(:ta2) { create(:ta) }

        before do
          Grouping.assign_all_tas(assignment.groupings, Ta.all, assignment)
          get_as role, :grade_distribution, params: params
        end

        it 'should contain the right data' do
          response.parsed_body['ta_data']['datasets'].each do |data_response|
            data = data_response['data']
            tas = assignment.tas.uniq
            curr_ta = nil
            tas.each do |ta|
              if data_response['label'].include?("#{ta.first_name} #{ta.last_name}")
                curr_ta = ta
              end
            end
            expect(curr_ta.grade_distribution_array(assignment, 20)).to eq(data)
          end
        end

        it 'should contain one entry per ta' do
          tas = assignment.tas.uniq
          expect(response.parsed_body['ta_data']['datasets'].length).to eq(tas.length)
        end
      end
    end

    context 'criteria_summary' do
      let(:params) { { course_id: assignment.course.id, id: assignment.id, get_criteria_data: true } }

      it 'should contain the right keys' do
        keys = response.parsed_body['criteria_summary'].first.keys
        expect(keys).to contain_exactly('name',
                                        'average',
                                        'median',
                                        'max_mark',
                                        'standard_deviation',
                                        'position',
                                        'num_zeros')
      end

      it 'should contain the right values' do
        summary = response.parsed_body['criteria_summary'].first
        criterion = assignment.criteria.first
        expected = { name: criterion.name,
                     average: criterion.average,
                     median: criterion.median || 0,
                     max_mark: criterion.max_mark.to_f,
                     standard_deviation: criterion.standard_deviation || 0,
                     position: criterion.position,
                     num_zeros: criterion.grades_array.count(&:zero?) }
        expect(summary).to eq expected.as_json
      end
    end

    context 'criteria_distributions' do
      let(:params) { { course_id: assignment.course.id, id: assignment.id, get_criteria_data: true } }

      it 'should contain the right data' do
        expected_data = assignment.criteria.map(&:grade_distribution_array)
        received_data = response.parsed_body['criteria_distributions']['datasets'].pluck('data')
        expect(received_data).to match_array(expected_data)
      end
    end
  end

  describe '#switch' do
    let(:assignment) { create(:assignment) }
    let(:assignment2) { create(:assignment) }
    let(:course) { assignment.course }
    let(:grouping) { create(:grouping, assignment: assignment) }
    let(:submission) { create(:submission, grouping: grouping) }
    let(:result) { create(:incomplete_result, submission: submission) }

    shared_examples 'switch assignment tests' do
      subject { get_as role, 'switch', params: { course_id: course.id, id: assignment2.id } }

      before { controller.request.headers.merge(HTTP_REFERER: referer) }

      context 'referred from an assignment url' do
        let(:referer) { course_assignment_url(course_id: course.id, id: assignment.id) }

        it 'should redirect to the equivalent assignment page' do
          expect(subject).to redirect_to(course_assignment_url(course_id: course.id, id: assignment2.id))
        end
      end

      context 'referred from a non assignment url' do
        let(:referer) { non_assignment_url.call(course_id: course.id, assignment_id: assignment.id) }

        it 'should redirect to the equivalent non assignment page' do
          expect(subject).to redirect_to(non_assignment_url.call(course_id: course.id, assignment_id: assignment2.id))
        end
      end

      context 'referred from a submission collection url' do
        let(:referer) { submission_collection_url.call(course_id: course.id, assignment_id: assignment.id) }

        it 'should redirect to the equivalent non assignment page (or to the fallback page if a student)' do
          if role.student?
            expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
          else
            expect(subject).to redirect_to(submission_collection_url.call(course_id: course.id,
                                                                          assignment_id: assignment2.id))
          end
        end
      end

      context 'referred from a result member url' do
        let(:referer) do
          result_member_url.call(course_id: course.id, id: result.id)
        end

        it 'should redirect to the fallback url' do
          expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
        end
      end

      context 'referer is nil' do
        let(:referer) { nil }

        it 'should redirect to the fallback url' do
          expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
        end
      end

      context 'referer is a url that does not include the assignment at all' do
        let(:referer) { course_students_url(course) }

        it 'should redirect to the fallback url' do
          expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
        end
      end

      context 'the referer url is some other site entirely' do
        let(:referer) { 'https://test.com' }

        it 'should redirect to the fallback url' do
          expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
        end
      end

      context 'the referer url is not valid' do
        let(:referer) { '1234567' }

        it 'should redirect to the fallback url' do
          expect(subject).to redirect_to(fallback_url.call(course_id: course.id, id: assignment2.id))
        end
      end
    end

    context 'an instructor' do
      let(:role) { create(:instructor) }
      let(:non_assignment_url) { ->(params) { course_assignment_graders_url(params) } }
      let(:fallback_url) { ->(params) { edit_course_assignment_url(params) } }
      let(:submission_collection_url) { ->(params) { browse_course_assignment_submissions_url(params) } }
      let(:result_member_url) { ->(params) { course_result_url(params) } }

      it_behaves_like 'switch assignment tests'
    end

    context 'a grader' do
      let(:role) { create(:ta, manage_assessments: true) }
      let(:non_assignment_url) { ->(params) { course_assignment_graders_url(params) } }
      let(:fallback_url) { ->(params) { summary_course_assignment_url(params) } }
      let(:submission_collection_url) { ->(params) { browse_course_assignment_submissions_url(params) } }
      let(:result_member_url) { ->(params) { course_result_url(params) } }

      it_behaves_like 'switch assignment tests'
    end

    context 'a student' do
      let(:role) { create(:student) }
      let(:non_assignment_url) { ->(params) { course_assignment_submissions_url(params) } }
      let(:fallback_url) { ->(params) { course_assignment_url(params) } }
      let(:submission_collection_url) { ->(params) { file_manager_course_assignment_submissions_url(params) } }
      let(:result_member_url) { ->(params) { course_result_url(params) } }

      it_behaves_like 'switch assignment tests'
    end
  end

  describe '#download_sample_starter_files' do
    subject do
      get_as role, :download_sample_starter_files, params: { course_id: assignment.course.id, id: assignment.id }
    end

    let(:assignment) { create(:assignment) }

    shared_examples 'download sample starter files' do
      let(:structure) { { 'q1/': nil, 'q1/q1.txt': 'q1 content', 'q2.txt': 'q2 content' } }
      # NOTE: other starter_file_type are not tested because they involve randomness and so are not deterministic
      before { create(:starter_file_group_with_entries, assignment: assignment, structure: structure) }

      it 'should send a zip file containing the correct content' do
        expect(controller).to receive(:send_file) do |file_path|
          Zip::File.open(Rails.root + file_path) do |zipfile|
            expect(zipfile.entries.map(&:name)).to match_array(structure.stringify_keys.keys)
            structure.each do |path, content|
              next unless content

              expect(zipfile.find_entry(path).get_input_stream.read.strip).to eq content
            end
          end
        end
        subject
      end
    end

    context 'a student' do
      let(:role) { create(:student) }

      it 'should respond with 403' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'a grader' do
      let(:role) { create(:ta) }

      it_behaves_like 'download sample starter files'
    end

    context 'an instructor' do
      let(:role) { create(:instructor) }

      it_behaves_like 'download sample starter files'
    end
  end

  describe '#download_config_files' do
    subject do
      get_as user, :download_config_files,
             params: { id: downloaded_assignment.id, course_id: downloaded_assignment.course.id }
    end

    let!(:assignment) { create(:assignment_with_peer_review, due_date: Time.zone.parse('2042-02-10 15:30:45')) }
    # criterion name matches automated-test-specs.json
    let!(:criteria) { create(:checkbox_criterion, assignment: assignment, name: 'Optimal') }
    let!(:starter_files) { create(:starter_file_group_with_entries, assignment: assignment) }

    let(:downloaded_assignment) { assignment }

    before do
      create(:annotation_category, assignment: assignment)

      # Clear uploaded autotest files to prepare for next test
      FileUtils.rm_rf(assignment.autotest_files_dir)

      create_automated_test(assignment)
    end

    shared_examples 'download sample config files' do
      it 'should have an ok status' do
        subject
        expect(response).to have_http_status(:ok)
      end

      it 'should receive a zip file' do
        expected_file_name = "#{assignment.short_identifier}-config-files.zip"
        file_options = { filename: expected_file_name, type: 'application/zip', disposition: 'attachment' }
        expected_file_path = File.join('tmp', expected_file_name)
        expect(controller).to receive(:send_file).with(expected_file_path, file_options) {
          # to prevent a 'missing template' error
          @controller.head :ok
        }
        subject
      end

      # Check file content
      describe 'downloading a normal assignment' do
        it 'should have a valid properties file' do
          subject
          properties = read_file_from_zip(response.body, 'properties.yml')
          properties = properties.deep_symbolize_keys
          expect(properties).to include(short_identifier: assignment.short_identifier,
                                        description: assignment.description,
                                        due_date: assignment.due_date,
                                        message: assignment.message,
                                        is_hidden: assignment.is_hidden,
                                        show_total: assignment.show_total,
                                        assignment_properties_attributes: a_kind_of(Hash),
                                        submission_rule_attributes: a_kind_of(Hash),
                                        assignment_files_attributes: a_kind_of(Array))
        end

        it 'should not have peer review information in the properties file' do
          subject
          properties = read_file_from_zip(response.body, 'properties.yml')
          properties = properties.deep_symbolize_keys
          expect(properties).not_to include(:has_peer_review, :parent_assessment_short_identifier)
        end

        it 'should have a valid criteria file' do
          subject
          criteria_download = read_file_from_zip(response.body, 'criteria.yml')
          criteria_download = criteria_download.deep_symbolize_keys
          criteria_download.each_key do |key|
            expect(criteria_download[key]).to include(:type, :max_mark, :description)
          end
        end

        it 'should have a valid annotations file' do
          subject
          annotation_download = read_file_from_zip(response.body, 'annotations.yml')
          expect(annotation_download).to be_a(Hash)
        end

        it 'should have a valid automated test settings file' do
          subject
          spec_data = read_file_from_zip(response.body, 'automated-test-config-files/automated-test-specs.json')
          received_settings = {
            is_a_hash: spec_data.is_a?(Hash),
            tester_type: spec_data['testers'][0]['tester_type'],
            assoc_criterion: spec_data['testers'][0]['test_data'][0]['extra_info']['criterion']
          }
          expected_settings = {
            is_a_hash: true,
            tester_type: 'py',
            assoc_criterion: criteria.name
          }
          expect(received_settings).to eq(expected_settings)
        end

        it 'should have a valid starter file settings file' do
          subject
          starter_file_settings = read_file_from_zip(response.body, File.join('starter-file-config-files',
                                                                              'starter-file-rules.yml'))
          starter_file_settings = starter_file_settings.deep_symbolize_keys
          expect(starter_file_settings).to include(:default_starter_file_group, :starter_file_groups)
        end

        it 'should have a starter file group with the right number of files' do
          subject
          starter_file_count = 0
          starter_file_group_dir_name = ActiveStorage::Filename.new(starter_files.name).sanitized
          starter_file_group_dir = File.join('starter-file-config-files', starter_file_group_dir_name)
          Zip::InputStream.open(StringIO.new(response.body)) do |io|
            while (entry = io.get_next_entry)
              starter_file_count += 1 if entry.name.match?(/^#{starter_file_group_dir}/)
            end
          end
          expect(starter_file_count).to eq(starter_files.files_and_dirs.length)
        end

        it 'should have a valid tags file' do
          tag1 = create(:tag, assessment_id: assignment.id)
          tag2 = create(:tag, assessment_id: assignment.id)
          subject
          tags = read_file_from_zip(response.body, 'tags.yml')
          tags = tags.map(&:symbolize_keys)
          expect(tags).to match_array([{ name: tag1.name, description: tag1.description },
                                       { name: tag2.name, description: tag2.description }])
        end
      end

      describe 'downloading a peer review zip file' do
        let(:downloaded_assignment) { Assignment.find_by(parent_assessment_id: assignment.id) }

        it 'should contain a tags file' do
          subject
          tags = read_file_from_zip(response.body, File.join('tags.yml'))
          expect(tags).to be_a(Array)
        end

        it 'should have a valid properties file' do
          subject
          properties = read_file_from_zip(response.body, File.join('properties.yml'))
          properties = properties.deep_symbolize_keys
          peer_review_assignment = Assignment.find_by(parent_assessment_id: assignment.id)
          expect(properties).to include(short_identifier: peer_review_assignment.short_identifier,
                                        description: peer_review_assignment.description,
                                        due_date: peer_review_assignment.due_date,
                                        message: peer_review_assignment.message,
                                        is_hidden: peer_review_assignment.is_hidden,
                                        show_total: peer_review_assignment.show_total,
                                        assignment_properties_attributes: a_kind_of(Hash),
                                        parent_assessment_short_identifier: assignment.short_identifier)
        end

        it 'should contain a criteria file' do
          subject
          criteria = read_file_from_zip(response.body, File.join('criteria.yml'))
          expect(criteria).to be_a(Hash)
        end

        it 'should contain an annotations file' do
          subject
          annotations = read_file_from_zip(response.body, File.join('annotations.yml'))
          expect(annotations).to be_a(Hash)
        end

        it 'should contain a starter file settings file' do
          subject
          starter_file_settings = read_file_from_zip(response.body, File.join('starter-file-config-files',
                                                                              'starter-file-rules.yml'))
          starter_file_settings = starter_file_settings.deep_symbolize_keys
          expect(starter_file_settings).to include(:default_starter_file_group, :starter_file_groups)
        end
      end
    end

    # Check to ensure appropriate access is given
    context 'a student' do
      let(:user) { create(:student) }

      it 'should respond with 403' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'a grader' do
      context 'without assignment management permissions' do
        let(:user) { create(:ta) }

        it 'should respond with 403' do
          subject
          expect(response).to have_http_status(:forbidden)
        end
      end

      context 'with assignment management permissions' do
        let(:user) { create(:ta, manage_assessments: true) }

        it_behaves_like 'download sample config files'
      end
    end

    context 'an instructor' do
      let(:user) { create(:instructor) }

      it_behaves_like 'download sample config files'
    end
  end

  describe '#upload_config_files' do
    subject do
      post_as user, :upload_config_files, params: { upload_files_for_config: @assignment_good_zip,
                                                    is_timed: true,
                                                    is_scanned: false,
                                                    course_id: user.course.id }
    end

    before do
      uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
      # Clear uploaded autotest files to prepare for next test
      unless uploaded_assignment.nil?
        FileUtils.rm_rf(uploaded_assignment.autotest_files_dir)
      end

      # Build sample assignment zip file
      base_dir = File.join('assignments', 'sample-timed-assessment-good')
      properties_good = fixture_file_upload(File.join(base_dir, 'properties.yml'), 'text/yaml')
      tags_good = fixture_file_upload(File.join(base_dir, 'tags.yml'), 'text/yaml')
      criteria_good = fixture_file_upload(File.join(base_dir, 'criteria.yml'), 'text/yaml')
      annotations_good = fixture_file_upload(File.join(base_dir, 'annotations.yml'), 'text/yaml')
      test_file_path = File.join('assignments', 'sample-timed-assessment-good',
                                 'automated-test-config-files')
      test_specs_good = fixture_file_upload(File.join(test_file_path, 'automated-test-specs.json'),
                                            'text/json')
      test_file1_good = fixture_file_upload(File.join(test_file_path,
                                                      'automated-test-files', 'tests.py'), 'text/py')
      test_file2_good = fixture_file_upload(File.join(test_file_path,
                                                      'automated-test-files', 'Helpers', 'test_helpers.py'), 'text/py')
      starter_settings_good = fixture_file_upload(File.join(base_dir,
                                                            'starter-file-config-files',
                                                            'starter-file-rules.yml'), 'text/yaml')
      starter_file_dir = File.join(base_dir, 'starter-file-config-files', 'sample_starter_group')
      starter_file1 = fixture_file_upload(File.join(starter_file_dir, 'c_file.c'), 'text/c')
      starter_file2 = fixture_file_upload(File.join(starter_file_dir, 'Helpers', 'template.tex'), 'text/tex')

      zip_name = 'mtt_ex_1-config-files.zip'
      zip_path = File.join('tmp', zip_name)
      FileUtils.rm_f(zip_path)
      Zip::File.open(zip_path, create: true) do |zip_file|
        zip_file.add('properties.yml', properties_good.path)
        zip_file.add('tags.yml', tags_good.path)
        zip_file.add('criteria.yml', criteria_good.path)
        zip_file.add('annotations.yml', annotations_good.path)
        zip_file.add('starter-file-config-files/starter-file-rules.yml', starter_settings_good.path)
        zip_file.add('starter-file-config-files/sample_starter_group/c_file.c', starter_file1.path)
        zip_file.mkdir(File.join(starter_file_dir, 'Helpers'))
        zip_file.add('starter-file-config-files/sample_starter_group/Helpers/template.tex', starter_file2.path)
        zip_file.add('automated-test-config-files/automated-test-specs.json', test_specs_good.path)
        zip_file.add('automated-test-config-files/automated-test-files/tests.py', test_file1_good.path)
        zip_file.add('automated-test-config-files/automated-test-files/Helpers/test_helpers.py', test_file2_good.path)
      end
      @assignment_good_zip = fixture_file_upload(zip_path, 'application/zip')
    end

    shared_examples 'check valid assignment config files' do
      it 'gives the appropriate response status' do
        subject
        expect(response).to have_http_status(:found)
      end

      it 'uploads with no errors' do
        subject
        expect(flash[:error]).to be_nil
      end

      it "properly configures an assignment's properties" do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        uploaded_sample_attributes = {
          message: uploaded_assignment.message,
          is_timed: uploaded_assignment.is_timed,
          scanned_exam: uploaded_assignment.scanned_exam,
          assignment_file_count: uploaded_assignment.assignment_files.count,
          submission_rule_type: uploaded_assignment.submission_rule.type
        }
        expected_sample_attributes = {
          message: 'Sample midterm to help you practice for our second midterm',
          is_timed: true,
          scanned_exam: false,
          assignment_file_count: 2,
          submission_rule_type: 'PenaltyDecayPeriodSubmissionRule'
        }
        expect(uploaded_sample_attributes).to eq(expected_sample_attributes)
      end

      it 'uploads all the criteria for an assignment' do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        expect(uploaded_assignment.criteria.count).to eq(2)
      end

      it 'uploads all the annotation categories for an assignment' do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        category = AnnotationCategory.find_by(annotation_category_name: 'quod laboriosam')
        uploaded_category_checks = {
          annotation_category_count: uploaded_assignment.annotation_categories.count,
          belongs_to_uploaded_assignment: category.assessment_id == uploaded_assignment.id
        }
        expected_category_checks = { annotation_category_count: 1, belongs_to_uploaded_assignment: true }
        expect(uploaded_category_checks).to eq(expected_category_checks)
      end

      it 'properly configures all the annotation text for an assignment' do
        subject
        category = AnnotationCategory.find_by(annotation_category_name: 'quod laboriosam')
        uploaded_annotation_text = AnnotationText.where(annotation_category_id: category.id)
                                                 .pluck_to_hash(:content)
                                                 .map(&:symbolize_keys)
        expected_annotation_text = [{ content: 'Sunt optio.' }, { content: 'Quibusdam ut ipsa.' },
                                    { content: 'Earum voluptate.' }, { content: 'Saepe.' }, { content: 'Non eum.' }]
        expect(uploaded_annotation_text).to match_array(expected_annotation_text)
      end

      it 'properly uploads all the automated test files for an assignment' do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        spec_data = autotest_settings_for(uploaded_assignment)
        received_automated_test_data = {
          num_test_groups: spec_data['testers'].length,
          num_test_data: spec_data['testers'][0]['test_data'].length,
          tester_type: 'custom',
          num_automated_test_files: uploaded_assignment.autotest_files.length
        }
        expected_automated_test_data = {
          num_test_groups: 1,
          num_test_data: 1,
          tester_type: 'custom',
          num_automated_test_files: 3
        }
        expect(received_automated_test_data).to eq(expected_automated_test_data)
      end

      it 'properly configures the starter files for an assignment' do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        starter_file_group = uploaded_assignment.starter_file_groups
        uploaded_starter_file_checks = {
          starter_file_group_count: starter_file_group.count,
          starter_file_group_name: starter_file_group.first.name,
          starter_file_entry_count: starter_file_group.first.files_and_dirs.length
        }
        expected_starter_file_checks = {
          starter_file_group_count: 1,
          starter_file_group_name: 'sample_starter_group',
          starter_file_entry_count: 3
        }
        expect(uploaded_starter_file_checks).to eq(expected_starter_file_checks)
      end

      it 'uploads all the tags for an assignment' do
        subject
        uploaded_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1')
        expect(uploaded_assignment.tags.count).to eq(2)
      end

      it 'does not automatically configure a peer review assignment' do
        subject
        expected_peer_review_data = {
          uploaded_child_assignment: nil,
          uploaded_assignment_has_peer_review: false
        }
        received_peer_review_data = {
          uploaded_child_assignment: Assignment.find_by(short_identifier: 'mtt_ex_1_peer_review'),
          uploaded_assignment_has_peer_review: Assignment.find_by(short_identifier: 'mtt_ex_1').has_peer_review
        }
        expect(received_peer_review_data).to eq(expected_peer_review_data)
      end
    end

    # Check to ensure appropriate access is given
    context 'a student' do
      let(:user) { create(:student) }

      it 'should respond with 403' do
        subject
        expect(response).to have_http_status(:forbidden)
      end
    end

    context 'a grader' do
      context 'without assignment management permissions' do
        let(:user) { create(:ta) }

        it 'should respond with 403' do
          subject
          expect(response).to have_http_status(:forbidden)
        end
      end

      context 'with assignment management permissions' do
        let(:user) { create(:ta, manage_assessments: true) }

        it_behaves_like 'check valid assignment config files'
      end
    end

    context 'an instructor' do
      let(:user) { create(:instructor) }

      it_behaves_like 'check valid assignment config files'
    end
  end

  describe 'download_and_upload_config_file' do
    let(:user) { create(:instructor) }

    shared_examples 'assignment content is copied over' do
      it 'copies over the main assignment attributes' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        received = uploaded_assignment.attributes.except('created_at', 'updated_at', 'id', 'parent_assessment_id')
        expected = assignment.attributes.except('created_at', 'updated_at', 'id', 'parent_assessment_id')
        expect(received).to eq(expected)
      end

      it 'copies over additional assignment properties' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_properties = uploaded_assignment.assignment_properties
        received = uploaded_properties.attributes.except('created_at', 'updated_at', 'id', 'assessment_id',
                                                         'remote_autotest_settings_id', 'autotest_settings',
                                                         'starter_file_updated_at')
        expected = assignment_properties.attributes.except('created_at', 'updated_at', 'id', 'assessment_id',
                                                           'remote_autotest_settings_id', 'autotest_settings',
                                                           'starter_file_updated_at')
        expect(received).to eq(expected)
      end

      it 'copies over tags' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_tags = uploaded_assignment.tags.pluck_to_hash(:name, :description)
        uploaded_tags = uploaded_tags.map(&:symbolize_keys)
        expected_tags = [{ name: tag1.name, description: tag1.description },
                         { name: tag2.name, description: tag2.description },
                         { name: tag3.name, description: tag3.description }]
        expect(uploaded_tags).to match_array(expected_tags)
      end

      it 'copies over annotations' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_annotation = uploaded_assignment.annotation_categories
                                                 .first
                                                 .attributes
                                                 .except('created_at', 'updated_at', 'id', 'assessment_id')
        expected_annotation = annotation.attributes.except('created_at', 'updated_at', 'id', 'assessment_id')
        expect(uploaded_annotation).to eq(expected_annotation)
      end

      it 'copies over criteria' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_criteria = uploaded_assignment.criteria
                                               .first
                                               .attributes
                                               .except('created_at', 'updated_at', 'id', 'assessment_id')
        expected_criteria = criteria.attributes.except('created_at', 'updated_at', 'id', 'assessment_id')
        expect(uploaded_criteria).to eq(expected_criteria)
      end

      it 'copies over starter files' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_starter_files = uploaded_assignment.starter_file_groups.map do |group|
          {
            name: group.name,
            use_rename: group.use_rename,
            entry_rename: group.entry_rename,
            files_and_dirs: group.files_and_dirs
          }
        end
        expected_starter_files = [
          {
            name: starter_group1.name,
            use_rename: starter_group1.use_rename,
            entry_rename: starter_group1.entry_rename,
            files_and_dirs: starter_group1_entries
          },
          {
            name: starter_group2.name,
            use_rename: starter_group2.use_rename,
            entry_rename: starter_group2.entry_rename,
            files_and_dirs: starter_group2_entries
          }
        ]
        expect(uploaded_starter_files).to match_array(expected_starter_files)
      end
    end

    context 'Normal assignment with everything' do
      # Disable this check because these variables are required for the shared_examples block.
      # The initial values must be saved because the assignment is deleted in the before block.
      # rubocop:disable RSpec/LetSetup
      let!(:assignment) { create(:assignment, due_date: Time.zone.parse('2042-02-10 15:30:45')) }
      let!(:criteria) { create(:flexible_criterion, assignment: assignment) }
      let!(:annotation) { create(:annotation_category, assignment: assignment) }
      let!(:submission_rule) { create(:grace_period_submission_rule, assignment: assignment) }
      let!(:assignment_file1) { create(:assignment_file, assignment: assignment) }
      let!(:assignment_file2) { create(:assignment_file, filename: 'sample.txt', assignment: assignment) }
      let!(:tag1) { create(:tag, assessment_id: assignment.id) }
      let!(:tag2) { create(:tag, assessment_id: assignment.id) }
      let!(:tag3) { create(:tag, assessment_id: assignment.id) }
      let!(:starter_group1) { create(:starter_file_group_with_entries, assignment: assignment) }
      let!(:starter_group1_entries) { starter_group1.files_and_dirs }
      let!(:starter_group2) { create(:starter_file_group_with_entries, assignment: assignment) }
      let!(:starter_group2_entries) { starter_group2.files_and_dirs }
      let!(:assignment_properties) do
        create_automated_test(assignment)
        assignment.assignment_properties
      end
      # rubocop:enable RSpec/LetSetup

      before do
        # Download and upload assignment
        get_as user, :download_config_files, params: { id: assignment.id, course_id: assignment.course.id }
        zip_name = 'assignment-copy-test-config-files.zip'
        zip_path = File.join('tmp', zip_name)
        FileUtils.rm_f(zip_path)
        File.write(zip_path, response.body, mode: 'wb')
        assignment_zip = fixture_file_upload(zip_path, 'application/zip')
        Tag.destroy_all
        Assignment.destroy_all
        post_as user, :upload_config_files, params: { upload_files_for_config: assignment_zip,
                                                      is_timed: false, is_scanned: false,
                                                      course_id: assignment.course.id }
      end

      after do
        # Clear uploaded autotest files to prepare for next test
        FileUtils.rm_rf(assignment.autotest_files_dir)
      end

      it_behaves_like 'assignment content is copied over'

      it 'copies over submission rules' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        received_rule = {
          type: uploaded_assignment.submission_rule.type,
          periods: uploaded_assignment.submission_rule.periods.pluck_to_hash(:deduction, :hours, :interval)
        }
        expected_rule = {
          type: submission_rule.type,
          periods: submission_rule.periods.pluck_to_hash(:deduction, :hours, :interval)
        }
        expect(received_rule).to eq(expected_rule)
      end

      it 'copies over required assignments' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        received = uploaded_assignment.assignment_files
                                      .pluck_to_hash(:filename)
                                      .map(&:symbolize_keys)
        expected = [{ filename: assignment_file1.filename },
                    { filename: assignment_file2.filename }]
        expect(received).to match_array(expected)
      end

      it 'copies over automated tests' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_criterion = uploaded_assignment.criteria.find_by(name: criteria.name)
        uploaded_test_groups = uploaded_assignment.test_groups
        received_automated_test_data = {
          uploaded_a_test_group: uploaded_test_groups.one?,
          spec_file: autotest_settings_for(uploaded_assignment),
          autotest_files: uploaded_assignment.autotest_files.to_set
        }
        sample_spec_file = create_sample_spec_file(uploaded_criterion)
        sample_spec_file['testers'][0]['test_data'][0]['extra_info']['test_group_id'] = uploaded_test_groups.first.id
        expected_automated_test_data = {
          uploaded_a_test_group: true,
          spec_file: sample_spec_file,
          autotest_files: ['tests.py', 'Helpers', File.join('Helpers', 'test_helpers.py')].to_set
        }
        expect(received_automated_test_data).to match(expected_automated_test_data)
      end
    end

    context 'Peer review assignment with everything' do
      # Disable this check because these variables are required for the shared_examples block.
      # The initial values must be saved because the assignment is deleted in the before block.
      # rubocop:disable RSpec/LetSetup
      let!(:parent_assignment) do
        create(:assignment_with_peer_review, due_date: Time.zone.parse('2042-02-10 15:30:45'))
      end
      let!(:assignment) { Assignment.find_by(parent_assessment_id: parent_assignment.id) }
      let!(:criteria) { create(:flexible_criterion, assignment: assignment) }
      let!(:annotation) { create(:annotation_category, assignment: assignment) }
      let!(:tag1) { create(:tag, assessment_id: assignment.id) }
      let!(:tag2) { create(:tag, assessment_id: assignment.id) }
      let!(:tag3) { create(:tag, assessment_id: assignment.id) }
      let!(:tag4) { create(:tag, assessment_id: parent_assignment.id) }
      let!(:starter_group1) { create(:starter_file_group_with_entries, assignment: assignment) }
      let!(:starter_group1_entries) { starter_group1.files_and_dirs }
      let!(:starter_group2) { create(:starter_file_group_with_entries, assignment: assignment) }
      let!(:starter_group2_entries) { starter_group2.files_and_dirs }
      let!(:assignment_properties) { assignment.assignment_properties }
      # rubocop:enable RSpec/LetSetup

      before do
        parent_assignment.assignment_properties

        # Clear uploaded autotest files to prepare for next test
        FileUtils.rm_rf(parent_assignment.autotest_files_dir)
        FileUtils.rm_rf(assignment.autotest_files_dir)
        # Download Parent
        get_as user, :download_config_files,
               params: { id: parent_assignment.id, course_id: parent_assignment.course.id }
        parent_zip_name = 'parent-assignment-copy-test-config-files.zip'
        parent_zip_path = File.join('tmp', parent_zip_name)
        FileUtils.rm_f(parent_zip_path)
        File.write(parent_zip_path, response.body, mode: 'wb')
        parent_assignment_zip = fixture_file_upload(parent_zip_path, 'application/zip')
        # Download Peer Review
        get_as user, :download_config_files,
               params: { id: assignment.id, course_id: assignment.course.id }
        zip_name = 'assignment-copy-test-config-files.zip'
        zip_path = File.join('tmp', zip_name)
        FileUtils.rm_f(zip_path)
        File.write(zip_path, response.body, mode: 'wb')
        assignment_zip = fixture_file_upload(zip_path, 'application/zip')
        Tag.destroy_all
        Assignment.destroy_all
        # Upload parent first then peer review
        post_as user, :upload_config_files, params: { upload_files_for_config: parent_assignment_zip,
                                                      is_timed: false, is_scanned: false,
                                                      course_id: parent_assignment.course.id }
        post_as user, :upload_config_files, params: { upload_files_for_config: assignment_zip,
                                                      is_timed: false, is_scanned: false,
                                                      course_id: assignment.course.id }
      end

      it 'has a peer review assignment copied' do
        uploaded_parent_assignment = Assignment.find_by(short_identifier: parent_assignment.short_identifier)
        uploaded_peer_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        expect(uploaded_parent_assignment.has_peer_review).to be(true)
        expect(uploaded_peer_assignment.is_peer_review?).to be(true)
      end

      it_behaves_like 'assignment content is copied over'
    end
  end

  describe '#create_lti_grades' do
    let!(:course) { create(:course) }
    let(:instructor) { create(:instructor, course: course) }
    let!(:lti) { create(:lti_deployment, course: course) }
    let(:assignment) { create(:assignment_with_criteria_and_results) }
    let(:scope) { LtiDeployment::LTI_SCOPES['names_role'] }
    let(:lti_service_namesrole) { create(:lti_service_namesrole, lti_deployment: lti) }
    let(:status) { 'Active' }

    before do
      create(:lti_service_lineitem, lti_deployment: lti)
      Result.joins(grouping: :assignment)
            .where('assignment.id': assignment.id).update!(released_to_students: true)
      User.find_each { |usr| create(:lti_user, user: usr, lti_client: lti.lti_client) }
    end

    it 'redirects if not logged in' do
      post :create_lti_grades, params: { lti_deployments: [lti.id], id: assignment.id, course_id: course.id }
      expect(response).to have_http_status(:found)
    end

    it 'returns successfully when logged in' do
      post_as instructor, :create_lti_grades,
              params: { lti_deployments: [lti.id], id: assignment.id, course_id: course.id }
      expect(response).to have_http_status(:ok)
    end
  end

  describe '#destroy' do
    let!(:instructor) { create(:instructor) }
    let!(:course) { instructor.course }
    let!(:assignment) { create(:assignment) }

    # lazy initialized
    let(:grouping) { create(:grouping, assignment: assignment) }

    it 'should fail to DELETE because of unauthorized request' do
      delete :destroy, params: { course_id: course.id, id: assignment.id }
      expect(Assignment.exists?(assignment.id)).to be(true)
    end

    it 'should fail to DELETE because assignment has groups' do
      grouping # lazy initialization
      delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
      expect(Assignment.exists?(assignment.id)).to be(true)
      expect(flash[:error]).to have_message(I18n.t('assignments.assignment_has_groupings'))
      expect(flash.to_hash.length).to eq(1)
      expect(flash[:error].length).to eq(1)
      expect(response).to have_http_status(:found)
    end

    it 'should successfully DELETE assignment (no groups)' do
      delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
      expect(Assignment.exists?(assignment.id)).to be(false)
      expect(flash[:success]).to have_message(I18n.t('flash.actions.destroy.success',
                                                     resource_name: assignment.short_identifier))
      expect(flash.to_hash.length).to eq(1)
      expect(response).to have_http_status(:found)
    end

    shared_examples 'handling associated entity upon destroy' do |entity|
      it "should remove associated #{entity}" do
        # NOTE: the next line assumes that an `assignment` is sufficient for the factory of `entity`
        assoc_entity = create(entity, assignment: assignment)
        # Deleting the assignment - should be successful since there are no groupings
        delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
        expect(Assignment.exists?(assignment.id)).to be(false)
        # Ensure that the associated entity was also removed
        expect(assoc_entity.class.exists?(assoc_entity.id)).to be(false)
      end
    end

    describe 'successful removal of associated entities' do
      selected_associations =
        [:checkbox_criterion, :rubric_criterion, :test_group, :annotation_category, :assignment_file,
         :exam_template_midterm, :starter_file_group]
      selected_associations.each do |entity|
        it_behaves_like 'handling associated entity upon destroy', entity
      end
    end

    it 'rescues from StandardError and sets flash message' do
      allow_any_instance_of(Assignment).to receive(:destroy).and_raise(StandardError.new('some error'))

      delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }

      expect(flash[:error]).to contain_message(I18n.t('activerecord.errors.models.assignment_deletion',
                                                      problem_message: 'some error'))
      expect(Assignment.exists?(assignment.id)).to be true
      expect(response).to redirect_to(edit_course_assignment_path(course.id, assignment.id))
    end
  end
end
