# frozen_string_literal: true

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

# @API Quiz Submission Questions
#
# API for answering and flagging questions in a quiz-taking session.
#
# @model QuizSubmissionQuestion
#     {
#       "id": "QuizSubmissionQuestion",
#       "required": ["id"],
#       "properties": {
#         "id": {
#           "description": "The ID of the QuizQuestion this answer is for.",
#           "example": 1,
#           "type": "integer",
#           "format": "int64"
#         },
#         "flagged": {
#           "description": "Whether this question is flagged.",
#           "example": true,
#           "type": "boolean"
#         },
#         "answer": {
#           "description": "The provided answer (if any) for this question. The format of this parameter depends on the type of the question, see the Appendix for more information.",
#           "type": "string"
#         },
#         "answers": {
#           "description": "The possible answers for this question when those possible answers are necessary.  The presence of this parameter is dependent on permissions.",
#           "type": "array",
#           "items": {"type": "string"}
#         }
#       }
#     }
#
class Quizzes::QuizSubmissionQuestionsController < ApplicationController
  include Api::V1::QuizSubmissionQuestion
  include ::Filters::QuizSubmissions
  include ActionView::Helpers::NumberHelper

  before_action :require_user, :require_quiz_submission, :export_scopes
  before_action :require_question, only: %i[show flag unflag formatted_answer]
  before_action :prepare_service, only: %i[answer flag unflag]
  before_action :validate_ldb_status!, only: %i[answer flag unflag]

  # @API Get all quiz submission questions.
  #
  # Get a list of all the question records for this quiz submission.
  #
  # @argument include[] [String, "quiz_question"]
  #   Associations to include with the quiz submission question.
  #
  # <b>200 OK</b> response code is returned if the request was successful.
  #
  # @example_response
  #  {
  #    "quiz_submission_questions": [QuizSubmissionQuestion]
  #  }
  def index
    retrieve_quiz_submission_attempt!(params[:quiz_submission_attempt]) if params[:quiz_submission_attempt]

    reject! "Cannot receive one question at a time questions in the API", 401 if @quiz.one_question_at_a_time && censored?

    if @quiz_submission.completed? && !@quiz_submission.results_visible?(user: @current_user)
      reject! "Cannot view questions due to quiz settings", 401
    end

    if authorized_action(@quiz_submission, @current_user, :read)
      render json: quiz_submission_questions_json(@quiz_submission.quiz_questions,
                                                  @quiz_submission,
                                                  {
                                                    user: @current_user,
                                                    session:,
                                                    includes: extract_includes,
                                                    censored: censored?,
                                                    shuffle_answers: @quiz.shuffle_answers_for_user?(@current_user)
                                                  })
    end
  end

  # @API Answering questions
  #
  # Provide or update an answer to one or more QuizQuestions.
  #
  # @argument attempt [Required, Integer]
  #   The attempt number of the quiz submission being taken. Note that this
  #   must be the latest attempt index, as questions for earlier attempts can
  #   not be modified.
  #
  # @argument validation_token [Required, String]
  #   The unique validation token you received when the Quiz Submission was
  #   created.
  #
  # @argument access_code [String]
  #   Access code for the Quiz, if any.
  #
  # @argument quiz_questions[] [QuizSubmissionQuestion]
  #   Set of question IDs and the answer value.
  #
  #   See {Appendix: Question Answer Formats} for the accepted answer formats
  #   for each question type.
  #
  # @example_request
  #  {
  #    "attempt": 1,
  #    "validation_token": "YOUR_VALIDATION_TOKEN",
  #    "access_code": null,
  #    "quiz_questions": [{
  #      "id": "1",
  #      "answer": "Hello World!"
  #    }, {
  #      "id": "2",
  #      "answer": 42.0
  #    }]
  #  }
  #
  # @returns [QuizSubmissionQuestion]
  def answer
    unless @quiz_submission.grants_right?(@service.participant.user, :update)
      reject! "you are not allowed to update questions for this quiz submission", 403
    end

    answers = params.to_unsafe_h.fetch(:quiz_questions, []).each_with_object({}) do |p, hsh|
      if p[:id].present?
        hsh[p[:id].to_i] = p[:answer] || []
      end
    end

    quiz_questions = @quiz.quiz_questions.where(id: answers.keys)

    record = quiz_questions.reduce({}) do |hsh, quiz_question|
      question_data = @quiz_submission.quiz_data.find { |qd| qd[:id] == quiz_question.id }

      serializer = serializer_for quiz_question
      serializer.override_question_data(question_data) if question_data.present?
      serialization_rc = serializer.serialize(answers[quiz_question.id])

      unless serialization_rc.valid?
        reject! serialization_rc.error, 400
      end

      hsh.merge serialization_rc.answer
    end

    @service.update_question(record, @quiz_submission, params[:attempt])

    render json: quiz_submission_questions_json(quiz_questions.all, @quiz_submission.reload, censored: true)
  end

  # @API Get a formatted student numerical answer.
  #
  # Matches the intended behavior of the UI when a numerical answer is entered
  # and returns the resulting formatted number
  #
  # @argument answer [Required, Numeric]
  #
  # @example_response
  #  {
  #    "formatted_answer": 12.1234
  #  }
  def formatted_answer
    student_answer = params[:answer]

    if student_answer.blank?
      reject! "answer param is required", 422
    end

    question = @question.question_data.question
    question_type = question["question_type"]

    if question_type == "numerical_question"
      ## matches behavior of public/javascript/take_quiz.js when entering a numerical answer
      question_has_precision_answers =
        question[:answers].any? { |answer| answer[:numerical_answer_type] == "precision_answer" }
      formatting_options = {
        precision: question_has_precision_answers ? 16 : 4,
        strip_insignificant_zeros: true,
        significant: question_has_precision_answers
      }
      student_answer = number_with_precision(student_answer, formatting_options)
    end

    render json: { formatted_answer: student_answer }
  end

  # @API Flagging a question.
  #
  # Set a flag on a quiz question to indicate that you want to return to it
  # later.
  #
  # @argument attempt [Required, Integer]
  #   The attempt number of the quiz submission being taken. Note that this
  #   must be the latest attempt index, as questions for earlier attempts can
  #   not be modified.
  #
  # @argument validation_token [Required, String]
  #   The unique validation token you received when the Quiz Submission was
  #   created.
  #
  # @argument access_code [String]
  #   Access code for the Quiz, if any.
  #
  # @example_request
  #  {
  #    "attempt": 1,
  #    "validation_token": "YOUR_VALIDATION_TOKEN",
  #    "access_code": null
  #  }
  def flag
    unless @quiz_submission.grants_right?(@service.participant.user, :update)
      reject! "you are not allowed to update questions for this quiz submission", 403
    end
    flag_current_question(true)
    render json: quiz_submission_questions_json([@question],
                                                @quiz_submission.reload)
  end

  # @API Unflagging a question.
  #
  # Remove the flag that you previously set on a quiz question after you've
  # returned to it.
  #
  # @argument attempt [Required, Integer]
  #   The attempt number of the quiz submission being taken. Note that this
  #   must be the latest attempt index, as questions for earlier attempts can
  #   not be modified.
  #
  # @argument validation_token [Required, String]
  #   The unique validation token you received when the Quiz Submission was
  #   created.
  #
  # @argument access_code [String]
  #   Access code for the Quiz, if any.
  #
  # @example_request
  #  {
  #    "attempt": 1,
  #    "validation_token": "YOUR_VALIDATION_TOKEN",
  #    "access_code": null
  #  }
  def unflag
    unless @quiz_submission.grants_right?(@service.participant.user, :update)
      reject! "you are not allowed to update questions for this quiz submission", 403
    end
    flag_current_question(false)
    render json: quiz_submission_questions_json([@question],
                                                @quiz_submission.reload)
  end

  private

  def require_question
    @question = @quiz.quiz_questions.find(params[:id].to_i)
  end

  # Export the Quiz and Course from the resolved QS.
  def export_scopes
    @quiz = @quiz_submission.quiz

    require_overridden_quiz

    @context = @quiz.context
  end

  # This is duplicated from QuizSubmissionsApiController and will be moved into
  # a Controller Filter once CNVS-10071 is in.
  #
  # [Transient:CNVS-10071]
  def validate_ldb_status!(quiz = @quiz)
    if quiz.require_lockdown_browser? && !ldb_plugin.authorized?(self)
      reject! "this quiz requires the lockdown browser", 403
    end
  end

  # This is duplicated from QuizSubmissionsApiController and will be moved into
  # a Controller Filter once CNVS-10071 is in.
  #
  # [Transient:CNVS-10071]
  def ldb_plugin
    Canvas::LockdownBrowser.plugin.base
  end

  # Toggle a question's "flagged" status.
  #
  # @param [Boolean] flagged_unflagged
  #
  # @return [Hash] the QS's submission_data.
  def flag_current_question(flagged_unflagged)
    question_record = {}.with_indifferent_access
    question_record["question_#{@question.id}_marked"] = flagged_unflagged

    @service.update_question(question_record,
                             @quiz_submission,
                             params[:attempt],
                             # we don't want a snapshot generated for each flagging action
                             false)
  end

  def extract_includes(key = :include, hash = params)
    Array(hash[key])
  end

  def serializer_for(quiz_question)
    Quizzes::QuizQuestion::AnswerSerializers.serializer_for(quiz_question)
  end

  def censored?
    !@quiz.grants_right?(@current_user, session, :update)
  end

  # @!appendix Question Answer Formats
  #
  #  {include:file:doc/examples/quiz_question_answers.md}
end
