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

  let(:annotation_category) { FactoryBot.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, has_peer_review: 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

  context '#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 403
          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 403
      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 403
      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 eq 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 10
    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 = SortedSet.new(test_results.headers)
      assignment_results.each do |result|
        expect(headers.include?("#{result['name']}:#{result['test_result_name']}")).to eq 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 = SortedSet.new(headers)
      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 3
      end
    end

    context 'most recent test results with static names' do
      let(:assignment) { create(:static_assignment_with_criteria_and_test_results) }

      it 'returns the correct tests passed 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)
        test_results_fixture = fixture_file_upload('assignments/most_recent_test_results.csv', 'text/csv')
        test_results_static = CSV.parse(test_results_fixture, headers: true)

        test_results.each_with_index do |line, i|
          expect(line).to eq test_results_static[i]
        end
      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 }
          assert_response :success
        end
      end

      context 'where there are some assessments' do
        before :each do
          3.times { create(:assignment_with_criteria_and_results) }
          2.times { create(:grade_entry_form_with_data) }
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          assert_response :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 }
          assert_response :success
        end
      end

      context 'where there are some assessments' do
        before :each do
          3.times { create(:assignment_with_criteria_and_results) }
          2.times { create(:grade_entry_form_with_data) }
        end

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          assert_response :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 }
          assert_response :success
        end
      end

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

        it 'responds with a success' do
          get_as role, :index, params: { course_id: course.id }
          assert_response :success
        end
      end

      context 'where there are some assessments, including hidden assessments' do
        before :each do
          3.times do
            assignment = create(:assignment_with_criteria_and_results)
            create(:accepted_student_membership, role: role, grouping: assignment.groupings.first)
          end
          2.times { create(:grade_entry_form_with_data) }
          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 }
          assert_response :success
        end
      end
    end
  end

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

      before :each 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.status).to eq(200)
        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.status).to eq(400)
        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') { assert_response :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') { assert_response :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') { assert_response :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 }
        assert_response :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 }
        assert_response :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 }
        assert_response :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'
          assert_response :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
          assert_response :success
        end

        it 'responds with the correct keys' do
          expect(response.parsed_body.keys.to_set).to eq Set[
            'data', 'criteriaColumns', 'numAssigned', 'numMarked'
          ]
        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
          assert_response :success
        end
      end
    end

    describe 'When the role is instructor' do
      let(:role) { create(:instructor) }
      let(:assignment) { create(:assignment) }
      include_examples 'An authorized role viewing assignment summary'
    end

    describe 'When the role is grader' do
      let(:role) { create(:ta) }
      let(:assignment) { create(:assignment) }
      include_examples '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
      patch_as role, :update, params: params
    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, (Time.current - 1.hour).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: Time.current - 10.hours, 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 eq nil
    end
  end

  shared_examples 'An authorized role managing assignments' do
    context '#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 eq true
        end
        it 'does not assign @assignment as a timed assignment' do
          expect(assigns(:assignment).is_timed).to eq 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 eq true
        end
        it 'does not assign @assignment as a scanned assignment' do
          expect(assigns(:assignment).scanned_exam).to eq 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 eq false
        end
        it 'does not assign @assignment as a scanned assignment' do
          expect(assigns(:assignment).scanned_exam).to eq false
        end
      end
    end
    context '#create' do
      let(:params) { example_form_params }
      it 'should create an assignment without errors' do
        post_as role, :create, params: params
      end
      it 'should respond with 302' do
        post_as role, :create, params: params
        expect(response).to have_http_status 302
      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', :description, 'BBB'
      it_behaves_like 'create assignment', :message, 'BBB'
      it_behaves_like 'create assignment', :due_date, (Time.current - 1.hour).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] = Time.current - 10.hours
        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] = Time.current - 10.hours
        params[:assignment][:assignment_properties_attributes][:is_timed] = false
        post_as role, :create, params: params
        expect(assigns(:assignment).duration).to eq 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
    context '#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.status).to eq 200 }
    end
    context '#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, has_peer_review: 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) }
    context '#batch_runs' do
      before { get_as role, :batch_runs, params: { course_id: course.id, id: assignment.id } }
      it('should respond with 200') { expect(response.status).to eq 200 }
    end
    context '#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.status).to eq 302 }
    end
  end

  describe 'When the role is instructor' do
    let(:role) { create(:instructor) }
    let(:course) { role.course }
    include_examples 'An authorized role updating the assignment'
    include_examples 'An authorized role managing assignments'
    include_examples '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) }
      include_examples 'An authorized role updating the assignment'
      include_examples '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) }
      context '#new' do
        before { get_as role, :new, params: { course_id: course.id } }
        it('should respond with 403') { expect(response.status).to eq 403 }
      end
      context '#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.status).to eq 403 }
      end
      context '#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.status).to eq 403 }
      end
    end

    context 'When the grader is allowed to run tests' do
      let(:role) { create(:ta, manage_assessments: true) }
      include_examples '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) }
      context '#batch_runs' do
        before { get_as role, :batch_runs, params: { course_id: course.id, id: assignment.id } }
        it('should respond with 403') { expect(response.status).to eq 403 }
      end
      context '#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.status).to eq 403 }
      end
    end
  end
  describe '#starter_file' do
    before(:each) { get_as role, :starter_file, params: params }
    let(:assignment) { create :assignment }
    let(:params) { { course_id: assignment.course.id, id: assignment.id } }
    context 'an instructor' do
      let(:role) { create :instructor }
      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
          is_expected.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
          is_expected.to respond_with(:not_found)
        end
      end
    end
    context 'a grader' do
      let(:role) { create :ta }
      it 'should return a 403 error' do
        is_expected.to respond_with(:forbidden)
      end
    end
    context 'a student' do
      let(:role) { create :student }
      it 'should return a 403 error' do
        is_expected.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 } }
    context 'an instructor' do
      let(:role) { create :instructor }
      it 'should contain the right values' do
        expected = { available_after_due: true,
                     starterfileType: assignment.starter_file_type,
                     defaultStarterFileGroup: '',
                     files: [],
                     sections: [] }
        expect(JSON.parse(response.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 = JSON.parse(response.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 = JSON.parse(response.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 = JSON.parse(response.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 contain_exactly(*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
          file_data = JSON.parse(response.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
    context 'a grader' do
      let(:role) { create :ta }
      it 'should return a 404 error' do
        is_expected.to respond_with(:forbidden)
      end
    end
    context 'a student' do
      let(:role) { create :student }
      it 'should return a 404 error' do
        is_expected.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(: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(: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_group1.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
      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_group2).to(starter_file_group1)
          )
        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_group2
        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
    end
    context 'a grader' do
      let(:role) { create :ta }
      it 'should return a 404 error' do
        subject
        expect(response).to have_http_status(403)
      end
    end
    context 'a student' do
      let(:role) { create :student }
      it 'should return a 404 error' do
        subject
        expect(response).to have_http_status(403)
      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 } }
    context 'an instructor' do
      let(:role) { create :instructor }
      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 contain mappings' do
        expect(@controller).to receive(:send_data) do |file_content|
          mappings = file_content.split("\n")[1..-1].map { |m| m.split(',') }.sort
          expected = StarterFileEntry.pluck(:path).map { |p| [grouping.group.group_name, starter_file_group.name, p] }
          expect(mappings).to eq expected.sort
        end
        subject
      end
    end
    context 'a grader' do
      let(:role) { create :ta }
      it 'should return a 404 error' do
        subject
        expect(response).to have_http_status(403)
      end
    end
    context 'a student' do
      let(:role) { create :student }
      it 'should return a 404 error' do
        subject
        expect(response).to have_http_status(403)
      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 200
      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',
                                        'median',
                                        'num_submissions_collected',
                                        'num_submissions_graded',
                                        'num_fails',
                                        'num_zeros',
                                        'groupings_size',
                                        'num_outstanding_remark_requests')
      end

      it 'should contain the right values' do
        summary = response.parsed_body['summary']
        expected = { name: assignment.short_identifier +
          ': ' + assignment.description,
                     average: assignment.results_average || 0,
                     median: assignment.results_median,
                     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_outstanding_remark_requests: assignment.outstanding_remark_request_count }
        expect(summary).to eq expected.as_json
      end
    end
    context 'assignment_data' do
      context 'data' do
        it 'should contain the right keys' do
          data = response.parsed_body['assignment_data']
          expect(data.keys).to contain_exactly('labels', 'datasets')
        end
      end
      context 'labels' do
        it 'should contain the right values' do
          labels = response.parsed_body['assignment_data']['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['assignment_data']['datasets'].first['data']
          expected = assignment.grade_distribution_array
          expect(data).to contain_exactly(*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
    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
      before { controller.request.headers.merge('HTTP_REFERER': referer) }
      subject { expect get_as role, 'switch', params: { course_id: course.id, id: assignment2.id } }
      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 member url' do
        let(:referer) { submission_member_url.call(course_id: course.id, id: submission.id) }
        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 '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) } }
      let(:submission_member_url) { ->(params) { get_file_course_submission_url(params) } }
      include_examples '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) } }
      let(:submission_member_url) { ->(params) { get_file_course_submission_url(params) } }
      include_examples '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) } }
      let(:submission_member_url) { ->(params) { get_file_course_submission_url(params) } }
      include_examples 'switch assignment tests'
    end
  end
  describe '#download_sample_starter_files' do
    let(:assignment) { create :assignment }
    subject do
      get_as role, :download_sample_starter_files, params: { course_id: assignment.course.id, id: assignment.id }
    end
    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 contain_exactly(*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(403)
      end
    end
    context 'a grader' do
      context 'without assignment management permissions' do
        let(:role) { create :ta }
        it 'should respond with 403' do
          subject
          expect(response).to have_http_status(403)
        end
      end
      context 'with assignment management permissions' do
        let(:role) { create :ta, manage_assessments: true }
        include_examples 'download sample starter files'
      end
    end
    context 'an instructor' do
      let(:role) { create :instructor }
      include_examples 'download sample starter files'
    end
  end

  describe '#download_config_files' do
    let!(:assignment) { create :assignment_with_peer_review, due_date: Time.zone.parse('2042-02-10 15:30:45') }
    let!(:criteria) { create :checkbox_criterion, assignment: assignment }
    let!(:annotation) { create :annotation_category, assignment: assignment }
    let!(:starter_files) { create :starter_file_group_with_entries, assignment: assignment }
    subject { get_as user, :download_config_files, params: { id: assignment.id, course_id: assignment.course.id } }

    before :each do
      create_automated_test(assignment)
    end

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

    shared_examples 'download sample config files' do
      it 'should have an ok status' do
        subject
        expect(response).to have_http_status(200)
      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 'downloaded zip file' 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 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.type}:#{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 eq([{ name: tag1.name, description: tag1.description },
                              { name: tag2.name, description: tag2.description }])
        end

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

        it 'should have a valid peer review properties file' do
          subject
          properties = read_file_from_zip(response.body, File.join('peer-review-config-files', '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))
        end

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

        it 'should contain a peer review annotations file' do
          subject
          annotations = read_file_from_zip(response.body, File.join('peer-review-config-files',
                                                                    'annotations.yml'))
          expect(annotations).to be_a(Hash)
        end

        it 'should contain a peer review starter file settings file' do
          subject
          starter_file_settings = read_file_from_zip(response.body, File.join('peer-review-config-files',
                                                                              '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(403)
      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(403)
        end
      end
      context 'with assignment management permissions' do
        let(:user) { create :ta, manage_assessments: true }
        include_examples 'download sample config files'
      end
    end
    context 'an instructor' do
      let(:user) { create :instructor }
      include_examples '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

    after :each 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)
        FileUtils.rm_f(uploaded_assignment.autotest_settings_file)
      end
    end

    before :each do
      # 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')

      peer_review_dir = File.join('assignments', 'sample-timed-assessment-good', 'peer-review-config-files')
      pr_properties_good = fixture_file_upload(File.join(peer_review_dir, 'properties.yml'), 'text/yaml')
      pr_tags_good = fixture_file_upload(File.join(peer_review_dir, 'tags.yml'), 'text/yaml')
      pr_criteria_good = fixture_file_upload(File.join(peer_review_dir, 'criteria.yml'), 'text/yaml')
      pr_annotations_good = fixture_file_upload(File.join(peer_review_dir, 'annotations.yml'), 'text/yaml')
      pr_starter_settings_good = fixture_file_upload(File.join(peer_review_dir,
                                                               'starter-file-config-files',
                                                               'starter-file-rules.yml'), 'text/yaml')

      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('peer-review-config-files/properties.yml', pr_properties_good.path)
        zip_file.add('peer-review-config-files/tags.yml', pr_tags_good.path)
        zip_file.add('peer-review-config-files/criteria.yml', pr_criteria_good.path)
        zip_file.add('peer-review-config-files/annotations.yml', pr_annotations_good.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)
        zip_file.add('peer-review-config-files/starter-file-config-files/starter-file-rules.yml',
                     pr_starter_settings_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.status).to eq(302)
      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,
          has_peer_review: uploaded_assignment.has_peer_review,
          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,
          has_peer_review: true,
          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 eq(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 = JSON.parse File.open(uploaded_assignment.autotest_settings_file, &:read)
        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 'properly uploads a peer review assignment' do
        subject
        uploaded_child_assignment = Assignment.find_by(short_identifier: 'mtt_ex_1_peer_review')
        parent_assignment = Assignment.find(uploaded_child_assignment.parent_assessment_id)
        uploaded_pr_assessment_data = {
          message: uploaded_child_assignment.message,
          is_timed: uploaded_child_assignment.is_timed,
          scanned_exam: uploaded_child_assignment.scanned_exam,
          parent_short_id: parent_assignment.short_identifier,
          num_criteria: uploaded_child_assignment.criteria.count,
          num_annotations: uploaded_child_assignment.annotation_categories.count,
          has_no_starter_files: uploaded_child_assignment.starter_file_groups.count == 0,
          has_peer_review_assignment: uploaded_child_assignment.has_peer_review_assignment?,
          automated_tests_enabled: uploaded_child_assignment.enable_test,
          has_no_required_files: uploaded_child_assignment.assignment_files.count == 0,
          submission_rule_type: uploaded_child_assignment.submission_rule.type
        }
        expected_pr_assessment_data = {
          message: 'Review the sample midterm of your peers to help you practice for our second midterm',
          is_timed: false,
          scanned_exam: false,
          parent_short_id: 'mtt_ex_1',
          num_criteria: 1,
          num_annotations: 2,
          has_no_starter_files: true,
          has_peer_review_assignment: false,
          automated_tests_enabled: false,
          has_no_required_files: true,
          submission_rule_type: 'NoLateSubmissionRule'
        }
        expect(uploaded_pr_assessment_data).to eq(expected_pr_assessment_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(403)
      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(403)
        end
      end
      context 'with assignment management permissions' do
        let(:user) { create :ta, manage_assessments: true }
        include_examples 'check valid assignment config files'
      end
    end
    context 'an instructor' do
      let(:user) { create :instructor }
      include_examples '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',
                                                         'autotest_settings_id', 'starter_file_updated_at')
        expected = assignment_properties.attributes.except('created_at', 'updated_at', 'id', 'assessment_id',
                                                           'autotest_settings_id', '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 eq(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.each do |group|
          uploaded_starter_files << {
            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_files
          },
          {
            name: starter_group2.name,
            use_rename: starter_group2.use_rename,
            entry_rename: starter_group2.entry_rename,
            files_and_dirs: starter_group2_files
          }
        ]
        expect(uploaded_starter_files).to eq(expected_starter_files)
      end
    end

    context 'Normal assignment with everything' do
      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_files) { starter_group1.files_and_dirs }
      let!(:starter_group2) { create :starter_file_group_with_entries, assignment: assignment }
      let!(:starter_group2_files) { starter_group2.files_and_dirs }
      let!(:assignment_properties) do
        create_automated_test(assignment)
        assignment.assignment_properties
      end

      before :each 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.all.destroy_all
        Assignment.all.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 }
        expect(flash[:error]).to be_nil
      end

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

      include_examples '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 eq(expected)
      end

      it 'copies over automated tests' do
        uploaded_assignment = Assignment.find_by(short_identifier: assignment.short_identifier)
        uploaded_criteria = 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.count == 1,
          spec_file: JSON.parse(File.open(uploaded_assignment.autotest_settings_file, &:read)),
          autotest_files: uploaded_assignment.autotest_files.to_set
        }
        sample_spec_file = create_sample_spec_file(uploaded_criteria)
        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 eq(expected_automated_test_data)
      end
    end

    context 'Peer review assignment with everything' do
      let!(:parent_assignment) { create :assignment_with_peer_review, due_date: Time.zone.parse('2042-02-10 15:30:45') }
      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_files) { starter_group1.files_and_dirs }
      let!(:starter_group2) { create :starter_file_group_with_entries, assignment: assignment }
      let!(:starter_group2_files) { starter_group2.files_and_dirs }
      let!(:assignment_properties) { assignment.assignment_properties }

      before :each do
        get_as user, :download_config_files,
               params: { id: parent_assignment.id, course_id: parent_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.all.destroy_all
        Assignment.all.destroy_all
        post_as user, :upload_config_files, params: { upload_files_for_config: assignment_zip,
                                                      is_timed: false, is_scanned: false,
                                                      course_id: parent_assignment.course.id }
        expect(flash[:error]).to be_nil
      end

      after :each do
        # Clear uploaded autotest files to prepare for next test
        FileUtils.rm_rf(parent_assignment.autotest_files_dir)
        FileUtils.rm_f(parent_assignment.autotest_settings_file)
        FileUtils.rm_rf(assignment.autotest_files_dir)
        FileUtils.rm_f(assignment.autotest_settings_file)
      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 eq(true)
        expect(uploaded_peer_assignment.is_peer_review?).to eq(true)
      end

      include_examples 'assignment content is copied over'
    end
  end
end
