# frozen_string_literal: true

#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require 'spec_helper'

describe LatePolicy do
  describe 'relationships' do
    it { is_expected.to belong_to(:course).inverse_of(:late_policy) }
  end

  describe 'validations' do
    # Presence
    it { is_expected.to validate_presence_of(:course_id) }
    it { is_expected.to validate_presence_of(:late_submission_interval) }
    it { is_expected.to validate_presence_of(:late_submission_minimum_percent) }
    it { is_expected.to validate_presence_of(:late_submission_deduction) }
    it { is_expected.to validate_presence_of(:missing_submission_deduction) }

    # Numericality
    it do
      is_expected.to validate_numericality_of(:late_submission_minimum_percent).
        is_greater_than_or_equal_to(0).
        is_less_than_or_equal_to(100)
    end

    it do
      is_expected.to validate_numericality_of(:missing_submission_deduction).
        is_greater_than_or_equal_to(0).
        is_less_than_or_equal_to(100)
    end

    it do
      is_expected.to validate_numericality_of(:late_submission_deduction).
        is_greater_than_or_equal_to(0).
        is_less_than_or_equal_to(100)
    end

    # Inclusion
    it { is_expected.to validate_inclusion_of(:late_submission_interval).in_array(['day', 'hour']) }

    # Uniqueness
    describe 'uniqueness' do
      subject { course.create_late_policy }
      let(:course) { Course.create! }

      it { is_expected.to validate_uniqueness_of(:course_id) }
    end
  end

  describe 'default values' do
    it 'sets the late_submission_interval to "day" if not explicitly set' do
      policy = LatePolicy.new
      expect(policy.late_submission_interval).to eq 'day'
    end

    it 'sets the late_submission_minimum_percent to 0 if not explicitly set' do
      policy = LatePolicy.new
      expect(policy.late_submission_minimum_percent).to be_zero
    end

    describe "root_account_id" do
      before(:once) do
        @course = Course.create!
      end

      context "on create" do
        it "sets root_account_id to the course's root_account_id if root_account_id is nil" do
          policy = LatePolicy.create!(course: @course)
          expect(policy.root_account_id).to eq @course.root_account_id
        end

        it "does not modify root_account_id if it is already set" do
          second_account = account_model
          policy = LatePolicy.create!(course: @course, root_account_id: second_account.id)
          expect(policy.root_account_id).to eq second_account.id
        end
      end

      context "on update" do
        before(:once) do
          @policy = LatePolicy.create!(course: @course)
        end

        it "sets root_account_id to the course's root_account_id if root_account_id is nil" do
          @policy.update_column(:root_account_id, nil)
          @policy.update!(late_submission_minimum_percent: 20)
          expect(@policy.root_account_id).to eq @course.root_account_id
        end

        it "does not modify root_account_id if it is already set" do
          second_account = account_model
          second_course = Course.create!(root_account: second_account)
          @policy.update!(course: second_course)
          expect(@policy.root_account_id).to eq @course.root_account_id
        end
      end
    end
  end

  describe 'rounding' do
    it 'only keeps 2 digits after the decimal for late_submission_minimum_percent' do
      policy = LatePolicy.new(late_submission_minimum_percent: 25.223)
      expect(policy.late_submission_minimum_percent).to eql BigDecimal('25.22')
    end

    it 'rounds late_submission_minimum_percent' do
      policy = LatePolicy.new(late_submission_minimum_percent: 25.225)
      expect(policy.late_submission_minimum_percent).to eql BigDecimal('25.23')
    end

    it 'only keeps 2 digits after the decimal for missing_submission_deduction' do
      policy = LatePolicy.new(missing_submission_deduction: 25.223)
      expect(policy.missing_submission_deduction).to eql BigDecimal('25.22')
    end

    it 'rounds missing_submission_deduction' do
      policy = LatePolicy.new(missing_submission_deduction: 25.225)
      expect(policy.missing_submission_deduction).to eql BigDecimal('25.23')
    end

    it 'only keeps 2 digits after the decimal for late_submission_deduction' do
      policy = LatePolicy.new(late_submission_deduction: 25.223)
      expect(policy.late_submission_deduction).to eql BigDecimal('25.22')
    end

    it 'rounds late_submission_deduction' do
      policy = LatePolicy.new(late_submission_deduction: 25.225)
      expect(policy.late_submission_deduction).to eql BigDecimal('25.23')
    end
  end

  describe '#points_deducted' do
    it 'ignores disabled late submission deduction' do
      expect(late_policy_model.points_deducted(score: 500, possible: 1000, late_for: 1.day, grading_type: 'points')).to eq 0
    end

    it 'ignores ungraded assignments' do
      policy = late_policy_model(deduct: 5.0, every: :hour)
      expect(policy.points_deducted(score: 10, possible: nil, late_for: 1.day, grading_type: 'points')).to eq 0
    end

    it 'ignores pass_fail assignments' do
      policy = late_policy_model(deduct: 5.0, every: :hour)
      expect(policy.points_deducted(score: 10, possible: nil, late_for: 1.day, grading_type: 'pass_fail')).to eq 0
    end

    it 'ignores assignments that are not meant to be graded' do
      policy = late_policy_model(deduct: 5.0, every: :hour)
      expect(policy.points_deducted(score: 10, possible: nil, late_for: 1.day, grading_type: 'not_graded')).to eq 0
    end

    it 'increases by hour' do
      policy = late_policy_model(deduct: 5.0, every: :hour)
      expect(policy.points_deducted(score: 1000, possible: 1000, late_for: 12.hours, grading_type: 'points')).to eq 600
    end

    it 'increases by day' do
      policy = late_policy_model(deduct: 15.0, every: :day)
      expect(policy.points_deducted(score: 1000, possible: 1000, late_for: 3.days, grading_type: 'points')).to eq 450
    end

    it 'rounds partial late interval count upward' do
      policy = late_policy_model(deduct: 10.0, every: :day)
      expect(policy.points_deducted(score: 1000, possible: 1000, late_for: 25.hours, grading_type: 'points')).to eq 200
    end

    it 'honors the minimum percent deducted' do
      policy = late_policy_model(deduct: 10.0, every: :day, down_to: 30.0)
      expect(policy.points_deducted(score: 800, possible: 1000, late_for: 2.days, grading_type: 'points')).to eq 200
      expect(policy.points_deducted(score: 800, possible: 1000, late_for: 7.days, grading_type: 'points')).to eq 500
      expect(policy.points_deducted(score: 200, possible: 1000, late_for: 8.days, grading_type: 'points')).to eq 0
    end

    it 'can deduct fractional points' do
      policy = late_policy_model(deduct: 1.0, every: :hour)
      expect(policy.points_deducted(score: 10, possible: 10, late_for: 6.hours, grading_type: 'points')).to eq 0.6
    end
  end

  describe '#points_for_missing' do
    it 'returns 0 when assignment grading_type is pass_fail' do
      policy = late_policy_model
      expect(policy.points_for_missing(100, 'pass_fail')).to eq(0)
    end

    it 'computes expected value' do
      policy = late_policy_model(missing: 60)
      expect(policy.points_for_missing(100, 'foo')).to eq(40)
    end
  end

  describe '#missing_points_deducted' do
    it 'returns points_possible when assignment grading_type is pass_fail' do
      policy = late_policy_model
      expect(policy.missing_points_deducted(100, 'pass_fail')).to eq(100)
    end

    it 'computes expected value' do
      policy = late_policy_model(missing: 60)
      expect(policy.missing_points_deducted(100, 'foo')).to eq(60)
    end
  end

  describe '#update_late_submissions' do
    before :once do
      @course = Course.create(name: 'Late Policy Course')
      @late_policy = LatePolicy.create(course: @course)
    end

    it 'kicks off a late policy applicator for the course if late_submission_deduction_enabled_changed changes' do
      @late_policy.late_submission_deduction_enabled = !@late_policy.late_submission_deduction_enabled

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'kicks off a late policy applicator for the course if late_submission_deduction changes' do
      @late_policy.late_submission_deduction = 3.14

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'kicks off a late policy applicator for the course if late_submission_interval changes' do
      @late_policy.late_submission_interval = 'hour'

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'kicks off a late policy applicator for the course if late_submission_minimum_percent_enabled changes' do
      @late_policy.late_submission_minimum_percent_enabled = !@late_policy.late_submission_minimum_percent_enabled

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'kicks off a late policy applicator for the course if late_submission_minimum_percent changes' do
      @late_policy.late_submission_minimum_percent = 3.14

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'kicks off a late policy applicator if missing_submission_deduction_enabled changes' do
      @late_policy.missing_submission_deduction_enabled = !@late_policy.missing_submission_deduction_enabled

      expect(LatePolicyApplicator).to receive(:for_course).with(@course)

      @late_policy.save!
    end

    it 'does not kick off a late policy applicator if missing_submission_deduction changes' do
      @late_policy.missing_submission_deduction = 3.14

      expect(LatePolicyApplicator).not_to receive(:for_course)

      @late_policy.save!
    end
  end

end
