Source: includes/rest-api/class-sensei-rest-api-question-helpers-trait.php

<?php
/**
 * File containing the trait Sensei_REST_API_Question_Helpers_Trait.
 *
 * @package sensei
 * @author  Automattic
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Sensei Lesson Quiz REST API endpoints.
 *
 * @since   3.9.0
 */
trait Sensei_REST_API_Question_Helpers_Trait {

	/**
	 * Returns the schema array for a question type.
	 *
	 * @param string $type The question type.
	 *
	 * @return array The question schema.
	 */
	private function get_question_schema( string $type ): array {
		$schema = $this->get_common_question_properties_schema();
		switch ( $type ) {
			case 'category-question':
				$schema = $this->get_category_question_schema();
				break;
			case 'multiple-choice':
				$schema = $this->get_multiple_choice_schema();
				break;
			case 'boolean':
				$schema = $this->get_boolean_schema();
				break;
			case 'gap-fill':
				$schema = $this->get_gap_fill_schema();
				break;
			case 'single-line':
				$schema = $this->get_single_line_schema();
				break;
			case 'multi-line':
				$schema = $this->get_multi_line_schema();
				break;
			case 'file-upload':
				$schema = $this->get_file_upload_schema();
				break;
		}

		/**
		 * Modify or add REST API schema for a question type.
		 *
		 * @since 3.9.0
		 *
		 * @hook sensei_rest_api_schema_question_type
		 *
		 * @param {Array}  $schema Schema for a single question.
		 * @param {string} $type   Question type.
		 * @return {array} Filtered schema.
		 */
		return apply_filters( 'sensei_rest_api_schema_question_type', $schema, $type );
	}

	/**
	 * Helper method to save or update a category question.
	 *
	 * @param array $question The question JSON array.
	 *
	 * @return int|WP_Error Multiple question id on success.
	 */
	private function save_category_question( $question ) {
		$question_id = $question['id'] ?? null;

		if (
			$question_id
			&& (
				'multiple_question' !== get_post_type( $question_id )
				|| ! current_user_can( get_post_type_object( 'multiple_question' )->cap->edit_post, $question_id )
			)
		) {
			return new WP_Error( 'sensei_lesson_quiz_question_not_available', '', $question_id );
		}

		if ( ! isset( $question['options'] ) ) {
			$question['options'] = [];
		}

		$question_id          = isset( $question['id'] ) ? (int) $question['id'] : null;
		$question_number      = (int) $question['options']['number'];
		$question_category_id = (int) $question['options']['category'];
		$question_category    = false;

		if ( $question_category_id ) {
			$question_category = get_term( $question_category_id, 'question-category' );
		}

		if ( ! $question_category || is_wp_error( $question_category ) ) {
			return new WP_Error( 'sensei_lesson_quiz_question_invalid_category', esc_html__( 'Invalid question category selected.', 'sensei-lms' ), $question_id );
		}

		if ( ! $question_number ) {
			$question_number = 1;
		}

		// translators: Placeholders are the question number and the question category name.
		$post_title = sprintf( esc_html__( '%1$s Question(s) from %2$s', 'sensei-lms' ), $question_number, $question_category->name );

		$post_args = [
			'ID'          => (int) $question_id,
			'post_title'  => $post_title,
			'post_status' => 'publish',
			'post_type'   => 'multiple_question',
			'meta_input'  => [
				'category' => $question_category->term_id,
				'number'   => $question_number,
			],
		];

		$result = $question_id ? wp_update_post( $post_args ) : wp_insert_post( $post_args );

		/**
		 * This action is triggered when a category question is created or updated by the lesson quiz REST endpoint.
		 *
		 * @since 3.9.0
		 *
		 * @hook  sensei_rest_api_category_question_saved
		 *
		 * @param {int|WP_Error} $result   Result of wp_insert_post. Post ID on success or WP_Error on failure.
		 * @param {array}        $question The question JSON arguments.
		 */
		do_action( 'sensei_rest_api_category_question_saved', $result, $question );

		return $result;
	}

	/**
	 * Helper method to save or update a question.
	 *
	 * @param array  $question The question JSON array.
	 * @param string $status Question status.
	 *
	 * @return int|WP_Error Question id on success.
	 */
	private function save_question( $question, $status = 'publish' ) {
		$question_id = $question['id'] ?? null;

		if (
			$question_id
			&& (
				'question' !== get_post_type( $question_id )
				|| ! current_user_can( get_post_type_object( 'question' )->cap->edit_post, $question_id )
			)
		) {
			return new WP_Error( 'sensei_lesson_quiz_question_not_available', '', $question_id );
		}

		if ( empty( $question['title'] ) ) {
			return new WP_Error(
				'sensei_lesson_quiz_question_missing_title',
				__( 'Please ensure all questions have a title before saving.', 'sensei-lms' )
			);
		}

		if ( ! isset( $question['options'] ) ) {
			$question['options'] = [];
		}

		if ( ! isset( $question['type'] ) ) {
			$question['type'] = 'multiple-choice';
		}

		$is_new = null === $question_id;

		$post_args = [
			'ID'         => (int) $question_id,
			'post_title' => (string) $question['title'],
			'post_type'  => 'question',
			'meta_input' => $this->get_question_meta( $question ),
			'tax_input'  => [
				'question-type' => $question['type'],
			],
		];

		if ( $status ) {
			$post_args['post_status'] = (string) $status;
		}

		// Force publish the question if it's part of a quiz.
		if ( $this->is_question_used_in_quiz( $question_id ) ) {
			$post_args['post_status'] = 'publish';
		}

		if ( isset( $question['description'] ) ) {
			$post_args['post_content'] = (string) $question['description'];
		} else {
			$post_args['post_content'] = '';
		}

		$result = $question_id ? wp_update_post( $post_args ) : wp_insert_post( $post_args );

		if ( ! $is_new && ! is_wp_error( $result ) ) {
			$this->migrate_non_editor_question( $result, $question['type'] );
		}

		/**
		 * This action is triggered when a question is created or updated by the lesson quiz REST endpoint.
		 *
		 * @since 3.9.0
		 *
		 * @hook  sensei_rest_api_question_saved
		 *
		 * @param {int|WP_Error} $result        Result of wp_insert_post. Post ID on success or WP_Error on failure.
		 * @param {string}       $question_type The question type.
		 * @param {array}        $question      The question JSON arguments.
		 */
		do_action( 'sensei_rest_api_question_saved', $result, $question['type'], $question );

		return $result;
	}

	/**
	 * Check if question is being used in a quiz.
	 *
	 * @param int $question_id Question ID.
	 *
	 * @return boolean
	 */
	private function is_question_used_in_quiz( $question_id ) {
		if ( ! empty( get_post_meta( $question_id, '_quiz_id', false ) ) ) {
			return true;
		} else {
			$question_categories = wp_get_post_terms( $question_id, 'question-category' );

			foreach ( $question_categories as $question_category ) {
				$multiple_questions = get_posts(
					[
						'post_type'      => 'multiple_question',
						'posts_per_page' => 1,
						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Needed to identify if question is being used.
						'meta_query'     => [
							[
								'key'   => 'category',
								'value' => $question_category->term_id,
							],
							[
								'key'     => '_quiz_id',
								'compare' => 'EXISTS',
							],
						],
					]
				);

				if ( ! empty( $multiple_questions ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Helper method to delete question meta that were deprecated by the block editor.
	 *
	 * @param int    $question_id   Question post id.
	 * @param string $question_type Question type.
	 */
	private function migrate_non_editor_question( int $question_id, string $question_type ) {
		delete_post_meta( $question_id, '_question_media' );

		if ( 'file-upload' === $question_type ) {
			delete_post_meta( $question_id, '_question_wrong_answers' );
		}
	}

	/**
	 * Calculates the meta values for a question according to the input array.
	 *
	 * @param array $question The input array.
	 *
	 * @return array The calculated meta array.
	 */
	private function get_question_meta( array $question ): array {
		$meta    = [];
		$options = $question['options'] ?? [];

		if ( isset( $options['grade'] ) ) {
			$meta['_question_grade'] = $options['grade'];
		}

		if ( isset( $options['hideAnswerFeedback'] ) ) {
			$meta['_hide_answer_feedback'] = $options['hideAnswerFeedback'];
		}

		// Common meta.
		switch ( $question['type'] ) {
			case 'multiple-choice':
			case 'boolean':
			case 'gap-fill':
				if ( isset( $options['answerFeedback'] ) ) {
					$meta['_answer_feedback'] = $options['answerFeedback'];
				}
				break;
			case 'single-line':
			case 'multi-line':
			case 'file-upload':
				if ( array_key_exists( 'teacherNotes', $options ) ) {
					$meta['_question_right_answer'] = $options['teacherNotes'];
				}
				break;
		}

		// Type-specific meta.
		switch ( $question['type'] ) {
			case 'multiple-choice':
				$meta = array_merge( $meta, $this->get_multiple_choice_meta( $question ) );
				break;
			case 'boolean':
				if ( isset( $question['answer']['correct'] ) ) {
					$meta['_question_right_answer'] = $question['answer']['correct'] ? 'true' : 'false';
				} else {
					// If the correct field isn't defined, then it's true by default.
					$meta['_question_right_answer'] = 'true';
				}
				break;
			case 'gap-fill':
				$meta = array_merge( $meta, $this->get_gap_fill_meta( $question ) );
				break;
			case 'file-upload':
				if ( array_key_exists( 'studentHelp', $options ) ) {
					$meta['_question_wrong_answers'] = $options['studentHelp'];
				}
				break;
		}

		return $meta;
	}

	/**
	 * Calculates the meta values for a multiple choice question.
	 *
	 * @param array $question The multiple choice JSON array.
	 *
	 * @return array The calculated meta.
	 */
	private function get_multiple_choice_meta( array $question ): array {
		$meta = [];

		$meta['_random_order'] = empty( $question['options']['randomOrder'] ) ? 'no' : 'yes';

		if ( isset( $question['answer'] ) ) {
			$meta['_question_right_answer']  = [];
			$meta['_question_wrong_answers'] = [];
			$meta['_answer_order']           = [];

			foreach ( $question['answer']['answers'] ?? [] as $option ) {
				if ( ! isset( $option['label'] ) || '' === $option['label'] ) {
					continue;
				}

				if ( empty( $option['correct'] ) ) {
					$meta['_question_wrong_answers'][] = $option['label'];
				} else {
					$meta['_question_right_answer'][] = $option['label'];
				}

				$meta['_answer_order'][] = Sensei()->lesson->get_answer_id( $option['label'] );
			}

			$meta['_answer_order']       = implode( ',', $meta['_answer_order'] );
			$meta['_right_answer_count'] = count( $meta['_question_right_answer'] );
			$meta['_wrong_answer_count'] = count( $meta['_question_wrong_answers'] );
		}

		return $meta;
	}

	/**
	 * Calculates the meta values for a gap fill question.
	 *
	 * @param array $question The gap fill JSON array.
	 *
	 * @return array The calculated meta.
	 */
	private function get_gap_fill_meta( $question ): array {
		$answer = $question['answer'];
		if ( ! ( isset( $answer['before'] ) || isset( $answer['gap'] ) || isset( $answer['after'] ) ) ) {
			return [];
		}

		$old_text_values = [];
		if ( isset( $question['id'] ) ) {
			$old_meta        = get_post_meta( $question['id'], '_question_right_answer', true );
			$old_text_values = explode( '||', $old_meta );
		}

		$text_values = [];

		if ( array_key_exists( 'before', $answer ) ) {
			$text_values[0] = $answer['before'];
		} else {
			$text_values[0] = isset( $old_text_values[0] ) ? $old_text_values[0] : '';
		}

		if ( ! array_key_exists( 'gap', $answer ) ) {
			$text_values[1] = isset( $old_text_values[1] ) ? $old_text_values[1] : '';
		} else {
			$text_values[1] = implode( '|', $answer['gap'] );
		}

		if ( array_key_exists( 'after', $answer ) ) {
			$text_values[2] = $answer['after'];
		} else {
			$text_values[2] = isset( $old_text_values[2] ) ? $old_text_values[2] : '';
		}

		return [ '_question_right_answer' => implode( '||', $text_values ) ];
	}

	/**
	 * Returns a question as defined by the schema.
	 *
	 * @param WP_Post $question The question post.
	 *
	 * @return array The question array.
	 */
	private function get_question( WP_Post $question ): array {
		$common_properties        = $this->get_question_common_properties( $question );
		$type_specific_properties = $this->get_question_type_specific_properties( $question, $common_properties['type'] );

		return array_merge_recursive( $common_properties, $type_specific_properties );
	}

	/**
	 * Get the multiple/category question as defined by the schema.
	 *
	 * @param WP_Post $question The question post.
	 *
	 * @return array
	 */
	private function get_category_question( WP_Post $question ): array {
		$category = (int) get_post_meta( $question->ID, 'category', true );
		$number   = (int) get_post_meta( $question->ID, 'number', true );

		return [
			'type'    => 'category-question',
			'id'      => $question->ID,
			'options' => [
				'category' => $category,
				'number'   => $number,
			],
		];
	}

	/**
	 * Generates the common question properties.
	 *
	 * @param WP_Post $question The question post.
	 *
	 * @return array The question properties.
	 */
	private function get_question_common_properties( WP_Post $question ): array {
		$question_meta = get_post_meta( $question->ID );

		$common_properties = [
			'id'          => $question->ID,
			'title'       => 'auto-draft' !== $question->post_status ? $question->post_title : '',
			'description' => $question->post_content,
			'options'     => [
				'grade'              => Sensei()->question->get_question_grade( $question->ID ),
				'hideAnswerFeedback' => get_post_meta( $question->ID, '_hide_answer_feedback', true ),
			],
			'type'        => Sensei()->question->get_question_type( $question->ID ),
			'shared'      => ! empty( $question_meta['_quiz_id'] ) && count( $question_meta['_quiz_id'] ) > 1,
			'editable'    => current_user_can( get_post_type_object( 'question' )->cap->edit_post, $question->ID ),
			'categories'  => wp_get_post_terms( $question->ID, 'question-category', [ 'fields' => 'ids' ] ),
		];

		if ( ! empty( $question_meta['_question_media'][0] ) ) {
			$question_media = $this->get_question_media( (int) $question_meta['_question_media'][0], $question->ID );

			if ( ! empty( $question_media ) ) {
				$common_properties['media'] = $question_media;
			}
		}

		return $common_properties;
	}

	/**
	 * Helper method to get question media.
	 *
	 * @param int $question_media_id The attachment id.
	 * @param int $question_id       The question id.
	 *
	 * @return array Media info. It includes the type, id, url and title.
	 */
	private function get_question_media( int $question_media_id, int $question_id ): array {
		$question_media = [];
		$mimetype       = get_post_mime_type( $question_media_id );
		$attachment     = get_post( $question_media_id );

		if ( $mimetype || null !== $attachment ) {
			$mimetype_array = explode( '/', $mimetype );

			if ( ! empty( $mimetype_array[0] ) ) {
				if ( 'image' === $mimetype_array[0] ) {
					/**
					 * Filter the size of the question image.
					 *
					 * @hook sensei_question_image_size
					 *
					 * @param {string} $size        Image size.
					 * @param {int}    $question_id Question ID.
					 * @return {string} Image size.
					 */
					$image_size            = apply_filters( 'sensei_question_image_size', 'medium', $question_id );
					$attachment_src        = wp_get_attachment_image_src( $question_media_id, $image_size );
					$question_media['url'] = esc_url( $attachment_src[0] );
				} else {
					$question_media['url'] = esc_url( wp_get_attachment_url( $question_media_id ) );
				}

				$question_media['type']  = $mimetype_array[0];
				$question_media['id']    = $attachment->ID;
				$question_media['title'] = esc_html( $attachment->post_title );
			}
		}

		return $question_media;
	}

	/**
	 * Generates the type specific question properties.
	 *
	 * @param WP_Post $question      The question post.
	 * @param string  $question_type The question type.
	 *
	 * @return array The question properties.
	 */
	private function get_question_type_specific_properties( WP_Post $question, string $question_type ): array {
		$type_specific_properties = [
			'options' => [],
			'answer'  => [],
		];

		switch ( $question_type ) {
			case 'multiple-choice':
			case 'gap-fill':
			case 'boolean':
				$answer_feedback                                       = get_post_meta( $question->ID, '_answer_feedback', true );
				$type_specific_properties['options']['answerFeedback'] = empty( $answer_feedback ) ? null : $answer_feedback;
				break;
			case 'single-line':
			case 'multi-line':
			case 'file-upload':
				$teacher_notes                                       = get_post_meta( $question->ID, '_question_right_answer', true );
				$type_specific_properties['options']['teacherNotes'] = empty( $teacher_notes ) ? null : $teacher_notes;
				break;
		}

		switch ( $question_type ) {
			case 'multiple-choice':
				$type_specific_properties = array_merge_recursive( $type_specific_properties, $this->get_multiple_choice_properties( $question ) );
				break;
			case 'boolean':
				$right_answer                                  = get_post_meta( $question->ID, '_question_right_answer', true );
				$type_specific_properties['answer']['correct'] = 'true' === $right_answer || '' === $right_answer;
				break;
			case 'gap-fill':
				$type_specific_properties['answer'] = $this->get_gap_fill_properties( $question );
				break;
			case 'file-upload':
				$student_help                                       = get_post_meta( $question->ID, '_question_wrong_answers', true );
				$type_specific_properties['options']['studentHelp'] = empty( $student_help[0] ) ? null : esc_html( $student_help[0] );
				break;
		}

		/**
		 * Allows modification of type specific question properties.
		 *
		 * @since 3.9.0
		 *
		 * @hook sensei_question_type_specific_properties
		 *
		 * @param {array}   $type_specific_properties The properties of the question.
		 * @param {string}  $question_type            The question type.
		 * @param {WP_Post} $question                 The question post.
		 * @return {array} Filtered properties.
		 */
		return apply_filters( 'sensei_question_type_specific_properties', $type_specific_properties, $question_type, $question );
	}

	/**
	 * Helper method which generates the properties for gap fill questions.
	 *
	 * @param WP_Post $question The question post.
	 *
	 * @return array The gap fill question properties.
	 */
	private function get_gap_fill_properties( WP_Post $question ): array {
		$right_answer_meta = get_post_meta( $question->ID, '_question_right_answer', true );

		if ( empty( $right_answer_meta ) ) {
			return [
				'before' => '',
				'gap'    => [],
				'after'  => '',
			];
		}

		$result      = [];
		$text_values = explode( '||', $right_answer_meta );

		$result['before'] = isset( $text_values[0] ) ? $text_values[0] : '';
		$result['gap']    = ( ! isset( $text_values[1] ) || '' === $text_values[1] ) ? [] : explode( '|', $text_values[1] );
		$result['after']  = isset( $text_values[2] ) ? $text_values[2] : '';

		return $result;
	}

	/**
	 * Helper method which generates the properties for multiple choice questions.
	 *
	 * @param WP_Post $question The question post.
	 *
	 * @return array The multiple choice question properties.
	 */
	private function get_multiple_choice_properties( WP_Post $question ): array {
		$type_specific_properties = [
			'options' => [ 'randomOrder' => 'yes' === get_post_meta( $question->ID, '_random_order', true ) ],
		];

		$correct_answers = $this->get_answers_array( $question, '_question_right_answer', true );
		$wrong_answers   = $this->get_answers_array( $question, '_question_wrong_answers', false );

		$answer_order       = get_post_meta( $question->ID, '_answer_order', true );
		$all_answers_sorted = Sensei()->question->get_answers_sorted( array_merge( $correct_answers, $wrong_answers ), $answer_order );

		$type_specific_properties['answer'] = [ 'answers' => array_values( $all_answers_sorted ) ];

		return $type_specific_properties;
	}

	/**
	 * Helper method which transforms the answers question meta to an associative array. This is the format that is expected by
	 * Sensei_Question::get_answers_sorted.
	 *
	 * @param WP_Post $question   The question post.
	 * @param string  $meta_key   The answers meta key.
	 * @param bool    $is_correct Whether the questions are correct.
	 *
	 * @see Sensei_Question::get_answers_sorted
	 *
	 * @return array The answers array.
	 */
	private function get_answers_array( WP_Post $question, string $meta_key, bool $is_correct ): array {
		$answers = get_post_meta( $question->ID, $meta_key, true );

		if ( empty( $answers ) ) {
			return [];
		}

		if ( ! is_array( $answers ) ) {
			$answers = [ $answers ];
		}

		$result = [];
		foreach ( $answers as $correct_answer ) {
			$result[ Sensei()->lesson->get_answer_id( $correct_answer ) ] = [
				'label'   => $correct_answer,
				'correct' => $is_correct,
			];
		}

		return $result;
	}

	/**
	 * Get the schema for a single question.
	 *
	 * @return array
	 */
	public function get_single_question_schema() {
		$single_question_schema = [
			'oneOf' => [
				$this->get_category_question_schema(),
				$this->get_multiple_choice_schema(),
				$this->get_boolean_schema(),
				$this->get_gap_fill_schema(),
				$this->get_single_line_schema(),
				$this->get_multi_line_schema(),
				$this->get_file_upload_schema(),
			],
		];

		if ( ! is_wp_version_compatible( '5.6.0' ) ) {
			$single_question_schema = [
				'type' => 'object',
			];
		}

		/**
		 * Add additional question types to the REST API schema.
		 *
		 * @since 3.9.0
		 *
		 * @hook sensei_rest_api_schema_single_question
		 *
		 * @param {Array} $schema Schema for a single question.
		 * @return {array} Filtered schema.
		 */
		return apply_filters( 'sensei_rest_api_schema_single_question', $single_question_schema );
	}

	/**
	 * Helper method which returns the schema for category question properties.
	 *
	 * @return array The properties.
	 */
	private function get_category_question_schema(): array {
		$question_category_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'category-question',
				'required' => true,
			],
			'id'      => [
				'type'        => 'integer',
				'description' => 'Multiple question post ID',
			],
			'options' => [
				'type'       => 'object',
				'properties' => [
					'category' => [
						'type'        => [ 'integer', 'null' ],
						'description' => 'Term ID of the category where questions are picked',
						'required'    => true,
					],
					'number'   => [
						'type'        => 'integer',
						'description' => 'Number of questions to select from the category',
						'required'    => true,
					],
				],
			],
		];

		return [
			'title'      => 'Category Question',
			'type'       => 'object',
			'properties' => $question_category_properties,
		];
	}

	/**
	 * Helper method which returns the schema for common question properties.
	 *
	 * @return array The properties
	 */
	public function get_common_question_properties_schema(): array {
		return [
			'id'          => [
				'type'        => 'integer',
				'description' => 'Question post ID',
			],
			'title'       => [
				'type'        => 'string',
				'description' => 'Question text',
			],
			'description' => [
				'type'        => 'string',
				'description' => 'Question description',
			],
			'answer'      => [
				'type' => 'object',
			],
			'options'     => [
				'type'       => 'object',
				'properties' => [
					'grade'              => [
						'type'        => 'integer',
						'description' => 'Points this question is worth',
						'minimum'     => 0,
						'maximum'     => 100,
						'default'     => 1,
					],
					'hideAnswerFeedback' => [
						'type'        => 'string',
						'description' => 'Hide/show answer feedback for the question',
						'default'     => '',
					],
				],
			],
			'shared'      => [
				'type'        => 'boolean',
				'description' => 'Whether the question has been added on other quizzes',
				'readonly'    => true,
			],
			'editable'    => [
				'type'        => 'boolean',
				'description' => 'Whether the question can be edited by the current user',
				'readonly'    => true,
			],
			'categories'  => [
				'type'        => 'array',
				'readonly'    => true,
				'description' => 'Category term IDs attached to the question',
				'items'       => [
					'type'        => 'integer',
					'description' => 'Term IDs',
				],
			],
			'media'       => [
				'type'       => 'object',
				'readonly'   => true,
				'properties' => [
					'id'    => [
						'type'        => 'integer',
						'description' => 'Linked media id',
					],
					'type'  => [
						'type'        => 'string',
						'description' => 'Media type',
					],
					'url'   => [
						'type'        => 'string',
						'description' => 'Media url',
					],
					'title' => [
						'type'        => 'string',
						'description' => 'Media title',
					],
				],
			],
		];
	}

	/**
	 * Helper method which returns the schema for multiple choice question properties.
	 *
	 * @return array The properties
	 */
	private function get_multiple_choice_schema(): array {
		$multiple_choice_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'multiple-choice',
				'required' => true,
			],
			'answer'  => [
				'properties' => [
					'answers' => [
						'type'        => 'array',
						'description' => 'Options for the multiple choice',
						'items'       => [
							'type'       => 'object',
							'properties' => [
								'label'   => [
									'type'        => 'string',
									'description' => 'Label for answer option',
								],
								'correct' => [
									'type'        => 'boolean',
									'description' => 'Whether this answer is correct',
								],
							],
						],
					],
				],
			],
			'options' => [
				'properties' => [
					'randomOrder'    => [
						'type'        => 'boolean',
						'description' => 'Should options be randomized when displayed to quiz takers',
						'default'     => false,
					],
					'answerFeedback' => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Feedback to show quiz takers once quiz is submitted',
					],
				],
			],

		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $multiple_choice_properties ),
		];
	}

	/**
	 * Helper method which returns the schema for true/false question properties.
	 *
	 * @return array The properties
	 */
	private function get_boolean_schema(): array {
		$boolean_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'boolean',
				'required' => true,
			],
			'answer'  => [
				'properties' => [
					'correct' => [
						'type'        => 'boolean',
						'description' => 'Correct answer for question',
					],
				],
			],
			'options' => [
				'properties' => [
					'randomOrder'    => [
						'type'        => 'boolean',
						'description' => 'Should options be randomized when displayed to quiz takers',
						'default'     => true,
					],
					'answerFeedback' => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Feedback to show quiz takers once quiz is submitted',
					],
				],
			],
		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $boolean_properties ),
		];
	}

	/**
	 * Helper method which returns the schema for gap fill question properties.
	 *
	 * @return array The properties
	 */
	private function get_gap_fill_schema(): array {
		$gap_fill_properties = [
			'type'   => [
				'type'     => 'string',
				'pattern'  => 'gap-fill',
				'required' => true,
			],
			'answer' => [
				'description' => 'Answer before and after text, and correct answers.',
				'properties'  => [
					'before' => [
						'type'        => 'string',
						'description' => 'Text before the gap',
					],
					'gap'    => [
						'type'        => 'array',
						'description' => 'Gap text answers',
						'items'       => [
							'type'        => 'string',
							'description' => 'Gap answers',
						],
					],
					'after'  => [
						'type'        => 'string',
						'description' => 'Text after the gap',
					],
				],
			],
		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $gap_fill_properties ),
		];
	}

	/**
	 * Helper method which returns the schema for single line question properties.
	 *
	 * @return array The properties
	 */
	private function get_single_line_schema(): array {
		$single_line_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'single-line',
				'required' => true,
			],
			'options' => [
				'properties' => [
					'teacherNotes' => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Teacher notes for grading',
					],
				],
			],
		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $single_line_properties ),
		];
	}

	/**
	 * Helper method which returns the schema for multi line question properties.
	 *
	 * @return array The properties
	 */
	private function get_multi_line_schema(): array {
		$multiline_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'multi-line',
				'required' => true,
			],
			'options' => [
				'properties' => [
					'teacherNotes' => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Teacher notes for grading',
					],
				],
			],
		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $multiline_properties ),
		];
	}

	/**
	 * Helper method which returns the schema for file upload question properties.
	 *
	 * @return array The properties
	 */
	private function get_file_upload_schema(): array {
		$file_upload_properties = [
			'type'    => [
				'type'     => 'string',
				'pattern'  => 'file-upload',
				'required' => true,
			],
			'options' => [
				'properties' => [
					'teacherNotes' => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Teacher notes for grading',
					],
					'studentHelp'  => [
						'type'        => [ 'string', 'null' ],
						'description' => 'Description for student explaining what needs to be uploaded',
						'readonly'    => true,
					],
				],
			],
		];

		return [
			'title'      => 'Question',
			'type'       => 'object',
			'properties' => array_merge_recursive( $this->get_common_question_properties_schema(), $file_upload_properties ),
		];
	}
}