# Internal model used to link assignment attributes with the assignment STI model
class AssignmentProperties < ApplicationRecord
  belongs_to :assignment, inverse_of: :assignment_properties, dependent: :destroy, foreign_key: :assessment_id

  has_one :course, through: :assignment

  validates :group_min, numericality: { only_integer: true, greater_than: 0 }
  validates :group_max, numericality: { only_integer: true, greater_than: 0 }

  validates :repository_folder, presence: true, exclusion: { in: Repository.get_class.reserved_locations }
  validate :repository_folder_unchanged, on: :update
  validates :group_min, presence: true
  validates :group_max, presence: true

  # "validates_presence_of" for boolean values.
  validates :allow_web_submits, inclusion: { in: [true, false] }
  validates :vcs_submit, inclusion: { in: [true, false] }
  validates :display_grader_names_to_students, inclusion: { in: [true, false] }
  validates :display_median_to_students, inclusion: { in: [true, false] }
  validates :has_peer_review, inclusion: { in: [true, false] }
  validates :assign_graders_to_criteria, inclusion: { in: [true, false] }
  validates :student_form_groups, inclusion: { in: [true, false] }
  validates :group_name_autogenerated, inclusion: { in: [true, false] }
  validates :section_groups_only, inclusion: { in: [true, false] }
  validates :only_required_files, inclusion: { in: [true, false] }
  validates :allow_web_submits, inclusion: { in: [true, false] }
  validates :section_due_dates_type, inclusion: { in: [true, false] }
  validates :assign_graders_to_criteria, inclusion: { in: [true, false] }
  validates :unlimited_tokens, inclusion: { in: [true, false] }
  validates :non_regenerating_tokens, inclusion: { in: [true, false] }
  validates :url_submit, inclusion: { in: [true, false] }
  validates :api_submit, inclusion: { in: [true, false] }

  validates :enable_test, inclusion: { in: [true, false] }
  validates :enable_student_tests, inclusion: { in: [true, false], if: :enable_test }
  validates :non_regenerating_tokens, inclusion: { in: [true, false], if: :enable_student_tests }
  validates :unlimited_tokens, inclusion: { in: [true, false], if: :enable_student_tests }
  validates :token_start_date, presence: { if: :enable_student_tests }
  with_options if: :enable_student_tests, unless: :unlimited_tokens do
    validates :tokens_per_period,
              presence: true,
              numericality: { only_integer: true,
                              greater_than_or_equal_to: 0 }
  end
  with_options if: :enable_student_tests, unless: [:non_regenerating_tokens, :unlimited_tokens] do
    validates :token_period,
              presence: true,
              numericality: { greater_than: 0 }
  end

  validates :scanned_exam, inclusion: { in: [true, false] }

  validate :minimum_number_of_groups

  attribute :duration, :interval
  validates :duration, numericality: { greater_than: 0 }, allow_nil: false, if: :is_timed
  validates :start_time, presence: { if: :is_timed }
  validate :start_before_due, if: :is_timed
  validate :not_timed_and_scanned
  validate :remote_autotest_settings_id_uniqueness, if: :remote_autotest_settings_id

  STARTER_FILE_TYPES = %w[simple sections shuffle group].freeze

  validates :starter_file_type, inclusion: { in: STARTER_FILE_TYPES }

  DURATION_PARTS = [:hours, :minutes].freeze

  # Return the duration attribute for this assignment as a hash
  # with keys from +DURATION_PARTS+.
  #
  # For example:
  #
  # self.duration  # 10.hours and 4.minutes
  # self.duration_parts # {hours: 10, minutes: 4}
  def duration_parts
    AssignmentProperties.duration_parts(self.duration)
  end

  # Return +dur+ as a hash with keys from +DURATION_PARTS+.
  #
  # For example:
  #
  # dur  # 10.hours and 4.minutes
  # AssignmentProperties.duration_parts(dur) # {hours: 10, minutes: 4}
  def self.duration_parts(dur)
    dur = dur.to_i
    DURATION_PARTS.map do |part|
      amt = (dur / 1.public_send(part)).to_i
      dur -= amt.public_send(part)
      [part, amt]
    end.to_h
  end

  # Return the duration of this assignment plus any penalty periods
  def adjusted_duration
    duration + assignment.submission_rule.periods.pluck(:hours).sum.hours
  end

  private

  def minimum_number_of_groups
    return unless group_max && group_min && group_max < group_min
    errors.add(:group_max, 'must be greater than the minimum number of groups')
    false
  end

  def repository_folder_unchanged
    return unless repository_folder_changed?
    errors.add(:repo_folder_change, 'repository folder should not be changed once an assignment has been created')
    false
  end

  # Add an error if the start time is after the due date
  #
  # Note that this will fail silently if either the start_time or duration is nil since
  # those values are checked by other validations and so should not be checked twice.
  def start_before_due
    return if start_time.nil? || duration.nil?
    msg = I18n.t('activerecord.errors.models.assignment_properties.attributes.start_time.before_due_date')
    errors.add(:start_time, msg) if start_time > assignment.due_date
  end

  # Add an error if the is_timed and scanned_exam attributes for this assignment
  # are both true.
  def not_timed_and_scanned
    msg = I18n.t('activerecord.errors.models.assignment_properties.attributes.is_timed.not_scanned')
    errors.add(:base, msg) if is_timed && scanned_exam
  end

  # Ensure remote_autotest_settings_id is unique for a given autotester
  def remote_autotest_settings_id_uniqueness
    remote_autotest_settings_id_is_taken = AssignmentProperties.joins(assignment: :course).where(
      remote_autotest_settings_id: self.remote_autotest_settings_id,
      course: { autotest_setting_id: self.assignment.course.autotest_setting_id }
    ).where.not(id: self.id).exists?
    msg = I18n.t('activerecord.errors.models.assignment_properties.attributes.remote_autotest_settings_id.taken')
    errors.add(:remote_autotest_settings_id, msg) if remote_autotest_settings_id_is_taken
  end
end
