# frozen_string_literal: true

#
# Copyright (C) 2014 - 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/>.

class Enrollment
  class QueryBuilder
    # Generate SQL fragments used to return enrollments in their
    # respective workflow states. Where needed, these consider the state
    # of the course to ensure that students do not see their enrollments
    # on unpublished courses.
    #
    # strict_checks can be used to bypass the course state checks.
    # This is useful in places like the course settings UI, where we use
    # these conditions to search users in the course (rather than as an
    # association on a particular user)
    #
    # The course_workflow_state option can be used to simplify the
    # query when the enrollments are all known to come from one course
    # whose workflow state is already known. When provided, the method may
    # return nil, in which case the condition should be treated as 'always
    # false'
    #
    # The enforce_course_workflow_state option takes things a step
    # further; if you are querying enrollments in *multiple* courses and
    # want to ensure you only get ones from courses with a given
    # workflow_state, set this to true
    def initialize(state, options = {})
      @state = state || COURSE_ENROLLMENT_STATE_MAP[options[:course_workflow_state]]
      @options = options.reverse_merge(strict_checks: true)
      @builders = infer_sub_builders
    end

    def infer_sub_builders
      case @state
      when :current_and_invited
        [
          QueryBuilder.new(:active, @options),
          QueryBuilder.new(:invited, @options)
        ]
      when :current_and_future
        [
          QueryBuilder.new(:active, @options.merge(strict_checks: false)),
          QueryBuilder.new(:invited, @options)
        ]
      when :current_and_concluded
        [
          QueryBuilder.new(:active, @options),
          QueryBuilder.new(:completed, @options)
        ]
      end
    end

    # returns a relevant conditions string to be used in a query,
    # or nil if the builder's parameters are invalid (e.g. active
    # enrollments in deleted courses)
    def conditions
      return @builders.map(&:conditions).join(" OR ") if @builders

      conditions = case @state
                   when :active
                     if @options[:strict_checks]
                       case @options[:course_workflow_state]
                       when "available"
                         # all active enrollments in a published and active course count
                         "enrollments.workflow_state='active'"
                       when "claimed"
                         # student and observer enrollments don't count as active if the
                         # course is unpublished
                         "enrollments.workflow_state='active' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment')"
                       when nil
                         # combine the other branches dynamically based on joined course's
                         # workflow_state
                         "enrollments.workflow_state='active' AND (courses.workflow_state='available' OR courses.workflow_state IN ('created', 'claimed') AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment'))"
                       else
                         # never include enrollments from unclaimed/completed/deleted
                         # courses
                         nil
                       end
                     else
                       case @options[:course_workflow_state]
                       when "deleted"
                         # never include enrollments from deleted courses, even without
                         # strict checks
                         nil
                       when nil
                         # combine the other branches dynamically based on joined course's
                         # workflow_state
                         "enrollments.workflow_state='active' AND courses.workflow_state<>'deleted'"
                       else
                         # all active enrollments in a non-deleted course count
                         "enrollments.workflow_state='active'"
                       end
                     end
                   when :invited
                     if @options[:strict_checks]
                       case @options[:course_workflow_state]
                       when "available"
                         # all invited enrollments in a published and active course count
                         "enrollments.workflow_state='invited'"
                       when "deleted"
                         # never include enrollments from deleted courses
                         nil
                       when nil
                         # combine the other branches dynamically based on joined course's
                         # workflow_state
                         "enrollments.workflow_state='invited' AND (courses.workflow_state='available' OR courses.workflow_state<>'deleted' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment'))"
                       else
                         # student and observer enrollments don't count as invited if
                         # the course is unclaimed/unpublished/completed
                         "enrollments.workflow_state='invited' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment')"
                       end
                     else
                       case @options[:course_workflow_state]
                       when "deleted"
                         # never include enrollments from deleted courses
                         nil
                       when nil
                         # combine the other branches dynamically based on joined course's
                         # workflow_state
                         "enrollments.workflow_state IN ('invited','creation_pending') AND courses.workflow_state<>'deleted'"
                       else
                         # all invited and creation_pending enrollments in a non-deleted
                         # course count
                         "enrollments.workflow_state IN ('invited','creation_pending')"
                       end
                     end
                   when :deleted then          "enrollments.workflow_state = 'deleted'"
                   when :rejected then         "enrollments.workflow_state = 'rejected'"
                   when :completed then        "enrollments.workflow_state = 'completed'"
                   when :creation_pending then "enrollments.workflow_state = 'creation_pending'"
                   when :inactive then         "enrollments.workflow_state = 'inactive'"
                   end

      if conditions && @options[:course_workflow_state] && @options[:enforce_course_workflow_state]
        conditions += sanitize_sql(
          " AND courses.workflow_state = ?",
          @options[:course_workflow_state]
        )
      end
      conditions
    end

    def sanitize_sql(sql, *args)
      ActiveRecord::Base.send :sanitize_sql_array, [sql, *args]
    end

    # a map of Course#workflow_state <-> compatible #state arguments,
    # i.e. infer a compatible value for the latter from the former
    COURSE_ENROLLMENT_STATE_MAP = {
      available: :current_and_invited,
      completed: :completed,
      deleted: :deleted,
      created: :current_and_future,
      claimed: :current_and_future
    }.with_indifferent_access.freeze
  end
end
